diff --git a/.coveragerc b/.coveragerc index ad001e56048..aa8f2d8c03d 100644 --- a/.coveragerc +++ b/.coveragerc @@ -143,6 +143,7 @@ omit = homeassistant/components/dlna_dmr/media_player.py homeassistant/components/dnsip/sensor.py homeassistant/components/dominos/* + homeassistant/components/doods/* homeassistant/components/doorbird/* homeassistant/components/dovado/* homeassistant/components/downloader/* @@ -155,7 +156,12 @@ omit = homeassistant/components/ebox/sensor.py homeassistant/components/ebusd/* homeassistant/components/ecoal_boiler/* - homeassistant/components/ecobee/* + homeassistant/components/ecobee/__init__.py + homeassistant/components/ecobee/binary_sensor.py + homeassistant/components/ecobee/climate.py + homeassistant/components/ecobee/notify.py + homeassistant/components/ecobee/sensor.py + homeassistant/components/ecobee/weather.py homeassistant/components/econet/water_heater.py homeassistant/components/ecovacs/* homeassistant/components/eddystone_temperature/sensor.py @@ -164,7 +170,7 @@ omit = homeassistant/components/eight_sleep/* homeassistant/components/eliqonline/sensor.py homeassistant/components/elkm1/* - homeassistant/components/elv/switch.py + homeassistant/components/elv/* homeassistant/components/emby/media_player.py homeassistant/components/emoncms/sensor.py homeassistant/components/emoncms_history/* @@ -197,7 +203,6 @@ omit = homeassistant/components/evohome/* homeassistant/components/familyhub/camera.py homeassistant/components/fastdotcom/* - homeassistant/components/fedex/sensor.py homeassistant/components/ffmpeg/camera.py homeassistant/components/fibaro/* homeassistant/components/filesize/sensor.py @@ -288,11 +293,15 @@ omit = homeassistant/components/hydrawise/* homeassistant/components/hyperion/light.py homeassistant/components/ialarm/alarm_control_panel.py + homeassistant/components/iaqualink/binary_sensor.py homeassistant/components/iaqualink/climate.py homeassistant/components/iaqualink/light.py homeassistant/components/iaqualink/sensor.py homeassistant/components/iaqualink/switch.py homeassistant/components/icloud/device_tracker.py + homeassistant/components/izone/climate.py + homeassistant/components/izone/discovery.py + homeassistant/components/izone/__init__.py homeassistant/components/idteck_prox/* homeassistant/components/ifttt/* homeassistant/components/iglo/light.py @@ -313,6 +322,7 @@ omit = homeassistant/components/itunes/media_player.py homeassistant/components/joaoapps_join/* homeassistant/components/juicenet/* + homeassistant/components/kaiterra/* homeassistant/components/kankun/switch.py homeassistant/components/keba/* homeassistant/components/keenetic_ndms2/device_tracker.py @@ -343,7 +353,6 @@ omit = homeassistant/components/lifx_legacy/light.py homeassistant/components/lightwave/* homeassistant/components/limitlessled/light.py - homeassistant/components/linksys_ap/device_tracker.py homeassistant/components/linksys_smart/device_tracker.py homeassistant/components/linky/__init__.py homeassistant/components/linky/sensor.py @@ -434,12 +443,14 @@ omit = homeassistant/components/nuki/lock.py homeassistant/components/nut/sensor.py homeassistant/components/nx584/alarm_control_panel.py + homeassistant/components/nzbget/__init__.py homeassistant/components/nzbget/sensor.py homeassistant/components/obihai/* homeassistant/components/octoprint/* homeassistant/components/oem/climate.py homeassistant/components/oasa_telematics/sensor.py homeassistant/components/ohmconnect/sensor.py + homeassistant/components/ombi/* homeassistant/components/onewire/sensor.py homeassistant/components/onkyo/media_player.py homeassistant/components/onvif/camera.py @@ -476,7 +487,10 @@ omit = homeassistant/components/pioneer/media_player.py homeassistant/components/pjlink/media_player.py homeassistant/components/plaato/* - homeassistant/components/plex/* + homeassistant/components/plex/__init__.py + homeassistant/components/plex/media_player.py + homeassistant/components/plex/sensor.py + homeassistant/components/plex/server.py homeassistant/components/plugwise/* homeassistant/components/plum_lightpad/* homeassistant/components/pocketcasts/sensor.py @@ -544,6 +558,7 @@ omit = homeassistant/components/russound_rio/media_player.py homeassistant/components/russound_rnet/media_player.py homeassistant/components/sabnzbd/* + homeassistant/components/saj/sensor.py homeassistant/components/satel_integra/* homeassistant/components/scrape/sensor.py homeassistant/components/scsgate/* @@ -585,6 +600,8 @@ omit = homeassistant/components/solaredge/sensor.py homeassistant/components/solaredge_local/sensor.py homeassistant/components/solax/sensor.py + homeassistant/components/soma/cover.py + homeassistant/components/soma/__init__.py homeassistant/components/somfy/* homeassistant/components/somfy_mylink/* homeassistant/components/sonarr/sensor.py @@ -597,7 +614,6 @@ omit = homeassistant/components/spotcrime/sensor.py homeassistant/components/spotify/media_player.py homeassistant/components/squeezebox/media_player.py - homeassistant/components/srp_energy/sensor.py homeassistant/components/starlingbank/sensor.py homeassistant/components/steam_online/sensor.py homeassistant/components/stiebel_eltron/* @@ -618,7 +634,6 @@ omit = homeassistant/components/synologydsm/sensor.py homeassistant/components/syslog/notify.py homeassistant/components/systemmonitor/sensor.py - homeassistant/components/sytadin/sensor.py homeassistant/components/tado/* homeassistant/components/tado/device_tracker.py homeassistant/components/tahoma/* @@ -660,9 +675,14 @@ omit = homeassistant/components/trackr/device_tracker.py homeassistant/components/tradfri/* homeassistant/components/tradfri/light.py + homeassistant/components/tradfri/cover.py homeassistant/components/trafikverket_train/sensor.py homeassistant/components/trafikverket_weatherstation/sensor.py - homeassistant/components/transmission/* + homeassistant/components/transmission/__init__.py + homeassistant/components/transmission/sensor.py + homeassistant/components/transmission/switch.py + homeassistant/components/transmission/const.py + homeassistant/components/transmission/errors.py homeassistant/components/travisci/sensor.py homeassistant/components/tuya/* homeassistant/components/twentemilieu/const.py @@ -678,10 +698,8 @@ omit = homeassistant/components/upcloud/* homeassistant/components/upnp/* homeassistant/components/upc_connect/* - homeassistant/components/ups/sensor.py homeassistant/components/uptimerobot/binary_sensor.py homeassistant/components/uscis/sensor.py - homeassistant/components/usps/* homeassistant/components/vallox/* homeassistant/components/vasttrafik/sensor.py homeassistant/components/velbus/__init__.py @@ -738,6 +756,7 @@ omit = homeassistant/components/yale_smart_alarm/alarm_control_panel.py homeassistant/components/yamaha/media_player.py homeassistant/components/yamaha_musiccast/media_player.py + homeassistant/components/yandex_transport/* homeassistant/components/yeelight/* homeassistant/components/yeelightsunflower/light.py homeassistant/components/yi/camera.py @@ -755,6 +774,7 @@ omit = homeassistant/components/zha/core/device.py homeassistant/components/zha/core/gateway.py homeassistant/components/zha/core/helpers.py + homeassistant/components/zha/core/patches.py homeassistant/components/zha/core/registries.py homeassistant/components/zha/device_entity.py homeassistant/components/zha/entity.py diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index e78a8e6851c..afb273331aa 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -8,6 +8,7 @@ "runArgs": ["-e", "GIT_EDITOR=\"code --wait\""], "extensions": [ "ms-python.python", + "visualstudioexptteam.vscodeintellicode", "ms-azure-devops.azure-pipelines", "redhat.vscode-yaml", "esbenp.prettier-vscode" diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index 28dade82d98..1af7fc0490e 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -23,9 +23,9 @@ Please provide details about your environment. --> -**Component/platform:** +**Integration:** diff --git a/.github/ISSUE_TEMPLATE/Bug_report.md b/.github/ISSUE_TEMPLATE/Bug_report.md index 3b962f38caf..885164d7a34 100644 --- a/.github/ISSUE_TEMPLATE/Bug_report.md +++ b/.github/ISSUE_TEMPLATE/Bug_report.md @@ -29,9 +29,9 @@ about: Create a report to help us improve Please provide details about your environment. --> -**Component/platform:** +**Integration:** diff --git a/.github/stale.yml b/.github/stale.yml index a1a35e9f3b1..44cd95e1f5d 100644 --- a/.github/stale.yml +++ b/.github/stale.yml @@ -13,6 +13,7 @@ onlyLabels: [] # Issues or Pull Requests with these labels will never be considered stale. Set to `[]` to disable exemptLabels: - under investigation + - Help wanted # Set to true to ignore issues in a project (defaults to false) exemptProjects: true diff --git a/.travis.yml b/.travis.yml index 525a4c8e72c..0e9e030128e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -30,4 +30,4 @@ matrix: cache: pip install: pip install -U tox language: python -script: travis_wait 40 tox --develop +script: travis_wait 50 tox --develop diff --git a/CODEOWNERS b/CODEOWNERS index 1e45bcee4ec..d2cda1f1d07 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1,4 +1,4 @@ -# This file is generated by script/manifest/codeowners.py +# This file is generated by script/hassfest/codeowners.py # People marked here will be automatically requested for a review # when the code that they own is touched. # https://github.com/blog/2392-introducing-code-owners @@ -16,6 +16,7 @@ homeassistant/scripts/check_config.py @kellerza homeassistant/components/adguard/* @frenck homeassistant/components/airvisual/* @bachya homeassistant/components/alarm_control_panel/* @colinodell +homeassistant/components/alexa/* @home-assistant/cloud homeassistant/components/alpha_vantage/* @fabaff homeassistant/components/amazon_polly/* @robbiet480 homeassistant/components/ambiclimate/* @danielhiversen @@ -73,6 +74,7 @@ homeassistant/components/digital_ocean/* @fabaff homeassistant/components/discogs/* @thibmaek homeassistant/components/doorbird/* @oblogic7 homeassistant/components/dweet/* @fabaff +homeassistant/components/ecobee/* @marthoc homeassistant/components/ecovacs/* @OverloadUT homeassistant/components/egardia/* @jeroenterheerdt homeassistant/components/eight_sleep/* @mezz64 @@ -105,6 +107,7 @@ homeassistant/components/geonetnz_quakes/* @exxamalte homeassistant/components/gitter/* @fabaff homeassistant/components/glances/* @fabaff homeassistant/components/gntp/* @robbiet480 +homeassistant/components/google_assistant/* @home-assistant/cloud homeassistant/components/google_cloud/* @lufton homeassistant/components/google_translate/* @awarecan homeassistant/components/google_travel_time/* @robbiet480 @@ -115,6 +118,7 @@ homeassistant/components/gtfs/* @robbiet480 homeassistant/components/harmony/* @ehendrix23 homeassistant/components/hassio/* @home-assistant/hass-io homeassistant/components/heos/* @andrewsayre +homeassistant/components/here_travel_time/* @eifinger homeassistant/components/hikvision/* @mezz64 homeassistant/components/hikvisioncam/* @fbradyirl homeassistant/components/history/* @home-assistant/core @@ -144,7 +148,9 @@ homeassistant/components/ios/* @robbiet480 homeassistant/components/ipma/* @dgomes homeassistant/components/iqvia/* @bachya homeassistant/components/irish_rail_transport/* @ttroy50 +homeassistant/components/izone/* @Swamp-Ig homeassistant/components/jewish_calendar/* @tsvi +homeassistant/components/kaiterra/* @Michsior14 homeassistant/components/keba/* @dannerph homeassistant/components/knx/* @Julius2342 homeassistant/components/kodi/* @armills @@ -153,9 +159,6 @@ homeassistant/components/lametric/* @robbiet480 homeassistant/components/launch_library/* @ludeeus homeassistant/components/lcn/* @alengwenus homeassistant/components/life360/* @pnbruckner -homeassistant/components/lifx/* @amelchio -homeassistant/components/lifx_cloud/* @amelchio -homeassistant/components/lifx_legacy/* @amelchio homeassistant/components/linky/* @Quentame homeassistant/components/linux_battery/* @fabaff homeassistant/components/liveboxplaytv/* @pschmitt @@ -181,12 +184,12 @@ homeassistant/components/monoprice/* @etsinko homeassistant/components/moon/* @fabaff homeassistant/components/mpd/* @fabaff homeassistant/components/mqtt/* @home-assistant/core +homeassistant/components/mysensors/* @MartinHjelmare homeassistant/components/mystrom/* @fabaff homeassistant/components/nello/* @pschmitt homeassistant/components/ness_alarm/* @nickw444 homeassistant/components/nest/* @awarecan homeassistant/components/netdata/* @fabaff -homeassistant/components/netgear_lte/* @amelchio homeassistant/components/nextbus/* @vividboarder homeassistant/components/nissan_leaf/* @filcole homeassistant/components/nmbs/* @thibmaek @@ -197,8 +200,10 @@ homeassistant/components/nsw_fuel_station/* @nickw444 homeassistant/components/nsw_rural_fire_service_feed/* @exxamalte homeassistant/components/nuki/* @pvizeli homeassistant/components/nws/* @MatthewFlamm +homeassistant/components/nzbget/* @chriscla homeassistant/components/obihai/* @dshokouhi homeassistant/components/ohmconnect/* @robbiet480 +homeassistant/components/ombi/* @larssont homeassistant/components/onboarding/* @home-assistant/core homeassistant/components/opentherm_gw/* @mvn23 homeassistant/components/openuv/* @bachya @@ -209,7 +214,7 @@ homeassistant/components/panel_custom/* @home-assistant/frontend homeassistant/components/panel_iframe/* @home-assistant/frontend homeassistant/components/persistent_notification/* @home-assistant/core homeassistant/components/philips_js/* @elupus -homeassistant/components/pi_hole/* @fabaff +homeassistant/components/pi_hole/* @fabaff @johnluetke homeassistant/components/plaato/* @JohNan homeassistant/components/plant/* @ChristianKuehnel homeassistant/components/plex/* @jjlawren @@ -223,6 +228,7 @@ homeassistant/components/qld_bushfire/* @exxamalte homeassistant/components/qnap/* @colinodell homeassistant/components/quantum_gateway/* @cisasteelersfan homeassistant/components/qwikswitch/* @kellerza +homeassistant/components/rainbird/* @konikvranik homeassistant/components/raincloud/* @vanstinator homeassistant/components/rainforest_eagle/* @gtdiehl homeassistant/components/rainmachine/* @bachya @@ -231,6 +237,7 @@ homeassistant/components/repetier/* @MTrab homeassistant/components/rfxtrx/* @danielhiversen homeassistant/components/rmvtransport/* @cgtobi homeassistant/components/roomba/* @pschmitt +homeassistant/components/saj/* @fredericvl homeassistant/components/scene/* @home-assistant/core homeassistant/components/scrape/* @fabaff homeassistant/components/script/* @home-assistant/core @@ -248,11 +255,11 @@ homeassistant/components/smarthab/* @outadoc homeassistant/components/smartthings/* @andrewsayre homeassistant/components/smarty/* @z0mbieprocess homeassistant/components/smtp/* @fabaff -homeassistant/components/solaredge_local/* @drobtravels +homeassistant/components/solaredge_local/* @drobtravels @scheric homeassistant/components/solax/* @squishykid +homeassistant/components/soma/* @ratsept homeassistant/components/somfy/* @tetienne homeassistant/components/songpal/* @rytilahti -homeassistant/components/sonos/* @amelchio homeassistant/components/spaceapi/* @fabaff homeassistant/components/spider/* @peternijssen homeassistant/components/sql/* @dgomes @@ -270,7 +277,6 @@ homeassistant/components/switchmate/* @danielhiversen homeassistant/components/syncthru/* @nielstron homeassistant/components/synology_srm/* @aerialls homeassistant/components/syslog/* @fabaff -homeassistant/components/sytadin/* @gautric homeassistant/components/tahoma/* @philklei homeassistant/components/tautulli/* @ludeeus homeassistant/components/tellduslive/* @fredrike @@ -282,11 +288,13 @@ homeassistant/components/threshold/* @fabaff homeassistant/components/tibber/* @danielhiversen homeassistant/components/tile/* @bachya homeassistant/components/time_date/* @fabaff +homeassistant/components/todoist/* @boralyl homeassistant/components/toon/* @frenck homeassistant/components/tplink/* @rytilahti homeassistant/components/traccar/* @ludeeus homeassistant/components/tradfri/* @ggravlingen homeassistant/components/trafikverket_train/* @endor-force +homeassistant/components/transmission/* @engrbm87 homeassistant/components/tts/* @robbiet480 homeassistant/components/twentemilieu/* @frenck homeassistant/components/twilio_call/* @robbiet480 @@ -315,12 +323,14 @@ homeassistant/components/wemo/* @sqldiablo homeassistant/components/withings/* @vangorra homeassistant/components/worldclock/* @fabaff homeassistant/components/wwlln/* @bachya +homeassistant/components/xbox_live/* @MartinHjelmare homeassistant/components/xfinity/* @cisasteelersfan homeassistant/components/xiaomi_aqara/* @danielhiversen @syssi homeassistant/components/xiaomi_miio/* @rytilahti @syssi homeassistant/components/xiaomi_tv/* @simse homeassistant/components/xmpp/* @fabaff @flowolf homeassistant/components/yamaha_musiccast/* @jalmeroth +homeassistant/components/yandex_transport/* @rishatik92 homeassistant/components/yeelight/* @rytilahti @zewelor homeassistant/components/yeelightsunflower/* @lindsaymarkward homeassistant/components/yessssms/* @flowolf diff --git a/README.rst b/README.rst index 08f20778d70..ae9531456fd 100644 --- a/README.rst +++ b/README.rst @@ -32,4 +32,4 @@ of a component, check the `Home Assistant help section gcloud_auth.json + ./google-cloud-sdk/bin/gcloud auth activate-service-account --key-file gcloud_auth.json + rm -f gcloud_auth.json + displayName: 'Auth gCloud' + condition: eq(variables['homeassistantReleaseStable'], 'true') + - script: | + set -e + + export CLOUDSDK_CORE_DISABLE_PROMPTS=1 + + ./google-cloud-sdk/bin/gcloud functions deploy Analytics-Receiver \ + --project home-assistant-analytics \ + --update-env-vars VERSION=$(homeassistantRelease) \ + --source gs://analytics-src/function-source.zip + displayName: 'Push details to updater' + condition: eq(variables['homeassistantReleaseStable'], 'true') diff --git a/azure-pipelines-wheels.yml b/azure-pipelines-wheels.yml index eec3f678981..42815d8c8ae 100644 --- a/azure-pipelines-wheels.yml +++ b/azure-pipelines-wheels.yml @@ -45,7 +45,6 @@ jobs: requirement_files="requirements_wheels.txt requirements_diff.txt" for requirement_file in ${requirement_files}; do - sed -i "s|# pytradfri|pytradfri|g" ${requirement_file} sed -i "s|# pybluez|pybluez|g" ${requirement_file} sed -i "s|# bluepy|bluepy|g" ${requirement_file} sed -i "s|# beacontools|beacontools|g" ${requirement_file} @@ -63,9 +62,15 @@ jobs: sed -i "s|# homekit|homekit|g" ${requirement_file} sed -i "s|# decora_wifi|decora_wifi|g" ${requirement_file} sed -i "s|# decora|decora|g" ${requirement_file} + sed -i "s|# avion|avion|g" ${requirement_file} sed -i "s|# PySwitchbot|PySwitchbot|g" ${requirement_file} sed -i "s|# pySwitchmate|pySwitchmate|g" ${requirement_file} sed -i "s|# face_recognition|face_recognition|g" ${requirement_file} sed -i "s|# py_noaa|py_noaa|g" ${requirement_file} + sed -i "s|# bme680|bme680|g" ${requirement_file} + + if [[ "$(buildArch)" =~ arm ]]; then + sed -i "s|# VL53L1X|VL53L1X|g" ${requirement_file} + fi done displayName: 'Prepare requirements files for Hass.io' diff --git a/homeassistant/__main__.py b/homeassistant/__main__.py index f7e24d69884..b416b1f98d3 100644 --- a/homeassistant/__main__.py +++ b/homeassistant/__main__.py @@ -1,6 +1,4 @@ """Start Home Assistant.""" -from __future__ import print_function - import argparse import os import platform diff --git a/homeassistant/auth/mfa_modules/notify.py b/homeassistant/auth/mfa_modules/notify.py index a6a754fc2a6..b14f5fedc22 100644 --- a/homeassistant/auth/mfa_modules/notify.py +++ b/homeassistant/auth/mfa_modules/notify.py @@ -22,7 +22,7 @@ from . import ( SetupFlow, ) -REQUIREMENTS = ["pyotp==2.2.7"] +REQUIREMENTS = ["pyotp==2.3.0"] CONF_MESSAGE = "message" @@ -251,8 +251,10 @@ class NotifyAuthModule(MultiFactorAuthModule): _LOGGER.error("Cannot find user %s", user_id) return - await self.async_notify( # type: ignore - code, notify_setting.notify_service, notify_setting.target + await self.async_notify( + code, + notify_setting.notify_service, # type: ignore + notify_setting.target, ) async def async_notify( diff --git a/homeassistant/auth/mfa_modules/totp.py b/homeassistant/auth/mfa_modules/totp.py index d6d901ac3b1..9829044a53e 100644 --- a/homeassistant/auth/mfa_modules/totp.py +++ b/homeassistant/auth/mfa_modules/totp.py @@ -16,7 +16,7 @@ from . import ( SetupFlow, ) -REQUIREMENTS = ["pyotp==2.2.7", "PyQRCode==1.2.1"] +REQUIREMENTS = ["pyotp==2.3.0", "PyQRCode==1.2.1"] CONFIG_SCHEMA = MULTI_FACTOR_AUTH_MODULE_SCHEMA.extend({}, extra=vol.PREVENT_EXTRA) @@ -215,8 +215,9 @@ class TotpSetupFlow(SetupFlow): else: hass = self._auth_module.hass - self._ota_secret, self._url, self._image = await hass.async_add_executor_job( # type: ignore - _generate_secret_and_qr_code, str(self._user.name) + self._ota_secret, self._url, self._image = await hass.async_add_executor_job( + _generate_secret_and_qr_code, # type: ignore + str(self._user.name), ) return self.async_show_form( diff --git a/homeassistant/components/.translations/airly.ca.json b/homeassistant/components/.translations/airly.ca.json new file mode 100644 index 00000000000..bf50b4f23e5 --- /dev/null +++ b/homeassistant/components/.translations/airly.ca.json @@ -0,0 +1,22 @@ +{ + "config": { + "error": { + "auth": "La clau API no \u00e9s correcta.", + "name_exists": "El nom ja existeix.", + "wrong_location": "No hi ha estacions de mesura Airly en aquesta zona." + }, + "step": { + "user": { + "data": { + "api_key": "Clau API d'Airly", + "latitude": "Latitud", + "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", + "title": "Airly" + } + }, + "title": "Airly" + } +} \ No newline at end of file diff --git a/homeassistant/components/.translations/airly.da.json b/homeassistant/components/.translations/airly.da.json new file mode 100644 index 00000000000..652cc46a7b3 --- /dev/null +++ b/homeassistant/components/.translations/airly.da.json @@ -0,0 +1,22 @@ +{ + "config": { + "error": { + "auth": "API-n\u00f8glen er ikke korrekt.", + "name_exists": "Navnet findes allerede.", + "wrong_location": "Ingen Airly m\u00e5lestationer i dette omr\u00e5de." + }, + "step": { + "user": { + "data": { + "api_key": "Airly API-n\u00f8gle", + "latitude": "Breddegrad", + "longitude": "L\u00e6ngdegrad", + "name": "Integrationens navn" + }, + "description": "Konfigurer Airly luftkvalitet integration. For at generere API-n\u00f8gle, g\u00e5 til https://developer.airly.eu/register", + "title": "Airly" + } + }, + "title": "Airly" + } +} \ No newline at end of file diff --git a/homeassistant/components/.translations/airly.de.json b/homeassistant/components/.translations/airly.de.json new file mode 100644 index 00000000000..cb290dc46c0 --- /dev/null +++ b/homeassistant/components/.translations/airly.de.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "name_exists": "Name existiert bereits" + }, + "step": { + "user": { + "data": { + "latitude": "Breitengrad", + "longitude": "L\u00e4ngengrad", + "name": "Name der Integration" + }, + "title": "Airly" + } + }, + "title": "Airly" + } +} \ No newline at end of file diff --git a/homeassistant/components/.translations/airly.en.json b/homeassistant/components/.translations/airly.en.json new file mode 100644 index 00000000000..83284aaeb7b --- /dev/null +++ b/homeassistant/components/.translations/airly.en.json @@ -0,0 +1,22 @@ +{ + "config": { + "error": { + "auth": "API key is not correct.", + "name_exists": "Name already exists.", + "wrong_location": "No Airly measuring stations in this area." + }, + "step": { + "user": { + "data": { + "api_key": "Airly API key", + "latitude": "Latitude", + "longitude": "Longitude", + "name": "Name of the integration" + }, + "description": "Set up Airly air quality integration. To generate API key go to https://developer.airly.eu/register", + "title": "Airly" + } + }, + "title": "Airly" + } +} \ No newline at end of file diff --git a/homeassistant/components/.translations/airly.es.json b/homeassistant/components/.translations/airly.es.json new file mode 100644 index 00000000000..0c29ad0bc66 --- /dev/null +++ b/homeassistant/components/.translations/airly.es.json @@ -0,0 +1,22 @@ +{ + "config": { + "error": { + "auth": "La clave de la API no es correcta.", + "name_exists": "El nombre ya existe.", + "wrong_location": "No hay estaciones de medici\u00f3n Airly en esta zona." + }, + "step": { + "user": { + "data": { + "api_key": "Clave API de Airly", + "latitude": "Latitud", + "longitude": "Longitud", + "name": "Nombre de la integraci\u00f3n" + }, + "description": "Establecer la integraci\u00f3n de la calidad del aire de Airly. Para generar la clave de la API vaya a https://developer.airly.eu/register", + "title": "Airly" + } + }, + "title": "Airly" + } +} \ No newline at end of file diff --git a/homeassistant/components/.translations/airly.fr.json b/homeassistant/components/.translations/airly.fr.json new file mode 100644 index 00000000000..cf756a9f492 --- /dev/null +++ b/homeassistant/components/.translations/airly.fr.json @@ -0,0 +1,21 @@ +{ + "config": { + "error": { + "auth": "La cl\u00e9 API n'est pas correcte.", + "name_exists": "Le nom existe d\u00e9j\u00e0.", + "wrong_location": "Aucune station de mesure Airly dans cette zone." + }, + "step": { + "user": { + "data": { + "api_key": "Cl\u00e9 API Airly", + "latitude": "Latitude", + "longitude": "Longitude", + "name": "Nom de l'int\u00e9gration" + }, + "title": "Airly" + } + }, + "title": "Airly" + } +} \ No newline at end of file diff --git a/homeassistant/components/.translations/airly.it.json b/homeassistant/components/.translations/airly.it.json new file mode 100644 index 00000000000..e50f618575b --- /dev/null +++ b/homeassistant/components/.translations/airly.it.json @@ -0,0 +1,22 @@ +{ + "config": { + "error": { + "auth": "La chiave API non \u00e8 corretta.", + "name_exists": "Il nome \u00e8 gi\u00e0 esistente", + "wrong_location": "Nessuna stazione di misurazione Airly in quest'area." + }, + "step": { + "user": { + "data": { + "api_key": "Chiave API Airly", + "latitude": "Latitudine", + "longitude": "Logitudine", + "name": "Nome dell'integrazione" + }, + "description": "Configurazione dell'integrazione della qualit\u00e0 dell'aria Airly. Per generare la chiave API andare su https://developer.airly.eu/register", + "title": "Airly" + } + }, + "title": "Airly" + } +} \ No newline at end of file diff --git a/homeassistant/components/.translations/airly.lb.json b/homeassistant/components/.translations/airly.lb.json new file mode 100644 index 00000000000..08aac57d162 --- /dev/null +++ b/homeassistant/components/.translations/airly.lb.json @@ -0,0 +1,22 @@ +{ + "config": { + "error": { + "auth": "Api Schl\u00ebssel ass net korrekt.", + "name_exists": "Numm g\u00ebtt et schonn", + "wrong_location": "Keng Airly Moos Statioun an d\u00ebsem Ber\u00e4ich" + }, + "step": { + "user": { + "data": { + "api_key": "Airly API Schl\u00ebssel", + "latitude": "Breedegrad", + "longitude": "L\u00e4ngegrad", + "name": "Numm vun der Installatioun" + }, + "description": "Airly Loft Qualit\u00e9it Integratioun ariichten. Fir een API Schl\u00ebssel z'erstelle gitt op https://developer.airly.eu/register", + "title": "Airly" + } + }, + "title": "Airly" + } +} \ No newline at end of file diff --git a/homeassistant/components/.translations/airly.nn.json b/homeassistant/components/.translations/airly.nn.json new file mode 100644 index 00000000000..7e2f4f1ff6b --- /dev/null +++ b/homeassistant/components/.translations/airly.nn.json @@ -0,0 +1,10 @@ +{ + "config": { + "step": { + "user": { + "title": "Airly" + } + }, + "title": "Airly" + } +} \ No newline at end of file diff --git a/homeassistant/components/.translations/airly.no.json b/homeassistant/components/.translations/airly.no.json new file mode 100644 index 00000000000..70924bb7bf4 --- /dev/null +++ b/homeassistant/components/.translations/airly.no.json @@ -0,0 +1,22 @@ +{ + "config": { + "error": { + "auth": "API-n\u00f8kkelen er ikke korrekt.", + "name_exists": "Navnet finnes allerede.", + "wrong_location": "Ingen Airly m\u00e5lestasjoner i dette omr\u00e5det." + }, + "step": { + "user": { + "data": { + "api_key": "Airly API-n\u00f8kkel", + "latitude": "Breddegrad", + "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", + "title": "Airly" + } + }, + "title": "Airly" + } +} \ No newline at end of file diff --git a/homeassistant/components/.translations/airly.pl.json b/homeassistant/components/.translations/airly.pl.json new file mode 100644 index 00000000000..5d601b37591 --- /dev/null +++ b/homeassistant/components/.translations/airly.pl.json @@ -0,0 +1,22 @@ +{ + "config": { + "error": { + "auth": "Klucz API jest nieprawid\u0142owy.", + "name_exists": "Nazwa ju\u017c istnieje.", + "wrong_location": "Brak stacji pomiarowych Airly w tym rejonie." + }, + "step": { + "user": { + "data": { + "api_key": "Klucz API Airly", + "latitude": "Szeroko\u015b\u0107 geograficzna", + "longitude": "D\u0142ugo\u015b\u0107 geograficzna", + "name": "Nazwa integracji" + }, + "description": "Konfiguracja integracji Airly. By wygenerowa\u0107 klucz API, przejd\u017a na stron\u0119 https://developer.airly.eu/register", + "title": "Airly" + } + }, + "title": "Airly" + } +} \ No newline at end of file diff --git a/homeassistant/components/.translations/airly.ru.json b/homeassistant/components/.translations/airly.ru.json new file mode 100644 index 00000000000..36080c9f372 --- /dev/null +++ b/homeassistant/components/.translations/airly.ru.json @@ -0,0 +1,22 @@ +{ + "config": { + "error": { + "auth": "\u041d\u0435\u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u044b\u0439 \u043a\u043b\u044e\u0447 API.", + "name_exists": "\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.", + "wrong_location": "\u0412 \u044d\u0442\u043e\u0439 \u043e\u0431\u043b\u0430\u0441\u0442\u0438 \u043d\u0435\u0442 \u0438\u0437\u043c\u0435\u0440\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0445 \u0441\u0442\u0430\u043d\u0446\u0438\u0439 Airly." + }, + "step": { + "user": { + "data": { + "api_key": "\u041a\u043b\u044e\u0447 API", + "latitude": "\u0428\u0438\u0440\u043e\u0442\u0430", + "longitude": "\u0414\u043e\u043b\u0433\u043e\u0442\u0430", + "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435" + }, + "description": "\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \u0441\u0435\u0440\u0432\u0438\u0441\u0430 \u043f\u043e \u0430\u043d\u0430\u043b\u0438\u0437\u0443 \u043a\u0430\u0447\u0435\u0441\u0442\u0432\u0430 \u0432\u043e\u0437\u0434\u0443\u0445\u0430 Airly. \u0427\u0442\u043e\u0431\u044b \u0441\u043e\u0437\u0434\u0430\u0442\u044c \u043a\u043b\u044e\u0447 API, \u043f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043f\u043e \u0441\u0441\u044b\u043b\u043a\u0435 https://developer.airly.eu/register.", + "title": "Airly" + } + }, + "title": "Airly" + } +} \ No newline at end of file diff --git a/homeassistant/components/.translations/airly.sl.json b/homeassistant/components/.translations/airly.sl.json new file mode 100644 index 00000000000..08f57d88bcb --- /dev/null +++ b/homeassistant/components/.translations/airly.sl.json @@ -0,0 +1,22 @@ +{ + "config": { + "error": { + "auth": "Klju\u010d API ni pravilen.", + "name_exists": "Ime \u017ee obstaja", + "wrong_location": "Na tem obmo\u010dju ni merilnih postaj Airly." + }, + "step": { + "user": { + "data": { + "api_key": "Airly API klju\u010d", + "latitude": "Zemljepisna \u0161irina", + "longitude": "Zemljepisna dol\u017eina", + "name": "Ime integracije" + }, + "description": "Nastavite Airly integracijo za kakovost zraka. \u010ce \u017eelite ustvariti API klju\u010d pojdite na https://developer.airly.eu/register", + "title": "Airly" + } + }, + "title": "Airly" + } +} \ No newline at end of file diff --git a/homeassistant/components/.translations/airly.zh-Hant.json b/homeassistant/components/.translations/airly.zh-Hant.json new file mode 100644 index 00000000000..bb38d2b9b8c --- /dev/null +++ b/homeassistant/components/.translations/airly.zh-Hant.json @@ -0,0 +1,22 @@ +{ + "config": { + "error": { + "auth": "API \u5bc6\u9470\u4e0d\u6b63\u78ba\u3002", + "name_exists": "\u8a72\u540d\u7a31\u5df2\u5b58\u5728", + "wrong_location": "\u8a72\u5340\u57df\u6c92\u6709 Arily \u76e3\u6e2c\u7ad9\u3002" + }, + "step": { + "user": { + "data": { + "api_key": "Airly API \u5bc6\u9470", + "latitude": "\u7def\u5ea6", + "longitude": "\u7d93\u5ea6", + "name": "\u6574\u5408\u540d\u7a31" + }, + "description": "\u6b32\u8a2d\u5b9a Airly \u7a7a\u6c23\u54c1\u8cea\u6574\u5408\u3002\u8acb\u81f3 https://developer.airly.eu/register \u7522\u751f API \u5bc6\u9470", + "title": "Airly" + } + }, + "title": "Airly" + } +} \ No newline at end of file diff --git a/homeassistant/components/abode/manifest.json b/homeassistant/components/abode/manifest.json index 49e0c46fd55..793c19cc466 100644 --- a/homeassistant/components/abode/manifest.json +++ b/homeassistant/components/abode/manifest.json @@ -1,7 +1,7 @@ { "domain": "abode", "name": "Abode", - "documentation": "https://www.home-assistant.io/components/abode", + "documentation": "https://www.home-assistant.io/integrations/abode", "requirements": [ "abodepy==0.15.0" ], diff --git a/homeassistant/components/acer_projector/manifest.json b/homeassistant/components/acer_projector/manifest.json index 4b8d6967491..9f712ba5c7e 100644 --- a/homeassistant/components/acer_projector/manifest.json +++ b/homeassistant/components/acer_projector/manifest.json @@ -1,7 +1,7 @@ { "domain": "acer_projector", "name": "Acer projector", - "documentation": "https://www.home-assistant.io/components/acer_projector", + "documentation": "https://www.home-assistant.io/integrations/acer_projector", "requirements": [ "pyserial==3.1.1" ], diff --git a/homeassistant/components/actiontec/manifest.json b/homeassistant/components/actiontec/manifest.json index e233f430cfc..ddb4954794c 100644 --- a/homeassistant/components/actiontec/manifest.json +++ b/homeassistant/components/actiontec/manifest.json @@ -1,7 +1,7 @@ { "domain": "actiontec", "name": "Actiontec", - "documentation": "https://www.home-assistant.io/components/actiontec", + "documentation": "https://www.home-assistant.io/integrations/actiontec", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/adguard/.translations/da.json b/homeassistant/components/adguard/.translations/da.json index 0f854db0be6..813405cec62 100644 --- a/homeassistant/components/adguard/.translations/da.json +++ b/homeassistant/components/adguard/.translations/da.json @@ -9,7 +9,7 @@ }, "step": { "hassio_confirm": { - "description": "Vil du konfigurere Home Assistant til at oprette forbindelse til Adguard Home, der leveres af Hass.io add-on: {addon}?", + "description": "Vil du konfigurere Home Assistant til at oprette forbindelse til AdGuard Home, der leveres af Hass.io add-on: {addon}?", "title": "AdGuard Home via Hass.io add-on" }, "user": { diff --git a/homeassistant/components/adguard/.translations/hu.json b/homeassistant/components/adguard/.translations/hu.json new file mode 100644 index 00000000000..34b601027c2 --- /dev/null +++ b/homeassistant/components/adguard/.translations/hu.json @@ -0,0 +1,13 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "Jelsz\u00f3", + "port": "Port", + "username": "Felhaszn\u00e1l\u00f3n\u00e9v" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/adguard/.translations/nn.json b/homeassistant/components/adguard/.translations/nn.json new file mode 100644 index 00000000000..7c129cba3af --- /dev/null +++ b/homeassistant/components/adguard/.translations/nn.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "username": "Brukarnamn" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/adguard/.translations/no.json b/homeassistant/components/adguard/.translations/no.json index 94535d7e945..2cd6cd72f6d 100644 --- a/homeassistant/components/adguard/.translations/no.json +++ b/homeassistant/components/adguard/.translations/no.json @@ -2,7 +2,7 @@ "config": { "abort": { "existing_instance_updated": "Oppdatert eksisterende konfigurasjon.", - "single_instance_allowed": "Kun \u00e9n enkelt konfigurasjon av AdGuard Hjemer tillatt." + "single_instance_allowed": "Kun en konfigurasjon av AdGuard Hjemer tillatt." }, "error": { "connection_error": "Tilkobling mislyktes." diff --git a/homeassistant/components/adguard/.translations/pl.json b/homeassistant/components/adguard/.translations/pl.json index e58c901f364..f8f64d54260 100644 --- a/homeassistant/components/adguard/.translations/pl.json +++ b/homeassistant/components/adguard/.translations/pl.json @@ -22,7 +22,7 @@ "verify_ssl": "AdGuard Home u\u017cywa odpowiedniego certyfikatu." }, "description": "Skonfiguruj instancj\u0119 AdGuard Home, aby umo\u017cliwi\u0107 monitorowanie i kontrol\u0119.", - "title": "Po\u0142\u0105cz sw\u00f3j AdGuard Home" + "title": "Po\u0142\u0105cz AdGuard Home" } }, "title": "AdGuard Home" diff --git a/homeassistant/components/adguard/.translations/sl.json b/homeassistant/components/adguard/.translations/sl.json index 5c8d75d4cc8..f1ca796363d 100644 --- a/homeassistant/components/adguard/.translations/sl.json +++ b/homeassistant/components/adguard/.translations/sl.json @@ -9,7 +9,7 @@ }, "step": { "hassio_confirm": { - "description": "\u017delite konfigurirati Home Assistant-a za povezavo z AdGuard Home, ki ga ponuja hass.io add-on {addon} ?", + "description": "\u017delite konfigurirati Home Assistant-a za povezavo z AdGuard Home, ki ga ponuja Hass.io add-on {addon} ?", "title": "AdGuard Home preko dodatka Hass.io" }, "user": { diff --git a/homeassistant/components/adguard/manifest.json b/homeassistant/components/adguard/manifest.json index 0063f1ec370..f207e6dff09 100644 --- a/homeassistant/components/adguard/manifest.json +++ b/homeassistant/components/adguard/manifest.json @@ -2,7 +2,7 @@ "domain": "adguard", "name": "AdGuard Home", "config_flow": true, - "documentation": "https://www.home-assistant.io/components/adguard", + "documentation": "https://www.home-assistant.io/integrations/adguard", "requirements": [ "adguardhome==0.2.1" ], diff --git a/homeassistant/components/ads/manifest.json b/homeassistant/components/ads/manifest.json index 0c759f0ad60..cf3e621fd07 100644 --- a/homeassistant/components/ads/manifest.json +++ b/homeassistant/components/ads/manifest.json @@ -1,7 +1,7 @@ { "domain": "ads", "name": "Ads", - "documentation": "https://www.home-assistant.io/components/ads", + "documentation": "https://www.home-assistant.io/integrations/ads", "requirements": [ "pyads==3.0.7" ], diff --git a/homeassistant/components/aftership/manifest.json b/homeassistant/components/aftership/manifest.json index b9ee8939dc4..a7e8aeb8deb 100644 --- a/homeassistant/components/aftership/manifest.json +++ b/homeassistant/components/aftership/manifest.json @@ -1,7 +1,7 @@ { "domain": "aftership", "name": "Aftership", - "documentation": "https://www.home-assistant.io/components/aftership", + "documentation": "https://www.home-assistant.io/integrations/aftership", "requirements": [ "pyaftership==0.1.2" ], diff --git a/homeassistant/components/air_quality/manifest.json b/homeassistant/components/air_quality/manifest.json index 5bfe85547ff..17fa14a9cfa 100644 --- a/homeassistant/components/air_quality/manifest.json +++ b/homeassistant/components/air_quality/manifest.json @@ -1,7 +1,7 @@ { "domain": "air_quality", "name": "Air quality", - "documentation": "https://www.home-assistant.io/components/air_quality", + "documentation": "https://www.home-assistant.io/integrations/air_quality", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/airvisual/manifest.json b/homeassistant/components/airvisual/manifest.json index ddb109a99b0..e7ea23a43a1 100644 --- a/homeassistant/components/airvisual/manifest.json +++ b/homeassistant/components/airvisual/manifest.json @@ -1,7 +1,7 @@ { "domain": "airvisual", "name": "Airvisual", - "documentation": "https://www.home-assistant.io/components/airvisual", + "documentation": "https://www.home-assistant.io/integrations/airvisual", "requirements": [ "pyairvisual==3.0.1" ], diff --git a/homeassistant/components/aladdin_connect/manifest.json b/homeassistant/components/aladdin_connect/manifest.json index 0681d5df38b..7d440407427 100644 --- a/homeassistant/components/aladdin_connect/manifest.json +++ b/homeassistant/components/aladdin_connect/manifest.json @@ -1,7 +1,7 @@ { "domain": "aladdin_connect", "name": "Aladdin connect", - "documentation": "https://www.home-assistant.io/components/aladdin_connect", + "documentation": "https://www.home-assistant.io/integrations/aladdin_connect", "requirements": [ "aladdin_connect==0.3" ], diff --git a/homeassistant/components/alarm_control_panel/manifest.json b/homeassistant/components/alarm_control_panel/manifest.json index 95e26de53bc..04ef58769da 100644 --- a/homeassistant/components/alarm_control_panel/manifest.json +++ b/homeassistant/components/alarm_control_panel/manifest.json @@ -1,7 +1,7 @@ { "domain": "alarm_control_panel", "name": "Alarm control panel", - "documentation": "https://www.home-assistant.io/components/alarm_control_panel", + "documentation": "https://www.home-assistant.io/integrations/alarm_control_panel", "requirements": [], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/alarmdecoder/manifest.json b/homeassistant/components/alarmdecoder/manifest.json index 3e0d4112d27..5ab69d94cf2 100644 --- a/homeassistant/components/alarmdecoder/manifest.json +++ b/homeassistant/components/alarmdecoder/manifest.json @@ -1,7 +1,7 @@ { "domain": "alarmdecoder", "name": "Alarmdecoder", - "documentation": "https://www.home-assistant.io/components/alarmdecoder", + "documentation": "https://www.home-assistant.io/integrations/alarmdecoder", "requirements": [ "alarmdecoder==1.13.2" ], diff --git a/homeassistant/components/alarmdotcom/manifest.json b/homeassistant/components/alarmdotcom/manifest.json index 9d2c0a2056e..fd5c3010ab6 100644 --- a/homeassistant/components/alarmdotcom/manifest.json +++ b/homeassistant/components/alarmdotcom/manifest.json @@ -1,7 +1,7 @@ { "domain": "alarmdotcom", "name": "Alarmdotcom", - "documentation": "https://www.home-assistant.io/components/alarmdotcom", + "documentation": "https://www.home-assistant.io/integrations/alarmdotcom", "requirements": [ "pyalarmdotcom==0.3.2" ], diff --git a/homeassistant/components/alert/manifest.json b/homeassistant/components/alert/manifest.json index 2e27beb48e6..06269390753 100644 --- a/homeassistant/components/alert/manifest.json +++ b/homeassistant/components/alert/manifest.json @@ -1,7 +1,7 @@ { "domain": "alert", "name": "Alert", - "documentation": "https://www.home-assistant.io/components/alert", + "documentation": "https://www.home-assistant.io/integrations/alert", "requirements": [], "dependencies": [], "after_dependencies": [ diff --git a/homeassistant/components/alexa/capabilities.py b/homeassistant/components/alexa/capabilities.py index d769f797da1..b8bd3841a78 100644 --- a/homeassistant/components/alexa/capabilities.py +++ b/homeassistant/components/alexa/capabilities.py @@ -1,5 +1,4 @@ """Alexa capabilities.""" -from datetime import datetime import logging from homeassistant.const import ( @@ -16,6 +15,7 @@ from homeassistant.const import ( import homeassistant.components.climate.const as climate from homeassistant.components import light, fan, cover import homeassistant.util.color as color_util +import homeassistant.util.dt as dt_util from .const import ( API_TEMP_UNITS, @@ -109,7 +109,7 @@ class AlexaCapibility: "name": prop_name, "namespace": self.name(), "value": prop_value, - "timeOfSample": datetime.now().strftime(DATE_FORMAT), + "timeOfSample": dt_util.utcnow().strftime(DATE_FORMAT), "uncertaintyInMilliseconds": 0, } @@ -326,7 +326,7 @@ class AlexaColorTemperatureController(AlexaCapibility): return color_util.color_temperature_mired_to_kelvin( self.entity.attributes["color_temp"] ) - return 0 + return None class AlexaPercentageController(AlexaCapibility): @@ -445,7 +445,7 @@ class AlexaTemperatureSensor(AlexaCapibility): unit = self.hass.config.units.temperature_unit temp = self.entity.attributes.get(climate.ATTR_CURRENT_TEMPERATURE) - if temp in (STATE_UNAVAILABLE, STATE_UNKNOWN): + if temp in (STATE_UNAVAILABLE, STATE_UNKNOWN, None): return None try: @@ -572,6 +572,9 @@ class AlexaThermostatController(AlexaCapibility): def get_property(self, name): """Read and return a property.""" + if self.entity.state == STATE_UNAVAILABLE: + return None + if name == "thermostatMode": preset = self.entity.attributes.get(climate.ATTR_PRESET_MODE) diff --git a/homeassistant/components/alexa/entities.py b/homeassistant/components/alexa/entities.py index 03d153f5927..55b5878f667 100644 --- a/homeassistant/components/alexa/entities.py +++ b/homeassistant/components/alexa/entities.py @@ -52,6 +52,8 @@ from .capabilities import ( ENTITY_ADAPTERS = Registry() +TRANSLATION_TABLE = dict.fromkeys(map(ord, r"}{\/|\"()[]+~!><*%"), None) + class DisplayCategory: """Possible display categories for Discovery response. @@ -74,9 +76,18 @@ class DisplayCategory: # Indicates a door. DOOR = "DOOR" + # Indicates a doorbell. + DOOR_BELL = "DOORBELL" + + # Indicates a fan. + FAN = "FAN" + # Indicates light sources or fixtures. LIGHT = "LIGHT" + # Indicates a microwave oven. + MICROWAVE = "MICROWAVE" + # Indicates an endpoint that detects and reports motion. MOTION_SENSOR = "MOTION_SENSOR" @@ -89,6 +100,9 @@ class DisplayCategory: # order is unimportant. Applies to Scenes SCENE_TRIGGER = "SCENE_TRIGGER" + # Indicates a security panel. + SECURITY_PANEL = "SECURITY_PANEL" + # Indicates an endpoint that locks. SMARTLOCK = "SMARTLOCK" @@ -134,15 +148,18 @@ class AlexaEntity: def friendly_name(self): """Return the Alexa API friendly name.""" - return self.entity_conf.get(CONF_NAME, self.entity.name) + return self.entity_conf.get(CONF_NAME, self.entity.name).translate( + TRANSLATION_TABLE + ) def description(self): """Return the Alexa API description.""" - return self.entity_conf.get(CONF_DESCRIPTION, self.entity.entity_id) + description = self.entity_conf.get(CONF_DESCRIPTION) or self.entity_id + return f"{description} via Home Assistant".translate(TRANSLATION_TABLE) def alexa_id(self): """Return the Alexa API entity id.""" - return self.entity.entity_id.replace(".", "#") + return self.entity.entity_id.replace(".", "#").translate(TRANSLATION_TABLE) def display_categories(self): """Return a list of display categories.""" @@ -319,7 +336,7 @@ class FanCapabilities(AlexaEntity): def default_display_categories(self): """Return the display categories for this entity.""" - return [DisplayCategory.OTHER] + return [DisplayCategory.FAN] def interfaces(self): """Yield the supported interfaces.""" @@ -389,10 +406,11 @@ class SceneCapabilities(AlexaEntity): """Class to represent Scene capabilities.""" def description(self): - """Return the description of the entity.""" - # Required description as per Amazon Scene docs - scene_fmt = "{} (Scene connected via Home Assistant)" - return scene_fmt.format(AlexaEntity.description(self)) + """Return the Alexa API description.""" + description = AlexaEntity.description(self) + if "scene" not in description.casefold(): + return f"{description} (Scene)" + return description def default_display_categories(self): """Return the display categories for this entity.""" diff --git a/homeassistant/components/alexa/flash_briefings.py b/homeassistant/components/alexa/flash_briefings.py index 708d1592e4c..0b5c1243764 100644 --- a/homeassistant/components/alexa/flash_briefings.py +++ b/homeassistant/components/alexa/flash_briefings.py @@ -1,9 +1,9 @@ """Support for Alexa skill service end point.""" import copy -from datetime import datetime import logging import uuid +import homeassistant.util.dt as dt_util from homeassistant.components import http from homeassistant.core import callback from homeassistant.helpers import template @@ -89,7 +89,7 @@ class AlexaFlashBriefingView(http.HomeAssistantView): else: output[ATTR_REDIRECTION_URL] = item.get(CONF_DISPLAY_URL) - output[ATTR_UPDATE_DATE] = datetime.now().strftime(DATE_FORMAT) + output[ATTR_UPDATE_DATE] = dt_util.utcnow().strftime(DATE_FORMAT) briefing.append(output) diff --git a/homeassistant/components/alexa/handlers.py b/homeassistant/components/alexa/handlers.py index 1e636b96ee5..c72101460c4 100644 --- a/homeassistant/components/alexa/handlers.py +++ b/homeassistant/components/alexa/handlers.py @@ -1,5 +1,4 @@ """Alexa message handlers.""" -from datetime import datetime import logging import math @@ -28,6 +27,7 @@ from homeassistant.const import ( TEMP_FAHRENHEIT, ) import homeassistant.util.color as color_util +import homeassistant.util.dt as dt_util from homeassistant.util.decorator import Registry from homeassistant.util.temperature import convert as convert_temperature @@ -275,7 +275,7 @@ async def async_api_activate(hass, config, directive, context): payload = { "cause": {"type": Cause.VOICE_INTERACTION}, - "timestamp": "%sZ" % (datetime.utcnow().isoformat(),), + "timestamp": f"{dt_util.utcnow().replace(tzinfo=None).isoformat()}Z", } return directive.response( @@ -299,7 +299,7 @@ async def async_api_deactivate(hass, config, directive, context): payload = { "cause": {"type": Cause.VOICE_INTERACTION}, - "timestamp": "%sZ" % (datetime.utcnow().isoformat(),), + "timestamp": f"{dt_util.utcnow().replace(tzinfo=None).isoformat()}Z", } return directive.response( diff --git a/homeassistant/components/alexa/manifest.json b/homeassistant/components/alexa/manifest.json index e4fc9eb8680..9db7e270e61 100644 --- a/homeassistant/components/alexa/manifest.json +++ b/homeassistant/components/alexa/manifest.json @@ -1,10 +1,8 @@ { "domain": "alexa", "name": "Alexa", - "documentation": "https://www.home-assistant.io/components/alexa", + "documentation": "https://www.home-assistant.io/integrations/alexa", "requirements": [], - "dependencies": [ - "http" - ], - "codeowners": [] + "dependencies": ["http"], + "codeowners": ["@home-assistant/cloud"] } diff --git a/homeassistant/components/alexa/state_report.py b/homeassistant/components/alexa/state_report.py index b7ff9d17fe8..42c16919a45 100644 --- a/homeassistant/components/alexa/state_report.py +++ b/homeassistant/components/alexa/state_report.py @@ -131,6 +131,10 @@ async def async_send_add_or_update_message(hass, config, entity_ids): for entity_id in entity_ids: domain = entity_id.split(".", 1)[0] + + if domain not in ENTITY_ADAPTERS: + continue + alexa_entity = ENTITY_ADAPTERS[domain](hass, config, hass.states.get(entity_id)) endpoints.append(alexa_entity.serialize_discovery()) @@ -161,6 +165,10 @@ async def async_send_delete_message(hass, config, entity_ids): for entity_id in entity_ids: domain = entity_id.split(".", 1)[0] + + if domain not in ENTITY_ADAPTERS: + continue + alexa_entity = ENTITY_ADAPTERS[domain](hass, config, hass.states.get(entity_id)) endpoints.append({"endpointId": alexa_entity.alexa_id()}) diff --git a/homeassistant/components/alpha_vantage/manifest.json b/homeassistant/components/alpha_vantage/manifest.json index dacc428ea2e..9ac8d1ea1e0 100644 --- a/homeassistant/components/alpha_vantage/manifest.json +++ b/homeassistant/components/alpha_vantage/manifest.json @@ -1,7 +1,7 @@ { "domain": "alpha_vantage", "name": "Alpha vantage", - "documentation": "https://www.home-assistant.io/components/alpha_vantage", + "documentation": "https://www.home-assistant.io/integrations/alpha_vantage", "requirements": [ "alpha_vantage==2.1.0" ], diff --git a/homeassistant/components/amazon_polly/manifest.json b/homeassistant/components/amazon_polly/manifest.json index 19140aac939..45e382647f8 100644 --- a/homeassistant/components/amazon_polly/manifest.json +++ b/homeassistant/components/amazon_polly/manifest.json @@ -1,9 +1,9 @@ { "domain": "amazon_polly", "name": "Amazon polly", - "documentation": "https://www.home-assistant.io/components/amazon_polly", + "documentation": "https://www.home-assistant.io/integrations/amazon_polly", "requirements": [ - "boto3==1.9.16" + "boto3==1.9.233" ], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/amazon_polly/tts.py b/homeassistant/components/amazon_polly/tts.py index c7098867ee8..64b8b71457c 100644 --- a/homeassistant/components/amazon_polly/tts.py +++ b/homeassistant/components/amazon_polly/tts.py @@ -33,6 +33,7 @@ SUPPORTED_REGIONS = [ "sa-east-1", ] +CONF_ENGINE = "engine" CONF_VOICE = "voice" CONF_OUTPUT_FORMAT = "output_format" CONF_SAMPLE_RATE = "sample_rate" @@ -101,10 +102,12 @@ SUPPORTED_VOICES = [ SUPPORTED_OUTPUT_FORMATS = ["mp3", "ogg_vorbis", "pcm"] -SUPPORTED_SAMPLE_RATES = ["8000", "16000", "22050"] +SUPPORTED_ENGINES = ["neural", "standard"] + +SUPPORTED_SAMPLE_RATES = ["8000", "16000", "22050", "24000"] SUPPORTED_SAMPLE_RATES_MAP = { - "mp3": ["8000", "16000", "22050"], + "mp3": ["8000", "16000", "22050", "24000"], "ogg_vorbis": ["8000", "16000", "22050"], "pcm": ["8000", "16000"], } @@ -113,6 +116,7 @@ SUPPORTED_TEXT_TYPES = ["text", "ssml"] CONTENT_TYPE_EXTENSIONS = {"audio/mpeg": "mp3", "audio/ogg": "ogg", "audio/pcm": "pcm"} +DEFAULT_ENGINE = "standard" DEFAULT_VOICE = "Joanna" DEFAULT_OUTPUT_FORMAT = "mp3" DEFAULT_TEXT_TYPE = "text" @@ -126,6 +130,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( vol.Inclusive(CONF_SECRET_ACCESS_KEY, ATTR_CREDENTIALS): cv.string, vol.Exclusive(CONF_PROFILE_NAME, ATTR_CREDENTIALS): cv.string, vol.Optional(CONF_VOICE, default=DEFAULT_VOICE): vol.In(SUPPORTED_VOICES), + vol.Optional(CONF_ENGINE, default=DEFAULT_ENGINE): vol.In(SUPPORTED_ENGINES), vol.Optional(CONF_OUTPUT_FORMAT, default=DEFAULT_OUTPUT_FORMAT): vol.In( SUPPORTED_OUTPUT_FORMATS ), @@ -225,6 +230,7 @@ class AmazonPollyProvider(Provider): return None, None resp = self.client.synthesize_speech( + Engine=self.config[CONF_ENGINE], OutputFormat=self.config[CONF_OUTPUT_FORMAT], SampleRate=self.config[CONF_SAMPLE_RATE], Text=message, diff --git a/homeassistant/components/ambiclimate/.translations/ca.json b/homeassistant/components/ambiclimate/.translations/ca.json index 054b1a89ae8..f446bf7390f 100644 --- a/homeassistant/components/ambiclimate/.translations/ca.json +++ b/homeassistant/components/ambiclimate/.translations/ca.json @@ -3,7 +3,7 @@ "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.", - "no_config": "Necessites configurar Ambi Climate abans de poder autenticar-t'hi. Llegeix les [instruccions](https://www.home-assistant.io/components/ambiclimate/)." + "no_config": "Necessites configurar Ambiclimate abans de poder autenticar-t'hi. Llegeix les [instruccions](https://www.home-assistant.io/components/ambiclimate/)." }, "create_entry": { "default": "Autenticaci\u00f3 exitosa amb Ambi Climate." @@ -14,7 +14,7 @@ }, "step": { "auth": { - "description": "V\u00e9s a l'[enlla\u00e7]({authorization_url}) i Permet l'acc\u00e9s al teu compte de Ambi Climate, despr\u00e9s torna i prem Envia (a sota).\n(Assegura't que l'enlla\u00e7 de retorn \u00e9s el seg\u00fcent {cb_url})", + "description": "V\u00e9s a l'[enlla\u00e7]({authorization_url}) i Permet l'acc\u00e9s al teu compte de Ambiclimate, despr\u00e9s torna i prem Envia (a sota).\n(Assegura't que l'enlla\u00e7 de retorn \u00e9s el seg\u00fcent {cb_url})", "title": "Autenticaci\u00f3 amb Ambi Climate" } }, diff --git a/homeassistant/components/ambiclimate/.translations/ko.json b/homeassistant/components/ambiclimate/.translations/ko.json index be337bd3f0e..3b21726bcbe 100644 --- a/homeassistant/components/ambiclimate/.translations/ko.json +++ b/homeassistant/components/ambiclimate/.translations/ko.json @@ -3,7 +3,7 @@ "abort": { "access_token": "\uc561\uc138\uc2a4 \ud1a0\ud070 \uc0dd\uc131\uc5d0 \uc54c \uc218 \uc5c6\ub294 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4.", "already_setup": "Ambi Climate \uacc4\uc815\uc774 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", - "no_config": "Ambi Climate \ub97c \uc778\uc99d\ud558\ub824\uba74 \uba3c\uc800 Ambi Climate \ub97c \uad6c\uc131\ud574\uc57c \ud569\ub2c8\ub2e4. [\uc548\ub0b4](https://www.home-assistant.io/components/ambiclimate/) \ub97c \uc77d\uc5b4\ubcf4\uc138\uc694." + "no_config": "Ambiclimate \ub97c \uc778\uc99d\ud558\ub824\uba74 \uba3c\uc800 Ambiclimate \ub97c \uad6c\uc131\ud574\uc57c \ud569\ub2c8\ub2e4. [\uc548\ub0b4](https://www.home-assistant.io/components/ambiclimate/) \ub97c \uc77d\uc5b4\ubcf4\uc138\uc694." }, "create_entry": { "default": "Ambi Climate \ub85c \uc131\uacf5\uc801\uc73c\ub85c \uc778\uc99d\ub418\uc5c8\uc2b5\ub2c8\ub2e4." diff --git a/homeassistant/components/ambiclimate/.translations/lb.json b/homeassistant/components/ambiclimate/.translations/lb.json index a6ce441749d..88be279ae7a 100644 --- a/homeassistant/components/ambiclimate/.translations/lb.json +++ b/homeassistant/components/ambiclimate/.translations/lb.json @@ -3,7 +3,7 @@ "abort": { "access_token": "Onbekannte Feeler beim gener\u00e9ieren vum Acc\u00e8s Jeton.", "already_setup": "Den Ambiclimate Kont ass konfigur\u00e9iert.", - "no_config": "Dir musst Ambiclimate konfigur\u00e9ieren, ier Dir d\u00ebs Authentifiz\u00e9ierung k\u00ebnnt benotzen.[Liest w.e.g. d'Instruktioune](https://www.home-assistant.io/components/ambiclimatet/)." + "no_config": "Dir musst Ambiclimate konfigur\u00e9ieren, ier Dir d\u00ebs Authentifiz\u00e9ierung k\u00ebnnt benotzen.[Liest w.e.g. d'Instruktioune](https://www.home-assistant.io/components/ambiclimate/)." }, "create_entry": { "default": "Erfollegr\u00e4ich mat Ambiclimate authentifiz\u00e9iert." diff --git a/homeassistant/components/ambiclimate/.translations/no.json b/homeassistant/components/ambiclimate/.translations/no.json index 567d0b95ff3..e84de4ffc22 100644 --- a/homeassistant/components/ambiclimate/.translations/no.json +++ b/homeassistant/components/ambiclimate/.translations/no.json @@ -9,12 +9,12 @@ "default": "Vellykket autentisering med Ambiclimate" }, "error": { - "follow_link": "Vennligst f\u00f8lg lenken og godkjen f\u00f8r du trykker p\u00e5 Send", + "follow_link": "Vennligst f\u00f8lg lenken og godkjenn f\u00f8r du trykker p\u00e5 Send", "no_token": "Ikke autentisert med Ambiclimate" }, "step": { "auth": { - "description": "Vennligst f\u00f8lg denne [linken]({authorization_url}) og Tillat tilgang til din Ambiclimate konto, og kom s\u00e5 tilbake og trykk Send nedenfor.\n(Kontroller at den angitte URL-adressen for tilbakeringing er {cb_url})", + "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" } }, diff --git a/homeassistant/components/ambiclimate/.translations/pl.json b/homeassistant/components/ambiclimate/.translations/pl.json index 7ba95b007c9..675c5e18776 100644 --- a/homeassistant/components/ambiclimate/.translations/pl.json +++ b/homeassistant/components/ambiclimate/.translations/pl.json @@ -3,7 +3,7 @@ "abort": { "access_token": "Nieznany b\u0142\u0105d podczas generowania tokena dost\u0119pu.", "already_setup": "Konto Ambiclimate jest skonfigurowane.", - "no_config": "Musisz skonfigurowa\u0107 Ambiclimate, zanim b\u0119dziesz m\u00f3g\u0142 si\u0119 w nim uwierzytelni\u0107. [Przeczytaj instrukcj\u0119](https://www.home-assistant.io/components/ambiclimate/)." + "no_config": "Musisz skonfigurowa\u0107 Ambiclimate, zanim b\u0119dziesz m\u00f3g\u0142 si\u0119 w nim uwierzytelni\u0107. [Przeczytaj instrukcj\u0119] (https://www.home-assistant.io/components/ambiclimate/)." }, "create_entry": { "default": "Pomy\u015blnie uwierzytelniono z Ambiclimate" diff --git a/homeassistant/components/ambiclimate/.translations/ru.json b/homeassistant/components/ambiclimate/.translations/ru.json index a4300e1e530..5a816bce140 100644 --- a/homeassistant/components/ambiclimate/.translations/ru.json +++ b/homeassistant/components/ambiclimate/.translations/ru.json @@ -3,7 +3,7 @@ "abort": { "access_token": "\u041f\u0440\u0438 \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u0438 \u0442\u043e\u043a\u0435\u043d\u0430 \u0434\u043e\u0441\u0442\u0443\u043f\u0430 \u043f\u0440\u043e\u0438\u0437\u043e\u0448\u043b\u0430 \u043e\u0448\u0438\u0431\u043a\u0430.", "already_setup": "\u0423\u0447\u0435\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c Ambi Climate \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u0430.", - "no_config": "\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 Ambi Climate \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/ambiclimate/)." + "no_config": "\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 Ambiclimate \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/ambiclimate/)." }, "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." diff --git a/homeassistant/components/ambiclimate/.translations/sl.json b/homeassistant/components/ambiclimate/.translations/sl.json index cae2e940d56..c5d84f030fa 100644 --- a/homeassistant/components/ambiclimate/.translations/sl.json +++ b/homeassistant/components/ambiclimate/.translations/sl.json @@ -3,7 +3,7 @@ "abort": { "access_token": "Neznana napaka pri ustvarjanju \u017eetona za dostop.", "already_setup": "Ra\u010dun Ambiclimate je konfiguriran.", - "no_config": "Ambiclimat morate konfigurirati, preden lahko z njo preverjate pristnost. [Preberite navodila] (https://www.home-assistant.io/components/ambiclimate/)." + "no_config": "Ambiclimate morate konfigurirati, preden lahko z njo preverjate pristnost. [Preberite navodila] (https://www.home-assistant.io/components/ambiclimate/)." }, "create_entry": { "default": "Uspe\u0161no overjeno z funkcijo Ambiclimate" diff --git a/homeassistant/components/ambiclimate/.translations/zh-Hant.json b/homeassistant/components/ambiclimate/.translations/zh-Hant.json index 28859cbf591..1539429d0ef 100644 --- a/homeassistant/components/ambiclimate/.translations/zh-Hant.json +++ b/homeassistant/components/ambiclimate/.translations/zh-Hant.json @@ -6,7 +6,7 @@ "no_config": "\u5fc5\u9808\u5148\u8a2d\u5b9a Ambiclimate \u65b9\u80fd\u9032\u884c\u8a8d\u8b49\u3002[\u8acb\u53c3\u95b1\u6559\u5b78\u6307\u5f15]\uff08https://www.home-assistant.io/components/ambiclimate/\uff09\u3002" }, "create_entry": { - "default": "\u5df2\u6210\u529f\u8a8d\u8b49 Ambiclimate \u88dd\u7f6e\u3002" + "default": "\u5df2\u6210\u529f\u8a8d\u8b49 Ambiclimate \u8a2d\u5099\u3002" }, "error": { "follow_link": "\u8acb\u65bc\u50b3\u9001\u524d\uff0c\u5148\u4f7f\u7528\u9023\u7d50\u4e26\u9032\u884c\u8a8d\u8b49\u3002", diff --git a/homeassistant/components/ambiclimate/manifest.json b/homeassistant/components/ambiclimate/manifest.json index 8e5ddb924ca..3d175165abd 100644 --- a/homeassistant/components/ambiclimate/manifest.json +++ b/homeassistant/components/ambiclimate/manifest.json @@ -2,7 +2,7 @@ "domain": "ambiclimate", "name": "Ambiclimate", "config_flow": true, - "documentation": "https://www.home-assistant.io/components/ambiclimate", + "documentation": "https://www.home-assistant.io/integrations/ambiclimate", "requirements": [ "ambiclimate==0.2.1" ], diff --git a/homeassistant/components/ambient_station/.translations/es.json b/homeassistant/components/ambient_station/.translations/es.json index d4b0075aa65..d4222f1d2eb 100644 --- a/homeassistant/components/ambient_station/.translations/es.json +++ b/homeassistant/components/ambient_station/.translations/es.json @@ -14,6 +14,6 @@ "title": "Completa tu informaci\u00f3n" } }, - "title": "Ambient PWS" + "title": "Ambiente PWS" } } \ No newline at end of file diff --git a/homeassistant/components/ambient_station/.translations/ru.json b/homeassistant/components/ambient_station/.translations/ru.json index d1264010b75..2d7964f18eb 100644 --- a/homeassistant/components/ambient_station/.translations/ru.json +++ b/homeassistant/components/ambient_station/.translations/ru.json @@ -1,7 +1,7 @@ { "config": { "error": { - "identifier_exists": "\u041a\u043b\u044e\u0447 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u0438/\u0438\u043b\u0438 \u043a\u043b\u044e\u0447 API \u0443\u0436\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d", + "identifier_exists": "\u041a\u043b\u044e\u0447 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u0438/\u0438\u043b\u0438 \u043a\u043b\u044e\u0447 API \u0443\u0436\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d.", "invalid_key": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043a\u043b\u044e\u0447 API \u0438/\u0438\u043b\u0438 \u043a\u043b\u044e\u0447 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f", "no_devices": "\u0412 \u0443\u0447\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b" }, diff --git a/homeassistant/components/ambient_station/.translations/zh-Hant.json b/homeassistant/components/ambient_station/.translations/zh-Hant.json index 7e3ed3ef888..6c7c88a8045 100644 --- a/homeassistant/components/ambient_station/.translations/zh-Hant.json +++ b/homeassistant/components/ambient_station/.translations/zh-Hant.json @@ -3,7 +3,7 @@ "error": { "identifier_exists": "API \u5bc6\u9470\u53ca/\u6216\u61c9\u7528\u5bc6\u9470\u5df2\u8a3b\u518a", "invalid_key": "API \u5bc6\u9470\u53ca/\u6216\u61c9\u7528\u5bc6\u9470\u7121\u6548", - "no_devices": "\u5e33\u865f\u4e2d\u627e\u4e0d\u5230\u4efb\u4f55\u88dd\u7f6e" + "no_devices": "\u5e33\u865f\u4e2d\u627e\u4e0d\u5230\u4efb\u4f55\u8a2d\u5099" }, "step": { "user": { diff --git a/homeassistant/components/ambient_station/manifest.json b/homeassistant/components/ambient_station/manifest.json index 056930edfc7..8f363ba219f 100644 --- a/homeassistant/components/ambient_station/manifest.json +++ b/homeassistant/components/ambient_station/manifest.json @@ -2,7 +2,7 @@ "domain": "ambient_station", "name": "Ambient station", "config_flow": true, - "documentation": "https://www.home-assistant.io/components/ambient_station", + "documentation": "https://www.home-assistant.io/integrations/ambient_station", "requirements": [ "aioambient==0.3.2" ], diff --git a/homeassistant/components/amcrest/manifest.json b/homeassistant/components/amcrest/manifest.json index f79ce34897b..4453687b895 100644 --- a/homeassistant/components/amcrest/manifest.json +++ b/homeassistant/components/amcrest/manifest.json @@ -1,7 +1,7 @@ { "domain": "amcrest", "name": "Amcrest", - "documentation": "https://www.home-assistant.io/components/amcrest", + "documentation": "https://www.home-assistant.io/integrations/amcrest", "requirements": [ "amcrest==1.5.3" ], diff --git a/homeassistant/components/ampio/manifest.json b/homeassistant/components/ampio/manifest.json index d20b10b2d15..6bf79e27f85 100644 --- a/homeassistant/components/ampio/manifest.json +++ b/homeassistant/components/ampio/manifest.json @@ -1,7 +1,7 @@ { "domain": "ampio", "name": "Ampio", - "documentation": "https://www.home-assistant.io/components/ampio", + "documentation": "https://www.home-assistant.io/integrations/ampio", "requirements": [ "asmog==0.0.6" ], diff --git a/homeassistant/components/android_ip_webcam/manifest.json b/homeassistant/components/android_ip_webcam/manifest.json index 28909f7e053..c9602d75751 100644 --- a/homeassistant/components/android_ip_webcam/manifest.json +++ b/homeassistant/components/android_ip_webcam/manifest.json @@ -1,7 +1,7 @@ { "domain": "android_ip_webcam", "name": "Android ip webcam", - "documentation": "https://www.home-assistant.io/components/android_ip_webcam", + "documentation": "https://www.home-assistant.io/integrations/android_ip_webcam", "requirements": [ "pydroid-ipcam==0.8" ], diff --git a/homeassistant/components/androidtv/manifest.json b/homeassistant/components/androidtv/manifest.json index 6643faa85bd..e84ed35c763 100644 --- a/homeassistant/components/androidtv/manifest.json +++ b/homeassistant/components/androidtv/manifest.json @@ -1,9 +1,10 @@ { "domain": "androidtv", "name": "Androidtv", - "documentation": "https://www.home-assistant.io/components/androidtv", + "documentation": "https://www.home-assistant.io/integrations/androidtv", "requirements": [ - "androidtv==0.0.27" + "adb-shell==0.0.4", + "androidtv==0.0.30" ], "dependencies": [], "codeowners": ["@JeffLIrion"] diff --git a/homeassistant/components/androidtv/media_player.py b/homeassistant/components/androidtv/media_player.py index d68f47b1b0a..fcf4950f5e2 100644 --- a/homeassistant/components/androidtv/media_player.py +++ b/homeassistant/components/androidtv/media_player.py @@ -3,6 +3,12 @@ import functools import logging import voluptuous as vol +from adb_shell.exceptions import ( + InvalidChecksumError, + InvalidCommandError, + InvalidResponseError, + TcpTimeoutException, +) from androidtv import setup, ha_state_detection_rules_validator from androidtv.constants import APPS, KEYS @@ -123,11 +129,15 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Android TV / Fire TV platform.""" hass.data.setdefault(ANDROIDTV_DOMAIN, {}) - host = "{0}:{1}".format(config[CONF_HOST], config[CONF_PORT]) + host = f"{config[CONF_HOST]}:{config[CONF_PORT]}" if CONF_ADB_SERVER_IP not in config: - # Use "python-adb" (Python ADB implementation) - adb_log = "using Python ADB implementation " + # Use "adb_shell" (Python ADB implementation) + adb_log = "using Python ADB implementation " + ( + f"with adbkey='{config[CONF_ADBKEY]}'" + if CONF_ADBKEY in config + else "without adbkey authentication" + ) if CONF_ADBKEY in config: aftv = setup( host, @@ -135,7 +145,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): device_class=config[CONF_DEVICE_CLASS], state_detection_rules=config[CONF_STATE_DETECTION_RULES], ) - adb_log += "with adbkey='{0}'".format(config[CONF_ADBKEY]) else: aftv = setup( @@ -143,7 +152,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): device_class=config[CONF_DEVICE_CLASS], state_detection_rules=config[CONF_STATE_DETECTION_RULES], ) - adb_log += "without adbkey authentication" else: # Use "pure-python-adb" (communicate with ADB server) aftv = setup( @@ -153,9 +161,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): device_class=config[CONF_DEVICE_CLASS], state_detection_rules=config[CONF_STATE_DETECTION_RULES], ) - adb_log = "using ADB server at {0}:{1}".format( - config[CONF_ADB_SERVER_IP], config[CONF_ADB_SERVER_PORT] - ) + adb_log = f"using ADB server at {config[CONF_ADB_SERVER_IP]}:{config[CONF_ADB_SERVER_PORT]}" if not aftv.available: # Determine the name that will be used for the device in the log @@ -251,6 +257,7 @@ def adb_decorator(override_available=False): "establishing attempt in the next update. Error: %s", err, ) + self.aftv.adb.close() self._available = False # pylint: disable=protected-access return None @@ -278,14 +285,7 @@ class ADBDevice(MediaPlayerDevice): # ADB exceptions to catch if not self.aftv.adb_server_ip: - # Using "python-adb" (Python ADB implementation) - from adb.adb_protocol import ( - InvalidChecksumError, - InvalidCommandError, - InvalidResponseError, - ) - from adb.usb_exceptions import TcpTimeoutException - + # Using "adb_shell" (Python ADB implementation) self.exceptions = ( AttributeError, BrokenPipeError, diff --git a/homeassistant/components/anel_pwrctrl/manifest.json b/homeassistant/components/anel_pwrctrl/manifest.json index 17802918cd2..d4055a4068f 100644 --- a/homeassistant/components/anel_pwrctrl/manifest.json +++ b/homeassistant/components/anel_pwrctrl/manifest.json @@ -1,7 +1,7 @@ { "domain": "anel_pwrctrl", "name": "Anel pwrctrl", - "documentation": "https://www.home-assistant.io/components/anel_pwrctrl", + "documentation": "https://www.home-assistant.io/integrations/anel_pwrctrl", "requirements": [ "anel_pwrctrl-homeassistant==0.0.1.dev2" ], diff --git a/homeassistant/components/anthemav/manifest.json b/homeassistant/components/anthemav/manifest.json index 9b2e3c697bb..7c648d37b10 100644 --- a/homeassistant/components/anthemav/manifest.json +++ b/homeassistant/components/anthemav/manifest.json @@ -1,7 +1,7 @@ { "domain": "anthemav", "name": "Anthemav", - "documentation": "https://www.home-assistant.io/components/anthemav", + "documentation": "https://www.home-assistant.io/integrations/anthemav", "requirements": [ "anthemav==1.1.10" ], diff --git a/homeassistant/components/apache_kafka/manifest.json b/homeassistant/components/apache_kafka/manifest.json index ac36af7fa48..fb2bd643525 100644 --- a/homeassistant/components/apache_kafka/manifest.json +++ b/homeassistant/components/apache_kafka/manifest.json @@ -1,7 +1,7 @@ { "domain": "apache_kafka", "name": "Apache Kafka", - "documentation": "https://www.home-assistant.io/components/apache_kafka", + "documentation": "https://www.home-assistant.io/integrations/apache_kafka", "requirements": [ "aiokafka==0.5.1" ], diff --git a/homeassistant/components/apcupsd/manifest.json b/homeassistant/components/apcupsd/manifest.json index 813176728f2..08cac545249 100644 --- a/homeassistant/components/apcupsd/manifest.json +++ b/homeassistant/components/apcupsd/manifest.json @@ -1,7 +1,7 @@ { "domain": "apcupsd", "name": "Apcupsd", - "documentation": "https://www.home-assistant.io/components/apcupsd", + "documentation": "https://www.home-assistant.io/integrations/apcupsd", "requirements": [ "apcaccess==0.0.13" ], diff --git a/homeassistant/components/api/manifest.json b/homeassistant/components/api/manifest.json index 25d9a76036e..830fc04445a 100644 --- a/homeassistant/components/api/manifest.json +++ b/homeassistant/components/api/manifest.json @@ -1,7 +1,7 @@ { "domain": "api", "name": "Home Assistant API", - "documentation": "https://www.home-assistant.io/components/api", + "documentation": "https://www.home-assistant.io/integrations/api", "requirements": [], "dependencies": [ "http" diff --git a/homeassistant/components/apns/manifest.json b/homeassistant/components/apns/manifest.json index 9a310a096a5..4845c45a963 100644 --- a/homeassistant/components/apns/manifest.json +++ b/homeassistant/components/apns/manifest.json @@ -1,7 +1,7 @@ { "domain": "apns", "name": "Apns", - "documentation": "https://www.home-assistant.io/components/apns", + "documentation": "https://www.home-assistant.io/integrations/apns", "requirements": [ "apns2==0.3.0" ], diff --git a/homeassistant/components/apple_tv/manifest.json b/homeassistant/components/apple_tv/manifest.json index c391fb0e14b..f8fd2e0efa2 100644 --- a/homeassistant/components/apple_tv/manifest.json +++ b/homeassistant/components/apple_tv/manifest.json @@ -1,7 +1,7 @@ { "domain": "apple_tv", "name": "Apple tv", - "documentation": "https://www.home-assistant.io/components/apple_tv", + "documentation": "https://www.home-assistant.io/integrations/apple_tv", "requirements": [ "pyatv==0.3.13" ], diff --git a/homeassistant/components/aprs/manifest.json b/homeassistant/components/aprs/manifest.json index fbe13ca8578..c7615817b50 100644 --- a/homeassistant/components/aprs/manifest.json +++ b/homeassistant/components/aprs/manifest.json @@ -1,7 +1,7 @@ { "domain": "aprs", "name": "APRS", - "documentation": "https://www.home-assistant.io/components/aprs", + "documentation": "https://www.home-assistant.io/integrations/aprs", "dependencies": [], "codeowners": ["@PhilRW"], "requirements": [ diff --git a/homeassistant/components/aqualogic/manifest.json b/homeassistant/components/aqualogic/manifest.json index 40f1805d83a..2e5dded4af6 100644 --- a/homeassistant/components/aqualogic/manifest.json +++ b/homeassistant/components/aqualogic/manifest.json @@ -1,7 +1,7 @@ { "domain": "aqualogic", "name": "Aqualogic", - "documentation": "https://www.home-assistant.io/components/aqualogic", + "documentation": "https://www.home-assistant.io/integrations/aqualogic", "requirements": [ "aqualogic==1.0" ], diff --git a/homeassistant/components/aquostv/manifest.json b/homeassistant/components/aquostv/manifest.json index 16865905ae9..d4c491cb70d 100644 --- a/homeassistant/components/aquostv/manifest.json +++ b/homeassistant/components/aquostv/manifest.json @@ -1,7 +1,7 @@ { "domain": "aquostv", "name": "Aquostv", - "documentation": "https://www.home-assistant.io/components/aquostv", + "documentation": "https://www.home-assistant.io/integrations/aquostv", "requirements": [ "sharp_aquos_rc==0.3.2" ], diff --git a/homeassistant/components/arcam_fmj/.translations/bg.json b/homeassistant/components/arcam_fmj/.translations/bg.json new file mode 100644 index 00000000000..b0ad4660d0f --- /dev/null +++ b/homeassistant/components/arcam_fmj/.translations/bg.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Arcam FMJ" + } +} \ No newline at end of file diff --git a/homeassistant/components/arcam_fmj/.translations/es-419.json b/homeassistant/components/arcam_fmj/.translations/es-419.json new file mode 100644 index 00000000000..b0ad4660d0f --- /dev/null +++ b/homeassistant/components/arcam_fmj/.translations/es-419.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Arcam FMJ" + } +} \ No newline at end of file diff --git a/homeassistant/components/arcam_fmj/.translations/nn.json b/homeassistant/components/arcam_fmj/.translations/nn.json new file mode 100644 index 00000000000..b0ad4660d0f --- /dev/null +++ b/homeassistant/components/arcam_fmj/.translations/nn.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Arcam FMJ" + } +} \ No newline at end of file diff --git a/homeassistant/components/arcam_fmj/.translations/pt-BR.json b/homeassistant/components/arcam_fmj/.translations/pt-BR.json new file mode 100644 index 00000000000..b0ad4660d0f --- /dev/null +++ b/homeassistant/components/arcam_fmj/.translations/pt-BR.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Arcam FMJ" + } +} \ No newline at end of file diff --git a/homeassistant/components/arcam_fmj/manifest.json b/homeassistant/components/arcam_fmj/manifest.json index 59ab3c03d92..288b8fb3890 100644 --- a/homeassistant/components/arcam_fmj/manifest.json +++ b/homeassistant/components/arcam_fmj/manifest.json @@ -2,7 +2,7 @@ "domain": "arcam_fmj", "name": "Arcam FMJ Receiver control", "config_flow": false, - "documentation": "https://www.home-assistant.io/components/arcam_fmj", + "documentation": "https://www.home-assistant.io/integrations/arcam_fmj", "requirements": [ "arcam-fmj==0.4.3" ], diff --git a/homeassistant/components/arduino/manifest.json b/homeassistant/components/arduino/manifest.json index cf21cbe87ea..3567ce71cd1 100644 --- a/homeassistant/components/arduino/manifest.json +++ b/homeassistant/components/arduino/manifest.json @@ -1,7 +1,7 @@ { "domain": "arduino", "name": "Arduino", - "documentation": "https://www.home-assistant.io/components/arduino", + "documentation": "https://www.home-assistant.io/integrations/arduino", "requirements": [ "PyMata==2.14" ], diff --git a/homeassistant/components/arest/manifest.json b/homeassistant/components/arest/manifest.json index d5bcf92a39d..ee6b915e658 100644 --- a/homeassistant/components/arest/manifest.json +++ b/homeassistant/components/arest/manifest.json @@ -1,7 +1,7 @@ { "domain": "arest", "name": "Arest", - "documentation": "https://www.home-assistant.io/components/arest", + "documentation": "https://www.home-assistant.io/integrations/arest", "requirements": [], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/arlo/manifest.json b/homeassistant/components/arlo/manifest.json index 35803d0d4f6..8779e051dc0 100644 --- a/homeassistant/components/arlo/manifest.json +++ b/homeassistant/components/arlo/manifest.json @@ -1,7 +1,7 @@ { "domain": "arlo", "name": "Arlo", - "documentation": "https://www.home-assistant.io/components/arlo", + "documentation": "https://www.home-assistant.io/integrations/arlo", "requirements": [ "pyarlo==0.2.3" ], diff --git a/homeassistant/components/aruba/manifest.json b/homeassistant/components/aruba/manifest.json index 597975619e6..ccc4f119092 100644 --- a/homeassistant/components/aruba/manifest.json +++ b/homeassistant/components/aruba/manifest.json @@ -1,7 +1,7 @@ { "domain": "aruba", "name": "Aruba", - "documentation": "https://www.home-assistant.io/components/aruba", + "documentation": "https://www.home-assistant.io/integrations/aruba", "requirements": [ "pexpect==4.6.0" ], diff --git a/homeassistant/components/arwn/manifest.json b/homeassistant/components/arwn/manifest.json index 1c861aa67e2..d84202cbac8 100644 --- a/homeassistant/components/arwn/manifest.json +++ b/homeassistant/components/arwn/manifest.json @@ -1,7 +1,7 @@ { "domain": "arwn", "name": "Arwn", - "documentation": "https://www.home-assistant.io/components/arwn", + "documentation": "https://www.home-assistant.io/integrations/arwn", "requirements": [], "dependencies": [ "mqtt" diff --git a/homeassistant/components/asterisk_cdr/manifest.json b/homeassistant/components/asterisk_cdr/manifest.json index db1308b0483..56018ba777b 100644 --- a/homeassistant/components/asterisk_cdr/manifest.json +++ b/homeassistant/components/asterisk_cdr/manifest.json @@ -1,7 +1,7 @@ { "domain": "asterisk_cdr", "name": "Asterisk cdr", - "documentation": "https://www.home-assistant.io/components/asterisk_cdr", + "documentation": "https://www.home-assistant.io/integrations/asterisk_cdr", "requirements": [], "dependencies": [ "asterisk_mbox" diff --git a/homeassistant/components/asterisk_mbox/manifest.json b/homeassistant/components/asterisk_mbox/manifest.json index bafe43c480f..cf793328d93 100644 --- a/homeassistant/components/asterisk_mbox/manifest.json +++ b/homeassistant/components/asterisk_mbox/manifest.json @@ -1,7 +1,7 @@ { "domain": "asterisk_mbox", "name": "Asterisk mbox", - "documentation": "https://www.home-assistant.io/components/asterisk_mbox", + "documentation": "https://www.home-assistant.io/integrations/asterisk_mbox", "requirements": [ "asterisk_mbox==0.5.0" ], diff --git a/homeassistant/components/asuswrt/manifest.json b/homeassistant/components/asuswrt/manifest.json index f36819f133d..3fcdcf42ab1 100644 --- a/homeassistant/components/asuswrt/manifest.json +++ b/homeassistant/components/asuswrt/manifest.json @@ -1,7 +1,7 @@ { "domain": "asuswrt", "name": "Asuswrt", - "documentation": "https://www.home-assistant.io/components/asuswrt", + "documentation": "https://www.home-assistant.io/integrations/asuswrt", "requirements": [ "aioasuswrt==1.1.21" ], diff --git a/homeassistant/components/atome/manifest.json b/homeassistant/components/atome/manifest.json index 621faba4fc0..55739cad8cc 100644 --- a/homeassistant/components/atome/manifest.json +++ b/homeassistant/components/atome/manifest.json @@ -1,7 +1,7 @@ { "domain": "atome", "name": "Atome", - "documentation": "https://www.home-assistant.io/components/atome", + "documentation": "https://www.home-assistant.io/integrations/atome", "dependencies": [], "codeowners": ["@baqs"], "requirements": ["pyatome==0.1.1"] diff --git a/homeassistant/components/august/manifest.json b/homeassistant/components/august/manifest.json index e41491c4b0a..ebaa564736f 100644 --- a/homeassistant/components/august/manifest.json +++ b/homeassistant/components/august/manifest.json @@ -1,7 +1,7 @@ { "domain": "august", "name": "August", - "documentation": "https://www.home-assistant.io/components/august", + "documentation": "https://www.home-assistant.io/integrations/august", "requirements": [ "py-august==0.7.0" ], diff --git a/homeassistant/components/aurora/manifest.json b/homeassistant/components/aurora/manifest.json index 56ba3fe9356..204327043f9 100644 --- a/homeassistant/components/aurora/manifest.json +++ b/homeassistant/components/aurora/manifest.json @@ -1,7 +1,7 @@ { "domain": "aurora", "name": "Aurora", - "documentation": "https://www.home-assistant.io/components/aurora", + "documentation": "https://www.home-assistant.io/integrations/aurora", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/aurora_abb_powerone/manifest.json b/homeassistant/components/aurora_abb_powerone/manifest.json index 56325dd40af..f49421ea954 100644 --- a/homeassistant/components/aurora_abb_powerone/manifest.json +++ b/homeassistant/components/aurora_abb_powerone/manifest.json @@ -1,7 +1,7 @@ { "domain": "aurora_abb_powerone", "name": "Aurora ABB Solar PV", - "documentation": "https://www.home-assistant.io/components/aurora_abb_powerone/", + "documentation": "https://www.home-assistant.io/integrations/aurora_abb_powerone/", "dependencies": [], "codeowners": [ "@davet2001" diff --git a/homeassistant/components/auth/.translations/es.json b/homeassistant/components/auth/.translations/es.json index dd1d6f54377..5603c14fe1a 100644 --- a/homeassistant/components/auth/.translations/es.json +++ b/homeassistant/components/auth/.translations/es.json @@ -13,7 +13,7 @@ "title": "Configure una contrase\u00f1a de un solo uso entregada por el componente de notificaci\u00f3n" }, "setup": { - "description": "Se ha enviado una contrase\u00f1a de un solo uso a trav\u00e9s de ** notificar. {notify_service} **. Por favor introd\u00facela a continuaci\u00f3n:", + "description": "Se ha enviado una contrase\u00f1a de un solo uso a trav\u00e9s de ** notify. {notify_service} **. Por favor introd\u00facela a continuaci\u00f3n:", "title": "Verificar la configuraci\u00f3n" } }, diff --git a/homeassistant/components/auth/manifest.json b/homeassistant/components/auth/manifest.json index 10be545f5e1..1b0ab33f381 100644 --- a/homeassistant/components/auth/manifest.json +++ b/homeassistant/components/auth/manifest.json @@ -1,7 +1,7 @@ { "domain": "auth", "name": "Auth", - "documentation": "https://www.home-assistant.io/components/auth", + "documentation": "https://www.home-assistant.io/integrations/auth", "requirements": [], "dependencies": [ "http" diff --git a/homeassistant/components/automatic/manifest.json b/homeassistant/components/automatic/manifest.json index 9743835af20..63cf0da0f46 100644 --- a/homeassistant/components/automatic/manifest.json +++ b/homeassistant/components/automatic/manifest.json @@ -1,7 +1,7 @@ { "domain": "automatic", "name": "Automatic", - "documentation": "https://www.home-assistant.io/components/automatic", + "documentation": "https://www.home-assistant.io/integrations/automatic", "requirements": [ "aioautomatic==0.6.5" ], diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index 03eedd6d162..f669d415854 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -3,6 +3,7 @@ import asyncio from functools import partial import importlib import logging +from typing import Any, Awaitable, Callable import voluptuous as vol @@ -22,7 +23,7 @@ from homeassistant.const import ( SERVICE_TURN_ON, STATE_ON, ) -from homeassistant.core import Context, CoreState +from homeassistant.core import Context, CoreState, HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import condition, extract_domain_configs, script import homeassistant.helpers.config_validation as cv @@ -30,11 +31,12 @@ from homeassistant.helpers.config_validation import ENTITY_SERVICE_SCHEMA from homeassistant.helpers.entity import ToggleEntity from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.restore_state import RestoreEntity +from homeassistant.helpers.typing import TemplateVarsType from homeassistant.loader import bind_hass from homeassistant.util.dt import parse_datetime, utcnow -# mypy: allow-incomplete-defs, allow-untyped-calls, allow-untyped-defs +# mypy: allow-untyped-calls, allow-untyped-defs # mypy: no-check-untyped-defs, no-warn-return-any DOMAIN = "automation" @@ -43,6 +45,7 @@ ENTITY_ID_FORMAT = DOMAIN + ".{}" GROUP_NAME_ALL_AUTOMATIONS = "all automations" CONF_ALIAS = "alias" +CONF_DESCRIPTION = "description" CONF_HIDE_ENTITY = "hide_entity" CONF_CONDITION = "condition" @@ -65,6 +68,8 @@ SERVICE_TRIGGER = "trigger" _LOGGER = logging.getLogger(__name__) +AutomationActionType = Callable[[HomeAssistant, TemplateVarsType], Awaitable[None]] + def _platform_validator(config): """Validate it is a valid platform.""" @@ -95,6 +100,7 @@ PLATFORM_SCHEMA = vol.Schema( # str on purpose CONF_ID: str, CONF_ALIAS: cv.string, + vol.Optional(CONF_DESCRIPTION): cv.string, vol.Optional(CONF_INITIAL_STATE): cv.boolean, vol.Optional(CONF_HIDE_ENTITY, default=DEFAULT_HIDE_ENTITY): cv.boolean, vol.Required(CONF_TRIGGER): _TRIGGER_SCHEMA, @@ -279,11 +285,11 @@ class AutomationEntity(ToggleEntity, RestoreEntity): if enable_automation: await self.async_enable() - async def async_turn_on(self, **kwargs) -> None: + async def async_turn_on(self, **kwargs: Any) -> None: """Turn the entity on and update the state.""" await self.async_enable() - async def async_turn_off(self, **kwargs) -> None: + async def async_turn_off(self, **kwargs: Any) -> None: """Turn the entity off.""" await self.async_disable() @@ -471,7 +477,7 @@ async def _async_process_trigger(hass, config, trigger_configs, name, action): platform = importlib.import_module(".{}".format(conf[CONF_PLATFORM]), __name__) try: - remove = await platform.async_trigger(hass, conf, action, info) + remove = await platform.async_attach_trigger(hass, conf, action, info) except InvalidDeviceAutomationConfig: remove = False diff --git a/homeassistant/components/automation/config.py b/homeassistant/components/automation/config.py new file mode 100644 index 00000000000..ebbd1771e84 --- /dev/null +++ b/homeassistant/components/automation/config.py @@ -0,0 +1,80 @@ +"""Config validation helper for the automation integration.""" +import asyncio +import importlib + +import voluptuous as vol + +from homeassistant.const import CONF_PLATFORM +from homeassistant.config import async_log_exception, config_without_domain +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import condition, config_per_platform, script +from homeassistant.loader import IntegrationNotFound + +from . import CONF_ACTION, CONF_CONDITION, CONF_TRIGGER, DOMAIN, PLATFORM_SCHEMA + +# mypy: allow-untyped-calls, allow-untyped-defs +# mypy: no-check-untyped-defs, no-warn-return-any + + +async def async_validate_config_item(hass, config, full_config=None): + """Validate config item.""" + config = PLATFORM_SCHEMA(config) + + triggers = [] + for trigger in config[CONF_TRIGGER]: + trigger_platform = importlib.import_module( + "..{}".format(trigger[CONF_PLATFORM]), __name__ + ) + if hasattr(trigger_platform, "async_validate_trigger_config"): + trigger = await trigger_platform.async_validate_trigger_config( + hass, trigger + ) + triggers.append(trigger) + 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 + + actions = [] + for action in config[CONF_ACTION]: + action = await script.async_validate_action_config(hass, action) + actions.append(action) + config[CONF_ACTION] = actions + + return config + + +async def _try_async_validate_config_item(hass, config, full_config=None): + """Validate config item.""" + try: + config = await async_validate_config_item(hass, config, full_config) + except (vol.Invalid, HomeAssistantError, IntegrationNotFound) as ex: + async_log_exception(ex, DOMAIN, full_config or config, hass) + return None + + return config + + +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) + + # Create a copy of the configuration with all config for current + # component removed and add validated config back in. + config = config_without_domain(config, DOMAIN) + config[DOMAIN] = automations + + return config diff --git a/homeassistant/components/automation/device.py b/homeassistant/components/automation/device.py index b090484ab67..dc65008c3fb 100644 --- a/homeassistant/components/automation/device.py +++ b/homeassistant/components/automation/device.py @@ -1,20 +1,29 @@ """Offer device oriented automation.""" import voluptuous as vol -from homeassistant.const import CONF_DOMAIN, CONF_PLATFORM -from homeassistant.loader import async_get_integration +from homeassistant.components.device_automation import ( + TRIGGER_BASE_SCHEMA, + async_get_device_automation_platform, +) +from homeassistant.const import CONF_DOMAIN # mypy: allow-untyped-defs, no-check-untyped-defs -TRIGGER_SCHEMA = vol.Schema( - {vol.Required(CONF_PLATFORM): "device", vol.Required(CONF_DOMAIN): str}, - extra=vol.ALLOW_EXTRA, -) +TRIGGER_SCHEMA = TRIGGER_BASE_SCHEMA.extend({}, extra=vol.ALLOW_EXTRA) -async def async_trigger(hass, config, action, automation_info): +async def async_validate_trigger_config(hass, config): + """Validate config.""" + platform = await async_get_device_automation_platform( + hass, config[CONF_DOMAIN], "trigger" + ) + return platform.TRIGGER_SCHEMA(config) + + +async def async_attach_trigger(hass, config, action, automation_info): """Listen for trigger.""" - integration = await async_get_integration(hass, config[CONF_DOMAIN]) - platform = integration.get_platform("device_automation") - return await platform.async_trigger(hass, config, action, automation_info) + platform = await async_get_device_automation_platform( + hass, config[CONF_DOMAIN], "trigger" + ) + return await platform.async_attach_trigger(hass, config, action, automation_info) diff --git a/homeassistant/components/automation/event.py b/homeassistant/components/automation/event.py index d372aedd1d7..26dacac974d 100644 --- a/homeassistant/components/automation/event.py +++ b/homeassistant/components/automation/event.py @@ -24,7 +24,9 @@ TRIGGER_SCHEMA = vol.Schema( ) -async def async_trigger(hass, config, action, automation_info): +async def async_attach_trigger( + hass, config, action, automation_info, *, platform_type="event" +): """Listen for events based on configuration.""" event_type = config.get(CONF_EVENT_TYPE) event_data_schema = ( @@ -47,7 +49,7 @@ async def async_trigger(hass, config, action, automation_info): hass.async_run_job( action( - {"trigger": {"platform": "event", "event": event}}, + {"trigger": {"platform": platform_type, "event": event}}, context=event.context, ) ) diff --git a/homeassistant/components/automation/geo_location.py b/homeassistant/components/automation/geo_location.py index 3f2aa1c00d7..0ef0884d329 100644 --- a/homeassistant/components/automation/geo_location.py +++ b/homeassistant/components/automation/geo_location.py @@ -37,7 +37,7 @@ def source_match(state, source): return state and state.attributes.get("source") == source -async def async_trigger(hass, config, action, automation_info): +async def async_attach_trigger(hass, config, action, automation_info): """Listen for state changes based on configuration.""" source = config.get(CONF_SOURCE).lower() zone_entity_id = config.get(CONF_ZONE) diff --git a/homeassistant/components/automation/homeassistant.py b/homeassistant/components/automation/homeassistant.py index bd1da7e7e1f..e4eb029d5aa 100644 --- a/homeassistant/components/automation/homeassistant.py +++ b/homeassistant/components/automation/homeassistant.py @@ -21,7 +21,7 @@ TRIGGER_SCHEMA = vol.Schema( ) -async def async_trigger(hass, config, action, automation_info): +async def async_attach_trigger(hass, config, action, automation_info): """Listen for events based on configuration.""" event = config.get(CONF_EVENT) diff --git a/homeassistant/components/automation/litejet.py b/homeassistant/components/automation/litejet.py index 7bc4c937765..9512db8261d 100644 --- a/homeassistant/components/automation/litejet.py +++ b/homeassistant/components/automation/litejet.py @@ -32,7 +32,7 @@ TRIGGER_SCHEMA = vol.Schema( ) -async def async_trigger(hass, config, action, automation_info): +async def async_attach_trigger(hass, config, action, automation_info): """Listen for events based on configuration.""" number = config.get(CONF_NUMBER) held_more_than = config.get(CONF_HELD_MORE_THAN) diff --git a/homeassistant/components/automation/manifest.json b/homeassistant/components/automation/manifest.json index 935cc7a9175..79a6877692e 100644 --- a/homeassistant/components/automation/manifest.json +++ b/homeassistant/components/automation/manifest.json @@ -1,7 +1,7 @@ { "domain": "automation", "name": "Automation", - "documentation": "https://www.home-assistant.io/components/automation", + "documentation": "https://www.home-assistant.io/integrations/automation", "requirements": [], "dependencies": [ "device_automation", diff --git a/homeassistant/components/automation/mqtt.py b/homeassistant/components/automation/mqtt.py index fd9a778dbfc..135a421f72e 100644 --- a/homeassistant/components/automation/mqtt.py +++ b/homeassistant/components/automation/mqtt.py @@ -25,7 +25,7 @@ TRIGGER_SCHEMA = vol.Schema( ) -async def async_trigger(hass, config, action, automation_info): +async def async_attach_trigger(hass, config, action, automation_info): """Listen for state changes based on configuration.""" topic = config[CONF_TOPIC] payload = config.get(CONF_PAYLOAD) diff --git a/homeassistant/components/automation/numeric_state.py b/homeassistant/components/automation/numeric_state.py index b33d724d770..8d88fe9cae6 100644 --- a/homeassistant/components/automation/numeric_state.py +++ b/homeassistant/components/automation/numeric_state.py @@ -40,7 +40,9 @@ TRIGGER_SCHEMA = vol.All( _LOGGER = logging.getLogger(__name__) -async def async_trigger(hass, config, action, automation_info): +async def async_attach_trigger( + hass, config, action, automation_info, *, platform_type="numeric_state" +): """Listen for state changes based on configuration.""" entity_id = config.get(CONF_ENTITY_ID) below = config.get(CONF_BELOW) @@ -84,7 +86,7 @@ async def async_trigger(hass, config, action, automation_info): action( { "trigger": { - "platform": "numeric_state", + "platform": platform_type, "entity_id": entity, "below": below, "above": above, diff --git a/homeassistant/components/automation/state.py b/homeassistant/components/automation/state.py index 5fbe97185a7..154394075a0 100644 --- a/homeassistant/components/automation/state.py +++ b/homeassistant/components/automation/state.py @@ -1,16 +1,19 @@ """Offer state listening automation rules.""" +from datetime import timedelta import logging +from typing import Dict import voluptuous as vol from homeassistant import exceptions -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, CALLBACK_TYPE, callback from homeassistant.const import MATCH_ALL, CONF_PLATFORM, CONF_FOR from homeassistant.helpers import config_validation as cv, template from homeassistant.helpers.event import async_track_state_change, async_track_same_state -# mypy: allow-untyped-calls, allow-untyped-defs, no-check-untyped-defs +# mypy: allow-incomplete-defs, allow-untyped-calls, allow-untyped-defs +# mypy: no-check-untyped-defs _LOGGER = logging.getLogger(__name__) @@ -37,7 +40,14 @@ TRIGGER_SCHEMA = vol.All( ) -async def async_trigger(hass, config, action, automation_info): +async def async_attach_trigger( + hass: HomeAssistant, + config, + action, + automation_info, + *, + platform_type: str = "state", +) -> CALLBACK_TYPE: """Listen for state changes based on configuration.""" entity_id = config.get(CONF_ENTITY_ID) from_state = config.get(CONF_FROM, MATCH_ALL) @@ -46,7 +56,7 @@ async def async_trigger(hass, config, action, automation_info): template.attach(hass, time_delta) match_all = from_state == MATCH_ALL and to_state == MATCH_ALL unsub_track_same = {} - period = {} + period: Dict[str, timedelta] = {} @callback def state_automation_listener(entity, from_s, to_s): @@ -59,7 +69,7 @@ async def async_trigger(hass, config, action, automation_info): action( { "trigger": { - "platform": "state", + "platform": platform_type, "entity_id": entity, "from_state": from_s, "to_state": to_s, diff --git a/homeassistant/components/automation/sun.py b/homeassistant/components/automation/sun.py index 7cbbe56f326..66892784a54 100644 --- a/homeassistant/components/automation/sun.py +++ b/homeassistant/components/automation/sun.py @@ -28,7 +28,7 @@ TRIGGER_SCHEMA = vol.Schema( ) -async def async_trigger(hass, config, action, automation_info): +async def async_attach_trigger(hass, config, action, automation_info): """Listen for events based on configuration.""" event = config.get(CONF_EVENT) offset = config.get(CONF_OFFSET) diff --git a/homeassistant/components/automation/template.py b/homeassistant/components/automation/template.py index c83d660912c..f2b4134de42 100644 --- a/homeassistant/components/automation/template.py +++ b/homeassistant/components/automation/template.py @@ -28,7 +28,7 @@ TRIGGER_SCHEMA = IF_ACTION_SCHEMA = vol.Schema( ) -async def async_trigger(hass, config, action, automation_info): +async def async_attach_trigger(hass, config, action, automation_info): """Listen for state changes based on configuration.""" value_template = config.get(CONF_VALUE_TEMPLATE) value_template.hass = hass diff --git a/homeassistant/components/automation/time.py b/homeassistant/components/automation/time.py index 3942d0efadb..231bc346e14 100644 --- a/homeassistant/components/automation/time.py +++ b/homeassistant/components/automation/time.py @@ -18,7 +18,7 @@ TRIGGER_SCHEMA = vol.Schema( ) -async def async_trigger(hass, config, action, automation_info): +async def async_attach_trigger(hass, config, action, automation_info): """Listen for state changes based on configuration.""" at_time = config.get(CONF_AT) hours, minutes, seconds = at_time.hour, at_time.minute, at_time.second diff --git a/homeassistant/components/automation/time_pattern.py b/homeassistant/components/automation/time_pattern.py index f749a308bf7..ee092916112 100644 --- a/homeassistant/components/automation/time_pattern.py +++ b/homeassistant/components/automation/time_pattern.py @@ -30,7 +30,7 @@ TRIGGER_SCHEMA = vol.All( ) -async def async_trigger(hass, config, action, automation_info): +async def async_attach_trigger(hass, config, action, automation_info): """Listen for state changes based on configuration.""" hours = config.get(CONF_HOURS) minutes = config.get(CONF_MINUTES) diff --git a/homeassistant/components/automation/webhook.py b/homeassistant/components/automation/webhook.py index 706afbe9042..bbcf9bd9ddc 100644 --- a/homeassistant/components/automation/webhook.py +++ b/homeassistant/components/automation/webhook.py @@ -36,7 +36,7 @@ async def _handle_webhook(action, hass, webhook_id, request): hass.async_run_job(action, {"trigger": result}) -async def async_trigger(hass, config, action, automation_info): +async def async_attach_trigger(hass, config, action, automation_info): """Trigger based on incoming webhooks.""" webhook_id = config.get(CONF_WEBHOOK_ID) hass.components.webhook.async_register( diff --git a/homeassistant/components/automation/zone.py b/homeassistant/components/automation/zone.py index 35b11006024..535ef298a2a 100644 --- a/homeassistant/components/automation/zone.py +++ b/homeassistant/components/automation/zone.py @@ -31,7 +31,7 @@ TRIGGER_SCHEMA = vol.Schema( ) -async def async_trigger(hass, config, action, automation_info): +async def async_attach_trigger(hass, config, action, automation_info): """Listen for state changes based on configuration.""" entity_id = config.get(CONF_ENTITY_ID) zone_entity_id = config.get(CONF_ZONE) diff --git a/homeassistant/components/avea/manifest.json b/homeassistant/components/avea/manifest.json index 273cefcbcfa..4fb9ab9f420 100644 --- a/homeassistant/components/avea/manifest.json +++ b/homeassistant/components/avea/manifest.json @@ -1,7 +1,7 @@ { "domain": "avea", "name": "Elgato Avea", - "documentation": "https://www.home-assistant.io/components/avea", + "documentation": "https://www.home-assistant.io/integrations/avea", "dependencies": [], "codeowners": ["@pattyland"], "requirements": ["avea==1.2.8"] diff --git a/homeassistant/components/avion/manifest.json b/homeassistant/components/avion/manifest.json index e7d97f13313..8bb8b56cb9d 100644 --- a/homeassistant/components/avion/manifest.json +++ b/homeassistant/components/avion/manifest.json @@ -1,7 +1,7 @@ { "domain": "avion", "name": "Avion", - "documentation": "https://www.home-assistant.io/components/avion", + "documentation": "https://www.home-assistant.io/integrations/avion", "requirements": [ "avion==0.10" ], diff --git a/homeassistant/components/awair/manifest.json b/homeassistant/components/awair/manifest.json index dfa5bec3c00..f4e632cb7d2 100644 --- a/homeassistant/components/awair/manifest.json +++ b/homeassistant/components/awair/manifest.json @@ -1,7 +1,7 @@ { "domain": "awair", "name": "Awair", - "documentation": "https://www.home-assistant.io/components/awair", + "documentation": "https://www.home-assistant.io/integrations/awair", "requirements": [ "python_awair==0.0.4" ], diff --git a/homeassistant/components/aws/manifest.json b/homeassistant/components/aws/manifest.json index a473a23f917..a4543cc4b0f 100644 --- a/homeassistant/components/aws/manifest.json +++ b/homeassistant/components/aws/manifest.json @@ -1,7 +1,7 @@ { "domain": "aws", "name": "Aws", - "documentation": "https://www.home-assistant.io/components/aws", + "documentation": "https://www.home-assistant.io/integrations/aws", "requirements": [ "aiobotocore==0.10.2" ], diff --git a/homeassistant/components/axis/.translations/hu.json b/homeassistant/components/axis/.translations/hu.json index b0c8051e69f..41dd3c00d2b 100644 --- a/homeassistant/components/axis/.translations/hu.json +++ b/homeassistant/components/axis/.translations/hu.json @@ -14,6 +14,7 @@ "username": "Felhaszn\u00e1l\u00f3n\u00e9v" } } - } + }, + "title": "Axis eszk\u00f6z" } } \ No newline at end of file diff --git a/homeassistant/components/axis/.translations/nn.json b/homeassistant/components/axis/.translations/nn.json index 33644469359..b6296d1acab 100644 --- a/homeassistant/components/axis/.translations/nn.json +++ b/homeassistant/components/axis/.translations/nn.json @@ -5,7 +5,8 @@ "data": { "host": "Vert", "password": "Passord", - "port": "Port" + "port": "Port", + "username": "Brukarnamn" } } } diff --git a/homeassistant/components/axis/.translations/ru.json b/homeassistant/components/axis/.translations/ru.json index 67d720aa85f..951263d53f9 100644 --- a/homeassistant/components/axis/.translations/ru.json +++ b/homeassistant/components/axis/.translations/ru.json @@ -1,13 +1,13 @@ { "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.", "bad_config_file": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u0438\u0437 \u0444\u0430\u0439\u043b\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438", "link_local_address": "\u0421\u0441\u044b\u043b\u043a\u0430 \u043d\u0430 \u043b\u043e\u043a\u0430\u043b\u044c\u043d\u044b\u0435 \u0430\u0434\u0440\u0435\u0441\u0430 \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f", "not_axis_device": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0435 \u044f\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u043c Axis" }, "error": { - "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 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u043d\u0430\u0447\u0430\u0442\u0430.", "device_unavailable": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u043d\u043e", "faulty_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435" diff --git a/homeassistant/components/axis/.translations/zh-Hant.json b/homeassistant/components/axis/.translations/zh-Hant.json index 1457db2b600..c0d0df02135 100644 --- a/homeassistant/components/axis/.translations/zh-Hant.json +++ b/homeassistant/components/axis/.translations/zh-Hant.json @@ -1,15 +1,15 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "bad_config_file": "\u8a2d\u5b9a\u6a94\u6848\u8cc7\u6599\u7121\u6548", "link_local_address": "\u4e0d\u652f\u63f4\u9023\u7d50\u672c\u5730\u7aef\u4f4d\u5740", - "not_axis_device": "\u6240\u767c\u73fe\u7684\u88dd\u7f6e\u4e26\u975e Axis \u88dd\u7f6e" + "not_axis_device": "\u6240\u767c\u73fe\u7684\u8a2d\u5099\u4e26\u975e Axis \u8a2d\u5099" }, "error": { - "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", - "already_in_progress": "\u88dd\u7f6e\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d\u3002", - "device_unavailable": "\u88dd\u7f6e\u7121\u6cd5\u4f7f\u7528", + "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_in_progress": "\u8a2d\u5099\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d\u3002", + "device_unavailable": "\u8a2d\u5099\u7121\u6cd5\u4f7f\u7528", "faulty_credentials": "\u4f7f\u7528\u8005\u6191\u8b49\u7121\u6548" }, "step": { @@ -20,9 +20,9 @@ "port": "\u901a\u8a0a\u57e0", "username": "\u4f7f\u7528\u8005\u540d\u7a31" }, - "title": "\u8a2d\u5b9a Axis \u88dd\u7f6e" + "title": "\u8a2d\u5b9a Axis \u8a2d\u5099" } }, - "title": "Axis \u88dd\u7f6e" + "title": "Axis \u8a2d\u5099" } } \ No newline at end of file diff --git a/homeassistant/components/axis/manifest.json b/homeassistant/components/axis/manifest.json index 2b1bef9081e..348f6148386 100644 --- a/homeassistant/components/axis/manifest.json +++ b/homeassistant/components/axis/manifest.json @@ -2,7 +2,7 @@ "domain": "axis", "name": "Axis", "config_flow": true, - "documentation": "https://www.home-assistant.io/components/axis", + "documentation": "https://www.home-assistant.io/integrations/axis", "requirements": ["axis==25"], "dependencies": [], "zeroconf": ["_axis-video._tcp.local."], diff --git a/homeassistant/components/azure_event_hub/manifest.json b/homeassistant/components/azure_event_hub/manifest.json index e2223fc97c3..0b705bddc29 100644 --- a/homeassistant/components/azure_event_hub/manifest.json +++ b/homeassistant/components/azure_event_hub/manifest.json @@ -1,7 +1,7 @@ { "domain": "azure_event_hub", "name": "Azure Event Hub", - "documentation": "https://www.home-assistant.io/components/azure_event_hub", + "documentation": "https://www.home-assistant.io/integrations/azure_event_hub", "requirements": ["azure-eventhub==1.3.1"], "dependencies": [], "codeowners": ["@eavanvalkenburg"] diff --git a/homeassistant/components/baidu/manifest.json b/homeassistant/components/baidu/manifest.json index 1dea1b7e37b..756a1c5adcc 100644 --- a/homeassistant/components/baidu/manifest.json +++ b/homeassistant/components/baidu/manifest.json @@ -1,7 +1,7 @@ { "domain": "baidu", "name": "Baidu", - "documentation": "https://www.home-assistant.io/components/baidu", + "documentation": "https://www.home-assistant.io/integrations/baidu", "requirements": [ "baidu-aip==1.6.6" ], diff --git a/homeassistant/components/bayesian/manifest.json b/homeassistant/components/bayesian/manifest.json index 25480ac8bdc..7060dbd396b 100644 --- a/homeassistant/components/bayesian/manifest.json +++ b/homeassistant/components/bayesian/manifest.json @@ -1,7 +1,7 @@ { "domain": "bayesian", "name": "Bayesian", - "documentation": "https://www.home-assistant.io/components/bayesian", + "documentation": "https://www.home-assistant.io/integrations/bayesian", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/bbb_gpio/manifest.json b/homeassistant/components/bbb_gpio/manifest.json index 5632836bfbb..edd60326828 100644 --- a/homeassistant/components/bbb_gpio/manifest.json +++ b/homeassistant/components/bbb_gpio/manifest.json @@ -1,7 +1,7 @@ { "domain": "bbb_gpio", "name": "Bbb gpio", - "documentation": "https://www.home-assistant.io/components/bbb_gpio", + "documentation": "https://www.home-assistant.io/integrations/bbb_gpio", "requirements": [ "Adafruit_BBIO==1.0.0" ], diff --git a/homeassistant/components/bbox/manifest.json b/homeassistant/components/bbox/manifest.json index 54cd9a3af64..15a648167c5 100644 --- a/homeassistant/components/bbox/manifest.json +++ b/homeassistant/components/bbox/manifest.json @@ -1,7 +1,7 @@ { "domain": "bbox", "name": "Bbox", - "documentation": "https://www.home-assistant.io/components/bbox", + "documentation": "https://www.home-assistant.io/integrations/bbox", "requirements": [ "pybbox==0.0.5-alpha" ], diff --git a/homeassistant/components/beewi_smartclim/manifest.json b/homeassistant/components/beewi_smartclim/manifest.json index 3e9ad732b74..bc69efb6490 100644 --- a/homeassistant/components/beewi_smartclim/manifest.json +++ b/homeassistant/components/beewi_smartclim/manifest.json @@ -1,7 +1,7 @@ { "domain": "beewi_smartclim", "name": "BeeWi SmartClim BLE sensor", - "documentation": "https://www.home-assistant.io/components/beewi_smartclim", + "documentation": "https://www.home-assistant.io/integrations/beewi_smartclim", "requirements": [ "beewi_smartclim==0.0.7" ], diff --git a/homeassistant/components/bh1750/manifest.json b/homeassistant/components/bh1750/manifest.json index 90e62c78356..63816f967e7 100644 --- a/homeassistant/components/bh1750/manifest.json +++ b/homeassistant/components/bh1750/manifest.json @@ -1,7 +1,7 @@ { "domain": "bh1750", "name": "Bh1750", - "documentation": "https://www.home-assistant.io/components/bh1750", + "documentation": "https://www.home-assistant.io/integrations/bh1750", "requirements": [ "i2csense==0.0.4", "smbus-cffi==0.5.1" diff --git a/homeassistant/components/binary_sensor/.translations/bg.json b/homeassistant/components/binary_sensor/.translations/bg.json new file mode 100644 index 00000000000..9b9741b9601 --- /dev/null +++ b/homeassistant/components/binary_sensor/.translations/bg.json @@ -0,0 +1,92 @@ +{ + "device_automation": { + "condition_type": { + "is_bat_low": "{entity_name} \u0431\u0430\u0442\u0435\u0440\u0438\u044f\u0442\u0430 \u0435 \u0438\u0437\u0442\u043e\u0449\u0435\u043d\u0430", + "is_cold": "{entity_name} \u0435 \u0441\u0442\u0443\u0434\u0435\u043d", + "is_connected": "{entity_name} \u0435 \u0441\u0432\u044a\u0440\u0437\u0430\u043d", + "is_gas": "{entity_name} \u043e\u0442\u043a\u0440\u0438\u0432\u0430 \u0433\u0430\u0437", + "is_hot": "{entity_name} \u0435 \u0433\u043e\u0440\u0435\u0449", + "is_light": "{entity_name} \u0437\u0430\u0441\u0438\u0447\u0430 \u0441\u0432\u0435\u0442\u043b\u0438\u043d\u0430", + "is_locked": "{entity_name} \u0435 \u0437\u0430\u043a\u043b\u044e\u0447\u0435\u043d", + "is_moist": "{entity_name} \u0435 \u0432\u043b\u0430\u0436\u0435\u043d", + "is_motion": "{entity_name} \u043e\u0442\u043a\u0440\u0438\u0432\u0430 \u0434\u0432\u0438\u0436\u0435\u043d\u0438\u0435", + "is_moving": "{entity_name} \u0441\u0435 \u0434\u0432\u0438\u0436\u0438", + "is_no_gas": "{entity_name} \u043d\u0435 \u043e\u0442\u043a\u0440\u0438\u0432\u0430 \u0433\u0430\u0437", + "is_no_light": "{entity_name} \u043d\u0435 \u0437\u0430\u0441\u0438\u0447\u0430 \u0441\u0432\u0435\u0442\u043b\u0438\u043d\u0430", + "is_no_motion": "{entity_name} \u043d\u0435 \u043e\u0442\u043a\u0440\u0438\u0432\u0430 \u0434\u0432\u0438\u0436\u0435\u043d\u0438\u0435", + "is_no_problem": "{entity_name} \u043d\u0435 \u043e\u0442\u043a\u0440\u0438\u0432\u0430 \u043f\u0440\u043e\u0431\u043b\u0435\u043c", + "is_no_smoke": "{entity_name} \u043d\u0435 \u043e\u0442\u043a\u0440\u0438\u0432\u0430 \u0434\u0438\u043c", + "is_no_sound": "{entity_name} \u043d\u0435 \u0437\u0430\u0441\u0438\u0447\u0430 \u0437\u0432\u0443\u043a", + "is_no_vibration": "{entity_name} \u043d\u0435 \u0437\u0430\u0441\u0438\u0447\u0430 \u0432\u0438\u0431\u0440\u0430\u0446\u0438\u0438", + "is_not_bat_low": "{entity_name} \u0431\u0430\u0442\u0435\u0440\u0438\u044f\u0442\u0430 \u0435 \u0437\u0430\u0440\u0435\u0434\u0435\u043d\u0430", + "is_not_cold": "{entity_name} \u043d\u0435 \u0435 \u0441\u0442\u0443\u0434\u0435\u043d", + "is_not_connected": "{entity_name} \u0435 \u0440\u0430\u0437\u043a\u0430\u0447\u0435\u043d", + "is_not_hot": "{entity_name} \u043d\u0435 \u0435 \u0433\u043e\u0440\u0435\u0449", + "is_not_locked": "{entity_name} \u0435 \u043e\u0442\u043a\u043b\u044e\u0447\u0435\u043d", + "is_not_moist": "{entity_name} \u0435 \u0441\u0443\u0445", + "is_not_moving": "{entity_name} \u043d\u0435 \u0441\u0435 \u0434\u0432\u0438\u0436\u0438", + "is_not_occupied": "{entity_name} \u043d\u0435 \u0435 \u0437\u0430\u0435\u0442", + "is_not_open": "{entity_name} \u0435 \u0437\u0430\u0442\u0432\u043e\u0440\u0435\u043d", + "is_not_plugged_in": "{entity_name} \u0435 \u0438\u0437\u043a\u043b\u044e\u0447\u0435\u043d", + "is_not_powered": "{entity_name} \u043d\u0435 \u0441\u0435 \u0437\u0430\u0445\u0440\u0430\u043d\u0432\u0430", + "is_not_present": "{entity_name} \u043d\u0435 \u0435 \u043d\u0430\u043b\u0438\u0446\u0435", + "is_not_unsafe": "{entity_name} \u0435 \u0431\u0435\u0437\u043e\u043f\u0430\u0441\u0435\u043d", + "is_occupied": "{entity_name} \u0435 \u0437\u0430\u0435\u0442", + "is_off": "{entity_name} \u0435 \u0438\u0437\u043a\u043b\u044e\u0447\u0435\u043d", + "is_on": "{entity_name} \u0435 \u0432\u043a\u043b\u044e\u0447\u0435\u043d", + "is_open": "{entity_name} \u0435 \u043e\u0442\u0432\u043e\u0440\u0435\u043d", + "is_plugged_in": "{entity_name} \u0435 \u0432\u043a\u043b\u044e\u0447\u0435\u043d", + "is_powered": "{entity_name} \u0441\u0435 \u0437\u0430\u0445\u0440\u0430\u043d\u0432\u0430", + "is_present": "{entity_name} \u043f\u0440\u0438\u0441\u044a\u0441\u0442\u0432\u0430", + "is_problem": "{entity_name} \u043e\u0442\u043a\u0440\u0438\u0432\u0430 \u043f\u0440\u043e\u0431\u043b\u0435\u043c", + "is_smoke": "{entity_name} \u043e\u0442\u043a\u0440\u0438\u0432\u0430 \u0434\u0438\u043c", + "is_sound": "{entity_name} \u043e\u0442\u043a\u0440\u0438\u0432\u0430 \u0437\u0432\u0443\u043a", + "is_unsafe": "{entity_name} \u043d\u0435 \u0435 \u0431\u0435\u0437\u043e\u043f\u0430\u0441\u0435\u043d", + "is_vibration": "{entity_name} \u0437\u0430\u0441\u0438\u0447\u0430 \u0432\u0438\u0431\u0440\u0430\u0446\u0438\u0438" + }, + "trigger_type": { + "bat_low": "{entity_name} \u0438\u0437\u0442\u043e\u0449\u0435\u043d\u0430 \u0431\u0430\u0442\u0435\u0440\u0438\u044f", + "closed": "{entity_name} \u0437\u0430\u0442\u0432\u043e\u0440\u0435\u043d", + "cold": "{entity_name} \u0441\u0435 \u0438\u0437\u0441\u0442\u0443\u0434\u0438", + "connected": "{entity_name} \u0441\u0432\u044a\u0440\u0437\u0430\u043d", + "gas": "{entity_name} \u0437\u0430\u043f\u043e\u0447\u043d\u0430 \u0434\u0430 \u043e\u0442\u043a\u0440\u0438\u0432\u0430 \u0433\u0430\u0437", + "hot": "{entity_name} \u0441\u0435 \u0441\u0442\u043e\u043f\u043b\u0438", + "light": "{entity_name} \u0437\u0430\u043f\u043e\u0447\u043d\u0430 \u0434\u0430 \u043e\u0442\u043a\u0440\u0438\u0432\u0430 \u0441\u0432\u0435\u0442\u043b\u0438\u043d\u0430", + "locked": "{entity_name} \u0437\u0430\u043a\u043b\u044e\u0447\u0435\u043d", + "moist\u00a7": "{entity_name} \u0441\u0442\u0430\u0432\u0430 \u0432\u043b\u0430\u0436\u0435\u043d", + "motion": "{entity_name} \u0437\u0430\u043f\u043e\u0447\u043d\u0430 \u043e\u0442\u043a\u0440\u0438\u0432\u0430\u043d\u0435 \u043d\u0430 \u0434\u0432\u0438\u0436\u0435\u043d\u0438\u0435", + "moving": "{entity_name} \u0437\u0430\u043f\u043e\u0447\u043d\u0430 \u043e\u0442\u043a\u0440\u0438\u0432\u0430 \u0434\u0432\u0438\u0436\u0435\u043d\u0438\u0435", + "no_gas": "{entity_name} \u0441\u043f\u0440\u044f \u0434\u0430 \u043e\u0442\u043a\u0440\u0438\u0432\u0430 \u0433\u0430\u0437", + "no_light": "{entity_name} \u0441\u043f\u0440\u044f \u0434\u0430 \u0437\u0430\u0441\u0438\u0447\u0430 \u0441\u0432\u0435\u0442\u043b\u0438\u043d\u0430", + "no_motion": "{entity_name} \u0441\u043f\u0440\u044f \u0434\u0430 \u043e\u0442\u043a\u0440\u0438\u0432\u0430 \u0434\u0432\u0438\u0436\u0435\u043d\u0438\u0435", + "no_problem": "{entity_name} \u0441\u043f\u0440\u044f \u0434\u0430 \u043e\u0442\u043a\u0440\u0438\u0432\u0430 \u043f\u0440\u043e\u0431\u043b\u0435\u043c", + "no_smoke": "{entity_name} \u0441\u043f\u0440\u044f \u0434\u0430 \u043e\u0442\u043a\u0440\u0438\u0432\u0430 \u0434\u0438\u043c", + "no_sound": "{entity_name} \u0441\u043f\u0440\u044f \u0434\u0430 \u043e\u0442\u043a\u0440\u0438\u0432\u0430 \u0437\u0432\u0443\u043a", + "no_vibration": "{entity_name} \u0441\u043f\u0440\u044f \u0434\u0430 \u0437\u0430\u0441\u0438\u0447\u0430 \u0432\u0438\u0431\u0440\u0430\u0446\u0438\u0438", + "not_bat_low": "{entity_name} \u0431\u0430\u0442\u0435\u0440\u0438\u044f\u0442\u0430 \u043d\u0435 \u0435 \u0438\u0437\u0442\u043e\u0449\u0435\u043d\u0430", + "not_cold": "{entity_name} \u0441\u0435 \u0441\u0442\u043e\u043f\u043b\u0438", + "not_connected": "{entity_name} \u0438\u0437\u043a\u043b\u044e\u0447\u0435\u043d", + "not_hot": "{entity_name} \u043e\u0445\u043b\u0430\u0434\u043d\u044f", + "not_locked": "{entity_name} \u043e\u0442\u043a\u043b\u044e\u0447\u0435\u043d", + "not_moist": "{entity_name} \u0441\u0442\u0430\u0432\u0430 \u0441\u0443\u0445", + "not_moving": "{entity_name} \u0441\u043f\u0440\u044f \u0434\u0430 \u0441\u0435 \u0434\u0432\u0438\u0436\u0438", + "not_occupied": "{entity_name} \u0432\u0435\u0447\u0435 \u043d\u0435 \u0435 \u0437\u0430\u0435\u0442", + "not_plugged_in": "{entity_name} \u0438\u0437\u043a\u043b\u044e\u0447\u0435\u043d", + "not_powered": "{entity_name} \u043d\u0435 \u0441\u0435 \u0437\u0430\u0445\u0440\u0430\u043d\u0432\u0430", + "not_present": "{entity_name} \u043d\u0435 \u043f\u0440\u0438\u0441\u044a\u0441\u0442\u0432\u0430", + "not_unsafe": "{entity_name} \u0441\u0442\u0430\u043d\u0430 \u0431\u0435\u0437\u043e\u043f\u0430\u0441\u0435\u043d", + "occupied": "{entity_name} \u0441\u0442\u0430\u043d\u0430 \u0437\u0430\u0435\u0442", + "opened": "{entity_name} \u0441\u0435 \u043e\u0442\u0432\u043e\u0440\u0438", + "plugged_in": "{entity_name} \u0441\u0435 \u0432\u043a\u043b\u044e\u0447\u0438", + "powered": "{entity_name} \u0441\u0435 \u0437\u0430\u0445\u0440\u0430\u043d\u0432\u0430", + "present": "{entity_name} \u043f\u0440\u0438\u0441\u044a\u0441\u0442\u0432\u0430", + "problem": "{entity_name} \u0437\u0430\u043f\u043e\u0447\u043d\u0430 \u0434\u0430 \u043e\u0442\u043a\u0440\u0438\u0432\u0430 \u043f\u0440\u043e\u0431\u043b\u0435\u043c", + "smoke": "{entity_name} \u0437\u0430\u043f\u043e\u0447\u043d\u0430 \u0434\u0430 \u043e\u0442\u043a\u0440\u0438\u0432\u0430 \u0434\u0438\u043c", + "sound": "{entity_name} \u0437\u0430\u043f\u043e\u0447\u043d\u0430 \u0434\u0430 \u0437\u0430\u0441\u0438\u0447\u0430 \u0437\u0432\u0443\u043a", + "turned_off": "{entity_name} \u0435 \u0438\u0437\u043a\u043b\u044e\u0447\u0435\u043d", + "turned_on": "{entity_name} \u0435 \u0432\u043a\u043b\u044e\u0447\u0435\u043d", + "unsafe": "{entity_name} \u0441\u0442\u0430\u043d\u0430 \u043e\u043f\u0430\u0441\u0435\u043d", + "vibration": "{entity_name} \u0437\u0430\u043f\u043e\u0447\u043d\u0430 \u0434\u0430 \u0437\u0430\u0441\u0438\u0447\u0430 \u0432\u0438\u0431\u0440\u0430\u0446\u0438\u0438" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/binary_sensor/.translations/ca.json b/homeassistant/components/binary_sensor/.translations/ca.json new file mode 100644 index 00000000000..de7d837b12c --- /dev/null +++ b/homeassistant/components/binary_sensor/.translations/ca.json @@ -0,0 +1,92 @@ +{ + "device_automation": { + "condition_type": { + "is_bat_low": "Bateria de {entity_name} baixa", + "is_cold": "{entity_name} est\u00e0 fred", + "is_connected": "{entity_name} est\u00e0 connectat", + "is_gas": "{entity_name} est\u00e0 detectant gas", + "is_hot": "{entity_name} est\u00e0 calent", + "is_light": "{entity_name} est\u00e0 detectant llum", + "is_locked": "{entity_name} est\u00e0 bloquejat", + "is_moist": "{entity_name} est\u00e0 humit", + "is_motion": "{entity_name} est\u00e0 detectant moviment", + "is_moving": "{entity_name} s'est\u00e0 movent", + "is_no_gas": "{entity_name} no detecta gas", + "is_no_light": "{entity_name} no detecta llum", + "is_no_motion": "{entity_name} no detecta moviment", + "is_no_problem": "{entity_name} no est\u00e0 detectant cap problema", + "is_no_smoke": "{entity_name} no detecta fum", + "is_no_sound": "{entity_name} no detecta so", + "is_no_vibration": "{entity_name} no detecta vibraci\u00f3", + "is_not_bat_low": "Bateria de {entity_name} normal", + "is_not_cold": "{entity_name} no est\u00e0 fred", + "is_not_connected": "{entity_name} est\u00e0 desconnectat", + "is_not_hot": "{entity_name} no est\u00e0 calent", + "is_not_locked": "{entity_name} est\u00e0 desbloquejat", + "is_not_moist": "{entity_name} est\u00e0 sec", + "is_not_moving": "{entity_name} no s'est\u00e0 movent", + "is_not_occupied": "{entity_name} no est\u00e0 ocupat", + "is_not_open": "{entity_name} est\u00e0 tancat", + "is_not_plugged_in": "{entity_name} est\u00e0 desendollat", + "is_not_powered": "{entity_name} no est\u00e0 alimentat", + "is_not_present": "{entity_name} no est\u00e0 present", + "is_not_unsafe": "{entity_name} \u00e9s segur", + "is_occupied": "{entity_name} est\u00e0 ocupat", + "is_off": "{entity_name} est\u00e0 apagat", + "is_on": "{entity_name} est\u00e0 enc\u00e8s", + "is_open": "{entity_name} est\u00e0 obert", + "is_plugged_in": "{entity_name} est\u00e0 endollat", + "is_powered": "{entity_name} est\u00e0 alimentat", + "is_present": "{entity_name} est\u00e0 present", + "is_problem": "{entity_name} est\u00e0 detectant un problema", + "is_smoke": "{entity_name} est\u00e0 detectant fum", + "is_sound": "{entity_name} est\u00e0 detectant so", + "is_unsafe": "{entity_name} \u00e9s insegur", + "is_vibration": "{entity_name} est\u00e0 detectant vibraci\u00f3" + }, + "trigger_type": { + "bat_low": "Bateria de {entity_name} baixa", + "closed": "{entity_name} est\u00e0 tancat", + "cold": "{entity_name} es torna fred", + "connected": "{entity_name} est\u00e0 connectat", + "gas": "{entity_name} ha comen\u00e7at a detectar gas", + "hot": "{entity_name} es torna calent", + "light": "{entity_name} ha comen\u00e7at a detectar llum", + "locked": "{entity_name} est\u00e0 bloquejat", + "moist\u00a7": "{entity_name} es torna humit", + "motion": "{entity_name} ha comen\u00e7at a detectar moviment", + "moving": "{entity_name} ha comen\u00e7at a moure's", + "no_gas": "{entity_name} ha deixat de detectar gas", + "no_light": "{entity_name} ha deixat de detectar llum", + "no_motion": "{entity_name} ha deixat de detectar moviment", + "no_problem": "{entity_name} ha deixat de detectar un problema", + "no_smoke": "{entity_name} ha deixat de detectar fum", + "no_sound": "{entity_name} ha deixat de detectar so", + "no_vibration": "{entity_name} ha deixat de detectar vibraci\u00f3", + "not_bat_low": "Bateria de {entity_name} normal", + "not_cold": "{entity_name} es torna no-fred", + "not_connected": "{entity_name} est\u00e0 desconnectat", + "not_hot": "{entity_name} es torna no-calent", + "not_locked": "{entity_name} est\u00e0 desbloquejat", + "not_moist": "{entity_name} es torna sec", + "not_moving": "{entity_name} ha parat de moure's", + "not_occupied": "{entity_name} es desocupa", + "not_plugged_in": "{entity_name} desendollat", + "not_powered": "{entity_name} no est\u00e0 alimentat", + "not_present": "{entity_name} no est\u00e0 present", + "not_unsafe": "{entity_name} es torna segur", + "occupied": "{entity_name} s'ocupa", + "opened": "{entity_name} s'ha obert", + "plugged_in": "{entity_name} s'ha endollat", + "powered": "{entity_name} alimentat", + "present": "{entity_name} present", + "problem": "{entity_name} ha comen\u00e7at a detectar un problema", + "smoke": "{entity_name} ha comen\u00e7at a detectar fum", + "sound": "{entity_name} ha comen\u00e7at a detectar so", + "turned_off": "{entity_name} apagat", + "turned_on": "{entity_name} enc\u00e8s", + "unsafe": "{entity_name} es torna insegur", + "vibration": "{entity_name} ha comen\u00e7at a detectar vibraci\u00f3" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/binary_sensor/.translations/da.json b/homeassistant/components/binary_sensor/.translations/da.json new file mode 100644 index 00000000000..56822c2365c --- /dev/null +++ b/homeassistant/components/binary_sensor/.translations/da.json @@ -0,0 +1,65 @@ +{ + "device_automation": { + "condition_type": { + "is_cold": "{entity_name} er kold", + "is_connected": "{entity_name} er tilsluttet", + "is_gas": "{entity_name} registrerer gas", + "is_hot": "{entity_name} er varm", + "is_light": "{entity_name} registrerer lys", + "is_locked": "{entity_name} er l\u00e5st", + "is_moist": "{entity_name} er fugtig", + "is_motion": "{entity_name} registrerer bev\u00e6gelse", + "is_moving": "{entity_name} bev\u00e6ger sig", + "is_no_gas": "{entity_name} registrerer ikke gas", + "is_no_light": "{entity_name} registrerer ikke lys", + "is_no_motion": "{entity_name} registrerer ikke bev\u00e6gelse", + "is_no_problem": "{entity_name} registrerer ikke noget problem", + "is_no_smoke": "{entity_name} registrerer ikke r\u00f8g", + "is_no_sound": "{entity_name} registrerer ikke lyd", + "is_no_vibration": "{entity_name} registrerer ikke vibration", + "is_not_cold": "{entity_name} er ikke kold", + "is_not_connected": "{entity_name} er afbrudt", + "is_not_hot": "{entity_name} er ikke varm", + "is_not_locked": "{entity_name} er l\u00e5st op", + "is_not_moist": "{entity_name} er t\u00f8r", + "is_not_moving": "{entity_name} bev\u00e6ger sig ikke", + "is_not_occupied": "{entity_name} er ikke optaget", + "is_not_open": "{entity_name} er lukket", + "is_not_present": "{entity_name} er ikke til stede", + "is_not_unsafe": "{entity_name} er sikker", + "is_occupied": "{entity_name} er optaget", + "is_open": "{entity_name} er \u00e5ben", + "is_problem": "{entity_name} registrerer problem", + "is_smoke": "{entity_name} registrerer r\u00f8g", + "is_sound": "{entity_name} registrerer lyd", + "is_unsafe": "{entity_name} er usikker", + "is_vibration": "{entity_name} registrerer vibration" + }, + "trigger_type": { + "closed": "{entity_name} lukket", + "cold": "{entity_name} blev kold", + "connected": "{entity_name} tilsluttet", + "moist\u00a7": "{entity_name} blev fugtig", + "motion": "{entity_name} begyndte at registrere bev\u00e6gelse", + "moving": "{entity_name} begyndte at bev\u00e6ge sig", + "no_gas": "{entity_name} stoppede med at registrere gas", + "no_light": "{entity_name} stoppede med at registrere lys", + "no_motion": "{entity_name} stoppede med at registrere bev\u00e6gelse", + "no_problem": "{entity_name} stoppede med at registrere problem", + "no_smoke": "{entity_name} stoppede med at registrere r\u00f8g", + "no_sound": "{entity_name} stoppede med at registrere lyd", + "no_vibration": "{entity_name} stoppede med at registrere vibration", + "not_connected": "{entity_name} afbrudt", + "not_hot": "{entity_name} blev ikke varm", + "not_locked": "{entity_name} l\u00e5st op", + "not_moist": "{entity_name} blev t\u00f8r", + "not_present": "{entity_name} ikke til stede", + "not_unsafe": "{entity_name} blev sikker", + "occupied": "{entity_name} blev optaget", + "present": "{entity_name} til stede", + "problem": "{entity_name} begyndte at registrere problem", + "smoke": "{entity_name} begyndte at registrere r\u00f8g", + "sound": "{entity_name} begyndte at registrere lyd" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/binary_sensor/.translations/en.json b/homeassistant/components/binary_sensor/.translations/en.json new file mode 100644 index 00000000000..93b61893980 --- /dev/null +++ b/homeassistant/components/binary_sensor/.translations/en.json @@ -0,0 +1,94 @@ +{ + "device_automation": { + "condition_type": { + "is_bat_low": "{entity_name} battery is low", + "is_cold": "{entity_name} is cold", + "is_connected": "{entity_name} is connected", + "is_gas": "{entity_name} is detecting gas", + "is_hot": "{entity_name} is hot", + "is_light": "{entity_name} is detecting light", + "is_locked": "{entity_name} is locked", + "is_moist": "{entity_name} is moist", + "is_motion": "{entity_name} is detecting motion", + "is_moving": "{entity_name} is moving", + "is_no_gas": "{entity_name} is not detecting gas", + "is_no_light": "{entity_name} is not detecting light", + "is_no_motion": "{entity_name} is not detecting motion", + "is_no_problem": "{entity_name} is not detecting problem", + "is_no_smoke": "{entity_name} is not detecting smoke", + "is_no_sound": "{entity_name} is not detecting sound", + "is_no_vibration": "{entity_name} is not detecting vibration", + "is_not_bat_low": "{entity_name} battery is normal", + "is_not_cold": "{entity_name} is not cold", + "is_not_connected": "{entity_name} is disconnected", + "is_not_hot": "{entity_name} is not hot", + "is_not_locked": "{entity_name} is unlocked", + "is_not_moist": "{entity_name} is dry", + "is_not_moving": "{entity_name} is not moving", + "is_not_occupied": "{entity_name} is not occupied", + "is_not_open": "{entity_name} is closed", + "is_not_plugged_in": "{entity_name} is unplugged", + "is_not_powered": "{entity_name} is not powered", + "is_not_present": "{entity_name} is not present", + "is_not_unsafe": "{entity_name} is safe", + "is_occupied": "{entity_name} is occupied", + "is_off": "{entity_name} is off", + "is_on": "{entity_name} is on", + "is_open": "{entity_name} is open", + "is_plugged_in": "{entity_name} is plugged in", + "is_powered": "{entity_name} is powered", + "is_present": "{entity_name} is present", + "is_problem": "{entity_name} is detecting problem", + "is_smoke": "{entity_name} is detecting smoke", + "is_sound": "{entity_name} is detecting sound", + "is_unsafe": "{entity_name} is unsafe", + "is_vibration": "{entity_name} is detecting vibration" + }, + "trigger_type": { + "bat_low": "{entity_name} battery low", + "closed": "{entity_name} closed", + "cold": "{entity_name} became cold", + "connected": "{entity_name} connected", + "gas": "{entity_name} started detecting gas", + "hot": "{entity_name} became hot", + "light": "{entity_name} started detecting light", + "locked": "{entity_name} locked", + "moist": "{entity_name} became moist", + "moist\u00a7": "{entity_name} became moist", + "motion": "{entity_name} started detecting motion", + "moving": "{entity_name} started moving", + "no_gas": "{entity_name} stopped detecting gas", + "no_light": "{entity_name} stopped detecting light", + "no_motion": "{entity_name} stopped detecting motion", + "no_problem": "{entity_name} stopped detecting problem", + "no_smoke": "{entity_name} stopped detecting smoke", + "no_sound": "{entity_name} stopped detecting sound", + "no_vibration": "{entity_name} stopped detecting vibration", + "not_bat_low": "{entity_name} battery normal", + "not_cold": "{entity_name} became not cold", + "not_connected": "{entity_name} disconnected", + "not_hot": "{entity_name} became not hot", + "not_locked": "{entity_name} unlocked", + "not_moist": "{entity_name} became dry", + "not_moving": "{entity_name} stopped moving", + "not_occupied": "{entity_name} became not occupied", + "not_opened": "{entity_name} closed", + "not_plugged_in": "{entity_name} unplugged", + "not_powered": "{entity_name} not powered", + "not_present": "{entity_name} not present", + "not_unsafe": "{entity_name} became safe", + "occupied": "{entity_name} became occupied", + "opened": "{entity_name} opened", + "plugged_in": "{entity_name} plugged in", + "powered": "{entity_name} powered", + "present": "{entity_name} present", + "problem": "{entity_name} started detecting problem", + "smoke": "{entity_name} started detecting smoke", + "sound": "{entity_name} started detecting sound", + "turned_off": "{entity_name} turned off", + "turned_on": "{entity_name} turned on", + "unsafe": "{entity_name} became unsafe", + "vibration": "{entity_name} started detecting vibration" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/binary_sensor/.translations/es-419.json b/homeassistant/components/binary_sensor/.translations/es-419.json new file mode 100644 index 00000000000..f1c20e5346b --- /dev/null +++ b/homeassistant/components/binary_sensor/.translations/es-419.json @@ -0,0 +1,65 @@ +{ + "device_automation": { + "condition_type": { + "is_bat_low": "{entity_name} la bater\u00eda est\u00e1 baja", + "is_cold": "{entity_name} est\u00e1 fr\u00edo", + "is_connected": "{entity_name} est\u00e1 conectado", + "is_gas": "{entity_name} est\u00e1 detectando gas", + "is_hot": "{entity_name} est\u00e1 caliente", + "is_light": "{entity_name} est\u00e1 detectando luz", + "is_locked": "{entity_name} est\u00e1 bloqueado", + "is_moist": "{entity_name} est\u00e1 h\u00famedo", + "is_motion": "{entity_name} est\u00e1 detectando movimiento", + "is_moving": "{entity_name} se est\u00e1 moviendo", + "is_no_gas": "{entity_name} no detecta gas", + "is_no_light": "{entity_name} no detecta luz", + "is_no_motion": "{entity_name} no detecta movimiento", + "is_no_problem": "{entity_name} no detecta el problema", + "is_no_smoke": "{entity_name} no detecta humo", + "is_no_sound": "{entity_name} no detecta sonido", + "is_no_vibration": "{entity_name} no detecta vibraciones", + "is_not_bat_low": "{entity_name} bater\u00eda est\u00e1 normal", + "is_not_cold": "{entity_name} no est\u00e1 fr\u00edo", + "is_not_connected": "{entity_name} est\u00e1 desconectado", + "is_not_hot": "{entity_name} no est\u00e1 caliente", + "is_not_locked": "{entity_name} est\u00e1 desbloqueado", + "is_not_moist": "{entity_name} est\u00e1 seco", + "is_not_moving": "{entity_name} no se mueve", + "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_powered": "{entity_name} est\u00e1 encendido", + "is_present": "{entity_name} est\u00e1 presente", + "is_problem": "{entity_name} est\u00e1 detectando un problema", + "is_smoke": "{entity_name} est\u00e1 detectando humo", + "is_sound": "{entity_name} est\u00e1 detectando sonido", + "is_unsafe": "{entity_name} es inseguro", + "is_vibration": "{entity_name} est\u00e1 detectando vibraciones" + }, + "trigger_type": { + "bat_low": "{entity_name} bater\u00eda baja", + "closed": "{entity_name} cerrado", + "cold": "{entity_name} se enfri\u00f3", + "connected": "{entity_name} conectado", + "gas": "{entity_name} comenz\u00f3 a detectar gas", + "hot": "{entity_name} se calent\u00f3", + "light": "{entity_name} comenz\u00f3 a detectar luz", + "locked": "{entity_name} bloqueado", + "moist\u00a7": "{entity_name} se humedeci\u00f3", + "motion": "{entity_name} comenz\u00f3 a detectar movimiento", + "moving": "{entity_name} comenz\u00f3 a moverse", + "no_gas": "{entity_name} dej\u00f3 de detectar gas", + "no_light": "{entity_name} dej\u00f3 de detectar luz", + "no_motion": "{entity_name} dej\u00f3 de detectar movimiento", + "no_problem": "{entity_name} dej\u00f3 de detectar problemas", + "no_smoke": "{entity_name} dej\u00f3 de detectar humo", + "no_sound": "{entity_name} dej\u00f3 de detectar sonido", + "no_vibration": "{entity_name} dej\u00f3 de detectar vibraciones", + "not_bat_low": "{entity_name} bater\u00eda normal", + "not_cold": "{entity_name} no se enfri\u00f3", + "not_connected": "{entity_name} desconectado", + "not_hot": "{entity_name} no se calent\u00f3", + "not_locked": "{entity_name} desbloqueado" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/binary_sensor/.translations/es.json b/homeassistant/components/binary_sensor/.translations/es.json new file mode 100644 index 00000000000..8e2d326d9d3 --- /dev/null +++ b/homeassistant/components/binary_sensor/.translations/es.json @@ -0,0 +1,92 @@ +{ + "device_automation": { + "condition_type": { + "is_bat_low": "{entity_name} la bater\u00eda est\u00e1 baja", + "is_cold": "{entity_name} est\u00e1 fr\u00edo", + "is_connected": "{entity_name} est\u00e1 conectado", + "is_gas": "{entity_name} est\u00e1 detectando gas", + "is_hot": "{entity_name} est\u00e1 caliente", + "is_light": "{entity_name} est\u00e1 detectando luz", + "is_locked": "{entity_name} est\u00e1 bloqueado", + "is_moist": "{entity_name} est\u00e1 h\u00famedo", + "is_motion": "{entity_name} est\u00e1 detectando movimiento", + "is_moving": "{entity_name} se est\u00e1 moviendo", + "is_no_gas": "{entity_name} no detecta gas", + "is_no_light": "{entity_name} no detecta la luz", + "is_no_motion": "{entity_name} no detecta movimiento", + "is_no_problem": "{entity_name} no detecta el problema", + "is_no_smoke": "{entity_name} no detecta humo", + "is_no_sound": "{entity_name} no detecta sonido", + "is_no_vibration": "{entity_name} no detecta vibraci\u00f3n", + "is_not_bat_low": "La bater\u00eda de {entity_name} es normal", + "is_not_cold": "{entity_name} no est\u00e1 fr\u00edo", + "is_not_connected": "{entity_name} est\u00e1 desconectado", + "is_not_hot": "{entity_name} no est\u00e1 caliente", + "is_not_locked": "{entity_name} est\u00e1 desbloqueado", + "is_not_moist": "{entity_name} est\u00e1 seco", + "is_not_moving": "{entity_name} no se mueve", + "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 alimentaci\u00f3n", + "is_not_present": "{entity_name} no est\u00e1 presente", + "is_not_unsafe": "{entity_name} es seguro", + "is_occupied": "{entity_name} est\u00e1 ocupado", + "is_off": "{entity_name} est\u00e1 apagado", + "is_on": "{entity_name} est\u00e1 activado", + "is_open": "{entity_name} est\u00e1 abierto", + "is_plugged_in": "{entity_name} est\u00e1 conectado", + "is_powered": "{entity_name} est\u00e1 activado", + "is_present": "{entity_name} est\u00e1 presente", + "is_problem": "{entity_name} est\u00e1 detectando un problema", + "is_smoke": "{entity_name} est\u00e1 detectando humo", + "is_sound": "{entity_name} est\u00e1 detectando sonido", + "is_unsafe": "{entity_name} no es seguro", + "is_vibration": "{entity_name} est\u00e1 detectando vibraciones" + }, + "trigger_type": { + "bat_low": "{entity_name} bater\u00eda baja", + "closed": "{entity_name} cerrado", + "cold": "{entity_name} se enfri\u00f3", + "connected": "{entity_name} conectado", + "gas": "{entity_name} empez\u00f3 a detectar gas", + "hot": "{entity_name} se est\u00e1 calentando", + "light": "{entity_name} empez\u00f3 a detectar la luz", + "locked": "{entity_name} bloqueado", + "moist\u00a7": "{entity_name} se humedeci\u00f3", + "motion": "{entity_name} comenz\u00f3 a detectar movimiento", + "moving": "{entity_name} empez\u00f3 a moverse", + "no_gas": "{entity_name} dej\u00f3 de detectar gas", + "no_light": "{entity_name} dej\u00f3 de detectar la luz", + "no_motion": "{entity_name} dej\u00f3 de detectar movimiento", + "no_problem": "{entity_name} dej\u00f3 de detectar el problema", + "no_smoke": "{entity_name} dej\u00f3 de detectar humo", + "no_sound": "{entity_name} dej\u00f3 de detectar sonido", + "no_vibration": "{entity_name} dej\u00f3 de detectar vibraci\u00f3n", + "not_bat_low": "{entity_name} bater\u00eda normal", + "not_cold": "{entity_name} no se enfri\u00f3", + "not_connected": "{entity_name} desconectado", + "not_hot": "{entity_name} no se calent\u00f3", + "not_locked": "{entity_name} desbloqueado", + "not_moist": "{entity_name} se sec\u00f3", + "not_moving": "{entity_name} dej\u00f3 de moverse", + "not_occupied": "{entity_name} no est\u00e1 ocupado", + "not_plugged_in": "{entity_name} desconectado", + "not_powered": "{entity_name} no est\u00e1 activado", + "not_present": "{entity_name} no est\u00e1 presente", + "not_unsafe": "{entity_name} se volvi\u00f3 seguro", + "occupied": "{entity_name} se convirti\u00f3 en ocupado", + "opened": "{entity_name} abierto", + "plugged_in": "{nombre_de_la_entidad} conectado", + "powered": "{entity_name} alimentado", + "present": "{entity_name} presente", + "problem": "{entity_name} empez\u00f3 a detectar problemas", + "smoke": "{entity_name} empez\u00f3 a detectar humo", + "sound": "{entity_name} empez\u00f3 a detectar sonido", + "turned_off": "{entity_name} desactivado", + "turned_on": "{entity_name} activado", + "unsafe": "{entity_name} se volvi\u00f3 inseguro", + "vibration": "{entity_name} empez\u00f3 a detectar vibraciones" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/binary_sensor/.translations/fr.json b/homeassistant/components/binary_sensor/.translations/fr.json new file mode 100644 index 00000000000..80792f16635 --- /dev/null +++ b/homeassistant/components/binary_sensor/.translations/fr.json @@ -0,0 +1,54 @@ +{ + "device_automation": { + "condition_type": { + "is_bat_low": "{entity_name} batterie faible", + "is_cold": "{entity_name} est froid", + "is_connected": "{entity_name} est connect\u00e9", + "is_gas": "{entity_name} d\u00e9tecte du gaz", + "is_hot": "{entity_name} est chaud", + "is_light": "{entity_name} d\u00e9tecte de la lumi\u00e8re", + "is_locked": "{entity_name} est verrouill\u00e9", + "is_moist": "{entity_name} est humide", + "is_motion": "{entity_name} d\u00e9tecte un mouvement", + "is_moving": "{entity_name} se d\u00e9place", + "is_no_gas": "{entity_name} ne d\u00e9tecte pas de gaz", + "is_no_light": "{entity_name} ne d\u00e9tecte pas de lumi\u00e8re", + "is_no_motion": "{entity_name} ne d\u00e9tecte pas de mouvement", + "is_no_problem": "{entity_name} ne d\u00e9tecte pas de probl\u00e8me", + "is_no_smoke": "{entity_name} ne d\u00e9tecte pas de fum\u00e9e", + "is_no_sound": "{entity_name} ne d\u00e9tecte pas de son", + "is_no_vibration": "{entity_name} ne d\u00e9tecte pas de vibration", + "is_not_bat_low": "{entity_name} batterie normale", + "is_not_cold": "{entity_name} n'est pas froid", + "is_not_connected": "{entity_name} est d\u00e9connect\u00e9", + "is_not_hot": "{entity_name} n'est pas chaud", + "is_not_locked": "{entity_name} est d\u00e9verrouill\u00e9", + "is_not_moist": "{entity_name} est sec", + "is_not_moving": "{entity_name} ne bouge pas", + "is_not_occupied": "{entity_name} n'est pas occup\u00e9", + "is_not_open": "{entity_name} est ferm\u00e9", + "is_not_plugged_in": "{entity_name} est d\u00e9branch\u00e9", + "is_not_powered": "{entity_name} n'est pas aliment\u00e9", + "is_not_present": "{entity_name} n'est pas pr\u00e9sent", + "is_not_unsafe": "{entity_name} est en s\u00e9curit\u00e9", + "is_occupied": "{entity_name} est occup\u00e9", + "is_off": "{entity_name} est d\u00e9sactiv\u00e9", + "is_on": "{entity_name} est activ\u00e9", + "is_open": "{entity_name} est ouvert", + "is_plugged_in": "{entity_name} est branch\u00e9", + "is_powered": "{entity_name} est aliment\u00e9", + "is_present": "{entity_name} est pr\u00e9sent", + "is_problem": "{entity_name} d\u00e9tecte un probl\u00e8me", + "is_smoke": "{entity_name} d\u00e9tecte de la fum\u00e9e", + "is_sound": "{entity_name} d\u00e9tecte du son" + }, + "trigger_type": { + "smoke": "{entity_name} commenc\u00e9 \u00e0 d\u00e9tecter la fum\u00e9e", + "sound": "{entity_name} commenc\u00e9 \u00e0 d\u00e9tecter le son", + "turned_off": "{entity_name} d\u00e9sactiv\u00e9", + "turned_on": "{entity_name} activ\u00e9", + "unsafe": "{entity_name} est devenu dangereux", + "vibration": "{entity_name} a commenc\u00e9 \u00e0 d\u00e9tecter les vibrations" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/binary_sensor/.translations/hu.json b/homeassistant/components/binary_sensor/.translations/hu.json new file mode 100644 index 00000000000..e53d918f98d --- /dev/null +++ b/homeassistant/components/binary_sensor/.translations/hu.json @@ -0,0 +1,92 @@ +{ + "device_automation": { + "condition_type": { + "is_bat_low": "{entity_name} akkufesz\u00fclts\u00e9g alacsony", + "is_cold": "{entity_name} hideg", + "is_connected": "{entity_name} csatlakoztatva van", + "is_gas": "{entity_name} g\u00e1zt \u00e9rz\u00e9kel", + "is_hot": "{entity_name} forr\u00f3", + "is_light": "{entity_name} f\u00e9nyt \u00e9rz\u00e9kel", + "is_locked": "{entity_name} z\u00e1rva van", + "is_moist": "{entity_name} nedves", + "is_motion": "{entity_name} mozg\u00e1st \u00e9rz\u00e9kel", + "is_moving": "{entity_name} mozog", + "is_no_gas": "{entity_name} nem \u00e9rz\u00e9kel g\u00e1zt", + "is_no_light": "{entity_name} nem \u00e9rz\u00e9kel f\u00e9nyt", + "is_no_motion": "{entity_name} nem \u00e9rz\u00e9kel mozg\u00e1st", + "is_no_problem": "{entity_name} nem \u00e9szlel probl\u00e9m\u00e1t", + "is_no_smoke": "{entity_name} nem \u00e9rz\u00e9kel f\u00fcst\u00f6t", + "is_no_sound": "{entity_name} nem \u00e9rz\u00e9kel hangot", + "is_no_vibration": "{entity_name} nem \u00e9rz\u00e9kel rezg\u00e9st", + "is_not_bat_low": "{entity_name} akkufesz\u00fclts\u00e9g megfelel\u0151", + "is_not_cold": "{entity_name} nem hideg", + "is_not_connected": "{entity_name} le van csatlakoztatva", + "is_not_hot": "{entity_name} nem forr\u00f3", + "is_not_locked": "{entity_name} nyitva van", + "is_not_moist": "{entity_name} sz\u00e1raz", + "is_not_moving": "{entity_name} nem mozog", + "is_not_occupied": "{entity_name} nem foglalt", + "is_not_open": "{entity_name} z\u00e1rva van", + "is_not_plugged_in": "{entity_name} nincs csatlakoztatva", + "is_not_powered": "{entity_name} nincs fesz\u00fcts\u00e9g alatt", + "is_not_present": "{entity_name} nincs jelen", + "is_not_unsafe": "{entity_name} biztons\u00e1gos", + "is_occupied": "{entity_name} foglalt", + "is_off": "{entity_name} ki van kapcsolva", + "is_on": "{entity_name} be van kapcsolva", + "is_open": "{entity_name} nyitva van", + "is_plugged_in": "{entity_name} csatlakoztatva van", + "is_powered": "{entity_name} fesz\u00fclts\u00e9g alatt van", + "is_present": "{entity_name} jelen van", + "is_problem": "{entity_name} probl\u00e9m\u00e1t \u00e9szlel", + "is_smoke": "{entity_name} f\u00fcst\u00f6t \u00e9rz\u00e9kel", + "is_sound": "{entity_name} hangot \u00e9rz\u00e9kel", + "is_unsafe": "{entity_name} nem biztons\u00e1gos", + "is_vibration": "{entity_name} rezg\u00e9st \u00e9rz\u00e9kel" + }, + "trigger_type": { + "bat_low": "{entity_name} akkufesz\u00fclts\u00e9g alacsony", + "closed": "{entity_name} be lett z\u00e1rva", + "cold": "{entity_name} hideg lett", + "connected": "{entity_name} csatlakozott", + "gas": "{entity_name} g\u00e1zt \u00e9rz\u00e9kel", + "hot": "{entity_name} felforr\u00f3sodott", + "light": "{entity_name} f\u00e9nyt \u00e9rz\u00e9kel", + "locked": "{entity_name} be lett z\u00e1rva", + "moist\u00a7": "{entity_name} nedves lett", + "motion": "{entity_name} mozg\u00e1st \u00e9rz\u00e9kel", + "moving": "{entity_name} mozog", + "no_gas": "{entity_name} m\u00e1r nem \u00e9rz\u00e9kel g\u00e1zt", + "no_light": "{entity_name} m\u00e1r nem \u00e9rz\u00e9kel f\u00e9nyt", + "no_motion": "{entity_name} m\u00e1r nem \u00e9rz\u00e9kel mozg\u00e1st", + "no_problem": "{entity_name} m\u00e1r nem \u00e9szlel probl\u00e9m\u00e1t", + "no_smoke": "{entity_name} m\u00e1r nem \u00e9rz\u00e9kel f\u00fcst\u00f6t", + "no_sound": "{entity_name} m\u00e1r nem \u00e9rz\u00e9kel hangot", + "no_vibration": "{entity_name} m\u00e1r nem \u00e9rz\u00e9kel rezg\u00e9st", + "not_bat_low": "{entity_name} akkufesz\u00fclts\u00e9g megfelel\u0151", + "not_cold": "{entity_name} m\u00e1r nem hideg", + "not_connected": "{entity_name} lecsatlakozott", + "not_hot": "{entity_name} m\u00e1r nem forr\u00f3", + "not_locked": "{entity_name} ki lett nyitva", + "not_moist": "{entity_name} sz\u00e1raz lett", + "not_moving": "{entity_name} m\u00e1r nem mozog", + "not_occupied": "{entity_name} m\u00e1r nem foglalt", + "not_plugged_in": "{entity_name} m\u00e1r nincs csatlakoztatva", + "not_powered": "{entity_name} m\u00e1r nincs fesz\u00fcts\u00e9g alatt", + "not_present": "{entity_name} m\u00e1r nincs jelen", + "not_unsafe": "{entity_name} biztons\u00e1gos lett", + "occupied": "{entity_name} foglalt lett", + "opened": "{entity_name} ki lett nyitva", + "plugged_in": "{entity_name} csatlakoztatva lett", + "powered": "{entity_name} m\u00e1r fesz\u00fclts\u00e9g alatt van", + "present": "{entity_name} m\u00e1r jelen van", + "problem": "{entity_name} probl\u00e9m\u00e1t \u00e9szlel", + "smoke": "{entity_name} f\u00fcst\u00f6t \u00e9rz\u00e9kel", + "sound": "{entity_name} hangot \u00e9rz\u00e9kel", + "turned_off": "{entity_name} ki lett kapcsolva", + "turned_on": "{entity_name} be lett kapcsolva", + "unsafe": "{entity_name} m\u00e1r nem biztons\u00e1gos", + "vibration": "{entity_name} rezg\u00e9st \u00e9rz\u00e9kel" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/binary_sensor/.translations/it.json b/homeassistant/components/binary_sensor/.translations/it.json new file mode 100644 index 00000000000..0583a4d4f74 --- /dev/null +++ b/homeassistant/components/binary_sensor/.translations/it.json @@ -0,0 +1,92 @@ +{ + "device_automation": { + "condition_type": { + "is_bat_low": "{entity_name} la batteria \u00e8 scarica", + "is_cold": "{entity_name} \u00e8 freddo", + "is_connected": "{entity_name} \u00e8 collegato", + "is_gas": "{entity_name} sta rilevando il gas", + "is_hot": "{entity_name} \u00e8 caldo", + "is_light": "{entity_name} sta rilevando la luce", + "is_locked": "{entity_name} \u00e8 bloccato", + "is_moist": "{entity_name} \u00e8 umido", + "is_motion": "{entity_name} sta rilevando il movimento", + "is_moving": "{entity_name} si sta muovendo", + "is_no_gas": "{entity_name} non sta rilevando il gas", + "is_no_light": "{entity_name} non sta rilevando la luce", + "is_no_motion": "{entity_name} non sta rilevando il movimento", + "is_no_problem": "{entity_name} non sta rilevando un problema", + "is_no_smoke": "{entity_name} non sta rilevando il fumo", + "is_no_sound": "{entity_name} non sta rilevando il suono", + "is_no_vibration": "{entity_name} non sta rilevando la vibrazione", + "is_not_bat_low": "{entity_name} la batteria \u00e8 normale", + "is_not_cold": "{entity_name} non \u00e8 freddo", + "is_not_connected": "{entity_name} \u00e8 disconnesso", + "is_not_hot": "{entity_name} non \u00e8 caldo", + "is_not_locked": "{entity_name} \u00e8 sbloccato", + "is_not_moist": "{entity_name} \u00e8 asciutto", + "is_not_moving": "{entity_name} non si sta muovendo", + "is_not_occupied": "{entity_name} non \u00e8 occupato", + "is_not_open": "{entity_name} \u00e8 chiuso", + "is_not_plugged_in": "{entity_name} \u00e8 collegato", + "is_not_powered": "{entity_name} non \u00e8 alimentato", + "is_not_present": "{entity_name} non \u00e8 presente", + "is_not_unsafe": "{entity_name} \u00e8 sicuro", + "is_occupied": "{entity_name} \u00e8 occupato", + "is_off": "{entity_name} \u00e8 spento", + "is_on": "{entity_name} \u00e8 acceso", + "is_open": "{entity_name} \u00e8 aperto", + "is_plugged_in": "{entity_name} \u00e8 collegato", + "is_powered": "{entity_name} \u00e8 alimentato", + "is_present": "{entity_name} \u00e8 presente", + "is_problem": "{entity_name} sta rilevando un problema", + "is_smoke": "{entity_name} sta rilevando il fumo", + "is_sound": "{entity_name} sta rilevando il suono", + "is_unsafe": "{entity_name} non \u00e8 sicuro", + "is_vibration": "{entity_name} sta rilevando la vibrazione" + }, + "trigger_type": { + "bat_low": "{entity_name} batteria scarica", + "closed": "{entity_name} \u00e8 chiuso", + "cold": "{entity_name} \u00e8 diventato freddo", + "connected": "{entity_name} connesso", + "gas": "{entity_name} ha iniziato a rilevare il gas", + "hot": "{entity_name} \u00e8 diventato caldo", + "light": "{entity_name} ha iniziato a rilevare la luce", + "locked": "{entity_name} bloccato", + "moist\u00a7": "{entity_name} \u00e8 diventato umido", + "motion": "{entity_name} ha iniziato a rilevare il movimento", + "moving": "{entity_name} ha iniziato a muoversi", + "no_gas": "{entity_name} ha smesso la rilevazione di gas", + "no_light": "{entity_name} smesso il rilevamento di luce", + "no_motion": "{nome_entit\u00e0} ha smesso di rilevare il movimento", + "no_problem": "{nome_entit\u00e0} ha smesso di rilevare un problema", + "no_smoke": "{entity_name} ha smesso la rilevazione di fumo", + "no_sound": "{nome_entit\u00e0} ha smesso di rilevare il suono", + "no_vibration": "{nome_entit\u00e0} ha smesso di rilevare le vibrazioni", + "not_bat_low": "{entity_name} batteria normale", + "not_cold": "{entity_name} non \u00e8 diventato freddo", + "not_connected": "{entity_name} \u00e8 disconnesso", + "not_hot": "{entity_name} non \u00e8 diventato caldo", + "not_locked": "{entity_name} \u00e8 sbloccato", + "not_moist": "{entity_name} \u00e8 diventato asciutto", + "not_moving": "{entity_name} ha smesso di muoversi", + "not_occupied": "{entity_name} non \u00e8 occupato", + "not_plugged_in": "{entity_name} \u00e8 scollegato", + "not_powered": "{entity_name} non \u00e8 alimentato", + "not_present": "{entity_name} non \u00e8 presente", + "not_unsafe": "{entity_name} \u00e8 diventato sicuro", + "occupied": "{entity_name} \u00e8 diventato occupato", + "opened": "{entity_name} \u00e8 aperto", + "plugged_in": "{entity_name} \u00e8 collegato", + "powered": "{entity_name} \u00e8 alimentato", + "present": "{entity_name} \u00e8 presente", + "problem": "{entity_name} ha iniziato a rilevare un problema", + "smoke": "{entity_name} ha iniziato la rilevazione di fumo", + "sound": "{entity_name} ha iniziato il rilevamento del suono", + "turned_off": "{entity_name} disattivato", + "turned_on": "{entity_name} attivato", + "unsafe": "{entity_name} diventato non sicuro", + "vibration": "{entity_name} iniziato a rilevare le vibrazioni" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/binary_sensor/.translations/ko.json b/homeassistant/components/binary_sensor/.translations/ko.json new file mode 100644 index 00000000000..3c12eabe8ff --- /dev/null +++ b/homeassistant/components/binary_sensor/.translations/ko.json @@ -0,0 +1,52 @@ +{ + "device_automation": { + "condition_type": { + "is_bat_low": "{entity_name} \ubc30\ud130\ub9ac \uc794\ub7c9\uc774 \ubd80\uc871\ud569\ub2c8\ub2e4", + "is_cold": "{entity_name} \uc774(\uac00) \ucc28\uac11\uc2b5\ub2c8\ub2e4", + "is_connected": "{entity_name} \uc774(\uac00) \uc5f0\uacb0\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "is_gas": "{entity_name} \uc774(\uac00) \uac00\uc2a4\ub97c \uac10\uc9c0\ud588\uc2b5\ub2c8\ub2e4", + "is_hot": "{entity_name} \uc774(\uac00) \ub728\uac81\uc2b5\ub2c8\ub2e4", + "is_light": "{entity_name} \uc774(\uac00) \ube5b\uc744 \uac10\uc9c0\ud588\uc2b5\ub2c8\ub2e4", + "is_locked": "{entity_name} \uc774(\uac00) \uc7a0\uacbc\uc2b5\ub2c8\ub2e4", + "is_moist": "{entity_name} \uc774(\uac00) \uc2b5\ud569\ub2c8\ub2e4", + "is_motion": "{entity_name} \uc774(\uac00) \uc6c0\uc9c1\uc784\uc744 \uac10\uc9c0\ud588\uc2b5\ub2c8\ub2e4", + "is_moving": "{entity_name} \uc774(\uac00) \uc6c0\uc9c1\uc600\uc2b5\ub2c8\ub2e4", + "is_no_gas": "{entity_name} \uc774(\uac00) \uac00\uc2a4\ub97c \uac10\uc9c0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", + "is_no_light": "{entity_name} \uc774(\uac00) \ube5b\uc744 \uac10\uc9c0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", + "is_no_motion": "{entity_name} \uc774(\uac00) \uc6c0\uc9c1\uc784\uc744 \uac10\uc9c0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", + "is_no_problem": "{entity_name} \uc774(\uac00) \ubb38\uc81c\ub97c \uac10\uc9c0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", + "is_no_smoke": "{entity_name} \uc774(\uac00) \uc5f0\uae30\ub97c \uac10\uc9c0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", + "is_no_sound": "{entity_name} \uc774(\uac00) \uc18c\ub9ac\ub97c \uac10\uc9c0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", + "is_no_vibration": "{entity_name} \uc774(\uac00) \uc9c4\ub3d9\uc744 \uac10\uc9c0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", + "is_not_bat_low": "{entity_name} \ubc30\ud130\ub9ac\uac00 \uc815\uc0c1\uc785\ub2c8\ub2e4", + "is_not_cold": "{entity_name} \uc774(\uac00) \ucc28\uac11\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4.", + "is_not_connected": "{entity_name} \uc758 \uc5f0\uacb0\uc774 \ub04a\uc5b4\uc84c\uc2b5\ub2c8\ub2e4", + "is_not_hot": "{entity_name} \uc774(\uac00) \ub728\uac81\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4.", + "is_not_locked": "{entity_name} \uc758 \uc7a0\uae08\uc774 \ud574\uc81c\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "is_not_moist": "{entity_name} \uc774(\uac00) \uac74\uc870\ud569\ub2c8\ub2e4", + "is_not_moving": "{entity_name} \uc774(\uac00) \uc6c0\uc9c1\uc774\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4", + "is_not_occupied": "{entity_name} \uc774 (\uac00) \uc0ac\uc6a9\uc911\uc774\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4", + "is_not_open": "{entity_name} \uc774(\uac00) \ub2eb\ud614\uc2b5\ub2c8\ub2e4", + "is_not_plugged_in": "{entity_name} \uc774(\uac00) \ubf51\ud614\uc2b5\ub2c8\ub2e4", + "is_not_powered": "{entity_name} \uc5d0 \uc804\uc6d0\uc774 \uacf5\uae09\ub418\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4", + "is_not_present": "{entity_name} \uc774(\uac00) \uc5c6\uc2b5\ub2c8\ub2e4", + "is_not_unsafe": "{entity_name} \uc740(\ub294) \uc548\uc804\ud569\ub2c8\ub2e4", + "is_occupied": "{entity_name} \uc774 (\uac00) \uc0ac\uc6a9\uc911\uc785\ub2c8\ub2e4", + "is_off": "{entity_name} \uc774(\uac00) \uaebc\uc84c\uc2b5\ub2c8\ub2e4", + "is_on": "{entity_name} \uc774(\uac00) \ucf1c\uc84c\uc2b5\ub2c8\ub2e4", + "is_open": "{entity_name} \uc774(\uac00) \uc5f4\ub838\uc2b5\ub2c8\ub2e4", + "is_plugged_in": "{entity_name} \uc774(\uac00) \uaf3d\ud614\uc2b5\ub2c8\ub2e4", + "is_powered": "{entity_name} \uc5d0 \uc804\uc6d0\uc774 \uacf5\uae09\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "is_present": "{entity_name} \uc774(\uac00) \uc788\uc2b5\ub2c8\ub2e4", + "is_problem": "{entity_name} \uc774(\uac00) \ubb38\uc81c\ub97c \uac10\uc9c0\ud588\uc2b5\ub2c8\ub2e4", + "is_smoke": "{entity_name} \uc774(\uac00) \uc5f0\uae30\ub97c \uac10\uc9c0\ud588\uc2b5\ub2c8\ub2e4", + "is_sound": "{entity_name} \uc774(\uac00) \uc18c\ub9ac\ub97c \uac10\uc9c0\ud588\uc2b5\ub2c8\ub2e4", + "is_unsafe": "{entity_name} \uc740(\ub294) \uc548\uc804\ud558\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4", + "is_vibration": "{entity_name} \uc774(\uac00) \uc9c4\ub3d9\uc744 \uac10\uc9c0\ud588\uc2b5\ub2c8\ub2e4" + }, + "trigger_type": { + "bat_low": "{entity_name} \ubc30\ud130\ub9ac \uc794\ub7c9 \ubd80\uc871", + "closed": "{entity_name} \ub2eb\ud798" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/binary_sensor/.translations/lb.json b/homeassistant/components/binary_sensor/.translations/lb.json new file mode 100644 index 00000000000..0b10e1f51a5 --- /dev/null +++ b/homeassistant/components/binary_sensor/.translations/lb.json @@ -0,0 +1,92 @@ +{ + "device_automation": { + "condition_type": { + "is_bat_low": "{entity_name} Batterie ass niddereg", + "is_cold": "{entity_name} ass kal", + "is_connected": "{entity_name} ass verbonnen", + "is_gas": "{entity_name} entdeckt Gas", + "is_hot": "{entity_name} ass waarm", + "is_light": "{entity_name} entdeckt Luucht", + "is_locked": "{entity_name} ass gespaart", + "is_moist": "{entity_name} ass fiicht", + "is_motion": "{entity_name} entdeckt Beweegung", + "is_moving": "{entity_name} beweegt sech", + "is_no_gas": "{entity_name} entdeckt kee Gas", + "is_no_light": "{entity_name} entdeckt keng Luucht", + "is_no_motion": "{entity_name} entdeckt keng Beweegung", + "is_no_problem": "{entity_name} entdeckt keng Problemer", + "is_no_smoke": "{entity_name} entdeckt keen Damp", + "is_no_sound": "{entity_name} entdeckt keen Toun", + "is_no_vibration": "{entity_name} entdeckt keng Vibratiounen", + "is_not_bat_low": "{entity_name} Batterie ass normal", + "is_not_cold": "{entity_name} ass net kal", + "is_not_connected": "{entity_name} ass d\u00e9connect\u00e9iert", + "is_not_hot": "{entity_name} ass net waarm", + "is_not_locked": "{entity_name} ass entspaart", + "is_not_moist": "{entity_name} ass dr\u00e9chen", + "is_not_moving": "{entity_name} beweegt sech net", + "is_not_occupied": "{entity_name} ass fr\u00e4i", + "is_not_open": "{entity_name} ass zou", + "is_not_plugged_in": "{entity_name} ass net ugeschloss", + "is_not_powered": "{entity_name} ass net aliment\u00e9iert", + "is_not_present": "{entity_name} ass net pr\u00e4sent", + "is_not_unsafe": "{entity_name} ass s\u00e9cher", + "is_occupied": "{entity_name} ass besat", + "is_off": "{entity_name} ass aus", + "is_on": "{entity_name} ass un", + "is_open": "{entity_name} ass op", + "is_plugged_in": "{entity_name} ass ugeschloss", + "is_powered": "{entity_name} ass aliment\u00e9iert", + "is_present": "{entity_name} ass pr\u00e4sent", + "is_problem": "{entity_name} entdeckt Problemer", + "is_smoke": "{entity_name} entdeckt Damp", + "is_sound": "{entity_name} entdeckt Toun", + "is_unsafe": "{entity_name} ass ons\u00e9cher", + "is_vibration": "{entity_name} entdeckt Vibratiounen" + }, + "trigger_type": { + "bat_low": "{entity_name} Batterie niddereg", + "closed": "{entity_name} gouf zougemaach", + "cold": "{entity_name} gouf kal", + "connected": "{entity_name} ass verbonnen", + "gas": "{entity_name} huet ugefaangen Gas z'entdecken", + "hot": "{entity_name} gouf waarm", + "light": "{entity_name} huet ugefange Luucht z'entdecken", + "locked": "{entity_name} gespaart", + "moist\u00a7": "{entity_name} gouf fiicht", + "motion": "{entity_name} huet ugefaange Beweegung z'entdecken", + "moving": "{entity_name} huet ugefaangen sech ze beweegen", + "no_gas": "{entity_name} huet opgehale Gas z'entdecken", + "no_light": "{entity_name} huet opgehale Luucht z'entdecken", + "no_motion": "{entity_name} huet opgehale Beweegung z'entdecken", + "no_problem": "{entity_name} huet opgehale Problemer z'entdecken", + "no_smoke": "{entity_name} huet opgehale Damp z'entdecken", + "no_sound": "{entity_name} huet opgehale Toun z'entdecken", + "no_vibration": "{entity_name} huet opgehale Vibratiounen z'entdecken", + "not_bat_low": "{entity_name} Batterie normal", + "not_cold": "{entity_name} gouf net kal", + "not_connected": "{entity_name} d\u00e9connect\u00e9iert", + "not_hot": "{entity_name} gouf net waarm", + "not_locked": "{entity_name} entspaart", + "not_moist": "{entity_name} gouf dr\u00e9chen", + "not_moving": "{entity_name} huet opgehale sech ze beweegen", + "not_occupied": "{entity_name} gouf fr\u00e4i", + "not_plugged_in": "{entity_name} net ugeschloss", + "not_powered": "{entity_name} net aliment\u00e9iert", + "not_present": "{entity_name} net pr\u00e4sent", + "not_unsafe": "{entity_name} gouf s\u00e9cher", + "occupied": "{entity_name} gouf besat", + "opened": "{entity_name} gouf opgemaach", + "plugged_in": "{entity_name} ugeschloss", + "powered": "{entity_name} aliment\u00e9iert", + "present": "{entity_name} pr\u00e4sent", + "problem": "{entity_name} huet ugefaange Problemer z'entdecken", + "smoke": "{entity_name} huet ugefaangen Damp z'entdecken", + "sound": "{entity_name} huet ugefaangen Toun z'entdecken", + "turned_off": "{entity_name} gouf ausgeschalt", + "turned_on": "{entity_name} gouf ugeschalt", + "unsafe": "{entity_name} gouf ons\u00e9cher", + "vibration": "{entity_name} huet ugefaange Vibratiounen z'entdecken" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/binary_sensor/.translations/no.json b/homeassistant/components/binary_sensor/.translations/no.json new file mode 100644 index 00000000000..5a1916bce59 --- /dev/null +++ b/homeassistant/components/binary_sensor/.translations/no.json @@ -0,0 +1,92 @@ +{ + "device_automation": { + "condition_type": { + "is_bat_low": "{entity_name} batteriniv\u00e5et er lavt", + "is_cold": "{entity_name} er kald", + "is_connected": "{entity_name} er tilkoblet", + "is_gas": "{entity_name} registrerer gass", + "is_hot": "{entity_name} er varm", + "is_light": "{entity_name} registrerer lys", + "is_locked": "{entity_name} er l\u00e5st", + "is_moist": "{entity_name} er fuktig", + "is_motion": "{entity_name} registrerer bevegelse", + "is_moving": "{entity_name} er i bevegelse", + "is_no_gas": "{entity_name} registrerer ikke gass", + "is_no_light": "{entity_name} registrerer ikke lys", + "is_no_motion": "{entity_name} registrerer ikke bevegelse", + "is_no_problem": "{entity_name} registrerer ikke et problem", + "is_no_smoke": "{entity_name} registrerer ikke r\u00f8yk", + "is_no_sound": "{entity_name} registrerer ikke lyd", + "is_no_vibration": "{entity_name} registrerer ikke bevegelse", + "is_not_bat_low": "{entity_name} batteri er normalt", + "is_not_cold": "{entity_name} er ikke kald", + "is_not_connected": "{entity_name} er frakoblet", + "is_not_hot": "{entity_name} er ikke varm", + "is_not_locked": "{entity_name} er ul\u00e5st", + "is_not_moist": "{entity_name} er t\u00f8rr", + "is_not_moving": "{entity_name} er ikke i bevegelse", + "is_not_occupied": "{entity_name} er ledig", + "is_not_open": "{entity_name} er lukket", + "is_not_plugged_in": "{entity_name} er koblet fra", + "is_not_powered": "{entity_name} er spenningsl\u00f8s", + "is_not_present": "{entity_name} er ikke tilstede", + "is_not_unsafe": "{entity_name} er trygg", + "is_occupied": "{entity_name} er opptatt", + "is_off": "{entity_name} er sl\u00e5tt av", + "is_on": "{entity_name} er sl\u00e5tt p\u00e5", + "is_open": "{entity_name} er \u00e5pen", + "is_plugged_in": "{entity_name} er koblet til", + "is_powered": "{entity_name} er spenningssatt", + "is_present": "{entity_name} er tilstede", + "is_problem": "{entity_name} registrerer et problem", + "is_smoke": "{entity_name} registrerer r\u00f8yk", + "is_sound": "{entity_name} registrerer lyd", + "is_unsafe": "{entity_name} er utrygg", + "is_vibration": "{entity_name} registrerer vibrasjon" + }, + "trigger_type": { + "bat_low": "{entity_name} lavt batteri", + "closed": "{entity_name} stengt", + "cold": "{entity_name} ble kald", + "connected": "{entity_name} tilkoblet", + "gas": "{entity_name} begynte \u00e5 registrere gass", + "hot": "{entity_name} ble varm", + "light": "{entity_name} begynte \u00e5 registrere lys", + "locked": "{entity_name} l\u00e5st", + "moist\u00a7": "{entity_name} ble fuktig", + "motion": "{entity_name} begynte \u00e5 registrere bevegelse", + "moving": "{entity_name} begynte \u00e5 bevege seg", + "no_gas": "{entity_name} sluttet \u00e5 registrere gass", + "no_light": "{entity_name} sluttet \u00e5 registrere lys", + "no_motion": "{entity_name} sluttet \u00e5 registrere bevegelse", + "no_problem": "{entity_name} sluttet \u00e5 registrere problem", + "no_smoke": "{entity_name} sluttet \u00e5 registrere r\u00f8yk", + "no_sound": "{entity_name} sluttet \u00e5 registrere lyd", + "no_vibration": "{entity_name} sluttet \u00e5 registrere vibrasjon", + "not_bat_low": "{entity_name} batteri normalt", + "not_cold": "{entity_name} ble ikke lenger kald", + "not_connected": "{entity_name} koblet fra", + "not_hot": "{entity_name} ble ikke lenger varm", + "not_locked": "{entity_name} l\u00e5st opp", + "not_moist": "{entity_name} ble t\u00f8rr", + "not_moving": "{entity_name} sluttet \u00e5 bevege seg", + "not_occupied": "{entity_name} ble ledig", + "not_plugged_in": "{entity_name} koblet fra", + "not_powered": "{entity_name} spenningsl\u00f8s", + "not_present": "{entity_name} ikke til stede", + "not_unsafe": "{entity_name} ble trygg", + "occupied": "{entity_name} ble opptatt", + "opened": "{entity_name} \u00e5pnet", + "plugged_in": "{entity_name} koblet til", + "powered": "{entity_name} spenningssatt", + "present": "{entity_name} tilstede", + "problem": "{entity_name} begynte \u00e5 registrere et problem", + "smoke": "{entity_name} begynte \u00e5 registrere r\u00f8yk", + "sound": "{entity_name} begynte \u00e5 registrere lyd", + "turned_off": "{entity_name} sl\u00e5tt av", + "turned_on": "{entity_name} sl\u00e5tt p\u00e5", + "unsafe": "{entity_name} ble usikker", + "vibration": "{entity_name} begynte \u00e5 oppdage vibrasjon" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/binary_sensor/.translations/pl.json b/homeassistant/components/binary_sensor/.translations/pl.json new file mode 100644 index 00000000000..a7f0bd516a0 --- /dev/null +++ b/homeassistant/components/binary_sensor/.translations/pl.json @@ -0,0 +1,92 @@ +{ + "device_automation": { + "condition_type": { + "is_bat_low": "bateria {entity_name} jest roz\u0142adowana", + "is_cold": "sensor {entity_name} wykrywa zimno", + "is_connected": "sensor {entity_name} raportuje po\u0142\u0105czenie", + "is_gas": "sensor {entity_name} wykrywa gaz", + "is_hot": "sensor {entity_name} wykrywa gor\u0105co", + "is_light": "sensor {entity_name} wykrywa \u015bwiat\u0142o", + "is_locked": "sensor {entity_name} wykrywa zamkni\u0119cie", + "is_moist": "sensor {entity_name} wykrywa wilgo\u0107", + "is_motion": "sensor {entity_name} wykrywa ruch", + "is_moving": "sensor {entity_name} porusza si\u0119", + "is_no_gas": "sensor {entity_name} nie wykrywa gazu", + "is_no_light": "sensor {entity_name} nie wykrywa \u015bwiat\u0142a", + "is_no_motion": "sensor {entity_name} nie wykrywa ruchu", + "is_no_problem": "sensor {entity_name} nie wykrywa problemu", + "is_no_smoke": "sensor {entity_name} nie wykrywa dymu", + "is_no_sound": "sensor {entity_name} nie wykrywa d\u017awi\u0119ku", + "is_no_vibration": "sensor {entity_name} nie wykrywa wibracji", + "is_not_bat_low": "bateria {entity_name} nie jest roz\u0142adowana", + "is_not_cold": "sensor {entity_name} nie wykrywa zimna", + "is_not_connected": "sensor {entity_name} nie wykrywa roz\u0142\u0105czenia", + "is_not_hot": "sensor {entity_name} nie wykrywa gor\u0105ca", + "is_not_locked": "sensor {entity_name} nie wykrywa otwarcia", + "is_not_moist": "sensor {entity_name} nie wykrywa wilgoci", + "is_not_moving": "sensor {entity_name} nie porusza si\u0119", + "is_not_occupied": "sensor {entity_name} nie jest zaj\u0119ty", + "is_not_open": "sensor {entity_name} jest zamkni\u0119ty", + "is_not_plugged_in": "sensor {entity_name} wykrywa od\u0142\u0105czenie", + "is_not_powered": "sensor {entity_name} nie wykrywa zasilania", + "is_not_present": "sensor {entity_name} nie wykrywa obecno\u015bci", + "is_not_unsafe": "sensor {entity_name} nie wykrywa niebezpiecze\u0144stwa", + "is_occupied": "sensor {entity_name} jest zaj\u0119ty", + "is_off": "sensor {entity_name} jest wy\u0142\u0105czony", + "is_on": "sensor {entity_name} jest w\u0142\u0105czony", + "is_open": "sensor {entity_name} jest otwarty", + "is_plugged_in": "sensor {entity_name} wykrywa pod\u0142\u0105czenie", + "is_powered": "sensor {entity_name} wykrywa zasilanie", + "is_present": "sensor {entity_name} wykrywa obecno\u015b\u0107", + "is_problem": "sensor {entity_name} wykrywa problem", + "is_smoke": "sensor {entity_name} wykrywa dym", + "is_sound": "sensor {entity_name} wykrywa d\u017awi\u0119k", + "is_unsafe": "sensor {entity_name} wykrywa niebezpiecze\u0144stwo", + "is_vibration": "sensor {entity_name} wykrywa wibracje" + }, + "trigger_type": { + "bat_low": "bateria {entity_name} stanie si\u0119 roz\u0142adowana", + "closed": "zamkni\u0119cie {entity_name}", + "cold": "sensor {entity_name} wykryje zimno", + "connected": "pod\u0142\u0105czenie {entity_name}", + "gas": "sensor {entity_name} wykryje gaz", + "hot": "sensor {entity_name} wykryje gor\u0105co", + "light": "sensor {entity_name} wykryje \u015bwiat\u0142o", + "locked": "zamkni\u0119cie {entity_name}", + "moist\u00a7": "sensor {entity_name} wykryje wilgo\u0107", + "motion": "sensor {entity_name} wykryje ruch", + "moving": "sensor {entity_name} zacznie porusza\u0107 si\u0119", + "no_gas": "sensor {entity_name} przestanie wykrywa\u0107 gaz", + "no_light": "sensor {entity_name} przestanie wykrywa\u0107 \u015bwiat\u0142o", + "no_motion": "sensor {entity_name} przestanie wykrywa\u0107 ruch", + "no_problem": "sensor {entity_name} przestanie wykrywa\u0107 problem", + "no_smoke": "sensor {entity_name} przestanie wykrywa\u0107 dym", + "no_sound": "sensor {entity_name} przestanie wykrywa\u0107 d\u017awi\u0119k", + "no_vibration": "sensor {entity_name} przestanie wykrywa\u0107 wibracje", + "not_bat_low": "bateria {entity_name} staje si\u0119 na\u0142adowana", + "not_cold": "sensor {entity_name} przestanie wykrywa\u0107 zimno", + "not_connected": "roz\u0142\u0105czenie {entity_name}", + "not_hot": "sensor {entity_name} przestanie wykrywa\u0107 gor\u0105co", + "not_locked": "otwarcie {entity_name}", + "not_moist": "sensor {entity_name} przestanie wykrywa\u0107 wilgo\u0107", + "not_moving": "sensor {entity_name} przestanie porusza\u0107 si\u0119", + "not_occupied": "sensor {entity_name} przesta\u0142 by\u0107 zaj\u0119ty", + "not_plugged_in": "od\u0142\u0105czenie {entity_name}", + "not_powered": "od\u0142\u0105czenie zasilania {entity_name}", + "not_present": "sensor {entity_name} przestanie wykrywa\u0107 obecno\u015b\u0107", + "not_unsafe": "sensor {entity_name} przestanie wykrywa\u0107 niebezpiecze\u0144stwo", + "occupied": "sensor {entity_name} sta\u0142 si\u0119 zaj\u0119ty", + "opened": "otwarcie {entity_name}", + "plugged_in": "pod\u0142\u0105czenie {entity_name}", + "powered": "pod\u0142\u0105czenie zasilenia {entity_name}", + "present": "sensor {entity_name} wykryje obecno\u015b\u0107", + "problem": "sensor {entity_name} wykryje problem", + "smoke": "sensor {entity_name} wykryje dym", + "sound": "sensor {entity_name} wykryje d\u017awi\u0119k", + "turned_off": "wy\u0142\u0105czenie {entity_name}", + "turned_on": "w\u0142\u0105czenie {entity_name}", + "unsafe": "sensor {entity_name} wykryje niebezpiecze\u0144stwo", + "vibration": "sensor {entity_name} wykryje wibracje" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/binary_sensor/.translations/ro.json b/homeassistant/components/binary_sensor/.translations/ro.json new file mode 100644 index 00000000000..438822a97f5 --- /dev/null +++ b/homeassistant/components/binary_sensor/.translations/ro.json @@ -0,0 +1,45 @@ +{ + "device_automation": { + "condition_type": { + "is_off": "{entity_name} oprit", + "is_on": "{entity_name} pornit" + }, + "trigger_type": { + "gas": "{entity_name} a \u00eenceput s\u0103 detecteze gaz", + "hot": "{entity_name} a devenit fierbinte", + "locked": "{entity_name} blocat", + "motion": "{entity_name} a \u00eenceput s\u0103 detecteze mi\u0219care", + "moving": "{entity_name} a \u00eenceput s\u0103 se mi\u0219te", + "no_light": "{entity_name} a oprit detectarea luminii", + "no_motion": "{entity_name} a oprit detectarea mi\u0219c\u0103rii", + "no_problem": "{entity_name} a oprit detectarea problemei", + "no_smoke": "{entity_name} a oprit detectarea fumului", + "no_sound": "{entity_name} a oprit detectarea de sunet", + "no_vibration": "{entity_name} a oprit detectarea vibra\u021biilor", + "not_bat_low": "{entity_name} baterie normal\u0103", + "not_cold": "{entity_name} nu mai este rece", + "not_connected": "{entity_name} deconectat", + "not_hot": "{entity_name} nu mai este fierbinte", + "not_locked": "{entity_name} deblocat", + "not_moist": "{entity_name} a devenit uscat", + "not_moving": "{entity_name} a \u00eencetat mi\u0219carea", + "not_occupied": "{entity_name} a devenit neocupat", + "not_plugged_in": "{entity_name} deconectat", + "not_powered": "{entity_name} nu este alimentat", + "not_present": "{entity_name} nu este prezent", + "not_unsafe": "{entity_name} a devenit sigur", + "occupied": "{entity_name} a devenit ocupat", + "opened": "{entity_name} deschis", + "plugged_in": "{entity_name} conectat", + "powered": "{entity_name} alimentat", + "present": "{entity_name} prezent", + "problem": "{entity_name} a \u00eenceput detectarea unei probleme", + "smoke": "{entity_name} a \u00eenceput s\u0103 detecteze fum", + "sound": "{entity_name} a \u00eenceput s\u0103 detecteze sunetul", + "turned_off": "{entity_name} oprit", + "turned_on": "{entity_name} pornit", + "unsafe": "{entity_name} a devenit nesigur", + "vibration": "{entity_name} a \u00eenceput s\u0103 detecteze vibra\u021biile" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/binary_sensor/.translations/ru.json b/homeassistant/components/binary_sensor/.translations/ru.json new file mode 100644 index 00000000000..7d73cb8d4aa --- /dev/null +++ b/homeassistant/components/binary_sensor/.translations/ru.json @@ -0,0 +1,15 @@ +{ + "device_automation": { + "condition_type": { + "is_bat_low": "{entity_name}: \u043d\u0438\u0437\u043a\u0438\u0439 \u0437\u0430\u0440\u044f\u0434", + "is_cold": "{entity_name}: \u0445\u043e\u043b\u043e\u0434\u043d\u043e", + "is_connected": "{entity_name}: \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043e", + "is_gas": "{entity_name}: \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d \u0433\u0430\u0437", + "is_hot": "{entity_name}: \u0433\u043e\u0440\u044f\u0447\u043e", + "is_light": "{entity_name}: \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d \u0441\u0432\u0435\u0442", + "is_locked": "{entity_name}: \u0437\u0430\u0431\u043b\u043e\u043a\u0438\u0440\u043e\u0432\u0430\u043d\u043e", + "is_moist": "{entity_name}: \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u0430 \u0432\u043b\u0430\u0433\u0430", + "is_motion": "{entity_name} \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u0434\u0432\u0438\u0436\u0435\u043d\u0438\u0435" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/binary_sensor/.translations/sl.json b/homeassistant/components/binary_sensor/.translations/sl.json new file mode 100644 index 00000000000..6b4e144d9a6 --- /dev/null +++ b/homeassistant/components/binary_sensor/.translations/sl.json @@ -0,0 +1,92 @@ +{ + "device_automation": { + "condition_type": { + "is_bat_low": "{entity_name} ima prazno baterijo", + "is_cold": "{entity_name} je hladen", + "is_connected": "{entity_name} je povezan", + "is_gas": "{entity_name} zaznava plin", + "is_hot": "{entity_name} je vro\u010d", + "is_light": "{entity_name} zaznava svetlobo", + "is_locked": "{entity_name} je zaklenjen", + "is_moist": "{entity_name} je vla\u017een", + "is_motion": "{entity_name} zaznava gibanje", + "is_moving": "{entity_name} se premika", + "is_no_gas": "{entity_name} ne zaznava plina", + "is_no_light": "{entity_name} ne zaznava svetlobe", + "is_no_motion": "{entity_name} ne zaznava gibanja", + "is_no_problem": "{entity_name} ne zaznava te\u017eav", + "is_no_smoke": "{entity_name} ne zaznava dima", + "is_no_sound": "{entity_name} ne zaznava zvoka", + "is_no_vibration": "{entity_name} ne zazna vibracij", + "is_not_bat_low": "{entity_name} baterija je polna", + "is_not_cold": "{entity_name} ni hladen", + "is_not_connected": "{entity_name} ni povezan", + "is_not_hot": "{entity_name} ni vro\u010d", + "is_not_locked": "{entity_name} je odklenjen", + "is_not_moist": "{entity_name} je suh", + "is_not_moving": "{entity_name} se ne premika", + "is_not_occupied": "{entity_name} ni zaseden", + "is_not_open": "{entity_name} je zaprt", + "is_not_plugged_in": "{entity_name} je odklopljen", + "is_not_powered": "{entity_name} ni napajan", + "is_not_present": "{entity_name} ni prisoten", + "is_not_unsafe": "{entity_name} je varen", + "is_occupied": "{entity_name} je zaseden", + "is_off": "{entity_name} je izklopljen", + "is_on": "{entity_name} je vklopljen", + "is_open": "{entity_name} je odprt", + "is_plugged_in": "{entity_name} je priklju\u010den", + "is_powered": "{entity_name} je vklopljen", + "is_present": "{entity_name} je prisoten", + "is_problem": "{entity_name} zaznava te\u017eavo", + "is_smoke": "{entity_name} zaznava dim", + "is_sound": "{entity_name} zaznava zvok", + "is_unsafe": "{entity_name} ni varen", + "is_vibration": "{entity_name} zaznava vibracije" + }, + "trigger_type": { + "bat_low": "{entity_name} ima prazno baterijo", + "closed": "{entity_name} zaprto", + "cold": "{entity_name} je postal hladen", + "connected": "{entity_name} povezan", + "gas": "{entity_name} za\u010del zaznavati plin", + "hot": "{entity_name} je postal vro\u010d", + "light": "{entity_name} za\u010del zaznavati svetlobo", + "locked": "{entity_name} zaklenjen", + "moist\u00a7": "{entity_name} postal vla\u017een", + "motion": "{entity_name} za\u010del zaznavati gibanje", + "moving": "{entity_name} se je za\u010del premikati", + "no_gas": "{entity_name} prenehal zaznavati plin", + "no_light": "{entity_name} prenehal zaznavati svetlobo", + "no_motion": "{entity_name} prenehal zaznavati gibanje", + "no_problem": "{entity_name} prenehal odkrivati te\u017eavo", + "no_smoke": "{entity_name} prenehal zaznavati dim", + "no_sound": "{entity_name} prenehal zaznavati zvok", + "no_vibration": "{entity_name} prenehal zaznavati vibracije", + "not_bat_low": "{entity_name} ima polno baterijo", + "not_cold": "{entity_name} ni ve\u010d hladen", + "not_connected": "{entity_name} prekinjen", + "not_hot": "{entity_name} ni ve\u010d vro\u010d", + "not_locked": "{entity_name} odklenjen", + "not_moist": "{entity_name} je postalo suh", + "not_moving": "{entity_name} se je prenehal premikati", + "not_occupied": "{entity_name} ni zaseden", + "not_plugged_in": "{entity_name} odklopljen", + "not_powered": "{entity_name} ni napajan", + "not_present": "{entity_name} ni prisoten", + "not_unsafe": "{entity_name} je postal varen", + "occupied": "{entity_name} postal zaseden", + "opened": "{entity_name} odprl", + "plugged_in": "{entity_name} priklju\u010den", + "powered": "{entity_name} priklopljen", + "present": "{entity_name} prisoten", + "problem": "{entity_name} za\u010del odkrivati te\u017eavo", + "smoke": "{entity_name} za\u010del zaznavati dim", + "sound": "{entity_name} za\u010del zaznavati zvok", + "turned_off": "{entity_name} izklopljen", + "turned_on": "{entity_name} vklopljen", + "unsafe": "{entity_name} je postal nevaren", + "vibration": "{entity_name} je za\u010del odkrivat vibracije" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/binary_sensor/.translations/zh-Hant.json b/homeassistant/components/binary_sensor/.translations/zh-Hant.json new file mode 100644 index 00000000000..36c72dcb9e6 --- /dev/null +++ b/homeassistant/components/binary_sensor/.translations/zh-Hant.json @@ -0,0 +1,92 @@ +{ + "device_automation": { + "condition_type": { + "is_bat_low": "{entity_name} \u96fb\u91cf\u904e\u4f4e", + "is_cold": "{entity_name} \u51b7", + "is_connected": "{entity_name} \u5df2\u9023\u7dda", + "is_gas": "{entity_name} \u5075\u6e2c\u5230\u6c23\u9ad4", + "is_hot": "{entity_name} \u71b1", + "is_light": "{entity_name} \u5075\u6e2c\u5230\u5149\u7dda\u4e2d", + "is_locked": "{entity_name} \u5df2\u4e0a\u9396", + "is_moist": "{entity_name} \u6f6e\u6fd5", + "is_motion": "{entity_name} \u5075\u6e2c\u5230\u52d5\u4f5c\u4e2d", + "is_moving": "{entity_name} \u79fb\u52d5\u4e2d", + "is_no_gas": "{entity_name} \u672a\u5075\u6e2c\u5230\u6c23\u9ad4", + "is_no_light": "{entity_name} \u672a\u5075\u6e2c\u5230\u5149\u7dda", + "is_no_motion": "{entity_name} \u672a\u5075\u6e2c\u5230\u52d5\u4f5c", + "is_no_problem": "{entity_name} \u672a\u5075\u6e2c\u5230\u554f\u984c", + "is_no_smoke": "{entity_name} \u672a\u5075\u6e2c\u5230\u7159\u9727", + "is_no_sound": "{entity_name} \u672a\u5075\u6e2c\u5230\u8072\u97f3", + "is_no_vibration": "{entity_name} \u672a\u5075\u6e2c\u5230\u9707\u52d5", + "is_not_bat_low": "{entity_name} \u96fb\u91cf\u6b63\u5e38", + "is_not_cold": "{entity_name} \u4e0d\u51b7", + "is_not_connected": "{entity_name} \u65b7\u7dda", + "is_not_hot": "{entity_name} \u4e0d\u71b1", + "is_not_locked": "{entity_name} \u89e3\u9396", + "is_not_moist": "{entity_name} \u4e7e\u71e5", + "is_not_moving": "{entity_name} \u672a\u5728\u79fb\u52d5", + "is_not_occupied": "{entity_name} \u672a\u6709\u4eba", + "is_not_open": "{entity_name} \u95dc\u9589", + "is_not_plugged_in": "{entity_name} \u672a\u63d2\u5165", + "is_not_powered": "{entity_name} \u672a\u901a\u96fb", + "is_not_present": "{entity_name} \u672a\u51fa\u73fe", + "is_not_unsafe": "{entity_name} \u5b89\u5168", + "is_occupied": "{entity_name} \u6709\u4eba", + "is_off": "{entity_name} \u95dc\u9589", + "is_on": "{entity_name} \u958b\u555f", + "is_open": "{entity_name} \u958b\u555f", + "is_plugged_in": "{entity_name} \u63d2\u5165", + "is_powered": "{entity_name} \u901a\u96fb", + "is_present": "{entity_name} \u51fa\u73fe", + "is_problem": "{entity_name} \u6b63\u5075\u6e2c\u5230\u554f\u984c", + "is_smoke": "{entity_name} \u6b63\u5075\u6e2c\u5230\u7159\u9727", + "is_sound": "{entity_name} \u6b63\u5075\u6e2c\u5230\u8072\u97f3", + "is_unsafe": "{entity_name} \u4e0d\u5b89\u5168", + "is_vibration": "{entity_name} \u6b63\u5075\u6e2c\u5230\u9707\u52d5" + }, + "trigger_type": { + "bat_low": "{entity_name} \u96fb\u91cf\u4f4e", + "closed": "{entity_name} \u5df2\u95dc\u9589", + "cold": "{entity_name} \u5df2\u8b8a\u51b7", + "connected": "{entity_name} \u5df2\u9023\u7dda", + "gas": "{entity_name} \u5df2\u958b\u59cb\u5075\u6e2c\u6c23\u9ad4", + "hot": "{entity_name} \u5df2\u8b8a\u71b1", + "light": "{entity_name} \u5df2\u958b\u59cb\u5075\u6e2c\u5149\u7dda", + "locked": "{entity_name} \u5df2\u4e0a\u9396", + "moist\u00a7": "{entity_name} \u5df2\u8b8a\u6f6e\u6fd5", + "motion": "{entity_name} \u5df2\u5075\u6e2c\u5230\u52d5\u4f5c", + "moving": "{entity_name} \u958b\u59cb\u79fb\u52d5", + "no_gas": "{entity_name} \u5df2\u505c\u6b62\u5075\u6e2c\u6c23\u9ad4", + "no_light": "{entity_name} \u5df2\u505c\u6b62\u5075\u6e2c\u5149\u7dda", + "no_motion": "{entity_name} \u5df2\u505c\u6b62\u5075\u6e2c\u52d5\u4f5c", + "no_problem": "{entity_name} \u5df2\u505c\u6b62\u5075\u6e2c\u554f\u984c", + "no_smoke": "{entity_name} \u5df2\u505c\u6b62\u5075\u6e2c\u7159\u9727", + "no_sound": "{entity_name} \u5df2\u505c\u6b62\u5075\u6e2c\u8072\u97f3", + "no_vibration": "{entity_name} \u5df2\u505c\u6b62\u5075\u6e2c\u9707\u52d5", + "not_bat_low": "{entity_name} \u96fb\u91cf\u6b63\u5e38", + "not_cold": "{entity_name} \u5df2\u4e0d\u51b7", + "not_connected": "{entity_name} \u5df2\u65b7\u7dda", + "not_hot": "{entity_name} \u5df2\u4e0d\u71b1", + "not_locked": "{entity_name} \u5df2\u89e3\u9396", + "not_moist": "{entity_name} \u5df2\u8b8a\u4e7e", + "not_moving": "{entity_name} \u505c\u6b62\u79fb\u52d5", + "not_occupied": "{entity_name} \u672a\u6709\u4eba", + "not_plugged_in": "{entity_name} \u672a\u63d2\u5165", + "not_powered": "{entity_name} \u672a\u901a\u96fb", + "not_present": "{entity_name} \u672a\u51fa\u73fe", + "not_unsafe": "{entity_name} \u5df2\u5b89\u5168", + "occupied": "{entity_name} \u8b8a\u6210\u6709\u4eba", + "opened": "{entity_name} \u5df2\u958b\u555f", + "plugged_in": "{entity_name} \u5df2\u63d2\u5165", + "powered": "{entity_name} \u5df2\u901a\u96fb", + "present": "{entity_name} \u5df2\u51fa\u73fe", + "problem": "{entity_name} \u5df2\u5075\u6e2c\u5230\u554f\u984c", + "smoke": "{entity_name} \u5df2\u5075\u6e2c\u5230\u7159\u9727", + "sound": "{entity_name} \u5df2\u5075\u6e2c\u5230\u8072\u97f3", + "turned_off": "{entity_name} \u5df2\u95dc\u9589", + "turned_on": "{entity_name} \u5df2\u958b\u555f", + "unsafe": "{entity_name} \u5df2\u4e0d\u5b89\u5168", + "vibration": "{entity_name} \u5df2\u5075\u6e2c\u5230\u9707\u52d5" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/binary_sensor/device_condition.py b/homeassistant/components/binary_sensor/device_condition.py new file mode 100644 index 00000000000..1749ea91c5b --- /dev/null +++ b/homeassistant/components/binary_sensor/device_condition.py @@ -0,0 +1,248 @@ +"""Implemenet device conditions for binary sensor.""" +from typing import List +import voluptuous as vol + +from homeassistant.core import HomeAssistant +from homeassistant.components.device_automation.const import CONF_IS_OFF, CONF_IS_ON +from homeassistant.const import ATTR_DEVICE_CLASS, CONF_ENTITY_ID, CONF_TYPE +from homeassistant.helpers import condition, config_validation as cv +from homeassistant.helpers.entity_registry import ( + async_entries_for_device, + async_get_registry, +) +from homeassistant.helpers.typing import ConfigType + +from . import ( + DOMAIN, + DEVICE_CLASS_BATTERY, + DEVICE_CLASS_COLD, + DEVICE_CLASS_CONNECTIVITY, + DEVICE_CLASS_DOOR, + DEVICE_CLASS_GARAGE_DOOR, + DEVICE_CLASS_GAS, + DEVICE_CLASS_HEAT, + DEVICE_CLASS_LIGHT, + DEVICE_CLASS_LOCK, + DEVICE_CLASS_MOISTURE, + DEVICE_CLASS_MOTION, + DEVICE_CLASS_MOVING, + DEVICE_CLASS_OCCUPANCY, + DEVICE_CLASS_OPENING, + DEVICE_CLASS_PLUG, + DEVICE_CLASS_POWER, + DEVICE_CLASS_PRESENCE, + DEVICE_CLASS_PROBLEM, + DEVICE_CLASS_SAFETY, + DEVICE_CLASS_SMOKE, + DEVICE_CLASS_SOUND, + DEVICE_CLASS_VIBRATION, + DEVICE_CLASS_WINDOW, +) + +DEVICE_CLASS_NONE = "none" + +CONF_IS_BAT_LOW = "is_bat_low" +CONF_IS_NOT_BAT_LOW = "is_not_bat_low" +CONF_IS_COLD = "is_cold" +CONF_IS_NOT_COLD = "is_not_cold" +CONF_IS_CONNECTED = "is_connected" +CONF_IS_NOT_CONNECTED = "is_not_connected" +CONF_IS_GAS = "is_gas" +CONF_IS_NO_GAS = "is_no_gas" +CONF_IS_HOT = "is_hot" +CONF_IS_NOT_HOT = "is_not_hot" +CONF_IS_LIGHT = "is_light" +CONF_IS_NO_LIGHT = "is_no_light" +CONF_IS_LOCKED = "is_locked" +CONF_IS_NOT_LOCKED = "is_not_locked" +CONF_IS_MOIST = "is_moist" +CONF_IS_NOT_MOIST = "is_not_moist" +CONF_IS_MOTION = "is_motion" +CONF_IS_NO_MOTION = "is_no_motion" +CONF_IS_MOVING = "is_moving" +CONF_IS_NOT_MOVING = "is_not_moving" +CONF_IS_OCCUPIED = "is_occupied" +CONF_IS_NOT_OCCUPIED = "is_not_occupied" +CONF_IS_PLUGGED_IN = "is_plugged_in" +CONF_IS_NOT_PLUGGED_IN = "is_not_plugged_in" +CONF_IS_POWERED = "is_powered" +CONF_IS_NOT_POWERED = "is_not_powered" +CONF_IS_PRESENT = "is_present" +CONF_IS_NOT_PRESENT = "is_not_present" +CONF_IS_PROBLEM = "is_problem" +CONF_IS_NO_PROBLEM = "is_no_problem" +CONF_IS_UNSAFE = "is_unsafe" +CONF_IS_NOT_UNSAFE = "is_not_unsafe" +CONF_IS_SMOKE = "is_smoke" +CONF_IS_NO_SMOKE = "is_no_smoke" +CONF_IS_SOUND = "is_sound" +CONF_IS_NO_SOUND = "is_no_sound" +CONF_IS_VIBRATION = "is_vibration" +CONF_IS_NO_VIBRATION = "is_no_vibration" +CONF_IS_OPEN = "is_open" +CONF_IS_NOT_OPEN = "is_not_open" + +IS_ON = [ + CONF_IS_BAT_LOW, + CONF_IS_COLD, + CONF_IS_CONNECTED, + CONF_IS_GAS, + CONF_IS_HOT, + CONF_IS_LIGHT, + CONF_IS_LOCKED, + CONF_IS_MOIST, + CONF_IS_MOTION, + CONF_IS_MOVING, + CONF_IS_OCCUPIED, + CONF_IS_OPEN, + CONF_IS_PLUGGED_IN, + CONF_IS_POWERED, + CONF_IS_PRESENT, + CONF_IS_PROBLEM, + CONF_IS_SMOKE, + CONF_IS_SOUND, + CONF_IS_UNSAFE, + CONF_IS_VIBRATION, + CONF_IS_ON, +] + +IS_OFF = [ + CONF_IS_NOT_BAT_LOW, + CONF_IS_NOT_COLD, + CONF_IS_NOT_CONNECTED, + CONF_IS_NOT_HOT, + CONF_IS_NOT_LOCKED, + CONF_IS_NOT_MOIST, + CONF_IS_NOT_MOVING, + CONF_IS_NOT_OCCUPIED, + CONF_IS_NOT_OPEN, + CONF_IS_NOT_PLUGGED_IN, + CONF_IS_NOT_POWERED, + CONF_IS_NOT_PRESENT, + CONF_IS_NOT_UNSAFE, + CONF_IS_NO_GAS, + CONF_IS_NO_LIGHT, + CONF_IS_NO_MOTION, + CONF_IS_NO_PROBLEM, + CONF_IS_NO_SMOKE, + CONF_IS_NO_SOUND, + CONF_IS_NO_VIBRATION, + CONF_IS_OFF, +] + +ENTITY_CONDITIONS = { + DEVICE_CLASS_BATTERY: [ + {CONF_TYPE: CONF_IS_BAT_LOW}, + {CONF_TYPE: CONF_IS_NOT_BAT_LOW}, + ], + DEVICE_CLASS_COLD: [{CONF_TYPE: CONF_IS_COLD}, {CONF_TYPE: CONF_IS_NOT_COLD}], + DEVICE_CLASS_CONNECTIVITY: [ + {CONF_TYPE: CONF_IS_CONNECTED}, + {CONF_TYPE: CONF_IS_NOT_CONNECTED}, + ], + DEVICE_CLASS_DOOR: [{CONF_TYPE: CONF_IS_OPEN}, {CONF_TYPE: CONF_IS_NOT_OPEN}], + DEVICE_CLASS_GARAGE_DOOR: [ + {CONF_TYPE: CONF_IS_OPEN}, + {CONF_TYPE: CONF_IS_NOT_OPEN}, + ], + DEVICE_CLASS_GAS: [{CONF_TYPE: CONF_IS_GAS}, {CONF_TYPE: CONF_IS_NO_GAS}], + DEVICE_CLASS_HEAT: [{CONF_TYPE: CONF_IS_HOT}, {CONF_TYPE: CONF_IS_NOT_HOT}], + DEVICE_CLASS_LIGHT: [{CONF_TYPE: CONF_IS_LIGHT}, {CONF_TYPE: CONF_IS_NO_LIGHT}], + DEVICE_CLASS_LOCK: [{CONF_TYPE: CONF_IS_LOCKED}, {CONF_TYPE: CONF_IS_NOT_LOCKED}], + DEVICE_CLASS_MOISTURE: [{CONF_TYPE: CONF_IS_MOIST}, {CONF_TYPE: CONF_IS_NOT_MOIST}], + DEVICE_CLASS_MOTION: [{CONF_TYPE: CONF_IS_MOTION}, {CONF_TYPE: CONF_IS_NO_MOTION}], + DEVICE_CLASS_MOVING: [{CONF_TYPE: CONF_IS_MOVING}, {CONF_TYPE: CONF_IS_NOT_MOVING}], + DEVICE_CLASS_OCCUPANCY: [ + {CONF_TYPE: CONF_IS_OCCUPIED}, + {CONF_TYPE: CONF_IS_NOT_OCCUPIED}, + ], + DEVICE_CLASS_OPENING: [{CONF_TYPE: CONF_IS_OPEN}, {CONF_TYPE: CONF_IS_NOT_OPEN}], + DEVICE_CLASS_PLUG: [ + {CONF_TYPE: CONF_IS_PLUGGED_IN}, + {CONF_TYPE: CONF_IS_NOT_PLUGGED_IN}, + ], + DEVICE_CLASS_POWER: [ + {CONF_TYPE: CONF_IS_POWERED}, + {CONF_TYPE: CONF_IS_NOT_POWERED}, + ], + DEVICE_CLASS_PRESENCE: [ + {CONF_TYPE: CONF_IS_PRESENT}, + {CONF_TYPE: CONF_IS_NOT_PRESENT}, + ], + DEVICE_CLASS_PROBLEM: [ + {CONF_TYPE: CONF_IS_PROBLEM}, + {CONF_TYPE: CONF_IS_NO_PROBLEM}, + ], + DEVICE_CLASS_SAFETY: [{CONF_TYPE: CONF_IS_UNSAFE}, {CONF_TYPE: CONF_IS_NOT_UNSAFE}], + DEVICE_CLASS_SMOKE: [{CONF_TYPE: CONF_IS_SMOKE}, {CONF_TYPE: CONF_IS_NO_SMOKE}], + DEVICE_CLASS_SOUND: [{CONF_TYPE: CONF_IS_SOUND}, {CONF_TYPE: CONF_IS_NO_SOUND}], + DEVICE_CLASS_VIBRATION: [ + {CONF_TYPE: CONF_IS_VIBRATION}, + {CONF_TYPE: CONF_IS_NO_VIBRATION}, + ], + DEVICE_CLASS_WINDOW: [{CONF_TYPE: CONF_IS_OPEN}, {CONF_TYPE: CONF_IS_NOT_OPEN}], + DEVICE_CLASS_NONE: [{CONF_TYPE: CONF_IS_ON}, {CONF_TYPE: CONF_IS_OFF}], +} + +CONDITION_SCHEMA = cv.DEVICE_CONDITION_BASE_SCHEMA.extend( + { + vol.Required(CONF_ENTITY_ID): cv.entity_id, + vol.Required(CONF_TYPE): vol.In(IS_OFF + IS_ON), + } +) + + +async def async_get_conditions(hass: HomeAssistant, device_id: str) -> List[dict]: + """List device conditions.""" + conditions: List[dict] = [] + entity_registry = await async_get_registry(hass) + entries = [ + entry + for entry in async_entries_for_device(entity_registry, device_id) + if entry.domain == DOMAIN + ] + + for entry in entries: + device_class = DEVICE_CLASS_NONE + state = hass.states.get(entry.entity_id) + if state and ATTR_DEVICE_CLASS in state.attributes: + device_class = state.attributes[ATTR_DEVICE_CLASS] + + templates = ENTITY_CONDITIONS.get( + device_class, ENTITY_CONDITIONS[DEVICE_CLASS_NONE] + ) + + conditions.extend( + ( + { + **template, + "condition": "device", + "device_id": device_id, + "entity_id": entry.entity_id, + "domain": DOMAIN, + } + for template in templates + ) + ) + + return conditions + + +def async_condition_from_config( + config: ConfigType, config_validation: bool +) -> condition.ConditionCheckerType: + """Evaluate state based on configuration.""" + if config_validation: + config = CONDITION_SCHEMA(config) + condition_type = config[CONF_TYPE] + if condition_type in IS_ON: + stat = "on" + else: + stat = "off" + state_config = { + condition.CONF_CONDITION: "state", + condition.CONF_ENTITY_ID: config[CONF_ENTITY_ID], + condition.CONF_STATE: stat, + } + + return condition.state_from_config(state_config, config_validation) diff --git a/homeassistant/components/binary_sensor/device_trigger.py b/homeassistant/components/binary_sensor/device_trigger.py new file mode 100644 index 00000000000..f138bcfd5a8 --- /dev/null +++ b/homeassistant/components/binary_sensor/device_trigger.py @@ -0,0 +1,249 @@ +"""Provides device triggers for binary sensors.""" +import voluptuous as vol + +from homeassistant.components.automation import state as state_automation +from homeassistant.components.device_automation import TRIGGER_BASE_SCHEMA +from homeassistant.components.device_automation.const import ( + CONF_TURNED_OFF, + CONF_TURNED_ON, +) +from homeassistant.const import ATTR_DEVICE_CLASS, CONF_ENTITY_ID, CONF_FOR, CONF_TYPE +from homeassistant.helpers.entity_registry import async_entries_for_device +from homeassistant.helpers import config_validation as cv + +from . import ( + DOMAIN, + DEVICE_CLASS_BATTERY, + DEVICE_CLASS_COLD, + DEVICE_CLASS_CONNECTIVITY, + DEVICE_CLASS_DOOR, + DEVICE_CLASS_GARAGE_DOOR, + DEVICE_CLASS_GAS, + DEVICE_CLASS_HEAT, + DEVICE_CLASS_LIGHT, + DEVICE_CLASS_LOCK, + DEVICE_CLASS_MOISTURE, + DEVICE_CLASS_MOTION, + DEVICE_CLASS_MOVING, + DEVICE_CLASS_OCCUPANCY, + DEVICE_CLASS_OPENING, + DEVICE_CLASS_PLUG, + DEVICE_CLASS_POWER, + DEVICE_CLASS_PRESENCE, + DEVICE_CLASS_PROBLEM, + DEVICE_CLASS_SAFETY, + DEVICE_CLASS_SMOKE, + DEVICE_CLASS_SOUND, + DEVICE_CLASS_VIBRATION, + DEVICE_CLASS_WINDOW, +) + + +# mypy: allow-untyped-defs, no-check-untyped-defs + +DEVICE_CLASS_NONE = "none" + +CONF_BAT_LOW = "bat_low" +CONF_NOT_BAT_LOW = "not_bat_low" +CONF_COLD = "cold" +CONF_NOT_COLD = "not_cold" +CONF_CONNECTED = "connected" +CONF_NOT_CONNECTED = "not_connected" +CONF_GAS = "gas" +CONF_NO_GAS = "no_gas" +CONF_HOT = "hot" +CONF_NOT_HOT = "not_hot" +CONF_LIGHT = "light" +CONF_NO_LIGHT = "no_light" +CONF_LOCKED = "locked" +CONF_NOT_LOCKED = "not_locked" +CONF_MOIST = "moist" +CONF_NOT_MOIST = "not_moist" +CONF_MOTION = "motion" +CONF_NO_MOTION = "no_motion" +CONF_MOVING = "moving" +CONF_NOT_MOVING = "not_moving" +CONF_OCCUPIED = "occupied" +CONF_NOT_OCCUPIED = "not_occupied" +CONF_PLUGGED_IN = "plugged_in" +CONF_NOT_PLUGGED_IN = "not_plugged_in" +CONF_POWERED = "powered" +CONF_NOT_POWERED = "not_powered" +CONF_PRESENT = "present" +CONF_NOT_PRESENT = "not_present" +CONF_PROBLEM = "problem" +CONF_NO_PROBLEM = "no_problem" +CONF_UNSAFE = "unsafe" +CONF_NOT_UNSAFE = "not_unsafe" +CONF_SMOKE = "smoke" +CONF_NO_SMOKE = "no_smoke" +CONF_SOUND = "sound" +CONF_NO_SOUND = "no_sound" +CONF_VIBRATION = "vibration" +CONF_NO_VIBRATION = "no_vibration" +CONF_OPENED = "opened" +CONF_NOT_OPENED = "not_opened" + + +TURNED_ON = [ + CONF_BAT_LOW, + CONF_COLD, + CONF_CONNECTED, + CONF_GAS, + CONF_HOT, + CONF_LIGHT, + CONF_LOCKED, + CONF_MOIST, + CONF_MOTION, + CONF_MOVING, + CONF_OCCUPIED, + CONF_OPENED, + CONF_PLUGGED_IN, + CONF_POWERED, + CONF_PRESENT, + CONF_PROBLEM, + CONF_SMOKE, + CONF_SOUND, + CONF_UNSAFE, + CONF_VIBRATION, + CONF_TURNED_ON, +] + +TURNED_OFF = [ + CONF_NOT_BAT_LOW, + CONF_NOT_COLD, + CONF_NOT_CONNECTED, + CONF_NOT_HOT, + CONF_NOT_LOCKED, + CONF_NOT_MOIST, + CONF_NOT_MOVING, + CONF_NOT_OCCUPIED, + CONF_NOT_OPENED, + CONF_NOT_PLUGGED_IN, + CONF_NOT_POWERED, + CONF_NOT_PRESENT, + CONF_NOT_UNSAFE, + CONF_NO_GAS, + CONF_NO_LIGHT, + CONF_NO_MOTION, + CONF_NO_PROBLEM, + CONF_NO_SMOKE, + CONF_NO_SOUND, + CONF_NO_VIBRATION, + CONF_TURNED_OFF, +] + + +ENTITY_TRIGGERS = { + DEVICE_CLASS_BATTERY: [{CONF_TYPE: CONF_BAT_LOW}, {CONF_TYPE: CONF_NOT_BAT_LOW}], + DEVICE_CLASS_COLD: [{CONF_TYPE: CONF_COLD}, {CONF_TYPE: CONF_NOT_COLD}], + DEVICE_CLASS_CONNECTIVITY: [ + {CONF_TYPE: CONF_CONNECTED}, + {CONF_TYPE: CONF_NOT_CONNECTED}, + ], + DEVICE_CLASS_DOOR: [{CONF_TYPE: CONF_OPENED}, {CONF_TYPE: CONF_NOT_OPENED}], + DEVICE_CLASS_GARAGE_DOOR: [{CONF_TYPE: CONF_OPENED}, {CONF_TYPE: CONF_NOT_OPENED}], + DEVICE_CLASS_GAS: [{CONF_TYPE: CONF_GAS}, {CONF_TYPE: CONF_NO_GAS}], + DEVICE_CLASS_HEAT: [{CONF_TYPE: CONF_HOT}, {CONF_TYPE: CONF_NOT_HOT}], + DEVICE_CLASS_LIGHT: [{CONF_TYPE: CONF_LIGHT}, {CONF_TYPE: CONF_NO_LIGHT}], + DEVICE_CLASS_LOCK: [{CONF_TYPE: CONF_LOCKED}, {CONF_TYPE: CONF_NOT_LOCKED}], + DEVICE_CLASS_MOISTURE: [{CONF_TYPE: CONF_MOIST}, {CONF_TYPE: CONF_NOT_MOIST}], + DEVICE_CLASS_MOTION: [{CONF_TYPE: CONF_MOTION}, {CONF_TYPE: CONF_NO_MOTION}], + DEVICE_CLASS_MOVING: [{CONF_TYPE: CONF_MOVING}, {CONF_TYPE: CONF_NOT_MOVING}], + DEVICE_CLASS_OCCUPANCY: [ + {CONF_TYPE: CONF_OCCUPIED}, + {CONF_TYPE: CONF_NOT_OCCUPIED}, + ], + DEVICE_CLASS_OPENING: [{CONF_TYPE: CONF_OPENED}, {CONF_TYPE: CONF_NOT_OPENED}], + DEVICE_CLASS_PLUG: [{CONF_TYPE: CONF_PLUGGED_IN}, {CONF_TYPE: CONF_NOT_PLUGGED_IN}], + DEVICE_CLASS_POWER: [{CONF_TYPE: CONF_POWERED}, {CONF_TYPE: CONF_NOT_POWERED}], + DEVICE_CLASS_PRESENCE: [{CONF_TYPE: CONF_PRESENT}, {CONF_TYPE: CONF_NOT_PRESENT}], + DEVICE_CLASS_PROBLEM: [{CONF_TYPE: CONF_PROBLEM}, {CONF_TYPE: CONF_NO_PROBLEM}], + DEVICE_CLASS_SAFETY: [{CONF_TYPE: CONF_UNSAFE}, {CONF_TYPE: CONF_NOT_UNSAFE}], + DEVICE_CLASS_SMOKE: [{CONF_TYPE: CONF_SMOKE}, {CONF_TYPE: CONF_NO_SMOKE}], + DEVICE_CLASS_SOUND: [{CONF_TYPE: CONF_SOUND}, {CONF_TYPE: CONF_NO_SOUND}], + DEVICE_CLASS_VIBRATION: [ + {CONF_TYPE: CONF_VIBRATION}, + {CONF_TYPE: CONF_NO_VIBRATION}, + ], + DEVICE_CLASS_WINDOW: [{CONF_TYPE: CONF_OPENED}, {CONF_TYPE: CONF_NOT_OPENED}], + DEVICE_CLASS_NONE: [{CONF_TYPE: CONF_TURNED_ON}, {CONF_TYPE: CONF_TURNED_OFF}], +} + + +TRIGGER_SCHEMA = TRIGGER_BASE_SCHEMA.extend( + { + vol.Required(CONF_ENTITY_ID): cv.entity_id, + vol.Required(CONF_TYPE): vol.In(TURNED_OFF + TURNED_ON), + vol.Optional(CONF_FOR): cv.positive_time_period_dict, + } +) + + +async def async_attach_trigger(hass, config, action, automation_info): + """Listen for state changes based on configuration.""" + trigger_type = config[CONF_TYPE] + if trigger_type in TURNED_ON: + from_state = "off" + to_state = "on" + else: + from_state = "on" + to_state = "off" + + state_config = { + state_automation.CONF_ENTITY_ID: config[CONF_ENTITY_ID], + state_automation.CONF_FROM: from_state, + state_automation.CONF_TO: to_state, + } + if CONF_FOR in config: + state_config[CONF_FOR] = config[CONF_FOR] + + return await state_automation.async_attach_trigger( + hass, state_config, action, automation_info, platform_type="device" + ) + + +async def async_get_triggers(hass, device_id): + """List device triggers.""" + triggers = [] + entity_registry = await hass.helpers.entity_registry.async_get_registry() + + entries = [ + entry + for entry in async_entries_for_device(entity_registry, device_id) + if entry.domain == DOMAIN + ] + + for entry in entries: + device_class = DEVICE_CLASS_NONE + state = hass.states.get(entry.entity_id) + if state: + device_class = state.attributes.get(ATTR_DEVICE_CLASS) + + templates = ENTITY_TRIGGERS.get( + device_class, ENTITY_TRIGGERS[DEVICE_CLASS_NONE] + ) + + triggers.extend( + ( + { + **automation, + "platform": "device", + "device_id": device_id, + "entity_id": entry.entity_id, + "domain": DOMAIN, + } + for automation in templates + ) + ) + + return triggers + + +async def async_get_trigger_capabilities(hass, trigger): + """List trigger capabilities.""" + return { + "extra_fields": vol.Schema( + {vol.Optional(CONF_FOR): cv.positive_time_period_dict} + ) + } diff --git a/homeassistant/components/binary_sensor/manifest.json b/homeassistant/components/binary_sensor/manifest.json index d627351958d..ea29314eb1b 100644 --- a/homeassistant/components/binary_sensor/manifest.json +++ b/homeassistant/components/binary_sensor/manifest.json @@ -1,7 +1,7 @@ { "domain": "binary_sensor", "name": "Binary sensor", - "documentation": "https://www.home-assistant.io/components/binary_sensor", + "documentation": "https://www.home-assistant.io/integrations/binary_sensor", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/binary_sensor/strings.json b/homeassistant/components/binary_sensor/strings.json new file mode 100644 index 00000000000..e01af8d183e --- /dev/null +++ b/homeassistant/components/binary_sensor/strings.json @@ -0,0 +1,93 @@ +{ + "device_automation": { + "condition_type": { + "is_bat_low": "{entity_name} battery is low", + "is_not_bat_low": "{entity_name} battery is normal", + "is_cold": "{entity_name} is cold", + "is_not_cold": "{entity_name} is not cold", + "is_connected": "{entity_name} is connected", + "is_not_connected": "{entity_name} is disconnected", + "is_gas": "{entity_name} is detecting gas", + "is_no_gas": "{entity_name} is not detecting gas", + "is_hot": "{entity_name} is hot", + "is_not_hot": "{entity_name} is not hot", + "is_light": "{entity_name} is detecting light", + "is_no_light": "{entity_name} is not detecting light", + "is_locked": "{entity_name} is locked", + "is_not_locked": "{entity_name} is unlocked", + "is_moist": "{entity_name} is moist", + "is_not_moist": "{entity_name} is dry", + "is_motion": "{entity_name} is detecting motion", + "is_no_motion": "{entity_name} is not detecting motion", + "is_moving": "{entity_name} is moving", + "is_not_moving": "{entity_name} is not moving", + "is_occupied": "{entity_name} is occupied", + "is_not_occupied": "{entity_name} is not occupied", + "is_plugged_in": "{entity_name} is plugged in", + "is_not_plugged_in": "{entity_name} is unplugged", + "is_powered": "{entity_name} is powered", + "is_not_powered": "{entity_name} is not powered", + "is_present": "{entity_name} is present", + "is_not_present": "{entity_name} is not present", + "is_problem": "{entity_name} is detecting problem", + "is_no_problem": "{entity_name} is not detecting problem", + "is_unsafe": "{entity_name} is unsafe", + "is_not_unsafe": "{entity_name} is safe", + "is_smoke": "{entity_name} is detecting smoke", + "is_no_smoke": "{entity_name} is not detecting smoke", + "is_sound": "{entity_name} is detecting sound", + "is_no_sound": "{entity_name} is not detecting sound", + "is_vibration": "{entity_name} is detecting vibration", + "is_no_vibration": "{entity_name} is not detecting vibration", + "is_open": "{entity_name} is open", + "is_not_open": "{entity_name} is closed", + "is_on": "{entity_name} is on", + "is_off": "{entity_name} is off" + }, + "trigger_type": { + "bat_low": "{entity_name} battery low", + "not_bat_low": "{entity_name} battery normal", + "cold": "{entity_name} became cold", + "not_cold": "{entity_name} became not cold", + "connected": "{entity_name} connected", + "not_connected": "{entity_name} disconnected", + "gas": "{entity_name} started detecting gas", + "no_gas": "{entity_name} stopped detecting gas", + "hot": "{entity_name} became hot", + "not_hot": "{entity_name} became not hot", + "light": "{entity_name} started detecting light", + "no_light": "{entity_name} stopped detecting light", + "locked": "{entity_name} locked", + "not_locked": "{entity_name} unlocked", + "moist": "{entity_name} became moist", + "not_moist": "{entity_name} became dry", + "motion": "{entity_name} started detecting motion", + "no_motion": "{entity_name} stopped detecting motion", + "moving": "{entity_name} started moving", + "not_moving": "{entity_name} stopped moving", + "occupied": "{entity_name} became occupied", + "not_occupied": "{entity_name} became not occupied", + "plugged_in": "{entity_name} plugged in", + "not_plugged_in": "{entity_name} unplugged", + "powered": "{entity_name} powered", + "not_powered": "{entity_name} not powered", + "present": "{entity_name} present", + "not_present": "{entity_name} not present", + "problem": "{entity_name} started detecting problem", + "no_problem": "{entity_name} stopped detecting problem", + "unsafe": "{entity_name} became unsafe", + "not_unsafe": "{entity_name} became safe", + "smoke": "{entity_name} started detecting smoke", + "no_smoke": "{entity_name} stopped detecting smoke", + "sound": "{entity_name} started detecting sound", + "no_sound": "{entity_name} stopped detecting sound", + "vibration": "{entity_name} started detecting vibration", + "no_vibration": "{entity_name} stopped detecting vibration", + "opened": "{entity_name} opened", + "not_opened": "{entity_name} closed", + "turned_on": "{entity_name} turned on", + "turned_off": "{entity_name} turned off" + + } + } +} diff --git a/homeassistant/components/bitcoin/manifest.json b/homeassistant/components/bitcoin/manifest.json index 85da99a6885..1ffc34fcd9d 100644 --- a/homeassistant/components/bitcoin/manifest.json +++ b/homeassistant/components/bitcoin/manifest.json @@ -1,7 +1,7 @@ { "domain": "bitcoin", "name": "Bitcoin", - "documentation": "https://www.home-assistant.io/components/bitcoin", + "documentation": "https://www.home-assistant.io/integrations/bitcoin", "requirements": [ "blockchain==1.4.4" ], diff --git a/homeassistant/components/bizkaibus/manifest.json b/homeassistant/components/bizkaibus/manifest.json index 98cbbc9be56..63c0494c2f1 100644 --- a/homeassistant/components/bizkaibus/manifest.json +++ b/homeassistant/components/bizkaibus/manifest.json @@ -1,7 +1,7 @@ { "domain": "bizkaibus", "name": "Bizkaibus", - "documentation": "https://www.home-assistant.io/components/bizkaibus", + "documentation": "https://www.home-assistant.io/integrations/bizkaibus", "dependencies": [], "codeowners": ["@UgaitzEtxebarria"], "requirements": ["bizkaibus==0.1.1"] diff --git a/homeassistant/components/blackbird/manifest.json b/homeassistant/components/blackbird/manifest.json index 9e3e41290ea..c5b3a632c16 100644 --- a/homeassistant/components/blackbird/manifest.json +++ b/homeassistant/components/blackbird/manifest.json @@ -1,7 +1,7 @@ { "domain": "blackbird", "name": "Blackbird", - "documentation": "https://www.home-assistant.io/components/blackbird", + "documentation": "https://www.home-assistant.io/integrations/blackbird", "requirements": [ "pyblackbird==0.5" ], diff --git a/homeassistant/components/blink/manifest.json b/homeassistant/components/blink/manifest.json index 98c609731c6..a38ba0bd613 100644 --- a/homeassistant/components/blink/manifest.json +++ b/homeassistant/components/blink/manifest.json @@ -1,7 +1,7 @@ { "domain": "blink", "name": "Blink", - "documentation": "https://www.home-assistant.io/components/blink", + "documentation": "https://www.home-assistant.io/integrations/blink", "requirements": [ "blinkpy==0.14.1" ], diff --git a/homeassistant/components/blinksticklight/manifest.json b/homeassistant/components/blinksticklight/manifest.json index a5277c97d99..0a6e2540790 100644 --- a/homeassistant/components/blinksticklight/manifest.json +++ b/homeassistant/components/blinksticklight/manifest.json @@ -1,7 +1,7 @@ { "domain": "blinksticklight", "name": "Blinksticklight", - "documentation": "https://www.home-assistant.io/components/blinksticklight", + "documentation": "https://www.home-assistant.io/integrations/blinksticklight", "requirements": [ "blinkstick==1.1.8" ], diff --git a/homeassistant/components/blinkt/manifest.json b/homeassistant/components/blinkt/manifest.json index c11583ed59e..629bdebf27e 100644 --- a/homeassistant/components/blinkt/manifest.json +++ b/homeassistant/components/blinkt/manifest.json @@ -1,7 +1,7 @@ { "domain": "blinkt", "name": "Blinkt", - "documentation": "https://www.home-assistant.io/components/blinkt", + "documentation": "https://www.home-assistant.io/integrations/blinkt", "requirements": [ "blinkt==0.1.0" ], diff --git a/homeassistant/components/blockchain/manifest.json b/homeassistant/components/blockchain/manifest.json index 8a2a9f7b71f..773b52e724b 100644 --- a/homeassistant/components/blockchain/manifest.json +++ b/homeassistant/components/blockchain/manifest.json @@ -1,7 +1,7 @@ { "domain": "blockchain", "name": "Blockchain", - "documentation": "https://www.home-assistant.io/components/blockchain", + "documentation": "https://www.home-assistant.io/integrations/blockchain", "requirements": [ "python-blockchain-api==0.0.2" ], diff --git a/homeassistant/components/bloomsky/camera.py b/homeassistant/components/bloomsky/camera.py index 9b8c4ab283f..d62dede5abd 100644 --- a/homeassistant/components/bloomsky/camera.py +++ b/homeassistant/components/bloomsky/camera.py @@ -19,7 +19,7 @@ class BloomSkyCamera(Camera): def __init__(self, bs, device): """Initialize access to the BloomSky camera images.""" - super(BloomSkyCamera, self).__init__() + super().__init__() self._name = device["DeviceName"] self._id = device["DeviceID"] self._bloomsky = bs diff --git a/homeassistant/components/bloomsky/manifest.json b/homeassistant/components/bloomsky/manifest.json index 3a780507dd5..49da6534bae 100644 --- a/homeassistant/components/bloomsky/manifest.json +++ b/homeassistant/components/bloomsky/manifest.json @@ -1,7 +1,7 @@ { "domain": "bloomsky", "name": "Bloomsky", - "documentation": "https://www.home-assistant.io/components/bloomsky", + "documentation": "https://www.home-assistant.io/integrations/bloomsky", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/bluesound/manifest.json b/homeassistant/components/bluesound/manifest.json index 7731f845005..e64e3e61f16 100644 --- a/homeassistant/components/bluesound/manifest.json +++ b/homeassistant/components/bluesound/manifest.json @@ -1,7 +1,7 @@ { "domain": "bluesound", "name": "Bluesound", - "documentation": "https://www.home-assistant.io/components/bluesound", + "documentation": "https://www.home-assistant.io/integrations/bluesound", "requirements": [ "xmltodict==0.12.0" ], diff --git a/homeassistant/components/bluetooth_le_tracker/device_tracker.py b/homeassistant/components/bluetooth_le_tracker/device_tracker.py index 8cba3032f54..29eecdfd077 100644 --- a/homeassistant/components/bluetooth_le_tracker/device_tracker.py +++ b/homeassistant/components/bluetooth_le_tracker/device_tracker.py @@ -1,4 +1,5 @@ """Tracking for bluetooth low energy devices.""" +import asyncio import logging from homeassistant.helpers.event import track_point_in_utc_time @@ -14,7 +15,6 @@ from homeassistant.components.device_tracker.const import ( ) from homeassistant.const import EVENT_HOMEASSISTANT_STOP import homeassistant.util.dt as dt_util -from homeassistant.util.async_ import run_coroutine_threadsafe _LOGGER = logging.getLogger(__name__) @@ -89,7 +89,7 @@ def setup_scanner(hass, config, see, discovery_info=None): # Load all known devices. # We just need the devices so set consider_home and home range # to 0 - for device in run_coroutine_threadsafe( + for device in asyncio.run_coroutine_threadsafe( async_load_config(yaml_path, hass, 0), hass.loop ).result(): # check if device is a valid bluetooth device diff --git a/homeassistant/components/bluetooth_le_tracker/manifest.json b/homeassistant/components/bluetooth_le_tracker/manifest.json index d2f8f10290e..30ed924a9dc 100644 --- a/homeassistant/components/bluetooth_le_tracker/manifest.json +++ b/homeassistant/components/bluetooth_le_tracker/manifest.json @@ -1,7 +1,7 @@ { "domain": "bluetooth_le_tracker", "name": "Bluetooth le tracker", - "documentation": "https://www.home-assistant.io/components/bluetooth_le_tracker", + "documentation": "https://www.home-assistant.io/integrations/bluetooth_le_tracker", "requirements": [ "pygatt[GATTTOOL]==4.0.1" ], diff --git a/homeassistant/components/bluetooth_tracker/device_tracker.py b/homeassistant/components/bluetooth_tracker/device_tracker.py index e760f91070a..6a26775b0a8 100644 --- a/homeassistant/components/bluetooth_tracker/device_tracker.py +++ b/homeassistant/components/bluetooth_tracker/device_tracker.py @@ -1,25 +1,30 @@ """Tracking for bluetooth devices.""" +import asyncio import logging +from typing import List, Set, Tuple, Optional +# pylint: disable=import-error +import bluetooth +from bt_proximity import BluetoothRSSI import voluptuous as vol -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.event import track_point_in_utc_time from homeassistant.components.device_tracker import PLATFORM_SCHEMA +from homeassistant.components.device_tracker.const import ( + CONF_SCAN_INTERVAL, + CONF_TRACK_NEW, + DEFAULT_TRACK_NEW, + DOMAIN, + SCAN_INTERVAL, + SOURCE_TYPE_BLUETOOTH, +) from homeassistant.components.device_tracker.legacy import ( YAML_DEVICES, async_load_config, ) -from homeassistant.components.device_tracker.const import ( - CONF_TRACK_NEW, - CONF_SCAN_INTERVAL, - SCAN_INTERVAL, - DEFAULT_TRACK_NEW, - SOURCE_TYPE_BLUETOOTH, - DOMAIN, -) -import homeassistant.util.dt as dt_util -from homeassistant.util.async_ import run_coroutine_threadsafe +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.event import async_track_time_interval +from homeassistant.helpers.typing import HomeAssistantType + _LOGGER = logging.getLogger(__name__) @@ -42,100 +47,145 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( ) -def setup_scanner(hass, config, see, discovery_info=None): +def is_bluetooth_device(device) -> bool: + """Check whether a device is a bluetooth device by its mac.""" + return device.mac and device.mac[:3].upper() == BT_PREFIX + + +def discover_devices(device_id: int) -> List[Tuple[str, str]]: + """Discover Bluetooth devices.""" + result = bluetooth.discover_devices( + duration=8, + lookup_names=True, + flush_cache=True, + lookup_class=False, + device_id=device_id, + ) + _LOGGER.debug("Bluetooth devices discovered = %d", len(result)) + return result + + +async def see_device( + hass: HomeAssistantType, async_see, mac: str, device_name: str, rssi=None +) -> None: + """Mark a device as seen.""" + attributes = {} + if rssi is not None: + attributes["rssi"] = rssi + + await async_see( + mac=f"{BT_PREFIX}{mac}", + host_name=device_name, + attributes=attributes, + source_type=SOURCE_TYPE_BLUETOOTH, + ) + + +async def get_tracking_devices(hass: HomeAssistantType) -> Tuple[Set[str], Set[str]]: + """ + Load all known devices. + + We just need the devices so set consider_home and home range to 0 + """ + yaml_path: str = hass.config.path(YAML_DEVICES) + + devices = await async_load_config(yaml_path, hass, 0) + bluetooth_devices = [device for device in devices if is_bluetooth_device(device)] + + devices_to_track: Set[str] = { + device.mac[3:] for device in bluetooth_devices if device.track + } + devices_to_not_track: Set[str] = { + device.mac[3:] for device in bluetooth_devices if not device.track + } + + return devices_to_track, devices_to_not_track + + +def lookup_name(mac: str) -> Optional[str]: + """Lookup a Bluetooth device name.""" + _LOGGER.debug("Scanning %s", mac) + return bluetooth.lookup_name(mac, timeout=5) + + +async def async_setup_scanner( + hass: HomeAssistantType, config: dict, async_see, discovery_info=None +): """Set up the Bluetooth Scanner.""" - # pylint: disable=import-error - import bluetooth - from bt_proximity import BluetoothRSSI - - def see_device(mac, name, rssi=None): - """Mark a device as seen.""" - attributes = {} - if rssi is not None: - attributes["rssi"] = rssi - see( - mac=f"{BT_PREFIX}{mac}", - host_name=name, - attributes=attributes, - source_type=SOURCE_TYPE_BLUETOOTH, - ) - - device_id = config.get(CONF_DEVICE_ID) - - def discover_devices(): - """Discover Bluetooth devices.""" - result = bluetooth.discover_devices( - duration=8, - lookup_names=True, - flush_cache=True, - lookup_class=False, - device_id=device_id, - ) - _LOGGER.debug("Bluetooth devices discovered = %d", len(result)) - return result - - yaml_path = hass.config.path(YAML_DEVICES) - devs_to_track = [] - devs_donot_track = [] - - # Load all known devices. - # We just need the devices so set consider_home and home range - # to 0 - for device in run_coroutine_threadsafe( - async_load_config(yaml_path, hass, 0), hass.loop - ).result(): - # Check if device is a valid bluetooth device - if device.mac and device.mac[:3].upper() == BT_PREFIX: - if device.track: - devs_to_track.append(device.mac[3:]) - else: - devs_donot_track.append(device.mac[3:]) + device_id: int = config.get(CONF_DEVICE_ID) + interval = config.get(CONF_SCAN_INTERVAL, SCAN_INTERVAL) + request_rssi = config.get(CONF_REQUEST_RSSI, False) + update_bluetooth_lock = asyncio.Lock() # If track new devices is true discover new devices on startup. - track_new = config.get(CONF_TRACK_NEW, DEFAULT_TRACK_NEW) - if track_new: - for dev in discover_devices(): - if dev[0] not in devs_to_track and dev[0] not in devs_donot_track: - devs_to_track.append(dev[0]) - see_device(dev[0], dev[1]) + track_new: bool = config.get(CONF_TRACK_NEW, DEFAULT_TRACK_NEW) + _LOGGER.debug("Tracking new devices is set to %s", track_new) - interval = config.get(CONF_SCAN_INTERVAL, SCAN_INTERVAL) + devices_to_track, devices_to_not_track = await get_tracking_devices(hass) - request_rssi = config.get(CONF_REQUEST_RSSI, False) + if not devices_to_track and not track_new: + _LOGGER.debug("No Bluetooth devices to track and not tracking new devices") - def update_bluetooth(_): - """Update Bluetooth and set timer for the next update.""" - update_bluetooth_once() - track_point_in_utc_time(hass, update_bluetooth, dt_util.utcnow() + interval) + if request_rssi: + _LOGGER.debug("Detecting RSSI for devices") + + async def perform_bluetooth_update(): + """Discover Bluetooth devices and update status.""" + + _LOGGER.debug("Performing Bluetooth devices discovery and update") + tasks = [] - def update_bluetooth_once(): - """Lookup Bluetooth device and update status.""" try: if track_new: - for dev in discover_devices(): - if dev[0] not in devs_to_track and dev[0] not in devs_donot_track: - devs_to_track.append(dev[0]) - for mac in devs_to_track: - _LOGGER.debug("Scanning %s", mac) - result = bluetooth.lookup_name(mac, timeout=5) + devices = await hass.async_add_executor_job(discover_devices, device_id) + for mac, device_name in devices: + if mac not in devices_to_track and mac not in devices_to_not_track: + devices_to_track.add(mac) + + for mac in devices_to_track: + device_name = await hass.async_add_executor_job(lookup_name, mac) + if device_name is None: + # Could not lookup device name + continue + rssi = None if request_rssi: client = BluetoothRSSI(mac) - rssi = client.request_rssi() + rssi = await hass.async_add_executor_job(client.request_rssi) client.close() - if result is None: - # Could not lookup device name - continue - see_device(mac, result, rssi) + + tasks.append(see_device(hass, async_see, mac, device_name, rssi)) + + if tasks: + await asyncio.wait(tasks) + except bluetooth.BluetoothError: _LOGGER.exception("Error looking up Bluetooth device") - def handle_update_bluetooth(call): + async def update_bluetooth(now=None): + """Lookup Bluetooth devices and update status.""" + + # If an update is in progress, we don't do anything + if update_bluetooth_lock.locked(): + _LOGGER.debug( + "Previous execution of update_bluetooth is taking longer than the scheduled update of interval %s", + interval, + ) + return + + async with update_bluetooth_lock: + await perform_bluetooth_update() + + async def handle_manual_update_bluetooth(call): """Update bluetooth devices on demand.""" - update_bluetooth_once() - update_bluetooth(dt_util.utcnow()) + await update_bluetooth() - hass.services.register(DOMAIN, "bluetooth_tracker_update", handle_update_bluetooth) + hass.async_create_task(update_bluetooth()) + async_track_time_interval(hass, update_bluetooth, interval) + + hass.services.async_register( + DOMAIN, "bluetooth_tracker_update", handle_manual_update_bluetooth + ) return True diff --git a/homeassistant/components/bluetooth_tracker/manifest.json b/homeassistant/components/bluetooth_tracker/manifest.json index c853bc5a838..20fe51c561b 100644 --- a/homeassistant/components/bluetooth_tracker/manifest.json +++ b/homeassistant/components/bluetooth_tracker/manifest.json @@ -1,7 +1,7 @@ { "domain": "bluetooth_tracker", "name": "Bluetooth tracker", - "documentation": "https://www.home-assistant.io/components/bluetooth_tracker", + "documentation": "https://www.home-assistant.io/integrations/bluetooth_tracker", "requirements": [ "bt_proximity==0.2", "pybluez==0.22" diff --git a/homeassistant/components/bme280/manifest.json b/homeassistant/components/bme280/manifest.json index 2342c8418eb..9d01b301d43 100644 --- a/homeassistant/components/bme280/manifest.json +++ b/homeassistant/components/bme280/manifest.json @@ -1,7 +1,7 @@ { "domain": "bme280", "name": "Bme280", - "documentation": "https://www.home-assistant.io/components/bme280", + "documentation": "https://www.home-assistant.io/integrations/bme280", "requirements": [ "i2csense==0.0.4", "smbus-cffi==0.5.1" diff --git a/homeassistant/components/bme680/manifest.json b/homeassistant/components/bme680/manifest.json index 976be85ca94..c062d14f8c9 100644 --- a/homeassistant/components/bme680/manifest.json +++ b/homeassistant/components/bme680/manifest.json @@ -1,7 +1,7 @@ { "domain": "bme680", "name": "Bme680", - "documentation": "https://www.home-assistant.io/components/bme680", + "documentation": "https://www.home-assistant.io/integrations/bme680", "requirements": [ "bme680==1.0.5", "smbus-cffi==0.5.1" diff --git a/homeassistant/components/bmw_connected_drive/__init__.py b/homeassistant/components/bmw_connected_drive/__init__.py index 160c8a5e455..8e67da86dc3 100644 --- a/homeassistant/components/bmw_connected_drive/__init__.py +++ b/homeassistant/components/bmw_connected_drive/__init__.py @@ -1,5 +1,4 @@ """Reads vehicle status from BMW connected drive portal.""" -import datetime import logging import voluptuous as vol @@ -8,6 +7,7 @@ from homeassistant.const import CONF_USERNAME, CONF_PASSWORD from homeassistant.helpers import discovery from homeassistant.helpers.event import track_utc_time_change import homeassistant.helpers.config_validation as cv +import homeassistant.util.dt as dt_util _LOGGER = logging.getLogger(__name__) @@ -100,7 +100,7 @@ def setup_account(account_config: dict, hass, name: str) -> "BMWConnectedDriveAc # update every UPDATE_INTERVAL minutes, starting now # this should even out the load on the servers - now = datetime.datetime.now() + now = dt_util.utcnow() track_utc_time_change( hass, cd_account.update, diff --git a/homeassistant/components/bmw_connected_drive/manifest.json b/homeassistant/components/bmw_connected_drive/manifest.json index 0cc875c50f9..29366715d23 100644 --- a/homeassistant/components/bmw_connected_drive/manifest.json +++ b/homeassistant/components/bmw_connected_drive/manifest.json @@ -1,7 +1,7 @@ { "domain": "bmw_connected_drive", "name": "BMW Connected Drive", - "documentation": "https://www.home-assistant.io/components/bmw_connected_drive", + "documentation": "https://www.home-assistant.io/integrations/bmw_connected_drive", "requirements": [ "bimmer_connected==0.6.0" ], diff --git a/homeassistant/components/bmw_connected_drive/sensor.py b/homeassistant/components/bmw_connected_drive/sensor.py index 96d541b1955..28a4e853f2c 100644 --- a/homeassistant/components/bmw_connected_drive/sensor.py +++ b/homeassistant/components/bmw_connected_drive/sensor.py @@ -24,6 +24,8 @@ ATTR_TO_HA_METRIC = { "remaining_fuel": ["mdi:gas-station", VOLUME_LITERS], "charging_time_remaining": ["mdi:update", "h"], "charging_status": ["mdi:battery-charging", None], + # No icon as this is dealt with directly as a special case in icon() + "charging_level_hv": [None, "%"], } ATTR_TO_HA_IMPERIAL = { @@ -35,6 +37,8 @@ ATTR_TO_HA_IMPERIAL = { "remaining_fuel": ["mdi:gas-station", VOLUME_GALLONS], "charging_time_remaining": ["mdi:update", "h"], "charging_status": ["mdi:battery-charging", None], + # No icon as this is dealt with directly as a special case in icon() + "charging_level_hv": [None, "%"], } diff --git a/homeassistant/components/bom/manifest.json b/homeassistant/components/bom/manifest.json index eb1f1d8ca94..b2e7eb08ef6 100644 --- a/homeassistant/components/bom/manifest.json +++ b/homeassistant/components/bom/manifest.json @@ -1,7 +1,7 @@ { "domain": "bom", "name": "Bom", - "documentation": "https://www.home-assistant.io/components/bom", + "documentation": "https://www.home-assistant.io/integrations/bom", "requirements": [ "bomradarloop==0.1.3" ], diff --git a/homeassistant/components/bom/sensor.py b/homeassistant/components/bom/sensor.py index 33444f10996..ed22be003ad 100644 --- a/homeassistant/components/bom/sensor.py +++ b/homeassistant/components/bom/sensor.py @@ -13,6 +13,7 @@ import requests import voluptuous as vol import homeassistant.helpers.config_validation as cv +import homeassistant.util.dt as dt_util from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( CONF_MONITORED_CONDITIONS, @@ -240,7 +241,7 @@ class BOMCurrentData: # Never updated before, therefore an update should occur. return True - now = datetime.datetime.now() + now = dt_util.utcnow() update_due_at = self.last_updated + datetime.timedelta(minutes=35) return now > update_due_at @@ -251,8 +252,8 @@ class BOMCurrentData: _LOGGER.debug( "BOM was updated %s minutes ago, skipping update as" " < 35 minutes, Now: %s, LastUpdate: %s", - (datetime.datetime.now() - self.last_updated), - datetime.datetime.now(), + (dt_util.utcnow() - self.last_updated), + dt_util.utcnow(), self.last_updated, ) return @@ -263,8 +264,10 @@ class BOMCurrentData: # set lastupdate using self._data[0] as the first element in the # array is the latest date in the json - self.last_updated = datetime.datetime.strptime( - str(self._data[0]["local_date_time_full"]), "%Y%m%d%H%M%S" + self.last_updated = dt_util.as_utc( + datetime.datetime.strptime( + str(self._data[0]["local_date_time_full"]), "%Y%m%d%H%M%S" + ) ) return diff --git a/homeassistant/components/braviatv/manifest.json b/homeassistant/components/braviatv/manifest.json index 52e8e1bec76..e49e45865c4 100644 --- a/homeassistant/components/braviatv/manifest.json +++ b/homeassistant/components/braviatv/manifest.json @@ -1,7 +1,7 @@ { "domain": "braviatv", "name": "Braviatv", - "documentation": "https://www.home-assistant.io/components/braviatv", + "documentation": "https://www.home-assistant.io/integrations/braviatv", "requirements": [ "braviarc-homeassistant==0.3.7.dev0", "getmac==0.8.1" diff --git a/homeassistant/components/broadlink/manifest.json b/homeassistant/components/broadlink/manifest.json index 45ed2003026..d77c32966b1 100644 --- a/homeassistant/components/broadlink/manifest.json +++ b/homeassistant/components/broadlink/manifest.json @@ -1,7 +1,7 @@ { "domain": "broadlink", "name": "Broadlink", - "documentation": "https://www.home-assistant.io/components/broadlink", + "documentation": "https://www.home-assistant.io/integrations/broadlink", "requirements": [ "broadlink==0.11.1" ], diff --git a/homeassistant/components/brottsplatskartan/manifest.json b/homeassistant/components/brottsplatskartan/manifest.json index d3b0657fed8..f3dd46c96fc 100644 --- a/homeassistant/components/brottsplatskartan/manifest.json +++ b/homeassistant/components/brottsplatskartan/manifest.json @@ -1,7 +1,7 @@ { "domain": "brottsplatskartan", "name": "Brottsplatskartan", - "documentation": "https://www.home-assistant.io/components/brottsplatskartan", + "documentation": "https://www.home-assistant.io/integrations/brottsplatskartan", "requirements": [ "brottsplatskartan==0.0.1" ], diff --git a/homeassistant/components/browser/manifest.json b/homeassistant/components/browser/manifest.json index 61823564fe9..2905bfcfe96 100644 --- a/homeassistant/components/browser/manifest.json +++ b/homeassistant/components/browser/manifest.json @@ -1,7 +1,7 @@ { "domain": "browser", "name": "Browser", - "documentation": "https://www.home-assistant.io/components/browser", + "documentation": "https://www.home-assistant.io/integrations/browser", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/brunt/manifest.json b/homeassistant/components/brunt/manifest.json index a47e3f69d5c..6ee4344b946 100644 --- a/homeassistant/components/brunt/manifest.json +++ b/homeassistant/components/brunt/manifest.json @@ -1,7 +1,7 @@ { "domain": "brunt", "name": "Brunt", - "documentation": "https://www.home-assistant.io/components/brunt", + "documentation": "https://www.home-assistant.io/integrations/brunt", "requirements": [ "brunt==0.1.3" ], diff --git a/homeassistant/components/bt_home_hub_5/manifest.json b/homeassistant/components/bt_home_hub_5/manifest.json index 927d9ea9412..bee9cefce17 100644 --- a/homeassistant/components/bt_home_hub_5/manifest.json +++ b/homeassistant/components/bt_home_hub_5/manifest.json @@ -1,7 +1,7 @@ { "domain": "bt_home_hub_5", "name": "Bt home hub 5", - "documentation": "https://www.home-assistant.io/components/bt_home_hub_5", + "documentation": "https://www.home-assistant.io/integrations/bt_home_hub_5", "requirements": [ "bthomehub5-devicelist==0.1.1" ], diff --git a/homeassistant/components/bt_smarthub/manifest.json b/homeassistant/components/bt_smarthub/manifest.json index 725541082e7..985f3012422 100644 --- a/homeassistant/components/bt_smarthub/manifest.json +++ b/homeassistant/components/bt_smarthub/manifest.json @@ -1,7 +1,7 @@ { "domain": "bt_smarthub", "name": "Bt smarthub", - "documentation": "https://www.home-assistant.io/components/bt_smarthub", + "documentation": "https://www.home-assistant.io/integrations/bt_smarthub", "requirements": [ "btsmarthub_devicelist==0.1.3" ], diff --git a/homeassistant/components/buienradar/manifest.json b/homeassistant/components/buienradar/manifest.json index d25a2526a5d..cc3c3b02981 100644 --- a/homeassistant/components/buienradar/manifest.json +++ b/homeassistant/components/buienradar/manifest.json @@ -1,7 +1,7 @@ { "domain": "buienradar", "name": "Buienradar", - "documentation": "https://www.home-assistant.io/components/buienradar", + "documentation": "https://www.home-assistant.io/integrations/buienradar", "requirements": [ "buienradar==1.0.1" ], diff --git a/homeassistant/components/caldav/manifest.json b/homeassistant/components/caldav/manifest.json index 55cd555d989..896ace7ba6a 100644 --- a/homeassistant/components/caldav/manifest.json +++ b/homeassistant/components/caldav/manifest.json @@ -1,7 +1,7 @@ { "domain": "caldav", "name": "Caldav", - "documentation": "https://www.home-assistant.io/components/caldav", + "documentation": "https://www.home-assistant.io/integrations/caldav", "requirements": [ "caldav==0.6.1" ], diff --git a/homeassistant/components/calendar/manifest.json b/homeassistant/components/calendar/manifest.json index 3a09cd090a5..3e6ee8422b4 100644 --- a/homeassistant/components/calendar/manifest.json +++ b/homeassistant/components/calendar/manifest.json @@ -1,7 +1,7 @@ { "domain": "calendar", "name": "Calendar", - "documentation": "https://www.home-assistant.io/components/calendar", + "documentation": "https://www.home-assistant.io/integrations/calendar", "requirements": [], "dependencies": [ "http" diff --git a/homeassistant/components/camera/manifest.json b/homeassistant/components/camera/manifest.json index 3af6a15ca52..a3395965e4f 100644 --- a/homeassistant/components/camera/manifest.json +++ b/homeassistant/components/camera/manifest.json @@ -1,7 +1,7 @@ { "domain": "camera", "name": "Camera", - "documentation": "https://www.home-assistant.io/components/camera", + "documentation": "https://www.home-assistant.io/integrations/camera", "requirements": [], "dependencies": [ "http" diff --git a/homeassistant/components/canary/manifest.json b/homeassistant/components/canary/manifest.json index 346c1c99f6d..f76d521853d 100644 --- a/homeassistant/components/canary/manifest.json +++ b/homeassistant/components/canary/manifest.json @@ -1,7 +1,7 @@ { "domain": "canary", "name": "Canary", - "documentation": "https://www.home-assistant.io/components/canary", + "documentation": "https://www.home-assistant.io/integrations/canary", "requirements": [ "py-canary==0.5.0" ], diff --git a/homeassistant/components/cast/.translations/no.json b/homeassistant/components/cast/.translations/no.json index d36c929e721..6b8166f23c0 100644 --- a/homeassistant/components/cast/.translations/no.json +++ b/homeassistant/components/cast/.translations/no.json @@ -2,7 +2,7 @@ "config": { "abort": { "no_devices_found": "Ingen Google Cast enheter funnet p\u00e5 nettverket.", - "single_instance_allowed": "Kun en enkelt konfigurasjon av Google Cast er n\u00f8dvendig." + "single_instance_allowed": "Kun en konfigurasjon av Google Cast er n\u00f8dvendig." }, "step": { "confirm": { diff --git a/homeassistant/components/cast/.translations/zh-Hant.json b/homeassistant/components/cast/.translations/zh-Hant.json index d5383fb1a2b..711ac320397 100644 --- a/homeassistant/components/cast/.translations/zh-Hant.json +++ b/homeassistant/components/cast/.translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "no_devices_found": "\u5728\u7db2\u8def\u4e0a\u627e\u4e0d\u5230 Google Cast \u88dd\u7f6e\u3002", + "no_devices_found": "\u5728\u7db2\u8def\u4e0a\u627e\u4e0d\u5230 Google Cast \u8a2d\u5099\u3002", "single_instance_allowed": "\u50c5\u9700\u8a2d\u5b9a\u4e00\u6b21 Google Cast \u5373\u53ef\u3002" }, "step": { diff --git a/homeassistant/components/cast/manifest.json b/homeassistant/components/cast/manifest.json index 84a6a6e2934..b6776a17f7c 100644 --- a/homeassistant/components/cast/manifest.json +++ b/homeassistant/components/cast/manifest.json @@ -2,7 +2,7 @@ "domain": "cast", "name": "Cast", "config_flow": true, - "documentation": "https://www.home-assistant.io/components/cast", + "documentation": "https://www.home-assistant.io/integrations/cast", "requirements": ["pychromecast==4.0.1"], "dependencies": [], "zeroconf": ["_googlecast._tcp.local."], diff --git a/homeassistant/components/cert_expiry/.translations/bg.json b/homeassistant/components/cert_expiry/.translations/bg.json new file mode 100644 index 00000000000..7c82ef8b9ba --- /dev/null +++ b/homeassistant/components/cert_expiry/.translations/bg.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "host_port_exists": "\u0422\u0430\u0437\u0438 \u043a\u043e\u043c\u0431\u0438\u043d\u0430\u0446\u0438\u044f \u043e\u0442 \u0430\u0434\u0440\u0435\u0441 \u0438 \u043f\u043e\u0440\u0442 \u0435 \u0432\u0435\u0447\u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0430" + }, + "error": { + "certificate_fetch_failed": "\u041d\u0435 \u043c\u043e\u0436\u0435 \u0434\u0430 \u0441\u0435 \u043c\u0438\u0437\u0432\u043b\u0435\u0447\u0435 \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442 \u043e\u0442 \u0442\u0430\u0437\u0438 \u043a\u043e\u043c\u0431\u0438\u043d\u0430\u0446\u0438\u044f \u043e\u0442 \u0430\u0434\u0440\u0435\u0441 \u0438 \u043f\u043e\u0440\u0442", + "connection_timeout": "\u041d\u0435\u0432\u044a\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442 \u0437\u0430 \u0441\u0432\u043e\u0435\u0432\u0440\u0435\u043c\u0435\u043d\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435 \u0441 \u0442\u043e\u0437\u0438 \u0430\u0434\u0440\u0435\u0441", + "host_port_exists": "\u0422\u0430\u0437\u0438 \u043a\u043e\u043c\u0431\u0438\u043d\u0430\u0446\u0438\u044f \u043e\u0442 \u0430\u0434\u0440\u0435\u0441 \u0438 \u043f\u043e\u0440\u0442 \u0435 \u0432\u0435\u0447\u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0430", + "resolve_failed": "\u0422\u043e\u0437\u0438 \u0430\u0434\u0440\u0435\u0441 \u043d\u0435 \u043c\u043e\u0436\u0435 \u0434\u0430 \u0431\u044a\u0434\u0435 \u043d\u0430\u043c\u0435\u0440\u0435\u043d" + }, + "step": { + "user": { + "data": { + "host": "\u0410\u0434\u0440\u0435\u0441 \u0432 \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u0430", + "name": "\u0418\u043c\u0435 \u043d\u0430 \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u0430", + "port": "\u041f\u043e\u0440\u0442 \u043d\u0430 \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u0430" + }, + "title": "\u0414\u0435\u0444\u0438\u043d\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u0430 \u0437\u0430 \u0442\u0435\u0441\u0442\u0432\u0430\u043d\u0435" + } + }, + "title": "\u0421\u0440\u043e\u043a \u043d\u0430 \u0432\u0430\u043b\u0438\u0434\u043d\u043e\u0441\u0442 \u043d\u0430 \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u0430" + } +} \ No newline at end of file diff --git a/homeassistant/components/cert_expiry/.translations/es-419.json b/homeassistant/components/cert_expiry/.translations/es-419.json new file mode 100644 index 00000000000..392dbf35f5a --- /dev/null +++ b/homeassistant/components/cert_expiry/.translations/es-419.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Expiraci\u00f3n del certificado" + } +} \ No newline at end of file diff --git a/homeassistant/components/cert_expiry/.translations/lb.json b/homeassistant/components/cert_expiry/.translations/lb.json index d6811728a22..9620526e363 100644 --- a/homeassistant/components/cert_expiry/.translations/lb.json +++ b/homeassistant/components/cert_expiry/.translations/lb.json @@ -1,7 +1,24 @@ { "config": { + "abort": { + "host_port_exists": "D\u00ebsen Host an Port sinn scho konfigur\u00e9iert" + }, "error": { - "connection_timeout": "Z\u00e4it Iwwerschreidung beim verbannen." - } + "certificate_fetch_failed": "Kann keen Zertifikat vun d\u00ebsen Host a Port recuper\u00e9ieren", + "connection_timeout": "Z\u00e4it Iwwerschreidung beim verbannen.", + "host_port_exists": "D\u00ebsen Host an Port sinn scho konfigur\u00e9iert", + "resolve_failed": "D\u00ebsen Host kann net opgel\u00e9ist ginn" + }, + "step": { + "user": { + "data": { + "host": "Den Hostnumm vum Zertifikat", + "name": "De Numm vum Zertifikat", + "port": "De Port vum Zertifikat" + }, + "title": "W\u00e9ieen Zertifikat soll getest ginn" + } + }, + "title": "Zertifikat Verfallsdatum" } } \ No newline at end of file diff --git a/homeassistant/components/cert_expiry/.translations/no.json b/homeassistant/components/cert_expiry/.translations/no.json index e095cc360a0..73e899106c1 100644 --- a/homeassistant/components/cert_expiry/.translations/no.json +++ b/homeassistant/components/cert_expiry/.translations/no.json @@ -5,7 +5,7 @@ }, "error": { "certificate_fetch_failed": "Kan ikke hente sertifikat fra denne verts- og portkombinasjonen", - "connection_timeout": "Timeout n\u00e5r det kobles til denne verten", + "connection_timeout": "Tidsavbrudd n\u00e5r du kobler til denne verten", "host_port_exists": "Denne verts- og portkombinasjonen er allerede konfigurert", "resolve_failed": "Denne verten kan ikke l\u00f8ses" }, diff --git a/homeassistant/components/cert_expiry/.translations/pt-BR.json b/homeassistant/components/cert_expiry/.translations/pt-BR.json new file mode 100644 index 00000000000..06534314e00 --- /dev/null +++ b/homeassistant/components/cert_expiry/.translations/pt-BR.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "host_port_exists": "Essa combina\u00e7\u00e3o de host e porta j\u00e1 est\u00e1 configurada" + }, + "error": { + "certificate_fetch_failed": "N\u00e3o \u00e9 poss\u00edvel buscar o certificado dessa combina\u00e7\u00e3o de host e porta", + "connection_timeout": "Tempo limite ao conectar-se a este host", + "host_port_exists": "Essa combina\u00e7\u00e3o de host e porta j\u00e1 est\u00e1 configurada", + "resolve_failed": "Este host n\u00e3o pode ser resolvido" + }, + "step": { + "user": { + "data": { + "host": "O nome do host do certificado", + "name": "O nome do certificado", + "port": "A porta do certificado" + }, + "title": "Defina o certificado para testar" + } + }, + "title": "Expira\u00e7\u00e3o do certificado" + } +} \ No newline at end of file diff --git a/homeassistant/components/cert_expiry/.translations/ru.json b/homeassistant/components/cert_expiry/.translations/ru.json index 6a795dee13e..d962c793121 100644 --- a/homeassistant/components/cert_expiry/.translations/ru.json +++ b/homeassistant/components/cert_expiry/.translations/ru.json @@ -1,12 +1,12 @@ { "config": { "abort": { - "host_port_exists": "\u042d\u0442\u0430 \u043a\u043e\u043c\u0431\u0438\u043d\u0430\u0446\u0438\u044f \u0445\u043e\u0441\u0442\u0430 \u0438 \u043f\u043e\u0440\u0442\u0430 \u0443\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u0430" + "host_port_exists": "\u042d\u0442\u0430 \u043a\u043e\u043c\u0431\u0438\u043d\u0430\u0446\u0438\u044f \u0445\u043e\u0441\u0442\u0430 \u0438 \u043f\u043e\u0440\u0442\u0430 \u0443\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u0430." }, "error": { "certificate_fetch_failed": "\u041d\u0435 \u0443\u0434\u0430\u0435\u0442\u0441\u044f \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442 \u0441 \u044d\u0442\u043e\u0439 \u043a\u043e\u043c\u0431\u0438\u043d\u0430\u0446\u0438\u0438 \u0445\u043e\u0441\u0442\u0430 \u0438 \u043f\u043e\u0440\u0442\u0430", "connection_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 \u0445\u043e\u0441\u0442\u0443", - "host_port_exists": "\u042d\u0442\u0430 \u043a\u043e\u043c\u0431\u0438\u043d\u0430\u0446\u0438\u044f \u0445\u043e\u0441\u0442\u0430 \u0438 \u043f\u043e\u0440\u0442\u0430 \u0443\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u0430", + "host_port_exists": "\u042d\u0442\u0430 \u043a\u043e\u043c\u0431\u0438\u043d\u0430\u0446\u0438\u044f \u0445\u043e\u0441\u0442\u0430 \u0438 \u043f\u043e\u0440\u0442\u0430 \u0443\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u0430.", "resolve_failed": "\u041d\u0435\u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u043f\u0440\u0435\u043e\u0431\u0440\u0430\u0437\u043e\u0432\u0430\u0442\u044c \u0445\u043e\u0441\u0442" }, "step": { diff --git a/homeassistant/components/cert_expiry/.translations/zh-Hans.json b/homeassistant/components/cert_expiry/.translations/zh-Hans.json new file mode 100644 index 00000000000..07affc990a8 --- /dev/null +++ b/homeassistant/components/cert_expiry/.translations/zh-Hans.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "connection_timeout": "\u8fde\u63a5\u5230\u6b64\u4e3b\u673a\u65f6\u7684\u8d85\u65f6" + }, + "step": { + "user": { + "data": { + "host": "\u8bc1\u4e66\u7684\u4e3b\u673a\u540d", + "name": "\u8bc1\u4e66\u7684\u540d\u79f0", + "port": "\u8bc1\u4e66\u7684\u7aef\u53e3" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/cert_expiry/manifest.json b/homeassistant/components/cert_expiry/manifest.json index 781f27afb5f..97f72f2ad11 100644 --- a/homeassistant/components/cert_expiry/manifest.json +++ b/homeassistant/components/cert_expiry/manifest.json @@ -1,7 +1,7 @@ { "domain": "cert_expiry", "name": "Cert expiry", - "documentation": "https://www.home-assistant.io/components/cert_expiry", + "documentation": "https://www.home-assistant.io/integrations/cert_expiry", "requirements": [], "config_flow": true, "dependencies": [], diff --git a/homeassistant/components/cert_expiry/sensor.py b/homeassistant/components/cert_expiry/sensor.py index b564cff7338..a5b879e5661 100644 --- a/homeassistant/components/cert_expiry/sensor.py +++ b/homeassistant/components/cert_expiry/sensor.py @@ -15,6 +15,7 @@ from homeassistant.const import ( CONF_PORT, EVENT_HOMEASSISTANT_START, ) +from homeassistant.core import callback from homeassistant.helpers.entity import Entity from .const import DOMAIN, DEFAULT_NAME, DEFAULT_PORT @@ -35,18 +36,26 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up certificate expiry sensor.""" - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_IMPORT}, data=dict(config) + + @callback + def do_import(_): + """Process YAML import after HA is fully started.""" + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=dict(config) + ) ) - ) + + # Delay to avoid validation during setup in case we're checking our own cert. + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, do_import) async def async_setup_entry(hass, entry, async_add_entities): """Add cert-expiry entry.""" async_add_entities( [SSLCertificate(entry.title, entry.data[CONF_HOST], entry.data[CONF_PORT])], - True, + False, + # Don't update in case we're checking our own cert. ) return True @@ -84,17 +93,22 @@ class SSLCertificate(Entity): @property def available(self): - """Icon to use in the frontend, if any.""" + """Return the availability of the sensor.""" return self._available async def async_added_to_hass(self): """Once the entity is added we should update to get the initial data loaded.""" + @callback def do_update(_): """Run the update method when the start event was fired.""" - self.update() + self.async_schedule_update_ha_state(True) - self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, do_update) + if self.hass.is_running: + self.async_schedule_update_ha_state(True) + else: + # Delay until HA is fully started in case we're checking our own cert. + self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, do_update) def update(self): """Fetch the certificate information.""" diff --git a/homeassistant/components/cert_expiry/strings.json b/homeassistant/components/cert_expiry/strings.json index 8943643e8b3..3e2fea2342e 100644 --- a/homeassistant/components/cert_expiry/strings.json +++ b/homeassistant/components/cert_expiry/strings.json @@ -14,7 +14,7 @@ "error": { "host_port_exists": "This host and port combination is already configured", "resolve_failed": "This host can not be resolved", - "connection_timeout": "Timeout whemn connecting to this host", + "connection_timeout": "Timeout when connecting to this host", "certificate_fetch_failed": "Can not fetch certificate from this host and port combination" }, "abort": { diff --git a/homeassistant/components/channels/manifest.json b/homeassistant/components/channels/manifest.json index 152c7d3a2dc..c6ef7f8f127 100644 --- a/homeassistant/components/channels/manifest.json +++ b/homeassistant/components/channels/manifest.json @@ -1,7 +1,7 @@ { "domain": "channels", "name": "Channels", - "documentation": "https://www.home-assistant.io/components/channels", + "documentation": "https://www.home-assistant.io/integrations/channels", "requirements": [ "pychannels==1.0.0" ], diff --git a/homeassistant/components/cisco_ios/manifest.json b/homeassistant/components/cisco_ios/manifest.json index 9a12ba252e3..4a04ffa32e6 100644 --- a/homeassistant/components/cisco_ios/manifest.json +++ b/homeassistant/components/cisco_ios/manifest.json @@ -1,7 +1,7 @@ { "domain": "cisco_ios", "name": "Cisco ios", - "documentation": "https://www.home-assistant.io/components/cisco_ios", + "documentation": "https://www.home-assistant.io/integrations/cisco_ios", "requirements": [ "pexpect==4.6.0" ], diff --git a/homeassistant/components/cisco_mobility_express/manifest.json b/homeassistant/components/cisco_mobility_express/manifest.json index abdd2400311..b4bc2ff86d9 100644 --- a/homeassistant/components/cisco_mobility_express/manifest.json +++ b/homeassistant/components/cisco_mobility_express/manifest.json @@ -1,7 +1,7 @@ { "domain": "cisco_mobility_express", "name": "Cisco mobility express", - "documentation": "https://www.home-assistant.io/components/cisco_mobility_express", + "documentation": "https://www.home-assistant.io/integrations/cisco_mobility_express", "requirements": [ "ciscomobilityexpress==0.3.3" ], diff --git a/homeassistant/components/cisco_webex_teams/manifest.json b/homeassistant/components/cisco_webex_teams/manifest.json index 21c4efe071c..3560b1e7fcd 100644 --- a/homeassistant/components/cisco_webex_teams/manifest.json +++ b/homeassistant/components/cisco_webex_teams/manifest.json @@ -1,7 +1,7 @@ { "domain": "cisco_webex_teams", "name": "Cisco webex teams", - "documentation": "https://www.home-assistant.io/components/cisco_webex_teams", + "documentation": "https://www.home-assistant.io/integrations/cisco_webex_teams", "requirements": [ "webexteamssdk==1.1.1" ], diff --git a/homeassistant/components/ciscospark/manifest.json b/homeassistant/components/ciscospark/manifest.json index 926925a7bf1..8058088bf8a 100644 --- a/homeassistant/components/ciscospark/manifest.json +++ b/homeassistant/components/ciscospark/manifest.json @@ -1,7 +1,7 @@ { "domain": "ciscospark", "name": "Ciscospark", - "documentation": "https://www.home-assistant.io/components/ciscospark", + "documentation": "https://www.home-assistant.io/integrations/ciscospark", "requirements": [ "ciscosparkapi==0.4.2" ], diff --git a/homeassistant/components/citybikes/manifest.json b/homeassistant/components/citybikes/manifest.json index ea1ceaa9531..f46c7ba708f 100644 --- a/homeassistant/components/citybikes/manifest.json +++ b/homeassistant/components/citybikes/manifest.json @@ -1,7 +1,7 @@ { "domain": "citybikes", "name": "Citybikes", - "documentation": "https://www.home-assistant.io/components/citybikes", + "documentation": "https://www.home-assistant.io/integrations/citybikes", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/clementine/manifest.json b/homeassistant/components/clementine/manifest.json index 4d835ed4e7c..35368dd6cdf 100644 --- a/homeassistant/components/clementine/manifest.json +++ b/homeassistant/components/clementine/manifest.json @@ -1,7 +1,7 @@ { "domain": "clementine", "name": "Clementine", - "documentation": "https://www.home-assistant.io/components/clementine", + "documentation": "https://www.home-assistant.io/integrations/clementine", "requirements": [ "python-clementine-remote==1.0.1" ], diff --git a/homeassistant/components/clickatell/manifest.json b/homeassistant/components/clickatell/manifest.json index ffd550eebee..a10da6e1cc0 100644 --- a/homeassistant/components/clickatell/manifest.json +++ b/homeassistant/components/clickatell/manifest.json @@ -1,7 +1,7 @@ { "domain": "clickatell", "name": "Clickatell", - "documentation": "https://www.home-assistant.io/components/clickatell", + "documentation": "https://www.home-assistant.io/integrations/clickatell", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/clicksend/manifest.json b/homeassistant/components/clicksend/manifest.json index 38319825094..6a28b3c30ca 100644 --- a/homeassistant/components/clicksend/manifest.json +++ b/homeassistant/components/clicksend/manifest.json @@ -1,7 +1,7 @@ { "domain": "clicksend", "name": "Clicksend", - "documentation": "https://www.home-assistant.io/components/clicksend", + "documentation": "https://www.home-assistant.io/integrations/clicksend", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/clicksend_tts/manifest.json b/homeassistant/components/clicksend_tts/manifest.json index c2a86f426e4..8aa3eacf405 100644 --- a/homeassistant/components/clicksend_tts/manifest.json +++ b/homeassistant/components/clicksend_tts/manifest.json @@ -1,7 +1,7 @@ { "domain": "clicksend_tts", "name": "Clicksend tts", - "documentation": "https://www.home-assistant.io/components/clicksend_tts", + "documentation": "https://www.home-assistant.io/integrations/clicksend_tts", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/climate/manifest.json b/homeassistant/components/climate/manifest.json index ca5312e7670..5933eaf9071 100644 --- a/homeassistant/components/climate/manifest.json +++ b/homeassistant/components/climate/manifest.json @@ -1,7 +1,7 @@ { "domain": "climate", "name": "Climate", - "documentation": "https://www.home-assistant.io/components/climate", + "documentation": "https://www.home-assistant.io/integrations/climate", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/climate/services.yaml b/homeassistant/components/climate/services.yaml index 4e9a4a3a4f4..f10e1b4bd69 100644 --- a/homeassistant/components/climate/services.yaml +++ b/homeassistant/components/climate/services.yaml @@ -72,26 +72,6 @@ set_swing_mode: swing_mode: description: New value of swing mode. -ecobee_set_fan_min_on_time: - description: Set the minimum fan on time. - fields: - entity_id: - description: Name(s) of entities to change. - example: 'climate.kitchen' - fan_min_on_time: - description: New value of fan min on time. - example: 5 - -ecobee_resume_program: - description: Resume the programmed schedule. - fields: - entity_id: - description: Name(s) of entities to change. - example: 'climate.kitchen' - resume_all: - description: Resume all events and return to the scheduled program. This default to false which removes only the top event. - example: true - mill_set_room_temperature: description: Set Mill room temperatures. fields: diff --git a/homeassistant/components/cloud/__init__.py b/homeassistant/components/cloud/__init__.py index 8b295634c99..71550fc37b1 100644 --- a/homeassistant/components/cloud/__init__.py +++ b/homeassistant/components/cloud/__init__.py @@ -34,6 +34,7 @@ from .const import ( CONF_REMOTE_API_URL, CONF_SUBSCRIPTION_INFO_URL, CONF_USER_POOL_ID, + CONF_GOOGLE_ACTIONS_REPORT_STATE_URL, DOMAIN, MODE_DEV, MODE_PROD, @@ -96,7 +97,8 @@ CONFIG_SCHEMA = vol.Schema( vol.Optional(CONF_ACME_DIRECTORY_SERVER): vol.Url(), vol.Optional(CONF_ALEXA): ALEXA_SCHEMA, vol.Optional(CONF_GOOGLE_ACTIONS): GACTIONS_SCHEMA, - vol.Optional(CONF_ALEXA_ACCESS_TOKEN_URL): str, + vol.Optional(CONF_ALEXA_ACCESS_TOKEN_URL): vol.Url(), + vol.Optional(CONF_GOOGLE_ACTIONS_REPORT_STATE_URL): vol.Url(), } ) }, diff --git a/homeassistant/components/cloud/client.py b/homeassistant/components/cloud/client.py index 07882d8dac2..38ae09ced93 100644 --- a/homeassistant/components/cloud/client.py +++ b/homeassistant/components/cloud/client.py @@ -98,7 +98,7 @@ class CloudClient(Interface): if not self._google_config: assert self.cloud is not None self._google_config = google_config.CloudGoogleConfig( - self.google_user_config, self._prefs, self.cloud + self._hass, self.google_user_config, self._prefs, self.cloud ) return self._google_config @@ -107,13 +107,17 @@ class CloudClient(Interface): """Initialize the client.""" self.cloud = cloud - if not self.alexa_config.should_report_state or not self.cloud.is_logged_in: + if not self.cloud.is_logged_in: return - try: - await self.alexa_config.async_enable_proactive_mode() - except alexa_errors.NoTokenAvailable: - pass + if self.alexa_config.should_report_state: + try: + await self.alexa_config.async_enable_proactive_mode() + except alexa_errors.NoTokenAvailable: + pass + + if self.google_config.should_report_state: + self.google_config.async_enable_report_state() async def cleanups(self) -> None: """Cleanup some stuff after logout.""" diff --git a/homeassistant/components/cloud/const.py b/homeassistant/components/cloud/const.py index df1b8ef165d..e28d75f017d 100644 --- a/homeassistant/components/cloud/const.py +++ b/homeassistant/components/cloud/const.py @@ -9,6 +9,7 @@ PREF_GOOGLE_SECURE_DEVICES_PIN = "google_secure_devices_pin" PREF_CLOUDHOOKS = "cloudhooks" PREF_CLOUD_USER = "cloud_user" PREF_GOOGLE_ENTITY_CONFIGS = "google_entity_configs" +PREF_GOOGLE_REPORT_STATE = "google_report_state" PREF_ALEXA_ENTITY_CONFIGS = "alexa_entity_configs" PREF_ALEXA_REPORT_STATE = "alexa_report_state" PREF_OVERRIDE_NAME = "override_name" @@ -18,6 +19,7 @@ PREF_SHOULD_EXPOSE = "should_expose" DEFAULT_SHOULD_EXPOSE = True DEFAULT_DISABLE_2FA = False DEFAULT_ALEXA_REPORT_STATE = False +DEFAULT_GOOGLE_REPORT_STATE = False CONF_ALEXA = "alexa" CONF_ALIASES = "aliases" @@ -33,6 +35,7 @@ CONF_CLOUDHOOK_CREATE_URL = "cloudhook_create_url" CONF_REMOTE_API_URL = "remote_api_url" CONF_ACME_DIRECTORY_SERVER = "acme_directory_server" CONF_ALEXA_ACCESS_TOKEN_URL = "alexa_access_token_url" +CONF_GOOGLE_ACTIONS_REPORT_STATE_URL = "google_actions_report_state_url" MODE_DEV = "development" MODE_PROD = "production" diff --git a/homeassistant/components/cloud/google_config.py b/homeassistant/components/cloud/google_config.py index 8986f8f3995..38e4aec56e0 100644 --- a/homeassistant/components/cloud/google_config.py +++ b/homeassistant/components/cloud/google_config.py @@ -1,6 +1,13 @@ """Google config for Cloud.""" +import asyncio +import logging + +import async_timeout +from hass_nabucasa.google_report_state import ErrorResponse + from homeassistant.const import CLOUD_NEVER_EXPOSED_ENTITIES from homeassistant.components.google_assistant.helpers import AbstractConfig +from homeassistant.helpers import entity_registry from .const import ( PREF_SHOULD_EXPOSE, @@ -10,15 +17,31 @@ from .const import ( DEFAULT_DISABLE_2FA, ) +_LOGGER = logging.getLogger(__name__) + class CloudGoogleConfig(AbstractConfig): """HA Cloud Configuration for Google Assistant.""" - def __init__(self, config, prefs, cloud): - """Initialize the Alexa config.""" + def __init__(self, hass, config, prefs, cloud): + """Initialize the Google config.""" + super().__init__(hass) self._config = config self._prefs = prefs self._cloud = cloud + self._cur_entity_prefs = self._prefs.google_entity_configs + self._sync_entities_lock = asyncio.Lock() + + prefs.async_listen_updates(self._async_prefs_updated) + hass.bus.async_listen( + entity_registry.EVENT_ENTITY_REGISTRY_UPDATED, + self._handle_entity_registry_updated, + ) + + @property + def enabled(self): + """Return if Google is enabled.""" + return self._prefs.google_enabled @property def agent_user_id(self): @@ -35,16 +58,25 @@ class CloudGoogleConfig(AbstractConfig): """Return entity config.""" return self._prefs.google_secure_devices_pin + @property + def should_report_state(self): + """Return if states should be proactively reported.""" + return self._prefs.google_report_state + def should_expose(self, state): - """If an entity should be exposed.""" - if state.entity_id in CLOUD_NEVER_EXPOSED_ENTITIES: + """If a state object should be exposed.""" + return self._should_expose_entity_id(state.entity_id) + + def _should_expose_entity_id(self, entity_id): + """If an entity ID should be exposed.""" + if entity_id in CLOUD_NEVER_EXPOSED_ENTITIES: return False if not self._config["filter"].empty_filter: - return self._config["filter"](state.entity_id) + return self._config["filter"](entity_id) entity_configs = self._prefs.google_entity_configs - entity_config = entity_configs.get(state.entity_id, {}) + entity_config = entity_configs.get(entity_id, {}) return entity_config.get(PREF_SHOULD_EXPOSE, DEFAULT_SHOULD_EXPOSE) def should_2fa(self, state): @@ -52,3 +84,72 @@ class CloudGoogleConfig(AbstractConfig): entity_configs = self._prefs.google_entity_configs entity_config = entity_configs.get(state.entity_id, {}) return not entity_config.get(PREF_DISABLE_2FA, DEFAULT_DISABLE_2FA) + + async def async_report_state(self, message): + """Send a state report to Google.""" + try: + await self._cloud.google_report_state.async_send_message(message) + except ErrorResponse as err: + _LOGGER.warning("Error reporting state - %s: %s", err.code, err.message) + + async def _async_request_sync_devices(self): + """Trigger a sync with Google.""" + if self._sync_entities_lock.locked(): + return 200 + + websession = self.hass.helpers.aiohttp_client.async_get_clientsession() + + async with self._sync_entities_lock: + with async_timeout.timeout(10): + await self._cloud.auth.async_check_token() + + _LOGGER.debug("Requesting sync") + + with async_timeout.timeout(30): + req = await websession.post( + self._cloud.google_actions_sync_url, + headers={"authorization": self._cloud.id_token}, + ) + _LOGGER.debug("Finished requesting syncing: %s", req.status) + return req.status + + async def async_deactivate_report_state(self): + """Turn off report state and disable further state reporting. + + Called when the user disconnects their account from Google. + """ + await self._prefs.async_update(google_report_state=False) + + async def _async_prefs_updated(self, prefs): + """Handle updated preferences.""" + if self.should_report_state != self.is_reporting_state: + if self.should_report_state: + self.async_enable_report_state() + else: + self.async_disable_report_state() + + # State reporting is reported as a property on entities. + # So when we change it, we need to sync all entities. + await self.async_sync_entities() + return + + # If entity prefs are the same or we have filter in config.yaml, + # don't sync. + if ( + self._cur_entity_prefs is prefs.google_entity_configs + or not self._config["filter"].empty_filter + ): + return + + self.async_schedule_google_sync() + + async def _handle_entity_registry_updated(self, event): + """Handle when entity registry updated.""" + if not self.enabled or not self._cloud.is_logged_in: + return + + entity_id = event.data["entity_id"] + + # Schedule a sync if a change was made to an entity that Google knows about + if self._should_expose_entity_id(entity_id): + await self.async_sync_entities() diff --git a/homeassistant/components/cloud/http_api.py b/homeassistant/components/cloud/http_api.py index fce530ddce5..f243eab8fd0 100644 --- a/homeassistant/components/cloud/http_api.py +++ b/homeassistant/components/cloud/http_api.py @@ -7,6 +7,7 @@ import attr import aiohttp import async_timeout import voluptuous as vol +from hass_nabucasa import Cloud from homeassistant.core import callback from homeassistant.components.http import HomeAssistantView @@ -28,6 +29,7 @@ from .const import ( InvalidTrustedNetworks, InvalidTrustedProxies, PREF_ALEXA_REPORT_STATE, + PREF_GOOGLE_REPORT_STATE, RequireRelink, ) @@ -171,18 +173,9 @@ class GoogleActionsSyncView(HomeAssistantView): async def post(self, request): """Trigger a Google Actions sync.""" hass = request.app["hass"] - cloud = hass.data[DOMAIN] - websession = hass.helpers.aiohttp_client.async_get_clientsession() - - with async_timeout.timeout(REQUEST_TIMEOUT): - await hass.async_add_job(cloud.auth.check_token) - - with async_timeout.timeout(REQUEST_TIMEOUT): - req = await websession.post( - cloud.google_actions_sync_url, headers={"authorization": cloud.id_token} - ) - - return self.json({}, status_code=req.status) + cloud: Cloud = hass.data[DOMAIN] + status = await cloud.client.google_config.async_sync_entities() + return self.json({}, status_code=status) class CloudLoginView(HomeAssistantView): @@ -366,6 +359,7 @@ async def websocket_subscription(hass, connection, msg): vol.Optional(PREF_ENABLE_GOOGLE): bool, vol.Optional(PREF_ENABLE_ALEXA): bool, vol.Optional(PREF_ALEXA_REPORT_STATE): bool, + vol.Optional(PREF_GOOGLE_REPORT_STATE): bool, vol.Optional(PREF_GOOGLE_SECURE_DEVICES_PIN): vol.Any(None, str), } ) diff --git a/homeassistant/components/cloud/manifest.json b/homeassistant/components/cloud/manifest.json index 3daeac43da9..c8fa6884563 100644 --- a/homeassistant/components/cloud/manifest.json +++ b/homeassistant/components/cloud/manifest.json @@ -1,8 +1,8 @@ { "domain": "cloud", "name": "Cloud", - "documentation": "https://www.home-assistant.io/components/cloud", - "requirements": ["hass-nabucasa==0.17"], + "documentation": "https://www.home-assistant.io/integrations/cloud", + "requirements": ["hass-nabucasa==0.22"], "dependencies": ["http", "webhook"], "codeowners": ["@home-assistant/cloud"] } diff --git a/homeassistant/components/cloud/prefs.py b/homeassistant/components/cloud/prefs.py index d6e78e87e25..a8ff775a227 100644 --- a/homeassistant/components/cloud/prefs.py +++ b/homeassistant/components/cloud/prefs.py @@ -20,6 +20,8 @@ from .const import ( PREF_ALEXA_ENTITY_CONFIGS, PREF_ALEXA_REPORT_STATE, DEFAULT_ALEXA_REPORT_STATE, + PREF_GOOGLE_REPORT_STATE, + DEFAULT_GOOGLE_REPORT_STATE, InvalidTrustedNetworks, InvalidTrustedProxies, ) @@ -74,6 +76,7 @@ class CloudPreferences: google_entity_configs=_UNDEF, alexa_entity_configs=_UNDEF, alexa_report_state=_UNDEF, + google_report_state=_UNDEF, ): """Update user preferences.""" for key, value in ( @@ -86,6 +89,7 @@ class CloudPreferences: (PREF_GOOGLE_ENTITY_CONFIGS, google_entity_configs), (PREF_ALEXA_ENTITY_CONFIGS, alexa_entity_configs), (PREF_ALEXA_REPORT_STATE, alexa_report_state), + (PREF_GOOGLE_REPORT_STATE, google_report_state), ): if value is not _UNDEF: self._prefs[key] = value @@ -164,6 +168,7 @@ class CloudPreferences: PREF_GOOGLE_ENTITY_CONFIGS: self.google_entity_configs, PREF_ALEXA_ENTITY_CONFIGS: self.alexa_entity_configs, PREF_ALEXA_REPORT_STATE: self.alexa_report_state, + PREF_GOOGLE_REPORT_STATE: self.google_report_state, PREF_CLOUDHOOKS: self.cloudhooks, PREF_CLOUD_USER: self.cloud_user, } @@ -196,6 +201,11 @@ class CloudPreferences: """Return if Google is enabled.""" return self._prefs[PREF_ENABLE_GOOGLE] + @property + def google_report_state(self): + """Return if Google report state is enabled.""" + return self._prefs.get(PREF_GOOGLE_REPORT_STATE, DEFAULT_GOOGLE_REPORT_STATE) + @property def google_secure_devices_pin(self): """Return if Google is allowed to unlock locks.""" diff --git a/homeassistant/components/cloudflare/manifest.json b/homeassistant/components/cloudflare/manifest.json index 7716ae65c4e..78bc6de99c8 100644 --- a/homeassistant/components/cloudflare/manifest.json +++ b/homeassistant/components/cloudflare/manifest.json @@ -1,7 +1,7 @@ { "domain": "cloudflare", "name": "Cloudflare", - "documentation": "https://www.home-assistant.io/components/cloudflare", + "documentation": "https://www.home-assistant.io/integrations/cloudflare", "requirements": [ "pycfdns==0.0.1" ], diff --git a/homeassistant/components/cmus/manifest.json b/homeassistant/components/cmus/manifest.json index 1528f4252b1..fe5b8e155c2 100644 --- a/homeassistant/components/cmus/manifest.json +++ b/homeassistant/components/cmus/manifest.json @@ -1,7 +1,7 @@ { "domain": "cmus", "name": "Cmus", - "documentation": "https://www.home-assistant.io/components/cmus", + "documentation": "https://www.home-assistant.io/integrations/cmus", "requirements": [ "pycmus==0.1.1" ], diff --git a/homeassistant/components/co2signal/manifest.json b/homeassistant/components/co2signal/manifest.json index ac42e374fdd..f07813b3db5 100644 --- a/homeassistant/components/co2signal/manifest.json +++ b/homeassistant/components/co2signal/manifest.json @@ -1,7 +1,7 @@ { "domain": "co2signal", "name": "Co2signal", - "documentation": "https://www.home-assistant.io/components/co2signal", + "documentation": "https://www.home-assistant.io/integrations/co2signal", "requirements": [ "co2signal==0.4.2" ], diff --git a/homeassistant/components/coinbase/manifest.json b/homeassistant/components/coinbase/manifest.json index 5f8a189c7d1..2da323f0815 100644 --- a/homeassistant/components/coinbase/manifest.json +++ b/homeassistant/components/coinbase/manifest.json @@ -1,7 +1,7 @@ { "domain": "coinbase", "name": "Coinbase", - "documentation": "https://www.home-assistant.io/components/coinbase", + "documentation": "https://www.home-assistant.io/integrations/coinbase", "requirements": [ "coinbase==2.1.0" ], diff --git a/homeassistant/components/coinmarketcap/manifest.json b/homeassistant/components/coinmarketcap/manifest.json index 0afb1b1c28f..ec9aec6c654 100644 --- a/homeassistant/components/coinmarketcap/manifest.json +++ b/homeassistant/components/coinmarketcap/manifest.json @@ -1,7 +1,7 @@ { "domain": "coinmarketcap", "name": "Coinmarketcap", - "documentation": "https://www.home-assistant.io/components/coinmarketcap", + "documentation": "https://www.home-assistant.io/integrations/coinmarketcap", "requirements": [ "coinmarketcap==5.0.3" ], diff --git a/homeassistant/components/comed_hourly_pricing/manifest.json b/homeassistant/components/comed_hourly_pricing/manifest.json index 47c7931a0e9..89fbb84e82d 100644 --- a/homeassistant/components/comed_hourly_pricing/manifest.json +++ b/homeassistant/components/comed_hourly_pricing/manifest.json @@ -1,7 +1,7 @@ { "domain": "comed_hourly_pricing", "name": "Comed hourly pricing", - "documentation": "https://www.home-assistant.io/components/comed_hourly_pricing", + "documentation": "https://www.home-assistant.io/integrations/comed_hourly_pricing", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/comfoconnect/manifest.json b/homeassistant/components/comfoconnect/manifest.json index 03319aeffa8..57daba7fdbd 100644 --- a/homeassistant/components/comfoconnect/manifest.json +++ b/homeassistant/components/comfoconnect/manifest.json @@ -1,7 +1,7 @@ { "domain": "comfoconnect", "name": "Comfoconnect", - "documentation": "https://www.home-assistant.io/components/comfoconnect", + "documentation": "https://www.home-assistant.io/integrations/comfoconnect", "requirements": [ "pycomfoconnect==0.3" ], diff --git a/homeassistant/components/command_line/manifest.json b/homeassistant/components/command_line/manifest.json index ff94522210d..4d7dfc8994f 100644 --- a/homeassistant/components/command_line/manifest.json +++ b/homeassistant/components/command_line/manifest.json @@ -1,7 +1,7 @@ { "domain": "command_line", "name": "Command line", - "documentation": "https://www.home-assistant.io/components/command_line", + "documentation": "https://www.home-assistant.io/integrations/command_line", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/concord232/alarm_control_panel.py b/homeassistant/components/concord232/alarm_control_panel.py index 68f0d77e307..e86ec02040e 100644 --- a/homeassistant/components/concord232/alarm_control_panel.py +++ b/homeassistant/components/concord232/alarm_control_panel.py @@ -69,7 +69,6 @@ class Concord232Alarm(alarm.AlarmControlPanel): self._url = url self._alarm = concord232_client.Client(self._url) self._alarm.partitions = self._alarm.list_partitions() - self._alarm.last_partition_update = datetime.datetime.now() @property def name(self): diff --git a/homeassistant/components/concord232/binary_sensor.py b/homeassistant/components/concord232/binary_sensor.py index 10643f134d7..1a406d743b7 100644 --- a/homeassistant/components/concord232/binary_sensor.py +++ b/homeassistant/components/concord232/binary_sensor.py @@ -12,6 +12,7 @@ from homeassistant.components.binary_sensor import ( ) from homeassistant.const import CONF_HOST, CONF_PORT import homeassistant.helpers.config_validation as cv +import homeassistant.util.dt as dt_util _LOGGER = logging.getLogger(__name__) @@ -53,7 +54,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): _LOGGER.debug("Initializing client") client = concord232_client.Client(f"http://{host}:{port}") client.zones = client.list_zones() - client.last_zone_update = datetime.datetime.now() + client.last_zone_update = dt_util.utcnow() except requests.exceptions.ConnectionError as ex: _LOGGER.error("Unable to connect to Concord232: %s", str(ex)) @@ -128,11 +129,11 @@ class Concord232ZoneSensor(BinarySensorDevice): def update(self): """Get updated stats from API.""" - last_update = datetime.datetime.now() - self._client.last_zone_update + last_update = dt_util.utcnow() - self._client.last_zone_update _LOGGER.debug("Zone: %s ", self._zone) if last_update > datetime.timedelta(seconds=1): self._client.zones = self._client.list_zones() - self._client.last_zone_update = datetime.datetime.now() + self._client.last_zone_update = dt_util.utcnow() _LOGGER.debug("Updated from zone: %s", self._zone["name"]) if hasattr(self._client, "zones"): diff --git a/homeassistant/components/concord232/manifest.json b/homeassistant/components/concord232/manifest.json index f26da49d3f1..f5ff021b6d1 100644 --- a/homeassistant/components/concord232/manifest.json +++ b/homeassistant/components/concord232/manifest.json @@ -1,7 +1,7 @@ { "domain": "concord232", "name": "Concord232", - "documentation": "https://www.home-assistant.io/components/concord232", + "documentation": "https://www.home-assistant.io/integrations/concord232", "requirements": [ "concord232==0.15" ], diff --git a/homeassistant/components/config/__init__.py b/homeassistant/components/config/__init__.py index 6d4b465fceb..569d1de6a02 100644 --- a/homeassistant/components/config/__init__.py +++ b/homeassistant/components/config/__init__.py @@ -5,10 +5,11 @@ import os import voluptuous as vol -from homeassistant.core import callback -from homeassistant.const import EVENT_COMPONENT_LOADED, CONF_ID -from homeassistant.setup import ATTR_COMPONENT from homeassistant.components.http import HomeAssistantView +from homeassistant.const import EVENT_COMPONENT_LOADED, CONF_ID +from homeassistant.core import callback +from homeassistant.exceptions import HomeAssistantError +from homeassistant.setup import ATTR_COMPONENT from homeassistant.util.yaml import load_yaml, dump DOMAIN = "config" @@ -80,6 +81,7 @@ class BaseEditConfigView(HomeAssistantView): data_schema, *, post_write_hook=None, + data_validator=None, ): """Initialize a config view.""" self.url = f"/api/config/{component}/{config_type}/{{config_key}}" @@ -88,6 +90,7 @@ class BaseEditConfigView(HomeAssistantView): self.key_schema = key_schema self.data_schema = data_schema self.post_write_hook = post_write_hook + self.data_validator = data_validator def _empty_config(self): """Empty config if file not found.""" @@ -128,14 +131,18 @@ class BaseEditConfigView(HomeAssistantView): except vol.Invalid as err: return self.json_message(f"Key malformed: {err}", 400) + hass = request.app["hass"] + try: # We just validate, we don't store that data because # we don't want to store the defaults. - self.data_schema(data) - except vol.Invalid as err: + if self.data_validator: + await self.data_validator(hass, data) + else: + self.data_schema(data) + except (vol.Invalid, HomeAssistantError) as err: return self.json_message(f"Message malformed: {err}", 400) - hass = request.app["hass"] path = hass.config.path(self.path) current = await self.read_config(hass) diff --git a/homeassistant/components/config/automation.py b/homeassistant/components/config/automation.py index 7ca71fc4f93..97ddf1e0714 100644 --- a/homeassistant/components/config/automation.py +++ b/homeassistant/components/config/automation.py @@ -3,6 +3,7 @@ from collections import OrderedDict import uuid from homeassistant.components.automation import DOMAIN, PLATFORM_SCHEMA +from homeassistant.components.automation.config import async_validate_config_item from homeassistant.const import CONF_ID, SERVICE_RELOAD import homeassistant.helpers.config_validation as cv @@ -26,6 +27,7 @@ async def async_setup(hass): cv.string, PLATFORM_SCHEMA, post_write_hook=hook, + data_validator=async_validate_config_item, ) ) return True @@ -53,7 +55,7 @@ class EditAutomationConfigView(EditIdBasedConfigView): # Iterate through some keys that we want to have ordered in the output updated_value = OrderedDict() - for key in ("id", "alias", "trigger", "condition", "action"): + for key in ("id", "alias", "description", "trigger", "condition", "action"): if key in cur_value: updated_value[key] = cur_value[key] if key in new_value: diff --git a/homeassistant/components/config/manifest.json b/homeassistant/components/config/manifest.json index 9c0c50a2595..c6e99b43c49 100644 --- a/homeassistant/components/config/manifest.json +++ b/homeassistant/components/config/manifest.json @@ -1,7 +1,7 @@ { "domain": "config", "name": "Config", - "documentation": "https://www.home-assistant.io/components/config", + "documentation": "https://www.home-assistant.io/integrations/config", "requirements": [], "dependencies": [ "http" diff --git a/homeassistant/components/configurator/manifest.json b/homeassistant/components/configurator/manifest.json index f01fe7324fa..10c067d4a22 100644 --- a/homeassistant/components/configurator/manifest.json +++ b/homeassistant/components/configurator/manifest.json @@ -1,7 +1,7 @@ { "domain": "configurator", "name": "Configurator", - "documentation": "https://www.home-assistant.io/components/configurator", + "documentation": "https://www.home-assistant.io/integrations/configurator", "requirements": [], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/conversation/manifest.json b/homeassistant/components/conversation/manifest.json index ddd3b6205ef..0d6d67cf254 100644 --- a/homeassistant/components/conversation/manifest.json +++ b/homeassistant/components/conversation/manifest.json @@ -1,7 +1,7 @@ { "domain": "conversation", "name": "Conversation", - "documentation": "https://www.home-assistant.io/components/conversation", + "documentation": "https://www.home-assistant.io/integrations/conversation", "requirements": [], "dependencies": [ "http" diff --git a/homeassistant/components/coolmaster/manifest.json b/homeassistant/components/coolmaster/manifest.json index 9489dc72689..69ab8ee3c4b 100644 --- a/homeassistant/components/coolmaster/manifest.json +++ b/homeassistant/components/coolmaster/manifest.json @@ -1,7 +1,7 @@ { "domain": "coolmaster", "name": "Coolmaster", - "documentation": "https://www.home-assistant.io/components/coolmaster", + "documentation": "https://www.home-assistant.io/integrations/coolmaster", "requirements": [ "pycoolmasternet==0.0.4" ], diff --git a/homeassistant/components/counter/manifest.json b/homeassistant/components/counter/manifest.json index ae7066ea82d..3fd533054d8 100644 --- a/homeassistant/components/counter/manifest.json +++ b/homeassistant/components/counter/manifest.json @@ -1,7 +1,7 @@ { "domain": "counter", "name": "Counter", - "documentation": "https://www.home-assistant.io/components/counter", + "documentation": "https://www.home-assistant.io/integrations/counter", "requirements": [], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/cover/__init__.py b/homeassistant/components/cover/__init__.py index d491765bb00..8d2b4430fe1 100644 --- a/homeassistant/components/cover/__init__.py +++ b/homeassistant/components/cover/__init__.py @@ -2,6 +2,7 @@ from datetime import timedelta import functools as ft import logging +from typing import Any import voluptuous as vol @@ -33,7 +34,7 @@ from homeassistant.const import ( ) -# mypy: allow-untyped-calls, allow-incomplete-defs, allow-untyped-defs +# mypy: allow-untyped-calls, allow-untyped-defs _LOGGER = logging.getLogger(__name__) @@ -263,7 +264,7 @@ class CoverDevice(Entity): """Return if the cover is closed or not.""" raise NotImplementedError() - def open_cover(self, **kwargs): + def open_cover(self, **kwargs: Any) -> None: """Open the cover.""" raise NotImplementedError() @@ -274,7 +275,7 @@ class CoverDevice(Entity): """ return self.hass.async_add_job(ft.partial(self.open_cover, **kwargs)) - def close_cover(self, **kwargs): + def close_cover(self, **kwargs: Any) -> None: """Close cover.""" raise NotImplementedError() @@ -285,7 +286,7 @@ class CoverDevice(Entity): """ return self.hass.async_add_job(ft.partial(self.close_cover, **kwargs)) - def toggle(self, **kwargs) -> None: + def toggle(self, **kwargs: Any) -> None: """Toggle the entity.""" if self.is_closed: self.open_cover(**kwargs) @@ -323,7 +324,7 @@ class CoverDevice(Entity): """ return self.hass.async_add_job(ft.partial(self.stop_cover, **kwargs)) - def open_cover_tilt(self, **kwargs): + def open_cover_tilt(self, **kwargs: Any) -> None: """Open the cover tilt.""" pass @@ -334,7 +335,7 @@ class CoverDevice(Entity): """ return self.hass.async_add_job(ft.partial(self.open_cover_tilt, **kwargs)) - def close_cover_tilt(self, **kwargs): + def close_cover_tilt(self, **kwargs: Any) -> None: """Close the cover tilt.""" pass @@ -369,7 +370,7 @@ class CoverDevice(Entity): """ return self.hass.async_add_job(ft.partial(self.stop_cover_tilt, **kwargs)) - def toggle_tilt(self, **kwargs) -> None: + def toggle_tilt(self, **kwargs: Any) -> None: """Toggle the entity.""" if self.current_cover_tilt_position == 0: self.open_cover_tilt(**kwargs) diff --git a/homeassistant/components/cover/manifest.json b/homeassistant/components/cover/manifest.json index da5a644334c..1d82dcb5b0c 100644 --- a/homeassistant/components/cover/manifest.json +++ b/homeassistant/components/cover/manifest.json @@ -1,7 +1,7 @@ { "domain": "cover", "name": "Cover", - "documentation": "https://www.home-assistant.io/components/cover", + "documentation": "https://www.home-assistant.io/integrations/cover", "requirements": [], "dependencies": [ "group" diff --git a/homeassistant/components/cppm_tracker/manifest.json b/homeassistant/components/cppm_tracker/manifest.json index 5a1bdbf5a45..b2abc40dca2 100644 --- a/homeassistant/components/cppm_tracker/manifest.json +++ b/homeassistant/components/cppm_tracker/manifest.json @@ -1,7 +1,7 @@ { "domain": "cppm_tracker", "name": "Cppm tracker", - "documentation": "https://www.home-assistant.io/components/cppm_tracker", + "documentation": "https://www.home-assistant.io/integrations/cppm_tracker", "requirements": [ "clearpasspy==1.0.2" ], diff --git a/homeassistant/components/cpuspeed/manifest.json b/homeassistant/components/cpuspeed/manifest.json index 9034cb7740d..7950cad9b8b 100644 --- a/homeassistant/components/cpuspeed/manifest.json +++ b/homeassistant/components/cpuspeed/manifest.json @@ -1,7 +1,7 @@ { "domain": "cpuspeed", "name": "Cpuspeed", - "documentation": "https://www.home-assistant.io/components/cpuspeed", + "documentation": "https://www.home-assistant.io/integrations/cpuspeed", "requirements": [ "py-cpuinfo==5.0.0" ], diff --git a/homeassistant/components/crimereports/manifest.json b/homeassistant/components/crimereports/manifest.json index 0f74216b9b2..c5cc45d3192 100644 --- a/homeassistant/components/crimereports/manifest.json +++ b/homeassistant/components/crimereports/manifest.json @@ -1,7 +1,7 @@ { "domain": "crimereports", "name": "Crimereports", - "documentation": "https://www.home-assistant.io/components/crimereports", + "documentation": "https://www.home-assistant.io/integrations/crimereports", "requirements": [ "crimereports==1.0.1" ], diff --git a/homeassistant/components/cups/manifest.json b/homeassistant/components/cups/manifest.json index def2846c4ca..e30d64510ff 100644 --- a/homeassistant/components/cups/manifest.json +++ b/homeassistant/components/cups/manifest.json @@ -1,7 +1,7 @@ { "domain": "cups", "name": "Cups", - "documentation": "https://www.home-assistant.io/components/cups", + "documentation": "https://www.home-assistant.io/integrations/cups", "requirements": [ "pycups==1.9.73" ], diff --git a/homeassistant/components/currencylayer/manifest.json b/homeassistant/components/currencylayer/manifest.json index 7064590bf25..2db35dead60 100644 --- a/homeassistant/components/currencylayer/manifest.json +++ b/homeassistant/components/currencylayer/manifest.json @@ -1,7 +1,7 @@ { "domain": "currencylayer", "name": "Currencylayer", - "documentation": "https://www.home-assistant.io/components/currencylayer", + "documentation": "https://www.home-assistant.io/integrations/currencylayer", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/daikin/.translations/nn.json b/homeassistant/components/daikin/.translations/nn.json new file mode 100644 index 00000000000..67d4f852625 --- /dev/null +++ b/homeassistant/components/daikin/.translations/nn.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Daikin AC" + } +} \ No newline at end of file diff --git a/homeassistant/components/daikin/.translations/ru.json b/homeassistant/components/daikin/.translations/ru.json index ce1f1ab3caa..98ab98e6b17 100644 --- a/homeassistant/components/daikin/.translations/ru.json +++ b/homeassistant/components/daikin/.translations/ru.json @@ -1,7 +1,7 @@ { "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.", "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." }, diff --git a/homeassistant/components/daikin/.translations/zh-Hant.json b/homeassistant/components/daikin/.translations/zh-Hant.json index 1699bcad8f0..457b7d1b89c 100644 --- a/homeassistant/components/daikin/.translations/zh-Hant.json +++ b/homeassistant/components/daikin/.translations/zh-Hant.json @@ -1,9 +1,9 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", - "device_fail": "\u5275\u5efa\u88dd\u7f6e\u6642\u767c\u751f\u672a\u77e5\u932f\u8aa4\u3002", - "device_timeout": "\u9023\u7dda\u81f3\u88dd\u7f6e\u903e\u6642\u3002" + "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "device_fail": "\u5275\u5efa\u8a2d\u5099\u6642\u767c\u751f\u672a\u77e5\u932f\u8aa4\u3002", + "device_timeout": "\u9023\u7dda\u81f3\u8a2d\u5099\u903e\u6642\u3002" }, "step": { "user": { diff --git a/homeassistant/components/daikin/manifest.json b/homeassistant/components/daikin/manifest.json index a60355efa0b..f81cb171580 100644 --- a/homeassistant/components/daikin/manifest.json +++ b/homeassistant/components/daikin/manifest.json @@ -2,7 +2,7 @@ "domain": "daikin", "name": "Daikin", "config_flow": true, - "documentation": "https://www.home-assistant.io/components/daikin", + "documentation": "https://www.home-assistant.io/integrations/daikin", "requirements": [ "pydaikin==1.6.1" ], diff --git a/homeassistant/components/danfoss_air/manifest.json b/homeassistant/components/danfoss_air/manifest.json index a210b5a78d1..189b685d4ce 100644 --- a/homeassistant/components/danfoss_air/manifest.json +++ b/homeassistant/components/danfoss_air/manifest.json @@ -1,7 +1,7 @@ { "domain": "danfoss_air", "name": "Danfoss air", - "documentation": "https://www.home-assistant.io/components/danfoss_air", + "documentation": "https://www.home-assistant.io/integrations/danfoss_air", "requirements": [ "pydanfossair==0.1.0" ], diff --git a/homeassistant/components/darksky/manifest.json b/homeassistant/components/darksky/manifest.json index e4e6482484c..0046e51463e 100644 --- a/homeassistant/components/darksky/manifest.json +++ b/homeassistant/components/darksky/manifest.json @@ -1,7 +1,7 @@ { "domain": "darksky", "name": "Darksky", - "documentation": "https://www.home-assistant.io/components/darksky", + "documentation": "https://www.home-assistant.io/integrations/darksky", "requirements": [ "python-forecastio==1.4.0" ], diff --git a/homeassistant/components/darksky/weather.py b/homeassistant/components/darksky/weather.py index e95381cdf73..5296f346626 100644 --- a/homeassistant/components/darksky/weather.py +++ b/homeassistant/components/darksky/weather.py @@ -1,5 +1,5 @@ """Support for retrieving meteorological data from Dark Sky.""" -from datetime import datetime, timedelta +from datetime import timedelta import logging from requests.exceptions import ConnectionError as ConnectError, HTTPError, Timeout @@ -29,6 +29,7 @@ from homeassistant.const import ( ) import homeassistant.helpers.config_validation as cv from homeassistant.util import Throttle +from homeassistant.util.dt import utc_from_timestamp from homeassistant.util.pressure import convert as convert_pressure _LOGGER = logging.getLogger(__name__) @@ -178,7 +179,7 @@ class DarkSkyWeather(WeatherEntity): if self._mode == "daily": data = [ { - ATTR_FORECAST_TIME: datetime.fromtimestamp( + ATTR_FORECAST_TIME: utc_from_timestamp( entry.d.get("time") ).isoformat(), ATTR_FORECAST_TEMP: entry.d.get("temperatureHigh"), @@ -195,7 +196,7 @@ class DarkSkyWeather(WeatherEntity): else: data = [ { - ATTR_FORECAST_TIME: datetime.fromtimestamp( + ATTR_FORECAST_TIME: utc_from_timestamp( entry.d.get("time") ).isoformat(), ATTR_FORECAST_TEMP: entry.d.get("temperature"), diff --git a/homeassistant/components/datadog/manifest.json b/homeassistant/components/datadog/manifest.json index 40a2e82d53a..36de2ff2101 100644 --- a/homeassistant/components/datadog/manifest.json +++ b/homeassistant/components/datadog/manifest.json @@ -1,7 +1,7 @@ { "domain": "datadog", "name": "Datadog", - "documentation": "https://www.home-assistant.io/components/datadog", + "documentation": "https://www.home-assistant.io/integrations/datadog", "requirements": [ "datadog==0.15.0" ], diff --git a/homeassistant/components/ddwrt/manifest.json b/homeassistant/components/ddwrt/manifest.json index 3c877a34841..874b24e370b 100644 --- a/homeassistant/components/ddwrt/manifest.json +++ b/homeassistant/components/ddwrt/manifest.json @@ -1,7 +1,7 @@ { "domain": "ddwrt", "name": "Ddwrt", - "documentation": "https://www.home-assistant.io/components/ddwrt", + "documentation": "https://www.home-assistant.io/integrations/ddwrt", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/deconz/.translations/bg.json b/homeassistant/components/deconz/.translations/bg.json index f3eead4aae0..c9963e49623 100644 --- a/homeassistant/components/deconz/.translations/bg.json +++ b/homeassistant/components/deconz/.translations/bg.json @@ -69,5 +69,23 @@ "remote_button_triple_press": "\"{subtype}\" \u0431\u0443\u0442\u043e\u043d\u044a\u0442 \u0431\u0435\u0448\u0435 \u043d\u0430\u0442\u0438\u0441\u043d\u0430\u0442 \u0442\u0440\u0438\u043a\u0440\u0430\u0442\u043d\u043e", "remote_gyro_activated": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0435 \u0440\u0430\u0437\u043a\u043b\u0430\u0442\u0435\u043d\u043e" } + }, + "options": { + "step": { + "async_step_deconz_devices": { + "data": { + "allow_clip_sensor": "\u0420\u0430\u0437\u0440\u0435\u0448\u0430\u0432\u0430\u043d\u0435 \u043d\u0430 deCONZ CLIP \u0441\u0435\u043d\u0437\u043e\u0440\u0438", + "allow_deconz_groups": "\u0420\u0430\u0437\u0440\u0435\u0448\u0430\u0432\u0430\u043d\u0435 \u043d\u0430 deCONZ \u0441\u0432\u0435\u0442\u043b\u0438\u043d\u043d\u0438 \u0433\u0440\u0443\u043f\u0438" + }, + "description": "\u041a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u0439\u0442\u0435 \u0432\u0438\u0434\u0438\u043c\u043e\u0441\u0442\u0442\u0430 \u043d\u0430 \u0442\u0438\u043f\u043e\u0432\u0435\u0442\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 deCONZ" + }, + "deconz_devices": { + "data": { + "allow_clip_sensor": "\u0420\u0430\u0437\u0440\u0435\u0448\u0430\u0432\u0430\u043d\u0435 \u043d\u0430 deCONZ CLIP \u0441\u0435\u043d\u0437\u043e\u0440\u0438", + "allow_deconz_groups": "\u0420\u0430\u0437\u0440\u0435\u0448\u0438 deCONZ \u0441\u0432\u0435\u0442\u043b\u0438\u043d\u043d\u0438 \u0433\u0440\u0443\u043f\u0438" + }, + "description": "\u041a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u0439\u0442\u0435 \u0432\u0438\u0434\u0438\u043c\u043e\u0441\u0442\u0442\u0430 \u043d\u0430 \u0442\u0438\u043f\u043e\u0432\u0435\u0442\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 deCONZ" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/cs.json b/homeassistant/components/deconz/.translations/cs.json index 0f4bdf98ac1..42b14833aa0 100644 --- a/homeassistant/components/deconz/.translations/cs.json +++ b/homeassistant/components/deconz/.translations/cs.json @@ -23,7 +23,7 @@ "options": { "data": { "allow_clip_sensor": "Povolit import virtu\u00e1ln\u00edch \u010didel", - "allow_deconz_groups": "Povolit import skupin deCONZ " + "allow_deconz_groups": "Povolit import skupin deCONZ" }, "title": "Dal\u0161\u00ed mo\u017enosti konfigurace pro deCONZ" } diff --git a/homeassistant/components/deconz/.translations/de.json b/homeassistant/components/deconz/.translations/de.json index 97e25e28965..830ae0fd13f 100644 --- a/homeassistant/components/deconz/.translations/de.json +++ b/homeassistant/components/deconz/.translations/de.json @@ -41,6 +41,16 @@ }, "title": "deCONZ Zigbee Gateway" }, + "device_automation": { + "trigger_subtype": { + "close": "Schlie\u00dfen", + "left": "Links", + "open": "Offen", + "right": "Rechts", + "turn_off": "Ausschalten", + "turn_on": "Einschalten" + } + }, "options": { "step": { "async_step_deconz_devices": { diff --git a/homeassistant/components/deconz/.translations/en.json b/homeassistant/components/deconz/.translations/en.json index ead71db8c27..c00bfca3564 100644 --- a/homeassistant/components/deconz/.translations/en.json +++ b/homeassistant/components/deconz/.translations/en.json @@ -64,6 +64,7 @@ "remote_button_quadruple_press": "\"{subtype}\" button quadruple clicked", "remote_button_quintuple_press": "\"{subtype}\" button quintuple clicked", "remote_button_rotated": "Button rotated \"{subtype}\"", + "remote_button_rotation_stopped": "Button rotation \"{subtype}\" stopped", "remote_button_short_press": "\"{subtype}\" button pressed", "remote_button_short_release": "\"{subtype}\" button released", "remote_button_triple_press": "\"{subtype}\" button triple clicked", diff --git a/homeassistant/components/deconz/.translations/es-419.json b/homeassistant/components/deconz/.translations/es-419.json index 1a5d992ef7b..448b654c86e 100644 --- a/homeassistant/components/deconz/.translations/es-419.json +++ b/homeassistant/components/deconz/.translations/es-419.json @@ -5,7 +5,8 @@ "already_in_progress": "El flujo de configuraci\u00f3n para el puente ya est\u00e1 en progreso.", "no_bridges": "No se descubrieron puentes deCONZ", "not_deconz_bridge": "No es un puente deCONZ", - "one_instance_only": "El componente solo admite una instancia deCONZ" + "one_instance_only": "El componente solo admite una instancia deCONZ", + "updated_instance": "Instancia deCONZ actualizada con nueva direcci\u00f3n de host" }, "error": { "no_key": "No se pudo obtener una clave de API" @@ -16,7 +17,8 @@ "allow_clip_sensor": "Permitir la importaci\u00f3n de sensores virtuales", "allow_deconz_groups": "Permitir la importaci\u00f3n de grupos deCONZ" }, - "description": "\u00bfDesea configurar Home Assistant para conectarse a la puerta de enlace deCONZ proporcionada por el complemento hass.io {addon}?" + "description": "\u00bfDesea configurar Home Assistant para conectarse a la puerta de enlace deCONZ proporcionada por el complemento hass.io {addon}?", + "title": "deCONZ Zigbee gateway a trav\u00e9s del complemento Hass.io" }, "init": { "data": { @@ -38,5 +40,35 @@ } }, "title": "deCONZ Zigbee gateway" + }, + "device_automation": { + "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", + "close": "Cerrar", + "left": "Izquierda", + "open": "Abrir", + "right": "Derecha", + "turn_off": "Apagar", + "turn_on": "Encender" + }, + "trigger_type": { + "remote_button_rotated": "Bot\u00f3n girado \"{subtype}\"", + "remote_gyro_activated": "Dispositivo agitado" + } + }, + "options": { + "step": { + "async_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" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/es.json b/homeassistant/components/deconz/.translations/es.json index 1bc6c8211a2..04a08d185b3 100644 --- a/homeassistant/components/deconz/.translations/es.json +++ b/homeassistant/components/deconz/.translations/es.json @@ -59,13 +59,14 @@ }, "trigger_type": { "remote_button_double_press": "Bot\u00f3n \"{subtype}\" pulsado dos veces consecutivas", - "remote_button_long_press": "bot\u00f3n \"{subtype}\" pulsado continuamente", + "remote_button_long_press": "Bot\u00f3n \"{subtype}\" pulsado continuamente", "remote_button_long_release": "Bot\u00f3n \"{subtype}\" liberado despu\u00e9s de un rato pulsado", "remote_button_quadruple_press": "Bot\u00f3n \"{subtype}\" pulsado cuatro veces consecutivas", "remote_button_quintuple_press": "Bot\u00f3n \"{subtype}\" pulsado cinco veces consecutivas", "remote_button_rotated": "Bot\u00f3n \"{subtype}\" girado", - "remote_button_short_press": "bot\u00f3n \"{subtype}\" pulsado", - "remote_button_short_release": "bot\u00f3n \"{subtype}\" liberado", + "remote_button_rotation_stopped": "Bot\u00f3n rotativo \"{subtipo}\" detenido", + "remote_button_short_press": "Bot\u00f3n \"{subtype}\" pulsado", + "remote_button_short_release": "Bot\u00f3n \"{subtype}\" liberado", "remote_button_triple_press": "Bot\u00f3n \"{subtype}\" pulsado cuatro veces consecutivas", "remote_gyro_activated": "Dispositivo sacudido" } diff --git a/homeassistant/components/deconz/.translations/fr.json b/homeassistant/components/deconz/.translations/fr.json index cc6d22945dc..3729f7f556a 100644 --- a/homeassistant/components/deconz/.translations/fr.json +++ b/homeassistant/components/deconz/.translations/fr.json @@ -64,6 +64,7 @@ "remote_button_quadruple_press": "Bouton \"{subtype}\" quadruple cliqu\u00e9", "remote_button_quintuple_press": "Bouton \"{subtype}\" quintuple cliqu\u00e9", "remote_button_rotated": "Bouton \"{subtype}\" tourn\u00e9", + "remote_button_rotation_stopped": "La rotation du bouton \" {subtype} \" s'est arr\u00eat\u00e9e", "remote_button_short_press": "Bouton \"{subtype}\" appuy\u00e9", "remote_button_short_release": "Bouton \"{subtype}\" rel\u00e2ch\u00e9", "remote_button_triple_press": "Bouton \"{subtype}\" triple cliqu\u00e9", diff --git a/homeassistant/components/deconz/.translations/hu.json b/homeassistant/components/deconz/.translations/hu.json index 5bf8db46841..9e810910743 100644 --- a/homeassistant/components/deconz/.translations/hu.json +++ b/homeassistant/components/deconz/.translations/hu.json @@ -29,5 +29,10 @@ } }, "title": "deCONZ Zigbee gateway" + }, + "device_automation": { + "trigger_subtype": { + "close": "Bez\u00e1r\u00e1s" + } } } \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/it.json b/homeassistant/components/deconz/.translations/it.json index 7a2b8832864..1f0b344a32d 100644 --- a/homeassistant/components/deconz/.translations/it.json +++ b/homeassistant/components/deconz/.translations/it.json @@ -64,6 +64,7 @@ "remote_button_quadruple_press": "Pulsante \"{subtype}\" cliccato quattro volte", "remote_button_quintuple_press": "Pulsante \"{subtype}\" cliccato cinque volte", "remote_button_rotated": "Pulsante ruotato \"{subtype}\"", + "remote_button_rotation_stopped": "La rotazione dei pulsanti \"{subtype}\" si \u00e8 arrestata", "remote_button_short_press": "Pulsante \"{subtype}\" premuto", "remote_button_short_release": "Pulsante \"{subtype}\" rilasciato", "remote_button_triple_press": "Pulsante \"{subtype}\" cliccato tre volte", diff --git a/homeassistant/components/deconz/.translations/lb.json b/homeassistant/components/deconz/.translations/lb.json index c536e577141..f5f41a28a32 100644 --- a/homeassistant/components/deconz/.translations/lb.json +++ b/homeassistant/components/deconz/.translations/lb.json @@ -49,6 +49,8 @@ "button_3": "Dr\u00ebtte Kn\u00e4ppchen", "button_4": "V\u00e9ierte Kn\u00e4ppchen", "close": "Zoumaachen", + "dim_down": "Verd\u00e4ischteren", + "dim_up": "Erhellen", "left": "L\u00e9nks", "open": "Op", "right": "Riets", @@ -62,6 +64,7 @@ "remote_button_quadruple_press": "\"{subtype}\" Kn\u00e4ppche v\u00e9ier mol gedr\u00e9ckt", "remote_button_quintuple_press": "\"{subtype}\" Kn\u00e4ppche f\u00ebnnef mol gedr\u00e9ckt", "remote_button_rotated": "Kn\u00e4ppche gedr\u00e9int \"{subtype}\"", + "remote_button_rotation_stopped": "Kn\u00e4ppchen Rotatioun \"{subtype}\" gestoppt", "remote_button_short_press": "\"{subtype}\" Kn\u00e4ppche gedr\u00e9ckt", "remote_button_short_release": "\"{subtype}\" Kn\u00e4ppche lassgelooss", "remote_button_triple_press": "\"{subtype}\" Kn\u00e4ppche dr\u00e4imol gedr\u00e9ckt", @@ -70,6 +73,13 @@ }, "options": { "step": { + "async_step_deconz_devices": { + "data": { + "allow_clip_sensor": "deCONZ Clip Sensoren erlaben", + "allow_deconz_groups": "deCONZ Luucht Gruppen erlaben" + }, + "description": "Visibilit\u00e9it vun deCONZ Apparater konfigur\u00e9ieren" + }, "deconz_devices": { "data": { "allow_clip_sensor": "deCONZ Clip Sensoren erlaben", diff --git a/homeassistant/components/deconz/.translations/no.json b/homeassistant/components/deconz/.translations/no.json index 7a93c6ff9cf..7d05a366cf2 100644 --- a/homeassistant/components/deconz/.translations/no.json +++ b/homeassistant/components/deconz/.translations/no.json @@ -58,15 +58,16 @@ "turn_on": "Sl\u00e5 p\u00e5" }, "trigger_type": { - "remote_button_double_press": "\"{under type}\"-knappen ble dobbeltklikket", - "remote_button_long_press": "\"{undertype}\" - knappen ble kontinuerlig trykket", - "remote_button_long_release": "\" {subtype} \" -knapp sluppet etter langt trykk", - "remote_button_quadruple_press": "\" {subtype} \" -knappen ble firedoblet klikket", - "remote_button_quintuple_press": "\"{undertype}\" - knappen femdobbelt klikket", - "remote_button_rotated": "Knappen roterte \" {subtype} \"", - "remote_button_short_press": "\" {subtype} \" -knappen ble trykket", - "remote_button_short_release": "\" {subtype} \" -knappen ble utgitt", - "remote_button_triple_press": "\"{under type}\"-knappen trippel klikket", + "remote_button_double_press": "\"{subtype}\"-knappen ble dobbeltklikket", + "remote_button_long_press": "\"{subtype}\"-knappen ble kontinuerlig trykket", + "remote_button_long_release": "\"{subtype}\"-knappen sluppet etter langt trykk", + "remote_button_quadruple_press": "\"{subtype}\"-knappen ble firedoblet klikket", + "remote_button_quintuple_press": "\"{subtype}\"-knappen femdobbelt klikket", + "remote_button_rotated": "Knappen roterte \"{subtype}\"", + "remote_button_rotation_stopped": "Knappe rotasjon \"{subtype}\" stoppet", + "remote_button_short_press": "\"{subtype}\" -knappen ble trykket", + "remote_button_short_release": "\"{subtype}\"-knappen sluppet", + "remote_button_triple_press": "\"{subtype}\"-knappen trippel klikket", "remote_gyro_activated": "Enhet er ristet" } }, diff --git a/homeassistant/components/deconz/.translations/pl.json b/homeassistant/components/deconz/.translations/pl.json index 70c33cf3c02..11a1beb10d6 100644 --- a/homeassistant/components/deconz/.translations/pl.json +++ b/homeassistant/components/deconz/.translations/pl.json @@ -43,31 +43,31 @@ }, "device_automation": { "trigger_subtype": { - "both_buttons": "Oba przyciski", - "button_1": "Pierwszy przycisk", - "button_2": "Drugi przycisk", - "button_3": "Trzeci przycisk", - "button_4": "Czwarty przycisk", - "close": "Zamknij", - "dim_down": "Przyciemnienie", - "dim_up": "Przyciemnienie", - "left": "Lewo", - "open": "Otw\u00f3rz", - "right": "Prawo", - "turn_off": "Wy\u0142\u0105cz", - "turn_on": "W\u0142\u0105cz" + "both_buttons": "oba przyciski", + "button_1": "pierwszy przycisk", + "button_2": "drugi przycisk", + "button_3": "trzeci przycisk", + "button_4": "czwarty przycisk", + "close": "zamkni\u0119cie", + "dim_down": "zmniejszenie jasno\u015bci", + "dim_up": "zwi\u0119kszenie jasno\u015bci", + "left": "w lewo", + "open": "otwarcie", + "right": "w prawo", + "turn_off": "wy\u0142\u0105czenie", + "turn_on": "wy\u0142\u0105czenie" }, "trigger_type": { - "remote_button_double_press": "Przycisk \"{subtype}\" podw\u00f3jnie naci\u015bni\u0119ty", - "remote_button_long_press": "Przycisk \"{subtype}\" naci\u015bni\u0119ty w spos\u00f3b ci\u0105g\u0142y", - "remote_button_long_release": "Przycisk \"{subtype}\" zwolniony po d\u0142ugim naci\u015bni\u0119ciu", - "remote_button_quadruple_press": "Przycisk \"{subtype}\" czterokrotnie naci\u015bni\u0119ty", - "remote_button_quintuple_press": "Przycisk \"{subtype}\" pi\u0119ciokrotnie naci\u015bni\u0119ty", - "remote_button_rotated": "Przycisk obr\u00f3cony \"{subtype}\"", - "remote_button_short_press": "Przycisk \"{subtype}\" naci\u015bni\u0119ty", - "remote_button_short_release": "Przycisk \"{subtype}\" zwolniony", - "remote_button_triple_press": "Przycisk \"{subtype}\" trzykrotnie naci\u015bni\u0119ty", - "remote_gyro_activated": "Urz\u0105dzenie potrz\u0105\u015bni\u0119te" + "remote_button_double_press": "przycisk \"{subtype}\" podw\u00f3jnie naci\u015bni\u0119ty", + "remote_button_long_press": "przycisk \"{subtype}\" naci\u015bni\u0119ty w spos\u00f3b ci\u0105g\u0142y", + "remote_button_long_release": "przycisk \"{subtype}\" zwolniony po d\u0142ugim naci\u015bni\u0119ciu", + "remote_button_quadruple_press": "przycisk \"{subtype}\" czterokrotnie naci\u015bni\u0119ty", + "remote_button_quintuple_press": "przycisk \"{subtype}\" pi\u0119ciokrotnie naci\u015bni\u0119ty", + "remote_button_rotated": "przycisk obr\u00f3cony \"{subtype}\"", + "remote_button_short_press": "przycisk \"{subtype}\" naci\u015bni\u0119ty", + "remote_button_short_release": "przycisk \"{subtype}\" zwolniony", + "remote_button_triple_press": "przycisk \"{subtype}\" trzykrotnie naci\u015bni\u0119ty", + "remote_gyro_activated": "potrz\u0105\u015bni\u0119cie urz\u0105dzeniem" } }, "options": { diff --git a/homeassistant/components/deconz/.translations/pt-BR.json b/homeassistant/components/deconz/.translations/pt-BR.json index d066cbcc510..8d54c470846 100644 --- a/homeassistant/components/deconz/.translations/pt-BR.json +++ b/homeassistant/components/deconz/.translations/pt-BR.json @@ -40,5 +40,16 @@ } }, "title": "Gateway deCONZ Zigbee" + }, + "options": { + "step": { + "async_step_deconz_devices": { + "data": { + "allow_clip_sensor": "Permitir sensores deCONZ CLIP", + "allow_deconz_groups": "Permitir grupos de luz deCONZ" + }, + "description": "Configurar visibilidade dos tipos de dispositivos deCONZ" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/ru.json b/homeassistant/components/deconz/.translations/ru.json index 92fd1e3e749..558fd9e5897 100644 --- a/homeassistant/components/deconz/.translations/ru.json +++ b/homeassistant/components/deconz/.translations/ru.json @@ -49,9 +49,13 @@ "button_3": "\u0422\u0440\u0435\u0442\u044c\u044f \u043a\u043d\u043e\u043f\u043a\u0430", "button_4": "\u0427\u0435\u0442\u0432\u0435\u0440\u0442\u0430\u044f \u043a\u043d\u043e\u043f\u043a\u0430", "close": "\u0417\u0430\u043a\u0440\u044b\u0442\u043e", + "dim_down": "\u0423\u0431\u0430\u0432\u0438\u0442\u044c \u044f\u0440\u043a\u043e\u0441\u0442\u044c", + "dim_up": "\u0414\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u044f\u0440\u043a\u043e\u0441\u0442\u044c", "left": "\u041d\u0430\u043b\u0435\u0432\u043e", "open": "\u041e\u0442\u043a\u0440\u044b\u0442\u043e", - "right": "\u041d\u0430\u043f\u0440\u0430\u0432\u043e" + "right": "\u041d\u0430\u043f\u0440\u0430\u0432\u043e", + "turn_off": "\u0412\u044b\u043a\u043b\u044e\u0447\u0435\u043d\u043e", + "turn_on": "\u0412\u043a\u043b\u044e\u0447\u0435\u043d\u043e" }, "trigger_type": { "remote_button_double_press": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043d\u0430\u0436\u0430\u0442\u0430 \u0434\u0432\u0430\u0436\u0434\u044b", @@ -62,7 +66,8 @@ "remote_button_rotated": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043f\u043e\u0432\u0451\u0440\u043d\u0443\u0442\u0430", "remote_button_short_press": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043d\u0430\u0436\u0430\u0442\u0430", "remote_button_short_release": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043e\u0442\u043f\u0443\u0449\u0435\u043d\u0430", - "remote_button_triple_press": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043d\u0430\u0436\u0430\u0442\u0430 \u0442\u0440\u0438\u0436\u0434\u044b" + "remote_button_triple_press": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043d\u0430\u0436\u0430\u0442\u0430 \u0442\u0440\u0438\u0436\u0434\u044b", + "remote_gyro_activated": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043f\u043e\u0442\u0440\u044f\u0441\u043b\u0438" } }, "options": { diff --git a/homeassistant/components/deconz/.translations/sl.json b/homeassistant/components/deconz/.translations/sl.json index 9aebb2a556f..0717bcfc39f 100644 --- a/homeassistant/components/deconz/.translations/sl.json +++ b/homeassistant/components/deconz/.translations/sl.json @@ -64,6 +64,7 @@ "remote_button_quadruple_press": "\"{subtype}\" gumb \u0161tirikrat kliknjen", "remote_button_quintuple_press": "\"{subtype}\" gumb petkrat kliknjen", "remote_button_rotated": "Gumb \"{subtype}\" zasukan", + "remote_button_rotation_stopped": "Vrtenje \"{subtype}\" gumba se je ustavilo", "remote_button_short_press": "Pritisnjen \"{subtype}\" gumb", "remote_button_short_release": "Gumb \"{subtype}\" spro\u0161\u010den", "remote_button_triple_press": "Gumb \"{subtype}\" trikrat kliknjen", diff --git a/homeassistant/components/deconz/.translations/zh-Hant.json b/homeassistant/components/deconz/.translations/zh-Hant.json index f024386aa0f..2ad613cde68 100644 --- a/homeassistant/components/deconz/.translations/zh-Hant.json +++ b/homeassistant/components/deconz/.translations/zh-Hant.json @@ -4,9 +4,9 @@ "already_configured": "Bridge \u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "already_in_progress": "Bridge \u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d\u3002", "no_bridges": "\u672a\u641c\u5c0b\u5230 deCONZ Bridfe", - "not_deconz_bridge": "\u975e deCONZ Bridge \u88dd\u7f6e", + "not_deconz_bridge": "\u975e deCONZ Bridge \u8a2d\u5099", "one_instance_only": "\u7d44\u4ef6\u50c5\u652f\u63f4\u4e00\u7d44 deCONZ \u7269\u4ef6", - "updated_instance": "\u4f7f\u7528\u65b0\u4e3b\u6a5f\u7aef\u4f4d\u5740\u66f4\u65b0 deCONZ \u5be6\u4f8b" + "updated_instance": "\u4f7f\u7528\u65b0\u4e3b\u6a5f\u7aef\u4f4d\u5740\u66f4\u65b0 deCONZ \u7269\u4ef6" }, "error": { "no_key": "\u7121\u6cd5\u53d6\u5f97 API key" @@ -64,6 +64,7 @@ "remote_button_quadruple_press": "\"{subtype}\" \u6309\u9215\u56db\u9023\u9ede\u64ca", "remote_button_quintuple_press": "\"{subtype}\" \u6309\u9215\u4e94\u9023\u9ede\u64ca", "remote_button_rotated": "\u65cb\u8f49 \"{subtype}\" \u6309\u9215", + "remote_button_rotation_stopped": "\u65cb\u8f49 \"{subtype}\" \u6309\u9215\u5df2\u505c\u6b62", "remote_button_short_press": "\"{subtype}\" \u6309\u9215\u5df2\u6309\u4e0b", "remote_button_short_release": "\"{subtype}\" \u6309\u9215\u5df2\u91cb\u653e", "remote_button_triple_press": "\"{subtype}\" \u6309\u9215\u4e09\u9023\u9ede\u64ca", @@ -77,14 +78,14 @@ "allow_clip_sensor": "\u5141\u8a31 deCONZ CLIP \u611f\u61c9\u5668", "allow_deconz_groups": "\u5141\u8a31 deCONZ \u71c8\u5149\u7fa4\u7d44" }, - "description": "\u8a2d\u5b9a deCONZ \u53ef\u8996\u88dd\u7f6e\u985e\u578b" + "description": "\u8a2d\u5b9a deCONZ \u53ef\u8996\u8a2d\u5099\u985e\u578b" }, "deconz_devices": { "data": { "allow_clip_sensor": "\u5141\u8a31 deCONZ CLIP \u611f\u61c9\u5668", "allow_deconz_groups": "\u5141\u8a31 deCONZ \u71c8\u5149\u7fa4\u7d44" }, - "description": "\u8a2d\u5b9a deCONZ \u53ef\u8996\u88dd\u7f6e\u985e\u578b" + "description": "\u8a2d\u5b9a deCONZ \u53ef\u8996\u8a2d\u5099\u985e\u578b" } } } diff --git a/homeassistant/components/deconz/__init__.py b/homeassistant/components/deconz/__init__.py index 56663c6b2da..0ea91d10b19 100644 --- a/homeassistant/components/deconz/__init__.py +++ b/homeassistant/components/deconz/__init__.py @@ -1,70 +1,20 @@ """Support for deCONZ devices.""" import voluptuous as vol -from homeassistant import config_entries -from homeassistant.const import ( - CONF_API_KEY, - CONF_HOST, - CONF_PORT, - EVENT_HOMEASSISTANT_STOP, -) -from homeassistant.helpers import config_validation as cv +from homeassistant.const import EVENT_HOMEASSISTANT_STOP -# Loading the config flow file will register the flow from .config_flow import get_master_gateway -from .const import CONF_BRIDGEID, CONF_MASTER_GATEWAY, DEFAULT_PORT, DOMAIN, _LOGGER -from .gateway import DeconzGateway +from .const import CONF_BRIDGEID, CONF_MASTER_GATEWAY, CONF_UUID, DOMAIN +from .gateway import DeconzGateway, get_gateway_from_config_entry +from .services import async_setup_services, async_unload_services CONFIG_SCHEMA = vol.Schema( - { - DOMAIN: vol.Schema( - { - vol.Optional(CONF_API_KEY): cv.string, - vol.Optional(CONF_HOST): cv.string, - vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, - } - ) - }, - extra=vol.ALLOW_EXTRA, + {DOMAIN: vol.Schema({}, extra=vol.ALLOW_EXTRA)}, extra=vol.ALLOW_EXTRA ) -SERVICE_DECONZ = "configure" - -SERVICE_FIELD = "field" -SERVICE_ENTITY = "entity" -SERVICE_DATA = "data" - -SERVICE_SCHEMA = vol.All( - vol.Schema( - { - vol.Optional(SERVICE_ENTITY): cv.entity_id, - vol.Optional(SERVICE_FIELD): cv.matches_regex("/.*"), - vol.Required(SERVICE_DATA): dict, - vol.Optional(CONF_BRIDGEID): str, - } - ), - cv.has_at_least_one_key(SERVICE_ENTITY, SERVICE_FIELD), -) - -SERVICE_DEVICE_REFRESH = "device_refresh" - -SERVICE_DEVICE_REFRESCH_SCHEMA = vol.All(vol.Schema({vol.Optional(CONF_BRIDGEID): str})) - async def async_setup(hass, config): - """Load configuration for deCONZ component. - - Discovery has loaded the component if DOMAIN is not present in config. - """ - if not hass.config_entries.async_entries(DOMAIN) and DOMAIN in config: - deconz_config = config[DOMAIN] - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data=deconz_config, - ) - ) + """Old way of setting up deCONZ integrations.""" return True @@ -89,100 +39,13 @@ async def async_setup_entry(hass, config_entry): await gateway.async_update_device_registry() - async def async_configure(call): - """Set attribute of device in deCONZ. + if CONF_UUID not in config_entry.data: + await async_add_uuid_to_config_entry(hass, config_entry) - Entity is used to resolve to a device path (e.g. '/lights/1'). - Field is a string representing either a full path - (e.g. '/lights/1/state') when entity is not specified, or a - subpath (e.g. '/state') when used together with entity. - Data is a json object with what data you want to alter - e.g. data={'on': true}. - { - "field": "/lights/1/state", - "data": {"on": true} - } - See Dresden Elektroniks REST API documentation for details: - http://dresden-elektronik.github.io/deconz-rest-doc/rest/ - """ - field = call.data.get(SERVICE_FIELD, "") - entity_id = call.data.get(SERVICE_ENTITY) - data = call.data[SERVICE_DATA] - - gateway = get_master_gateway(hass) - if CONF_BRIDGEID in call.data: - gateway = hass.data[DOMAIN][call.data[CONF_BRIDGEID]] - - if entity_id: - try: - field = gateway.deconz_ids[entity_id] + field - except KeyError: - _LOGGER.error("Could not find the entity %s", entity_id) - return - - await gateway.api.async_put_state(field, data) - - hass.services.async_register( - DOMAIN, SERVICE_DECONZ, async_configure, schema=SERVICE_SCHEMA - ) - - async def async_refresh_devices(call): - """Refresh available devices from deCONZ.""" - gateway = get_master_gateway(hass) - if CONF_BRIDGEID in call.data: - gateway = hass.data[DOMAIN][call.data[CONF_BRIDGEID]] - - groups = set(gateway.api.groups.keys()) - lights = set(gateway.api.lights.keys()) - scenes = set(gateway.api.scenes.keys()) - sensors = set(gateway.api.sensors.keys()) - - await gateway.api.async_load_parameters() - - gateway.async_add_device_callback( - "group", - [ - group - for group_id, group in gateway.api.groups.items() - if group_id not in groups - ], - ) - - gateway.async_add_device_callback( - "light", - [ - light - for light_id, light in gateway.api.lights.items() - if light_id not in lights - ], - ) - - gateway.async_add_device_callback( - "scene", - [ - scene - for scene_id, scene in gateway.api.scenes.items() - if scene_id not in scenes - ], - ) - - gateway.async_add_device_callback( - "sensor", - [ - sensor - for sensor_id, sensor in gateway.api.sensors.items() - if sensor_id not in sensors - ], - ) - - hass.services.async_register( - DOMAIN, - SERVICE_DEVICE_REFRESH, - async_refresh_devices, - schema=SERVICE_DEVICE_REFRESCH_SCHEMA, - ) + await async_setup_services(hass) hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, gateway.shutdown) + return True @@ -191,8 +54,7 @@ async def async_unload_entry(hass, config_entry): gateway = hass.data[DOMAIN].pop(config_entry.data[CONF_BRIDGEID]) if not hass.data[DOMAIN]: - hass.services.async_remove(DOMAIN, SERVICE_DECONZ) - hass.services.async_remove(DOMAIN, SERVICE_DEVICE_REFRESH) + await async_unload_services(hass) elif gateway.master: await async_update_master_gateway(hass, config_entry) @@ -209,11 +71,14 @@ async def async_update_master_gateway(hass, config_entry): Makes sure there is always one master available. """ master = not get_master_gateway(hass) - - old_options = dict(config_entry.options) - - new_options = {CONF_MASTER_GATEWAY: master} - - options = {**old_options, **new_options} + options = {**config_entry.options, CONF_MASTER_GATEWAY: master} hass.config_entries.async_update_entry(config_entry, options=options) + + +async def async_add_uuid_to_config_entry(hass, config_entry): + """Add UUID to config entry to help discovery identify entries.""" + gateway = get_gateway_from_config_entry(hass, config_entry) + config = {**config_entry.data, CONF_UUID: gateway.api.config.uuid} + + hass.config_entries.async_update_entry(config_entry, data=config) diff --git a/homeassistant/components/deconz/binary_sensor.py b/homeassistant/components/deconz/binary_sensor.py index 492b16a603a..b81ecdc5164 100644 --- a/homeassistant/components/deconz/binary_sensor.py +++ b/homeassistant/components/deconz/binary_sensor.py @@ -2,7 +2,7 @@ from pydeconz.sensor import Presence, Vibration from homeassistant.components.binary_sensor import BinarySensorDevice -from homeassistant.const import ATTR_BATTERY_LEVEL, ATTR_TEMPERATURE +from homeassistant.const import ATTR_TEMPERATURE from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -17,7 +17,6 @@ ATTR_VIBRATIONSTRENGTH = "vibrationstrength" async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Old way of setting up deCONZ platforms.""" - pass async def async_setup_entry(hass, config_entry, async_add_entities): @@ -56,7 +55,7 @@ class DeconzBinarySensor(DeconzDevice, BinarySensorDevice): def async_update_callback(self, force_update=False): """Update the sensor's state.""" changed = set(self._device.changed_keys) - keys = {"battery", "on", "reachable", "state"} + keys = {"on", "reachable", "state"} if force_update or any(key in changed for key in keys): self.async_schedule_update_ha_state() @@ -79,8 +78,6 @@ class DeconzBinarySensor(DeconzDevice, BinarySensorDevice): def device_state_attributes(self): """Return the state attributes of the sensor.""" attr = {} - if self._device.battery: - attr[ATTR_BATTERY_LEVEL] = self._device.battery if self._device.on is not None: attr[ATTR_ON] = self._device.on diff --git a/homeassistant/components/deconz/climate.py b/homeassistant/components/deconz/climate.py index 1844cb2c97c..b7a1ebce22a 100644 --- a/homeassistant/components/deconz/climate.py +++ b/homeassistant/components/deconz/climate.py @@ -8,7 +8,7 @@ from homeassistant.components.climate.const import ( HVAC_MODE_OFF, SUPPORT_TARGET_TEMPERATURE, ) -from homeassistant.const import ATTR_BATTERY_LEVEL, ATTR_TEMPERATURE, TEMP_CELSIUS +from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -21,7 +21,6 @@ SUPPORT_HVAC = [HVAC_MODE_AUTO, HVAC_MODE_HEAT, HVAC_MODE_OFF] async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Old way of setting up deCONZ platforms.""" - pass async def async_setup_entry(hass, config_entry, async_add_entities): @@ -120,9 +119,6 @@ class DeconzThermostat(DeconzDevice, ClimateDevice): """Return the state attributes of the thermostat.""" attr = {} - if self._device.battery: - attr[ATTR_BATTERY_LEVEL] = self._device.battery - if self._device.offset: attr[ATTR_OFFSET] = self._device.offset diff --git a/homeassistant/components/deconz/config_flow.py b/homeassistant/components/deconz/config_flow.py index 12e2e092f67..66df687047f 100644 --- a/homeassistant/components/deconz/config_flow.py +++ b/homeassistant/components/deconz/config_flow.py @@ -5,7 +5,7 @@ import async_timeout import voluptuous as vol from pydeconz.errors import ResponseError, RequestError -from pydeconz.utils import async_discovery, async_get_api_key, async_get_bridgeid +from pydeconz.utils import async_discovery, async_get_api_key, async_get_gateway_config from homeassistant import config_entries from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_PORT @@ -16,6 +16,7 @@ from .const import ( CONF_ALLOW_CLIP_SENSOR, CONF_ALLOW_DECONZ_GROUPS, CONF_BRIDGEID, + CONF_UUID, DEFAULT_ALLOW_CLIP_SENSOR, DEFAULT_ALLOW_DECONZ_GROUPS, DEFAULT_PORT, @@ -89,7 +90,7 @@ class DeconzFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): with async_timeout.timeout(10): self.bridges = await async_discovery(session) - except asyncio.TimeoutError: + except (asyncio.TimeoutError, ResponseError): self.bridges = [] if len(self.bridges) == 1: @@ -144,9 +145,11 @@ class DeconzFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): try: with async_timeout.timeout(10): - self.deconz_config[CONF_BRIDGEID] = await async_get_bridgeid( + gateway_config = await async_get_gateway_config( session, **self.deconz_config ) + self.deconz_config[CONF_BRIDGEID] = gateway_config.bridgeid + self.deconz_config[CONF_UUID] = gateway_config.uuid except asyncio.TimeoutError: return self.async_abort(reason="no_bridges") @@ -172,14 +175,10 @@ class DeconzFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return self.async_abort(reason="not_deconz_bridge") uuid = discovery_info[ATTR_UUID].replace("uuid:", "") - gateways = { - gateway.api.config.uuid: gateway - for gateway in self.hass.data.get(DOMAIN, {}).values() - } - if uuid in gateways: - entry = gateways[uuid].config_entry - return await self._update_entry(entry, discovery_info[CONF_HOST]) + for entry in self.hass.config_entries.async_entries(DOMAIN): + if uuid == entry.data.get(CONF_UUID): + return await self._update_entry(entry, discovery_info[CONF_HOST]) bridgeid = discovery_info[ATTR_SERIAL] if any( @@ -191,31 +190,12 @@ class DeconzFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): # pylint: disable=unsupported-assignment-operation self.context[CONF_BRIDGEID] = bridgeid - deconz_config = { + self.deconz_config = { CONF_HOST: discovery_info[CONF_HOST], CONF_PORT: discovery_info[CONF_PORT], } - return await self.async_step_import(deconz_config) - - async def async_step_import(self, import_config): - """Import a deCONZ bridge as a config entry. - - This flow is triggered by `async_setup` for configured bridges. - This flow is also triggered by `async_step_discovery`. - - This will execute for any bridge that does not have a - config entry yet (based on host). - - If an API key is provided, we will create an entry. - Otherwise we will delegate to `link` step which - will ask user to link the bridge. - """ - self.deconz_config = import_config - if CONF_API_KEY not in import_config: - return await self.async_step_link() - - return await self._create_entry() + return await self.async_step_link() async def async_step_hassio(self, user_input=None): """Prepare configuration for a Hass.io deCONZ bridge. diff --git a/homeassistant/components/deconz/const.py b/homeassistant/components/deconz/const.py index 62879a82724..ad23a564272 100644 --- a/homeassistant/components/deconz/const.py +++ b/homeassistant/components/deconz/const.py @@ -5,13 +5,15 @@ _LOGGER = logging.getLogger(__package__) DOMAIN = "deconz" +CONF_BRIDGEID = "bridgeid" +CONF_UUID = "uuid" + DEFAULT_PORT = 80 DEFAULT_ALLOW_CLIP_SENSOR = False DEFAULT_ALLOW_DECONZ_GROUPS = True CONF_ALLOW_CLIP_SENSOR = "allow_clip_sensor" CONF_ALLOW_DECONZ_GROUPS = "allow_deconz_groups" -CONF_BRIDGEID = "bridgeid" CONF_MASTER_GATEWAY = "master" SUPPORTED_PLATFORMS = [ diff --git a/homeassistant/components/deconz/cover.py b/homeassistant/components/deconz/cover.py index b82144d37c7..bcd408c25a7 100644 --- a/homeassistant/components/deconz/cover.py +++ b/homeassistant/components/deconz/cover.py @@ -17,7 +17,6 @@ from .gateway import get_gateway_from_config_entry async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Old way of setting up deCONZ platforms.""" - pass async def async_setup_entry(hass, config_entry, async_add_entities): diff --git a/homeassistant/components/deconz/deconz_device.py b/homeassistant/components/deconz/deconz_device.py index e6249b2304c..68daee6cf26 100644 --- a/homeassistant/components/deconz/deconz_device.py +++ b/homeassistant/components/deconz/deconz_device.py @@ -24,10 +24,10 @@ class DeconzBase: @property def serial(self): """Return a serial number for this device.""" - if self.unique_id is None or self.unique_id.count(":") != 7: + if self._device.uniqueid is None or self._device.uniqueid.count(":") != 7: return None - return self.unique_id.split("-", 1)[0] + return self._device.uniqueid.split("-", 1)[0] @property def device_info(self): diff --git a/homeassistant/components/deconz/deconz_event.py b/homeassistant/components/deconz/deconz_event.py index f6c2d471bbf..31588db1f23 100644 --- a/homeassistant/components/deconz/deconz_event.py +++ b/homeassistant/components/deconz/deconz_event.py @@ -27,6 +27,11 @@ class DeconzEvent(DeconzBase): self.event_id = slugify(self._device.name) _LOGGER.debug("deCONZ event created: %s", self.event_id) + @property + def device(self): + """Return Event device.""" + return self._device + @callback def async_will_remove_from_hass(self) -> None: """Disconnect event object when removed.""" diff --git a/homeassistant/components/deconz/device_automation.py b/homeassistant/components/deconz/device_trigger.py similarity index 88% rename from homeassistant/components/deconz/device_automation.py rename to homeassistant/components/deconz/device_trigger.py index 28f36b8f431..badbe8b8651 100644 --- a/homeassistant/components/deconz/device_automation.py +++ b/homeassistant/components/deconz/device_trigger.py @@ -3,6 +3,7 @@ import voluptuous as vol import homeassistant.components.automation.event as event +from homeassistant.components.device_automation import TRIGGER_BASE_SCHEMA from homeassistant.components.device_automation.exceptions import ( InvalidDeviceAutomationConfig, ) @@ -30,6 +31,7 @@ CONF_TRIPLE_PRESS = "remote_button_triple_press" CONF_QUADRUPLE_PRESS = "remote_button_quadruple_press" CONF_QUINTUPLE_PRESS = "remote_button_quintuple_press" CONF_ROTATED = "remote_button_rotated" +CONF_ROTATION_STOPPED = "remote_button_rotation_stopped" CONF_SHAKE = "remote_gyro_activated" CONF_TURN_ON = "turn_on" @@ -74,6 +76,17 @@ HUE_TAP_REMOTE = { (CONF_SHORT_PRESS, CONF_BUTTON_4): 18, } +SYMFONISK_SOUND_CONTROLLER_MODEL = "SYMFONISK Sound Controller" +SYMFONISK_SOUND_CONTROLLER = { + (CONF_SHORT_PRESS, CONF_TURN_ON): 1002, + (CONF_DOUBLE_PRESS, CONF_TURN_ON): 1004, + (CONF_TRIPLE_PRESS, CONF_TURN_ON): 1005, + (CONF_ROTATED, CONF_LEFT): 2001, + (CONF_ROTATION_STOPPED, CONF_LEFT): 2003, + (CONF_ROTATED, CONF_RIGHT): 3001, + (CONF_ROTATION_STOPPED, CONF_RIGHT): 3003, +} + TRADFRI_ON_OFF_SWITCH_MODEL = "TRADFRI on/off switch" TRADFRI_ON_OFF_SWITCH = { (CONF_SHORT_PRESS, CONF_TURN_ON): 1002, @@ -161,6 +174,7 @@ AQARA_SQUARE_SWITCH = { REMOTES = { HUE_DIMMER_REMOTE_MODEL: HUE_DIMMER_REMOTE, HUE_TAP_REMOTE_MODEL: HUE_TAP_REMOTE, + SYMFONISK_SOUND_CONTROLLER_MODEL: SYMFONISK_SOUND_CONTROLLER, TRADFRI_ON_OFF_SWITCH_MODEL: TRADFRI_ON_OFF_SWITCH, TRADFRI_OPEN_CLOSE_REMOTE_MODEL: TRADFRI_OPEN_CLOSE_REMOTE, TRADFRI_REMOTE_MODEL: TRADFRI_REMOTE, @@ -171,16 +185,8 @@ REMOTES = { AQARA_SQUARE_SWITCH_MODEL: AQARA_SQUARE_SWITCH, } -TRIGGER_SCHEMA = vol.All( - vol.Schema( - { - vol.Required(CONF_DEVICE_ID): str, - vol.Required(CONF_DOMAIN): DOMAIN, - vol.Required(CONF_PLATFORM): "device", - vol.Required(CONF_TYPE): str, - vol.Required(CONF_SUBTYPE): str, - } - ) +TRIGGER_SCHEMA = TRIGGER_BASE_SCHEMA.extend( + {vol.Required(CONF_TYPE): str, vol.Required(CONF_SUBTYPE): str} ) @@ -198,16 +204,14 @@ def _get_deconz_event_from_device_id(hass, device_id): return None -async def async_trigger(hass, config, action, automation_info): +async def async_attach_trigger(hass, config, action, automation_info): """Listen for state changes based on configuration.""" - config = TRIGGER_SCHEMA(config) - device_registry = await hass.helpers.device_registry.async_get_registry() device = device_registry.async_get(config[CONF_DEVICE_ID]) trigger = (config[CONF_TYPE], config[CONF_SUBTYPE]) - if device.model not in REMOTES and trigger not in REMOTES[device.model]: + if device.model not in REMOTES or trigger not in REMOTES[device.model]: raise InvalidDeviceAutomationConfig trigger = REMOTES[device.model][trigger] @@ -223,7 +227,9 @@ async def async_trigger(hass, config, action, automation_info): event.CONF_EVENT_DATA: {CONF_UNIQUE_ID: event_id, CONF_EVENT: trigger}, } - return await event.async_trigger(hass, state_config, action, automation_info) + return await event.async_attach_trigger( + hass, state_config, action, automation_info, platform_type="device" + ) async def async_get_triggers(hass, device_id): diff --git a/homeassistant/components/deconz/gateway.py b/homeassistant/components/deconz/gateway.py index 35cf63fc3d2..75898b0fdab 100644 --- a/homeassistant/components/deconz/gateway.py +++ b/homeassistant/components/deconz/gateway.py @@ -3,7 +3,6 @@ import asyncio import async_timeout from pydeconz import DeconzSession, errors -from pydeconz.sensor import Switch from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.const import CONF_HOST @@ -29,10 +28,9 @@ from .const import ( DEFAULT_ALLOW_DECONZ_GROUPS, DOMAIN, NEW_DEVICE, - NEW_SENSOR, SUPPORTED_PLATFORMS, ) -from .deconz_event import DeconzEvent + from .errors import AuthenticationRequired, CannotConnect @@ -119,14 +117,6 @@ class DeconzGateway: ) ) - self.listeners.append( - async_dispatcher_connect( - hass, self.async_signal_new_device(NEW_SENSOR), self.async_add_remote - ) - ) - - self.async_add_remote(self.api.sensors.values()) - self.api.start() self.config_entry.add_update_listener(self.async_new_address) @@ -185,17 +175,6 @@ class DeconzGateway: self.hass, self.async_signal_new_device(device_type), device ) - @callback - def async_add_remote(self, sensors): - """Set up remote from deCONZ.""" - for sensor in sensors: - if sensor.type in Switch.ZHATYPE and not ( - not self.option_allow_clip_sensor and sensor.type.startswith("CLIP") - ): - event = DeconzEvent(sensor, self) - self.hass.async_create_task(event.async_update_device_registry()) - self.events.append(event) - @callback def shutdown(self, event): """Wrap the call to deconz.close. @@ -205,11 +184,7 @@ class DeconzGateway: self.api.close() async def async_reset(self): - """Reset this gateway to default state. - - Will cancel any scheduled setup retry and will unload - the config entry. - """ + """Reset this gateway to default state.""" self.api.async_connection_status_callback = None self.api.close() @@ -224,7 +199,7 @@ class DeconzGateway: for event in self.events: event.async_will_remove_from_hass() - self.events.remove(event) + self.events.clear() self.deconz_ids = {} return True diff --git a/homeassistant/components/deconz/light.py b/homeassistant/components/deconz/light.py index ec1dfd2bcb1..bf4b05089a8 100644 --- a/homeassistant/components/deconz/light.py +++ b/homeassistant/components/deconz/light.py @@ -34,7 +34,6 @@ from .gateway import get_gateway_from_config_entry, DeconzEntityHandler async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Old way of setting up deCONZ platforms.""" - pass async def async_setup_entry(hass, config_entry, async_add_entities): @@ -194,9 +193,6 @@ class DeconzLight(DeconzDevice, Light): attributes = {} attributes["is_deconz_group"] = self._device.type == "LightGroup" - if self._device.type == "LightGroup": - attributes["all_on"] = self._device.all_on - return attributes @@ -207,9 +203,7 @@ class DeconzGroup(DeconzLight): """Set up group and create an unique id.""" super().__init__(device, gateway) - self._unique_id = "{}-{}".format( - self.gateway.api.config.bridgeid, self._device.deconz_id - ) + self._unique_id = f"{self.gateway.api.config.bridgeid}-{self._device.deconz_id}" @property def unique_id(self): @@ -228,3 +222,11 @@ class DeconzGroup(DeconzLight): "name": self._device.name, "via_device": (DECONZ_DOMAIN, bridgeid), } + + @property + def device_state_attributes(self): + """Return the device state attributes.""" + attributes = dict(super().device_state_attributes) + attributes["all_on"] = self._device.all_on + + return attributes diff --git a/homeassistant/components/deconz/manifest.json b/homeassistant/components/deconz/manifest.json index fe8f49d260a..1e5cd414425 100644 --- a/homeassistant/components/deconz/manifest.json +++ b/homeassistant/components/deconz/manifest.json @@ -2,9 +2,9 @@ "domain": "deconz", "name": "Deconz", "config_flow": true, - "documentation": "https://www.home-assistant.io/components/deconz", + "documentation": "https://www.home-assistant.io/integrations/deconz", "requirements": [ - "pydeconz==62" + "pydeconz==63" ], "ssdp": { "manufacturer": [ diff --git a/homeassistant/components/deconz/scene.py b/homeassistant/components/deconz/scene.py index 8d27d386da2..a84e799d44d 100644 --- a/homeassistant/components/deconz/scene.py +++ b/homeassistant/components/deconz/scene.py @@ -9,7 +9,6 @@ from .gateway import get_gateway_from_config_entry async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Old way of setting up deCONZ platforms.""" - pass async def async_setup_entry(hass, config_entry, async_add_entities): diff --git a/homeassistant/components/deconz/sensor.py b/homeassistant/components/deconz/sensor.py index d84a47c6aaf..cc3f3de3170 100644 --- a/homeassistant/components/deconz/sensor.py +++ b/homeassistant/components/deconz/sensor.py @@ -1,18 +1,13 @@ """Support for deCONZ sensors.""" -from pydeconz.sensor import Consumption, Daylight, LightLevel, Power, Switch +from pydeconz.sensor import Consumption, Daylight, LightLevel, Power, Switch, Thermostat -from homeassistant.const import ( - ATTR_BATTERY_LEVEL, - ATTR_TEMPERATURE, - ATTR_VOLTAGE, - DEVICE_CLASS_BATTERY, -) +from homeassistant.const import ATTR_TEMPERATURE, ATTR_VOLTAGE, DEVICE_CLASS_BATTERY from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.util import slugify from .const import ATTR_DARK, ATTR_ON, NEW_SENSOR from .deconz_device import DeconzDevice +from .deconz_event import DeconzEvent from .gateway import get_gateway_from_config_entry, DeconzEntityHandler ATTR_CURRENT = "current" @@ -23,32 +18,47 @@ ATTR_EVENT_ID = "event_id" async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Old way of setting up deCONZ platforms.""" - pass async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the deCONZ sensors.""" gateway = get_gateway_from_config_entry(hass, config_entry) + batteries = set() entity_handler = DeconzEntityHandler(gateway) @callback def async_add_sensor(sensors): - """Add sensors from deCONZ.""" + """Add sensors from deCONZ. + + Create DeconzEvent if part of ZHAType list. + Create DeconzSensor if not a ZHAType and not a binary sensor. + Create DeconzBattery if sensor has a battery attribute. + """ entities = [] for sensor in sensors: - if not sensor.BINARY: + if sensor.type in Switch.ZHATYPE: - if sensor.type in Switch.ZHATYPE: - if sensor.battery: - entities.append(DeconzBattery(sensor, gateway)) + if gateway.option_allow_clip_sensor or not sensor.type.startswith( + "CLIP" + ): + new_event = DeconzEvent(sensor, gateway) + hass.async_create_task(new_event.async_update_device_registry()) + gateway.events.append(new_event) - else: - new_sensor = DeconzSensor(sensor, gateway) - entity_handler.add_entity(new_sensor) - entities.append(new_sensor) + elif not sensor.BINARY and sensor.type not in Thermostat.ZHATYPE: + + new_sensor = DeconzSensor(sensor, gateway) + entity_handler.add_entity(new_sensor) + entities.append(new_sensor) + + if sensor.battery: + new_battery = DeconzBattery(sensor, gateway) + if new_battery.unique_id not in batteries: + batteries.add(new_battery.unique_id) + entities.append(new_battery) async_add_entities(entities, True) @@ -68,7 +78,7 @@ class DeconzSensor(DeconzDevice): def async_update_callback(self, force_update=False): """Update the sensor's state.""" changed = set(self._device.changed_keys) - keys = {"battery", "on", "reachable", "state"} + keys = {"on", "reachable", "state"} if force_update or any(key in changed for key in keys): self.async_schedule_update_ha_state() @@ -96,8 +106,6 @@ class DeconzSensor(DeconzDevice): def device_state_attributes(self): """Return the state attributes of the sensor.""" attr = {} - if self._device.battery: - attr[ATTR_BATTERY_LEVEL] = self._device.battery if self._device.on is not None: attr[ATTR_ON] = self._device.on @@ -124,13 +132,6 @@ class DeconzSensor(DeconzDevice): class DeconzBattery(DeconzDevice): """Battery class for when a device is only represented as an event.""" - def __init__(self, device, gateway): - """Register dispatcher callback for update of battery state.""" - super().__init__(device, gateway) - - self._name = "{} {}".format(self._device.name, "Battery Level") - self._unit_of_measurement = "%" - @callback def async_update_callback(self, force_update=False): """Update the battery's state, if needed.""" @@ -139,6 +140,11 @@ class DeconzBattery(DeconzDevice): if force_update or any(key in changed for key in keys): self.async_schedule_update_ha_state() + @property + def unique_id(self): + """Return a unique identifier for this device.""" + return f"{self.serial}-battery" + @property def state(self): """Return the state of the battery.""" @@ -147,7 +153,7 @@ class DeconzBattery(DeconzDevice): @property def name(self): """Return the name of the battery.""" - return self._name + return f"{self._device.name} Battery Level" @property def device_class(self): @@ -157,10 +163,16 @@ class DeconzBattery(DeconzDevice): @property def unit_of_measurement(self): """Return the unit of measurement of this entity.""" - return self._unit_of_measurement + return "%" @property def device_state_attributes(self): """Return the state attributes of the battery.""" - attr = {ATTR_EVENT_ID: slugify(self._device.name)} + attr = {} + + if self._device.type in Switch.ZHATYPE: + for event in self.gateway.events: + if self._device == event.device: + attr[ATTR_EVENT_ID] = event.event_id + return attr diff --git a/homeassistant/components/deconz/services.py b/homeassistant/components/deconz/services.py new file mode 100644 index 00000000000..3498b46d879 --- /dev/null +++ b/homeassistant/components/deconz/services.py @@ -0,0 +1,158 @@ +"""deCONZ services.""" +import voluptuous as vol + +from homeassistant.helpers import config_validation as cv + +from .config_flow import get_master_gateway +from .const import CONF_BRIDGEID, DOMAIN, _LOGGER + +DECONZ_SERVICES = "deconz_services" + +SERVICE_FIELD = "field" +SERVICE_ENTITY = "entity" +SERVICE_DATA = "data" + +SERVICE_CONFIGURE_DEVICE = "configure" +SERVICE_CONFIGURE_DEVICE_SCHEMA = vol.All( + vol.Schema( + { + vol.Optional(SERVICE_ENTITY): cv.entity_id, + vol.Optional(SERVICE_FIELD): cv.matches_regex("/.*"), + vol.Required(SERVICE_DATA): dict, + vol.Optional(CONF_BRIDGEID): str, + } + ), + cv.has_at_least_one_key(SERVICE_ENTITY, SERVICE_FIELD), +) + +SERVICE_DEVICE_REFRESH = "device_refresh" +SERVICE_DEVICE_REFRESH_SCHEMA = vol.All(vol.Schema({vol.Optional(CONF_BRIDGEID): str})) + + +async def async_setup_services(hass): + """Set up services for deCONZ integration.""" + if hass.data.get(DECONZ_SERVICES, False): + return + + hass.data[DECONZ_SERVICES] = True + + async def async_call_deconz_service(service_call): + """Call correct deCONZ service.""" + service = service_call.service + service_data = service_call.data + + if service == SERVICE_CONFIGURE_DEVICE: + await async_configure_service(hass, service_data) + + elif service == SERVICE_DEVICE_REFRESH: + await async_refresh_devices_service(hass, service_data) + + hass.services.async_register( + DOMAIN, + SERVICE_CONFIGURE_DEVICE, + async_call_deconz_service, + schema=SERVICE_CONFIGURE_DEVICE_SCHEMA, + ) + + hass.services.async_register( + DOMAIN, + SERVICE_DEVICE_REFRESH, + async_call_deconz_service, + schema=SERVICE_DEVICE_REFRESH_SCHEMA, + ) + + +async def async_unload_services(hass): + """Unload deCONZ services.""" + if not hass.data.get(DECONZ_SERVICES): + return + + hass.data[DECONZ_SERVICES] = False + + hass.services.async_remove(DOMAIN, SERVICE_CONFIGURE_DEVICE) + hass.services.async_remove(DOMAIN, SERVICE_DEVICE_REFRESH) + + +async def async_configure_service(hass, data): + """Set attribute of device in deCONZ. + + Entity is used to resolve to a device path (e.g. '/lights/1'). + Field is a string representing either a full path + (e.g. '/lights/1/state') when entity is not specified, or a + subpath (e.g. '/state') when used together with entity. + Data is a json object with what data you want to alter + e.g. data={'on': true}. + { + "field": "/lights/1/state", + "data": {"on": true} + } + See Dresden Elektroniks REST API documentation for details: + http://dresden-elektronik.github.io/deconz-rest-doc/rest/ + """ + bridgeid = data.get(CONF_BRIDGEID) + field = data.get(SERVICE_FIELD, "") + entity_id = data.get(SERVICE_ENTITY) + data = data[SERVICE_DATA] + + gateway = get_master_gateway(hass) + if bridgeid: + gateway = hass.data[DOMAIN][bridgeid] + + if entity_id: + try: + field = gateway.deconz_ids[entity_id] + field + except KeyError: + _LOGGER.error("Could not find the entity %s", entity_id) + return + + await gateway.api.async_put_state(field, data) + + +async def async_refresh_devices_service(hass, data): + """Refresh available devices from deCONZ.""" + gateway = get_master_gateway(hass) + if CONF_BRIDGEID in data: + gateway = hass.data[DOMAIN][data[CONF_BRIDGEID]] + + groups = set(gateway.api.groups.keys()) + lights = set(gateway.api.lights.keys()) + scenes = set(gateway.api.scenes.keys()) + sensors = set(gateway.api.sensors.keys()) + + await gateway.api.async_load_parameters() + + gateway.async_add_device_callback( + "group", + [ + group + for group_id, group in gateway.api.groups.items() + if group_id not in groups + ], + ) + + gateway.async_add_device_callback( + "light", + [ + light + for light_id, light in gateway.api.lights.items() + if light_id not in lights + ], + ) + + gateway.async_add_device_callback( + "scene", + [ + scene + for scene_id, scene in gateway.api.scenes.items() + if scene_id not in scenes + ], + ) + + gateway.async_add_device_callback( + "sensor", + [ + sensor + for sensor_id, sensor in gateway.api.sensors.items() + if sensor_id not in sensors + ], + ) diff --git a/homeassistant/components/deconz/services.yaml b/homeassistant/components/deconz/services.yaml index 4d77101cf0d..bd5c2eb6a0c 100644 --- a/homeassistant/components/deconz/services.yaml +++ b/homeassistant/components/deconz/services.yaml @@ -1,5 +1,5 @@ configure: - description: Set attribute of device in deCONZ. See https://home-assistant.io/components/deconz/#device-services for details. + description: Set attribute of device in deCONZ. See https://home-assistant.io/integrations/deconz/#device-services for details. fields: entity: description: Entity id representing a specific device in deCONZ. diff --git a/homeassistant/components/deconz/strings.json b/homeassistant/components/deconz/strings.json index 00aa463349c..db43c022822 100644 --- a/homeassistant/components/deconz/strings.json +++ b/homeassistant/components/deconz/strings.json @@ -63,6 +63,7 @@ "remote_button_quadruple_press": "\"{subtype}\" button quadruple clicked", "remote_button_quintuple_press": "\"{subtype}\" button quintuple clicked", "remote_button_rotated": "Button rotated \"{subtype}\"", + "remote_button_rotation_stopped": "Button rotation \"{subtype}\" stopped", "remote_gyro_activated": "Device shaken" }, "trigger_subtype": { diff --git a/homeassistant/components/deconz/switch.py b/homeassistant/components/deconz/switch.py index b1fd4b10f46..1b51256580a 100644 --- a/homeassistant/components/deconz/switch.py +++ b/homeassistant/components/deconz/switch.py @@ -10,7 +10,6 @@ from .gateway import get_gateway_from_config_entry async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Old way of setting up deCONZ platforms.""" - pass async def async_setup_entry(hass, config_entry, async_add_entities): diff --git a/homeassistant/components/decora/manifest.json b/homeassistant/components/decora/manifest.json index 923a543e827..5142b5fb2e2 100644 --- a/homeassistant/components/decora/manifest.json +++ b/homeassistant/components/decora/manifest.json @@ -1,7 +1,7 @@ { "domain": "decora", "name": "Decora", - "documentation": "https://www.home-assistant.io/components/decora", + "documentation": "https://www.home-assistant.io/integrations/decora", "requirements": [ "bluepy==1.1.4", "decora==0.6" diff --git a/homeassistant/components/decora_wifi/manifest.json b/homeassistant/components/decora_wifi/manifest.json index 42ab6bfd6c1..14b8829fae8 100644 --- a/homeassistant/components/decora_wifi/manifest.json +++ b/homeassistant/components/decora_wifi/manifest.json @@ -1,7 +1,7 @@ { "domain": "decora_wifi", "name": "Decora wifi", - "documentation": "https://www.home-assistant.io/components/decora_wifi", + "documentation": "https://www.home-assistant.io/integrations/decora_wifi", "requirements": [ "decora_wifi==1.4" ], diff --git a/homeassistant/components/default_config/manifest.json b/homeassistant/components/default_config/manifest.json index 6969d9bba7e..a240599c420 100644 --- a/homeassistant/components/default_config/manifest.json +++ b/homeassistant/components/default_config/manifest.json @@ -1,7 +1,7 @@ { "domain": "default_config", "name": "Default config", - "documentation": "https://www.home-assistant.io/components/default_config", + "documentation": "https://www.home-assistant.io/integrations/default_config", "requirements": [], "dependencies": [ "automation", diff --git a/homeassistant/components/delijn/manifest.json b/homeassistant/components/delijn/manifest.json index 90e1a4e3b15..2d550a0851f 100644 --- a/homeassistant/components/delijn/manifest.json +++ b/homeassistant/components/delijn/manifest.json @@ -1,7 +1,7 @@ { "domain": "delijn", "name": "De Lijn", - "documentation": "https://www.home-assistant.io/components/delijn", + "documentation": "https://www.home-assistant.io/integrations/delijn", "dependencies": [], "codeowners": ["@bollewolle"], "requirements": ["pydelijn==0.5.1"] diff --git a/homeassistant/components/deluge/manifest.json b/homeassistant/components/deluge/manifest.json index d33a140cedb..b2eb3ada65f 100644 --- a/homeassistant/components/deluge/manifest.json +++ b/homeassistant/components/deluge/manifest.json @@ -1,7 +1,7 @@ { "domain": "deluge", "name": "Deluge", - "documentation": "https://www.home-assistant.io/components/deluge", + "documentation": "https://www.home-assistant.io/integrations/deluge", "requirements": [ "deluge-client==1.7.1" ], diff --git a/homeassistant/components/demo/manifest.json b/homeassistant/components/demo/manifest.json index 4f167ecae25..a4802c3b67b 100644 --- a/homeassistant/components/demo/manifest.json +++ b/homeassistant/components/demo/manifest.json @@ -1,7 +1,7 @@ { "domain": "demo", "name": "Demo", - "documentation": "https://www.home-assistant.io/components/demo", + "documentation": "https://www.home-assistant.io/integrations/demo", "requirements": [], "dependencies": [ "conversation", diff --git a/homeassistant/components/demo/weather.py b/homeassistant/components/demo/weather.py index b81a2193bb5..2253f261ad2 100644 --- a/homeassistant/components/demo/weather.py +++ b/homeassistant/components/demo/weather.py @@ -1,5 +1,5 @@ """Demo platform that offers fake meteorological data.""" -from datetime import datetime, timedelta +from datetime import timedelta from homeassistant.components.weather import ( ATTR_FORECAST_CONDITION, @@ -10,6 +10,7 @@ from homeassistant.components.weather import ( WeatherEntity, ) from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT +import homeassistant.util.dt as dt_util CONDITION_CLASSES = { "cloudy": [], @@ -147,7 +148,7 @@ class DemoWeather(WeatherEntity): @property def forecast(self): """Return the forecast.""" - reftime = datetime.now().replace(hour=16, minute=00) + reftime = dt_util.now().replace(hour=16, minute=00) forecast_data = [] for entry in self._forecast: diff --git a/homeassistant/components/denon/manifest.json b/homeassistant/components/denon/manifest.json index 2068b72fa9d..92b2aebab40 100644 --- a/homeassistant/components/denon/manifest.json +++ b/homeassistant/components/denon/manifest.json @@ -1,7 +1,7 @@ { "domain": "denon", "name": "Denon", - "documentation": "https://www.home-assistant.io/components/denon", + "documentation": "https://www.home-assistant.io/integrations/denon", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/denonavr/manifest.json b/homeassistant/components/denonavr/manifest.json index 34699d666ad..9e084c78e21 100644 --- a/homeassistant/components/denonavr/manifest.json +++ b/homeassistant/components/denonavr/manifest.json @@ -1,7 +1,7 @@ { "domain": "denonavr", "name": "Denonavr", - "documentation": "https://www.home-assistant.io/components/denonavr", + "documentation": "https://www.home-assistant.io/integrations/denonavr", "requirements": [ "denonavr==0.7.10" ], diff --git a/homeassistant/components/deutsche_bahn/manifest.json b/homeassistant/components/deutsche_bahn/manifest.json index 463c7d03fbb..9a2bf666016 100644 --- a/homeassistant/components/deutsche_bahn/manifest.json +++ b/homeassistant/components/deutsche_bahn/manifest.json @@ -1,7 +1,7 @@ { "domain": "deutsche_bahn", "name": "Deutsche bahn", - "documentation": "https://www.home-assistant.io/components/deutsche_bahn", + "documentation": "https://www.home-assistant.io/integrations/deutsche_bahn", "requirements": [ "schiene==0.23" ], diff --git a/homeassistant/components/device_automation/__init__.py b/homeassistant/components/device_automation/__init__.py index 9508dd9c849..fa6deac40ba 100644 --- a/homeassistant/components/device_automation/__init__.py +++ b/homeassistant/components/device_automation/__init__.py @@ -1,23 +1,53 @@ """Helpers for device automations.""" import asyncio import logging -from typing import Callable, cast +from typing import Any, List, MutableMapping +from types import ModuleType import voluptuous as vol +import voluptuous_serialize +from homeassistant.const import CONF_PLATFORM, CONF_DOMAIN, CONF_DEVICE_ID from homeassistant.components import websocket_api -from homeassistant.const import CONF_DOMAIN -from homeassistant.core import split_entity_id, HomeAssistant -import homeassistant.helpers.config_validation as cv +from homeassistant.core import HomeAssistant +from homeassistant.helpers import config_validation as cv from homeassistant.helpers.entity_registry import async_entries_for_device -from homeassistant.helpers.typing import ConfigType from homeassistant.loader import async_get_integration, IntegrationNotFound +from .exceptions import InvalidDeviceAutomationConfig + + +# mypy: allow-untyped-calls, allow-untyped-defs + DOMAIN = "device_automation" _LOGGER = logging.getLogger(__name__) +TRIGGER_BASE_SCHEMA = vol.Schema( + { + vol.Required(CONF_PLATFORM): "device", + vol.Required(CONF_DOMAIN): str, + vol.Required(CONF_DEVICE_ID): str, + } +) + +TYPES = { + # platform name, get automations function, get capabilities function + "trigger": ( + "device_trigger", + "async_get_triggers", + "async_get_trigger_capabilities", + ), + "condition": ( + "device_condition", + "async_get_conditions", + "async_get_condition_capabilities", + ), + "action": ("device_action", "async_get_actions", "async_get_action_capabilities"), +} + + async def async_setup(hass, config): """Set up device automation.""" hass.components.websocket_api.async_register_command( @@ -29,43 +59,50 @@ async def async_setup(hass, config): hass.components.websocket_api.async_register_command( websocket_device_automation_list_triggers ) + hass.components.websocket_api.async_register_command( + websocket_device_automation_get_trigger_capabilities + ) return True -async def async_device_condition_from_config( - hass: HomeAssistant, config: ConfigType, config_validation: bool = True -) -> Callable[..., bool]: - """Wrap action method with state based condition.""" - if config_validation: - config = cv.DEVICE_CONDITION_SCHEMA(config) - integration = await async_get_integration(hass, config[CONF_DOMAIN]) - platform = integration.get_platform("device_automation") - return cast( - Callable[..., bool], - platform.async_condition_from_config(config, config_validation), # type: ignore - ) +async def async_get_device_automation_platform( + hass: HomeAssistant, domain: str, automation_type: str +) -> ModuleType: + """Load device automation platform for integration. - -async def _async_get_device_automations_from_domain(hass, domain, fname, device_id): - """List device automations.""" - integration = None + Throws InvalidDeviceAutomationConfig if the integration is not found or does not support device automation. + """ + platform_name = TYPES[automation_type][0] try: integration = await async_get_integration(hass, domain) + platform = integration.get_platform(platform_name) except IntegrationNotFound: - _LOGGER.warning("Integration %s not found", domain) - return None - - try: - platform = integration.get_platform("device_automation") + raise InvalidDeviceAutomationConfig(f"Integration '{domain}' not found") except ImportError: - # The domain does not have device automations + raise InvalidDeviceAutomationConfig( + f"Integration '{domain}' does not support device automation {automation_type}s" + ) + + return platform + + +async def _async_get_device_automations_from_domain( + hass, domain, automation_type, device_id +): + """List device automations.""" + try: + platform = await async_get_device_automation_platform( + hass, domain, automation_type + ) + except InvalidDeviceAutomationConfig: return None - if hasattr(platform, fname): - return await getattr(platform, fname)(hass, device_id) + function_name = TYPES[automation_type][1] + + return await getattr(platform, function_name)(hass, device_id) -async def _async_get_device_automations(hass, fname, device_id): +async def _async_get_device_automations(hass, automation_type, device_id): """List device automations.""" device_registry, entity_registry = await asyncio.gather( hass.helpers.device_registry.async_get_registry(), @@ -73,19 +110,21 @@ async def _async_get_device_automations(hass, fname, device_id): ) domains = set() - automations = [] + automations: List[MutableMapping[str, Any]] = [] device = device_registry.async_get(device_id) for entry_id in device.config_entries: config_entry = hass.config_entries.async_get_entry(entry_id) domains.add(config_entry.domain) - entities = async_entries_for_device(entity_registry, device_id) - for entity in entities: - domains.add(split_entity_id(entity.entity_id)[0]) + entity_entries = async_entries_for_device(entity_registry, device_id) + for entity_entry in entity_entries: + domains.add(entity_entry.domain) device_automations = await asyncio.gather( *( - _async_get_device_automations_from_domain(hass, domain, fname, device_id) + _async_get_device_automations_from_domain( + hass, domain, automation_type, device_id + ) for domain in domains ) ) @@ -96,6 +135,35 @@ async def _async_get_device_automations(hass, fname, device_id): return automations +async def _async_get_device_automation_capabilities(hass, automation_type, automation): + """List device automations.""" + try: + platform = await async_get_device_automation_platform( + hass, automation[CONF_DOMAIN], automation_type + ) + except InvalidDeviceAutomationConfig: + return {} + + function_name = TYPES[automation_type][2] + + if not hasattr(platform, function_name): + # The device automation has no capabilities + return {} + + capabilities = await getattr(platform, function_name)(hass, automation) + capabilities = capabilities.copy() + + extra_fields = capabilities.get("extra_fields") + if extra_fields is None: + capabilities["extra_fields"] = [] + else: + capabilities["extra_fields"] = voluptuous_serialize.convert( + extra_fields, custom_serializer=cv.custom_serializer + ) + + return capabilities + + @websocket_api.async_response @websocket_api.websocket_command( { @@ -106,7 +174,7 @@ async def _async_get_device_automations(hass, fname, device_id): async def websocket_device_automation_list_actions(hass, connection, msg): """Handle request for device actions.""" device_id = msg["device_id"] - actions = await _async_get_device_automations(hass, "async_get_actions", device_id) + actions = await _async_get_device_automations(hass, "action", device_id) connection.send_result(msg["id"], actions) @@ -120,9 +188,7 @@ async def websocket_device_automation_list_actions(hass, connection, msg): async def websocket_device_automation_list_conditions(hass, connection, msg): """Handle request for device conditions.""" device_id = msg["device_id"] - conditions = await _async_get_device_automations( - hass, "async_get_conditions", device_id - ) + conditions = await _async_get_device_automations(hass, "condition", device_id) connection.send_result(msg["id"], conditions) @@ -136,7 +202,21 @@ async def websocket_device_automation_list_conditions(hass, connection, msg): async def websocket_device_automation_list_triggers(hass, connection, msg): """Handle request for device triggers.""" device_id = msg["device_id"] - triggers = await _async_get_device_automations( - hass, "async_get_triggers", device_id - ) + triggers = await _async_get_device_automations(hass, "trigger", device_id) connection.send_result(msg["id"], triggers) + + +@websocket_api.async_response +@websocket_api.websocket_command( + { + vol.Required("type"): "device_automation/trigger/capabilities", + vol.Required("trigger"): dict, + } +) +async def websocket_device_automation_get_trigger_capabilities(hass, connection, msg): + """Handle request for device trigger capabilities.""" + trigger = msg["trigger"] + capabilities = await _async_get_device_automation_capabilities( + hass, "trigger", trigger + ) + connection.send_result(msg["id"], capabilities) diff --git a/homeassistant/components/device_automation/manifest.json b/homeassistant/components/device_automation/manifest.json index a95e9c4f68f..50b1f9d357a 100644 --- a/homeassistant/components/device_automation/manifest.json +++ b/homeassistant/components/device_automation/manifest.json @@ -1,7 +1,7 @@ { "domain": "device_automation", "name": "Device automation", - "documentation": "https://www.home-assistant.io/components/device_automation", + "documentation": "https://www.home-assistant.io/integrations/device_automation", "requirements": [], "dependencies": [ "webhook" diff --git a/homeassistant/components/device_automation/toggle_entity.py b/homeassistant/components/device_automation/toggle_entity.py index 1593e70771a..c9588c1efa7 100644 --- a/homeassistant/components/device_automation/toggle_entity.py +++ b/homeassistant/components/device_automation/toggle_entity.py @@ -1,7 +1,9 @@ """Device automation helpers for toggle entity.""" +from typing import Any, Dict, List import voluptuous as vol -import homeassistant.components.automation.state as state +from homeassistant.core import Context, HomeAssistant, CALLBACK_TYPE +from homeassistant.components.automation import state, AutomationActionType from homeassistant.components.device_automation.const import ( CONF_IS_OFF, CONF_IS_ON, @@ -11,17 +13,20 @@ from homeassistant.components.device_automation.const import ( CONF_TURNED_OFF, CONF_TURNED_ON, ) -from homeassistant.core import split_entity_id from homeassistant.const import ( CONF_CONDITION, - CONF_DEVICE_ID, - CONF_DOMAIN, CONF_ENTITY_ID, + CONF_FOR, CONF_PLATFORM, CONF_TYPE, ) from homeassistant.helpers.entity_registry import async_entries_for_device from homeassistant.helpers import condition, config_validation as cv, service +from homeassistant.helpers.typing import ConfigType, TemplateVarsType +from . import TRIGGER_BASE_SCHEMA + + +# mypy: allow-untyped-calls, allow-untyped-defs ENTITY_ACTIONS = [ { @@ -64,43 +69,37 @@ ENTITY_TRIGGERS = [ }, ] -ACTION_SCHEMA = vol.Schema( +ACTION_SCHEMA = cv.DEVICE_ACTION_BASE_SCHEMA.extend( { - vol.Required(CONF_DEVICE_ID): str, - vol.Required(CONF_DOMAIN): str, vol.Required(CONF_ENTITY_ID): cv.entity_id, vol.Required(CONF_TYPE): vol.In([CONF_TOGGLE, CONF_TURN_OFF, CONF_TURN_ON]), } ) -CONDITION_SCHEMA = vol.Schema( +CONDITION_SCHEMA = cv.DEVICE_CONDITION_BASE_SCHEMA.extend( { - vol.Required(CONF_CONDITION): "device", - vol.Required(CONF_DEVICE_ID): str, - vol.Required(CONF_DOMAIN): str, vol.Required(CONF_ENTITY_ID): cv.entity_id, vol.Required(CONF_TYPE): vol.In([CONF_IS_OFF, CONF_IS_ON]), } ) -TRIGGER_SCHEMA = vol.Schema( +TRIGGER_SCHEMA = TRIGGER_BASE_SCHEMA.extend( { - vol.Required(CONF_PLATFORM): "device", - vol.Required(CONF_DEVICE_ID): str, - vol.Required(CONF_DOMAIN): str, vol.Required(CONF_ENTITY_ID): cv.entity_id, vol.Required(CONF_TYPE): vol.In([CONF_TURNED_OFF, CONF_TURNED_ON]), + vol.Optional(CONF_FOR): cv.positive_time_period_dict, } ) -def _is_domain(entity, domain): - return split_entity_id(entity.entity_id)[0] == domain - - -async def async_call_action_from_config(hass, config, variables, context, domain): +async def async_call_action_from_config( + hass: HomeAssistant, + config: ConfigType, + variables: TemplateVarsType, + context: Context, + domain: str, +) -> None: """Change state based on configuration.""" - config = ACTION_SCHEMA(config) action_type = config[CONF_TYPE] if action_type == CONF_TURN_ON: action = "turn_on" @@ -119,7 +118,9 @@ async def async_call_action_from_config(hass, config, variables, context, domain ) -def async_condition_from_config(config, config_validation): +def async_condition_from_config( + config: ConfigType, config_validation: bool +) -> condition.ConditionCheckerType: """Evaluate state based on configuration.""" condition_type = config[CONF_TYPE] if condition_type == CONF_IS_ON: @@ -135,7 +136,12 @@ def async_condition_from_config(config, config_validation): return condition.state_from_config(state_config, config_validation) -async def async_attach_trigger(hass, config, action, automation_info): +async def async_attach_trigger( + hass: HomeAssistant, + config: ConfigType, + action: AutomationActionType, + automation_info: dict, +) -> CALLBACK_TYPE: """Listen for state changes based on configuration.""" trigger_type = config[CONF_TYPE] if trigger_type == CONF_TURNED_ON: @@ -149,38 +155,68 @@ async def async_attach_trigger(hass, config, action, automation_info): state.CONF_FROM: from_state, state.CONF_TO: to_state, } + if CONF_FOR in config: + state_config[CONF_FOR] = config[CONF_FOR] - return await state.async_trigger(hass, state_config, action, automation_info) + return await state.async_attach_trigger( + hass, state_config, action, automation_info, platform_type="device" + ) -async def _async_get_automations(hass, device_id, automation_templates, domain): +async def _async_get_automations( + hass: HomeAssistant, device_id: str, automation_templates: List[dict], domain: str +) -> List[dict]: """List device automations.""" - automations = [] + automations: List[Dict[str, Any]] = [] entity_registry = await hass.helpers.entity_registry.async_get_registry() - entities = async_entries_for_device(entity_registry, device_id) - domain_entities = [x for x in entities if _is_domain(x, domain)] - for entity in domain_entities: - for automation in automation_templates: - automation = dict(automation) - automation.update( - device_id=device_id, entity_id=entity.entity_id, domain=domain + entries = [ + entry + for entry in async_entries_for_device(entity_registry, device_id) + if entry.domain == domain + ] + + for entry in entries: + automations.extend( + ( + { + **template, + "device_id": device_id, + "entity_id": entry.entity_id, + "domain": domain, + } + for template in automation_templates ) - automations.append(automation) + ) return automations -async def async_get_actions(hass, device_id, domain): +async def async_get_actions( + hass: HomeAssistant, device_id: str, domain: str +) -> List[dict]: """List device actions.""" return await _async_get_automations(hass, device_id, ENTITY_ACTIONS, domain) -async def async_get_conditions(hass, device_id, domain): +async def async_get_conditions( + hass: HomeAssistant, device_id: str, domain: str +) -> List[dict]: """List device conditions.""" return await _async_get_automations(hass, device_id, ENTITY_CONDITIONS, domain) -async def async_get_triggers(hass, device_id, domain): +async def async_get_triggers( + hass: HomeAssistant, device_id: str, domain: str +) -> List[dict]: """List device triggers.""" return await _async_get_automations(hass, device_id, ENTITY_TRIGGERS, domain) + + +async def async_get_trigger_capabilities(hass: HomeAssistant, trigger: dict) -> dict: + """List trigger capabilities.""" + return { + "extra_fields": vol.Schema( + {vol.Optional(CONF_FOR): cv.positive_time_period_dict} + ) + } diff --git a/homeassistant/components/device_sun_light_trigger/manifest.json b/homeassistant/components/device_sun_light_trigger/manifest.json index 40ab85bc1e5..3bea097b831 100644 --- a/homeassistant/components/device_sun_light_trigger/manifest.json +++ b/homeassistant/components/device_sun_light_trigger/manifest.json @@ -1,7 +1,7 @@ { "domain": "device_sun_light_trigger", "name": "Device sun light trigger", - "documentation": "https://www.home-assistant.io/components/device_sun_light_trigger", + "documentation": "https://www.home-assistant.io/integrations/device_sun_light_trigger", "requirements": [], "dependencies": [ "device_tracker", diff --git a/homeassistant/components/device_tracker/manifest.json b/homeassistant/components/device_tracker/manifest.json index 7b32f7845a6..0e1e0e8cd81 100644 --- a/homeassistant/components/device_tracker/manifest.json +++ b/homeassistant/components/device_tracker/manifest.json @@ -1,7 +1,7 @@ { "domain": "device_tracker", "name": "Device tracker", - "documentation": "https://www.home-assistant.io/components/device_tracker", + "documentation": "https://www.home-assistant.io/integrations/device_tracker", "requirements": [], "dependencies": [ "group", diff --git a/homeassistant/components/dht/manifest.json b/homeassistant/components/dht/manifest.json index 05889bdd326..e1d9273b797 100644 --- a/homeassistant/components/dht/manifest.json +++ b/homeassistant/components/dht/manifest.json @@ -1,7 +1,7 @@ { "domain": "dht", "name": "Dht", - "documentation": "https://www.home-assistant.io/components/dht", + "documentation": "https://www.home-assistant.io/integrations/dht", "requirements": [ "Adafruit-DHT==1.4.0" ], diff --git a/homeassistant/components/dialogflow/.translations/cs.json b/homeassistant/components/dialogflow/.translations/cs.json index 21da9b4823b..db41ee98e01 100644 --- a/homeassistant/components/dialogflow/.translations/cs.json +++ b/homeassistant/components/dialogflow/.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 [integraci Dialogflow]({dialogflow_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 Podrobn\u011bj\u0161\u00ed informace naleznete v [dokumentaci]({docs_url})." + "default": "Chcete-li odeslat ud\u00e1losti do aplikace Home Assistant, budete muset nastavit [integraci Dialogflow]({dialogflow_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 Podrobn\u011bj\u0161\u00ed informace naleznete v [dokumentaci]({docs_url})." }, "step": { "user": { diff --git a/homeassistant/components/dialogflow/.translations/es.json b/homeassistant/components/dialogflow/.translations/es.json index 1d6a849f3a8..c106543e158 100644 --- a/homeassistant/components/dialogflow/.translations/es.json +++ b/homeassistant/components/dialogflow/.translations/es.json @@ -5,7 +5,7 @@ "one_instance_allowed": "Solo una instancia es necesaria." }, "create_entry": { - "default": "Para enviar eventos a Home Assistant, necesitas configurar [Integracion de flujos de dialogo de webhook]({dialogflow_url}).\n\nRellena la siguiente informaci\u00f3n:\n\n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/json\n\nVer [Documentaci\u00f3n]({docs_url}) para mas detalles." + "default": "Para enviar eventos a Home Assistant, necesitas configurar [Integraci\u00f3n de flujos de dialogo de webhook]({dialogflow_url}).\n\nRellena la siguiente informaci\u00f3n:\n\n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/json\n\nVer [Documentaci\u00f3n]({docs_url}) para m\u00e1s detalles." }, "step": { "user": { diff --git a/homeassistant/components/dialogflow/.translations/fr.json b/homeassistant/components/dialogflow/.translations/fr.json index e9eabeff638..0be75b94be9 100644 --- a/homeassistant/components/dialogflow/.translations/fr.json +++ b/homeassistant/components/dialogflow/.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] ( {dialogflow_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 Dialogflow] ( {dialogflow_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/dialogflow/.translations/nn.json b/homeassistant/components/dialogflow/.translations/nn.json new file mode 100644 index 00000000000..5a96b853eb0 --- /dev/null +++ b/homeassistant/components/dialogflow/.translations/nn.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Dialogflow" + } +} \ No newline at end of file diff --git a/homeassistant/components/dialogflow/.translations/no.json b/homeassistant/components/dialogflow/.translations/no.json index e27d59a40e3..4d23ac8aaba 100644 --- a/homeassistant/components/dialogflow/.translations/no.json +++ b/homeassistant/components/dialogflow/.translations/no.json @@ -2,7 +2,7 @@ "config": { "abort": { "not_internet_accessible": "Din Home Assistant forekomst m\u00e5 v\u00e6re tilgjengelig fra internett for \u00e5 kunne motta Dialogflow meldinger.", - "one_instance_allowed": "Kun en enkelt forekomst er n\u00f8dvendig." + "one_instance_allowed": "Kun en forekomst er n\u00f8dvendig." }, "create_entry": { "default": "For \u00e5 sende hendelser til Home Assistant, m\u00e5 du sette opp [webhook integrasjon av Dialogflow]({dialogflow_url}). \n\nFyll ut f\u00f8lgende informasjon: \n\n- URL: `{webhook_url}` \n- Metode: POST\n- Innholdstype: application/json\n\nSe [dokumentasjonen]({docs_url}) for ytterligere detaljer." diff --git a/homeassistant/components/dialogflow/.translations/sl.json b/homeassistant/components/dialogflow/.translations/sl.json index 18a476b6870..b6bd30f7997 100644 --- a/homeassistant/components/dialogflow/.translations/sl.json +++ b/homeassistant/components/dialogflow/.translations/sl.json @@ -5,7 +5,7 @@ "one_instance_allowed": "Potrebna je samo ena instanca." }, "create_entry": { - "default": "Za po\u0161iljanje dogodkov Home Assistant-u, boste morali nastaviti [webhook z dialogflow]({twilio_url}).\n\nIzpolnite naslednje informacije:\n\n- URL: `{webhook_url}`\n- Metoda: POST\n- Vrsta vsebine: application/x-www-form-urlencoded\n\nGlej [dokumentacijo]({docs_url}) za nadaljna navodila." + "default": "Za po\u0161iljanje dogodkov Home Assistant-u, boste morali nastaviti [webhook z Dialogflow]({twilio_url}).\n\nIzpolnite naslednje informacije:\n\n- URL: `{webhook_url}`\n- Metoda: POST\n- Vrsta vsebine: application/x-www-form-urlencoded\n\nGlej [dokumentacijo]({docs_url}) za nadaljna navodila." }, "step": { "user": { diff --git a/homeassistant/components/dialogflow/.translations/zh-Hant.json b/homeassistant/components/dialogflow/.translations/zh-Hant.json index 18d3d92e16b..3cb54145ad8 100644 --- a/homeassistant/components/dialogflow/.translations/zh-Hant.json +++ b/homeassistant/components/dialogflow/.translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "not_internet_accessible": "Home Assistant \u5be6\u4f8b\u5fc5\u9808\u80fd\u5920\u7531\u7db2\u969b\u7db2\u8def\u5b58\u53d6\uff0c\u65b9\u80fd\u63a5\u53d7 Dialogflow \u8a0a\u606f\u3002", + "not_internet_accessible": "Home Assistant \u7269\u4ef6\u5fc5\u9808\u80fd\u5920\u7531\u7db2\u969b\u7db2\u8def\u5b58\u53d6\uff0c\u65b9\u80fd\u63a5\u53d7 Dialogflow \u8a0a\u606f\u3002", "one_instance_allowed": "\u50c5\u9700\u8a2d\u5b9a\u4e00\u7d44\u7269\u4ef6\u5373\u53ef\u3002" }, "create_entry": { diff --git a/homeassistant/components/dialogflow/config_flow.py b/homeassistant/components/dialogflow/config_flow.py index 8e5be873f72..4f785392ffc 100644 --- a/homeassistant/components/dialogflow/config_flow.py +++ b/homeassistant/components/dialogflow/config_flow.py @@ -8,6 +8,6 @@ config_entry_flow.register_webhook_flow( "Dialogflow Webhook", { "dialogflow_url": "https://dialogflow.com/docs/fulfillment#webhook", - "docs_url": "https://www.home-assistant.io/components/dialogflow/", + "docs_url": "https://www.home-assistant.io/integrations/dialogflow/", }, ) diff --git a/homeassistant/components/dialogflow/manifest.json b/homeassistant/components/dialogflow/manifest.json index aa8b584aeca..d9e821c82fd 100644 --- a/homeassistant/components/dialogflow/manifest.json +++ b/homeassistant/components/dialogflow/manifest.json @@ -2,7 +2,7 @@ "domain": "dialogflow", "name": "Dialogflow", "config_flow": true, - "documentation": "https://www.home-assistant.io/components/dialogflow", + "documentation": "https://www.home-assistant.io/integrations/dialogflow", "requirements": [], "dependencies": [ "webhook" diff --git a/homeassistant/components/digital_ocean/manifest.json b/homeassistant/components/digital_ocean/manifest.json index 2ef940f60bd..eb19a5c3a85 100644 --- a/homeassistant/components/digital_ocean/manifest.json +++ b/homeassistant/components/digital_ocean/manifest.json @@ -1,7 +1,7 @@ { "domain": "digital_ocean", "name": "Digital ocean", - "documentation": "https://www.home-assistant.io/components/digital_ocean", + "documentation": "https://www.home-assistant.io/integrations/digital_ocean", "requirements": [ "python-digitalocean==1.13.2" ], diff --git a/homeassistant/components/digitalloggers/manifest.json b/homeassistant/components/digitalloggers/manifest.json index 990b39b21a5..4c58e090a95 100644 --- a/homeassistant/components/digitalloggers/manifest.json +++ b/homeassistant/components/digitalloggers/manifest.json @@ -1,7 +1,7 @@ { "domain": "digitalloggers", "name": "Digitalloggers", - "documentation": "https://www.home-assistant.io/components/digitalloggers", + "documentation": "https://www.home-assistant.io/integrations/digitalloggers", "requirements": [ "dlipower==0.7.165" ], diff --git a/homeassistant/components/directv/manifest.json b/homeassistant/components/directv/manifest.json index 7dbe6122ac1..8b65d7a680b 100644 --- a/homeassistant/components/directv/manifest.json +++ b/homeassistant/components/directv/manifest.json @@ -1,7 +1,7 @@ { "domain": "directv", "name": "Directv", - "documentation": "https://www.home-assistant.io/components/directv", + "documentation": "https://www.home-assistant.io/integrations/directv", "requirements": [ "directpy==0.5" ], diff --git a/homeassistant/components/discogs/manifest.json b/homeassistant/components/discogs/manifest.json index ca304bce88b..18282f07781 100644 --- a/homeassistant/components/discogs/manifest.json +++ b/homeassistant/components/discogs/manifest.json @@ -1,7 +1,7 @@ { "domain": "discogs", "name": "Discogs", - "documentation": "https://www.home-assistant.io/components/discogs", + "documentation": "https://www.home-assistant.io/integrations/discogs", "requirements": [ "discogs_client==2.2.1" ], diff --git a/homeassistant/components/discord/manifest.json b/homeassistant/components/discord/manifest.json index 31940c28c4e..50c03bad25d 100644 --- a/homeassistant/components/discord/manifest.json +++ b/homeassistant/components/discord/manifest.json @@ -1,7 +1,7 @@ { "domain": "discord", "name": "Discord", - "documentation": "https://www.home-assistant.io/components/discord", + "documentation": "https://www.home-assistant.io/integrations/discord", "requirements": [ "discord.py==1.2.3" ], diff --git a/homeassistant/components/discovery/__init__.py b/homeassistant/components/discovery/__init__.py index 827e05a424b..15fcfc15338 100644 --- a/homeassistant/components/discovery/__init__.py +++ b/homeassistant/components/discovery/__init__.py @@ -50,6 +50,7 @@ CONFIG_ENTRY_HANDLERS = { SERVICE_DAIKIN: "daikin", SERVICE_TELLDUSLIVE: "tellduslive", SERVICE_IGD: "upnp", + SERVICE_PLEX: "plex", } SERVICE_HANDLERS = { @@ -69,7 +70,6 @@ SERVICE_HANDLERS = { SERVICE_FREEBOX: ("freebox", None), SERVICE_YEELIGHT: ("yeelight", None), "panasonic_viera": ("media_player", "panasonic_viera"), - SERVICE_PLEX: ("plex", None), "yamaha": ("media_player", "yamaha"), "logitech_mediaserver": ("media_player", "squeezebox"), "directv": ("media_player", "directv"), diff --git a/homeassistant/components/discovery/manifest.json b/homeassistant/components/discovery/manifest.json index 845e1af15d4..56d968189cf 100644 --- a/homeassistant/components/discovery/manifest.json +++ b/homeassistant/components/discovery/manifest.json @@ -1,7 +1,7 @@ { "domain": "discovery", "name": "Discovery", - "documentation": "https://www.home-assistant.io/components/discovery", + "documentation": "https://www.home-assistant.io/integrations/discovery", "requirements": [ "netdisco==2.6.0" ], diff --git a/homeassistant/components/dlib_face_detect/manifest.json b/homeassistant/components/dlib_face_detect/manifest.json index c2ede62ee5b..431afc080f4 100644 --- a/homeassistant/components/dlib_face_detect/manifest.json +++ b/homeassistant/components/dlib_face_detect/manifest.json @@ -1,7 +1,7 @@ { "domain": "dlib_face_detect", "name": "Dlib face detect", - "documentation": "https://www.home-assistant.io/components/dlib_face_detect", + "documentation": "https://www.home-assistant.io/integrations/dlib_face_detect", "requirements": [ "face_recognition==1.2.3" ], diff --git a/homeassistant/components/dlib_face_identify/manifest.json b/homeassistant/components/dlib_face_identify/manifest.json index 388017f78bb..2f764ae2817 100644 --- a/homeassistant/components/dlib_face_identify/manifest.json +++ b/homeassistant/components/dlib_face_identify/manifest.json @@ -1,7 +1,7 @@ { "domain": "dlib_face_identify", "name": "Dlib face identify", - "documentation": "https://www.home-assistant.io/components/dlib_face_identify", + "documentation": "https://www.home-assistant.io/integrations/dlib_face_identify", "requirements": [ "face_recognition==1.2.3" ], diff --git a/homeassistant/components/dlink/manifest.json b/homeassistant/components/dlink/manifest.json index 8f7d07eb0db..b0b8c60121a 100644 --- a/homeassistant/components/dlink/manifest.json +++ b/homeassistant/components/dlink/manifest.json @@ -1,7 +1,7 @@ { "domain": "dlink", "name": "Dlink", - "documentation": "https://www.home-assistant.io/components/dlink", + "documentation": "https://www.home-assistant.io/integrations/dlink", "requirements": [ "pyW215==0.6.0" ], diff --git a/homeassistant/components/dlna_dmr/manifest.json b/homeassistant/components/dlna_dmr/manifest.json index bf05d5c7f63..008a1293e41 100644 --- a/homeassistant/components/dlna_dmr/manifest.json +++ b/homeassistant/components/dlna_dmr/manifest.json @@ -1,7 +1,7 @@ { "domain": "dlna_dmr", "name": "Dlna dmr", - "documentation": "https://www.home-assistant.io/components/dlna_dmr", + "documentation": "https://www.home-assistant.io/integrations/dlna_dmr", "requirements": [ "async-upnp-client==0.14.11" ], diff --git a/homeassistant/components/dlna_dmr/media_player.py b/homeassistant/components/dlna_dmr/media_player.py index c7c488950cc..5dd7ab7a88a 100644 --- a/homeassistant/components/dlna_dmr/media_player.py +++ b/homeassistant/components/dlna_dmr/media_player.py @@ -1,6 +1,5 @@ """Support for DLNA DMR (Device Media Renderer).""" import asyncio -from datetime import datetime from datetime import timedelta import functools import logging @@ -43,6 +42,7 @@ from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.typing import HomeAssistantType import homeassistant.helpers.config_validation as cv +import homeassistant.util.dt as dt_util from homeassistant.util import get_local_ip _LOGGER = logging.getLogger(__name__) @@ -241,14 +241,14 @@ class DlnaDmrDevice(MediaPlayerDevice): return # do we need to (re-)subscribe? - now = datetime.now() + now = dt_util.utcnow() should_renew = ( self._subscription_renew_time and now >= self._subscription_renew_time ) if should_renew or not was_available and self._available: try: timeout = await self._device.async_subscribe_services() - self._subscription_renew_time = datetime.now() + timeout / 2 + self._subscription_renew_time = dt_util.utcnow() + timeout / 2 except (asyncio.TimeoutError, aiohttp.ClientError): self._available = False _LOGGER.debug("Could not (re)subscribe") diff --git a/homeassistant/components/dnsip/manifest.json b/homeassistant/components/dnsip/manifest.json index 544ac9b0fba..4f3d84da476 100644 --- a/homeassistant/components/dnsip/manifest.json +++ b/homeassistant/components/dnsip/manifest.json @@ -1,7 +1,7 @@ { "domain": "dnsip", "name": "Dnsip", - "documentation": "https://www.home-assistant.io/components/dnsip", + "documentation": "https://www.home-assistant.io/integrations/dnsip", "requirements": [ "aiodns==2.0.0" ], diff --git a/homeassistant/components/dominos/manifest.json b/homeassistant/components/dominos/manifest.json index f8d13b49f93..933af93a77a 100644 --- a/homeassistant/components/dominos/manifest.json +++ b/homeassistant/components/dominos/manifest.json @@ -1,7 +1,7 @@ { "domain": "dominos", "name": "Dominos", - "documentation": "https://www.home-assistant.io/components/dominos", + "documentation": "https://www.home-assistant.io/integrations/dominos", "requirements": [ "pizzapi==0.0.3" ], diff --git a/homeassistant/components/doods/__init__.py b/homeassistant/components/doods/__init__.py new file mode 100644 index 00000000000..b6edb9be87b --- /dev/null +++ b/homeassistant/components/doods/__init__.py @@ -0,0 +1 @@ +"""The doods component.""" diff --git a/homeassistant/components/doods/image_processing.py b/homeassistant/components/doods/image_processing.py new file mode 100644 index 00000000000..3eec85b3e53 --- /dev/null +++ b/homeassistant/components/doods/image_processing.py @@ -0,0 +1,346 @@ +"""Support for the DOODS service.""" +import io +import logging +import time + +import voluptuous as vol +from PIL import Image, ImageDraw +from pydoods import PyDOODS + +from homeassistant.components.image_processing import ( + CONF_CONFIDENCE, + CONF_ENTITY_ID, + CONF_NAME, + CONF_SOURCE, + PLATFORM_SCHEMA, + ImageProcessingEntity, + draw_box, +) +from homeassistant.core import split_entity_id +from homeassistant.helpers import template +import homeassistant.helpers.config_validation as cv + +_LOGGER = logging.getLogger(__name__) + +ATTR_MATCHES = "matches" +ATTR_SUMMARY = "summary" +ATTR_TOTAL_MATCHES = "total_matches" + +CONF_URL = "url" +CONF_AUTH_KEY = "auth_key" +CONF_DETECTOR = "detector" +CONF_LABELS = "labels" +CONF_AREA = "area" +CONF_TOP = "top" +CONF_BOTTOM = "bottom" +CONF_RIGHT = "right" +CONF_LEFT = "left" +CONF_FILE_OUT = "file_out" + +AREA_SCHEMA = vol.Schema( + { + vol.Optional(CONF_BOTTOM, default=1): cv.small_float, + vol.Optional(CONF_LEFT, default=0): cv.small_float, + vol.Optional(CONF_RIGHT, default=1): cv.small_float, + vol.Optional(CONF_TOP, default=0): cv.small_float, + } +) + +LABEL_SCHEMA = vol.Schema( + { + vol.Required(CONF_NAME): cv.string, + vol.Optional(CONF_AREA): AREA_SCHEMA, + vol.Optional(CONF_CONFIDENCE, default=0.0): vol.Range(min=0, max=100), + } +) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_URL): cv.string, + vol.Required(CONF_DETECTOR): cv.string, + vol.Optional(CONF_AUTH_KEY, default=""): cv.string, + vol.Optional(CONF_FILE_OUT, default=[]): vol.All(cv.ensure_list, [cv.template]), + vol.Optional(CONF_CONFIDENCE, default=0.0): vol.Range(min=0, max=100), + vol.Optional(CONF_LABELS, default=[]): vol.All( + cv.ensure_list, [vol.Any(cv.string, LABEL_SCHEMA)] + ), + vol.Optional(CONF_AREA): AREA_SCHEMA, + } +) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the Doods client.""" + url = config[CONF_URL] + auth_key = config[CONF_AUTH_KEY] + detector_name = config[CONF_DETECTOR] + + doods = PyDOODS(url, auth_key) + response = doods.get_detectors() + if not isinstance(response, dict): + _LOGGER.warning("Could not connect to doods server: %s", url) + return + + detector = {} + for server_detector in response["detectors"]: + if server_detector["name"] == detector_name: + detector = server_detector + break + + if not detector: + _LOGGER.warning( + "Detector %s is not supported by doods server %s", detector_name, url + ) + return + + entities = [] + for camera in config[CONF_SOURCE]: + entities.append( + Doods( + hass, + camera[CONF_ENTITY_ID], + camera.get(CONF_NAME), + doods, + detector, + config, + ) + ) + add_entities(entities) + + +class Doods(ImageProcessingEntity): + """Doods image processing service client.""" + + def __init__(self, hass, camera_entity, name, doods, detector, config): + """Initialize the DOODS entity.""" + self.hass = hass + self._camera_entity = camera_entity + if name: + self._name = name + else: + name = split_entity_id(camera_entity)[1] + self._name = f"Doods {name}" + self._doods = doods + self._file_out = config[CONF_FILE_OUT] + self._detector_name = detector["name"] + + # detector config and aspect ratio + self._width = None + self._height = None + self._aspect = None + if detector["width"] and detector["height"]: + self._width = detector["width"] + self._height = detector["height"] + self._aspect = self._width / self._height + + # the base confidence + dconfig = {} + confidence = config[CONF_CONFIDENCE] + + # handle labels and specific detection areas + labels = config[CONF_LABELS] + self._label_areas = {} + for label in labels: + if isinstance(label, dict): + label_name = label[CONF_NAME] + if label_name not in detector["labels"] and label_name != "*": + _LOGGER.warning("Detector does not support label %s", label_name) + continue + + # Label Confidence + label_confidence = label[CONF_CONFIDENCE] + if label_name not in dconfig or dconfig[label_name] > label_confidence: + dconfig[label_name] = label_confidence + + # Label area + label_area = label.get(CONF_AREA) + self._label_areas[label_name] = [0, 0, 1, 1] + if label_area: + self._label_areas[label_name] = [ + label_area[CONF_TOP], + label_area[CONF_LEFT], + label_area[CONF_BOTTOM], + label_area[CONF_RIGHT], + ] + else: + if label not in detector["labels"] and label != "*": + _LOGGER.warning("Detector does not support label %s", label) + continue + self._label_areas[label] = [0, 0, 1, 1] + if label not in dconfig or dconfig[label] > confidence: + dconfig[label] = confidence + + if not dconfig: + dconfig["*"] = confidence + + # Handle global detection area + self._area = [0, 0, 1, 1] + area_config = config.get(CONF_AREA) + if area_config: + self._area = [ + area_config[CONF_TOP], + area_config[CONF_LEFT], + area_config[CONF_BOTTOM], + area_config[CONF_RIGHT], + ] + + template.attach(hass, self._file_out) + + self._dconfig = dconfig + self._matches = {} + self._total_matches = 0 + self._last_image = None + + @property + def camera_entity(self): + """Return camera entity id from process pictures.""" + return self._camera_entity + + @property + def name(self): + """Return the name of the image processor.""" + return self._name + + @property + def state(self): + """Return the state of the entity.""" + return self._total_matches + + @property + def device_state_attributes(self): + """Return device specific state attributes.""" + return { + ATTR_MATCHES: self._matches, + ATTR_SUMMARY: { + label: len(values) for label, values in self._matches.items() + }, + ATTR_TOTAL_MATCHES: self._total_matches, + } + + def _save_image(self, image, matches, paths): + img = Image.open(io.BytesIO(bytearray(image))).convert("RGB") + img_width, img_height = img.size + draw = ImageDraw.Draw(img) + + # Draw custom global region/area + if self._area != [0, 0, 1, 1]: + draw_box( + draw, self._area, img_width, img_height, "Detection Area", (0, 255, 255) + ) + + for label, values in matches.items(): + + # Draw custom label regions/areas + if label in self._label_areas and self._label_areas[label] != [0, 0, 1, 1]: + box_label = f"{label.capitalize()} Detection Area" + draw_box( + draw, + self._label_areas[label], + img_width, + img_height, + box_label, + (0, 255, 0), + ) + + # Draw detected objects + for instance in values: + box_label = f'{label} {instance["score"]:.1f}%' + # Already scaled, use 1 for width and height + draw_box( + draw, + instance["box"], + img_width, + img_height, + box_label, + (255, 255, 0), + ) + + for path in paths: + _LOGGER.info("Saving results image to %s", path) + img.save(path) + + def process_image(self, image): + """Process the image.""" + img = Image.open(io.BytesIO(bytearray(image))) + img_width, img_height = img.size + + if self._aspect and abs((img_width / img_height) - self._aspect) > 0.1: + _LOGGER.debug( + "The image aspect: %s and the detector aspect: %s differ by more than 0.1", + (img_width / img_height), + self._aspect, + ) + + # Run detection + start = time.time() + response = self._doods.detect( + image, dconfig=self._dconfig, detector_name=self._detector_name + ) + _LOGGER.debug( + "doods detect: %s response: %s duration: %s", + self._dconfig, + response, + time.time() - start, + ) + + matches = {} + total_matches = 0 + + if not response or "error" in response: + if "error" in response: + _LOGGER.error(response["error"]) + self._matches = matches + self._total_matches = total_matches + return + + for detection in response["detections"]: + score = detection["confidence"] + boxes = [ + detection["top"], + detection["left"], + detection["bottom"], + detection["right"], + ] + label = detection["label"] + + # Exclude unlisted labels + if "*" not in self._dconfig and label not in self._dconfig: + continue + + # Exclude matches outside global area definition + if ( + boxes[0] < self._area[0] + or boxes[1] < self._area[1] + or boxes[2] > self._area[2] + or boxes[3] > self._area[3] + ): + continue + + # Exclude matches outside label specific area definition + if self._label_areas and ( + boxes[0] < self._label_areas[label][0] + or boxes[1] < self._label_areas[label][1] + or boxes[2] > self._label_areas[label][2] + or boxes[3] > self._label_areas[label][3] + ): + continue + + if label not in matches: + matches[label] = [] + matches[label].append({"score": float(score), "box": boxes}) + total_matches += 1 + + # Save Images + if total_matches and self._file_out: + paths = [] + for path_template in self._file_out: + if isinstance(path_template, template.Template): + paths.append( + path_template.render(camera_entity=self._camera_entity) + ) + else: + paths.append(path_template) + self._save_image(image, matches, paths) + + self._matches = matches + self._total_matches = total_matches diff --git a/homeassistant/components/doods/manifest.json b/homeassistant/components/doods/manifest.json new file mode 100644 index 00000000000..e0dcb48527f --- /dev/null +++ b/homeassistant/components/doods/manifest.json @@ -0,0 +1,10 @@ +{ + "domain": "doods", + "name": "DOODS - Distributed Outside Object Detection Service", + "documentation": "https://www.home-assistant.io/integrations/doods", + "requirements": [ + "pydoods==1.0.2" + ], + "dependencies": [], + "codeowners": [] +} diff --git a/homeassistant/components/doorbird/camera.py b/homeassistant/components/doorbird/camera.py index eaae3f19236..457c319d9e1 100644 --- a/homeassistant/components/doorbird/camera.py +++ b/homeassistant/components/doorbird/camera.py @@ -8,6 +8,7 @@ import async_timeout from homeassistant.components.camera import Camera, SUPPORT_STREAM from homeassistant.helpers.aiohttp_client import async_get_clientsession +import homeassistant.util.dt as dt_util from . import DOMAIN as DOORBIRD_DOMAIN @@ -77,7 +78,7 @@ class DoorBirdCamera(Camera): async def async_camera_image(self): """Pull a still image from the camera.""" - now = datetime.datetime.now() + now = dt_util.utcnow() if self._last_image and now - self._last_update < self._interval: return self._last_image diff --git a/homeassistant/components/doorbird/manifest.json b/homeassistant/components/doorbird/manifest.json index 3fb9fdc753b..c9cdb32e18a 100644 --- a/homeassistant/components/doorbird/manifest.json +++ b/homeassistant/components/doorbird/manifest.json @@ -1,7 +1,7 @@ { "domain": "doorbird", "name": "Doorbird", - "documentation": "https://www.home-assistant.io/components/doorbird", + "documentation": "https://www.home-assistant.io/integrations/doorbird", "requirements": [ "doorbirdpy==2.0.8" ], diff --git a/homeassistant/components/doorbird/switch.py b/homeassistant/components/doorbird/switch.py index 643e006dfef..7a0dfa82e76 100644 --- a/homeassistant/components/doorbird/switch.py +++ b/homeassistant/components/doorbird/switch.py @@ -3,6 +3,7 @@ import datetime import logging from homeassistant.components.switch import SwitchDevice +import homeassistant.util.dt as dt_util from . import DOMAIN as DOORBIRD_DOMAIN @@ -66,7 +67,7 @@ class DoorBirdSwitch(SwitchDevice): else: self._state = self._doorstation.device.energize_relay(self._relay) - now = datetime.datetime.now() + now = dt_util.utcnow() self._assume_off = now + self._time def turn_off(self, **kwargs): @@ -75,6 +76,6 @@ class DoorBirdSwitch(SwitchDevice): def update(self): """Wait for the correct amount of assumed time to pass.""" - if self._state and self._assume_off <= datetime.datetime.now(): + if self._state and self._assume_off <= dt_util.utcnow(): self._state = False self._assume_off = datetime.datetime.min diff --git a/homeassistant/components/dovado/manifest.json b/homeassistant/components/dovado/manifest.json index 122d774c268..ac0044c7a89 100644 --- a/homeassistant/components/dovado/manifest.json +++ b/homeassistant/components/dovado/manifest.json @@ -1,7 +1,7 @@ { "domain": "dovado", "name": "Dovado", - "documentation": "https://www.home-assistant.io/components/dovado", + "documentation": "https://www.home-assistant.io/integrations/dovado", "requirements": [ "dovado==0.4.1" ], diff --git a/homeassistant/components/downloader/manifest.json b/homeassistant/components/downloader/manifest.json index 514509c004d..241dadddf4e 100644 --- a/homeassistant/components/downloader/manifest.json +++ b/homeassistant/components/downloader/manifest.json @@ -1,7 +1,7 @@ { "domain": "downloader", "name": "Downloader", - "documentation": "https://www.home-assistant.io/components/downloader", + "documentation": "https://www.home-assistant.io/integrations/downloader", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/dsmr/manifest.json b/homeassistant/components/dsmr/manifest.json index 21c98d56d1d..250adab263b 100644 --- a/homeassistant/components/dsmr/manifest.json +++ b/homeassistant/components/dsmr/manifest.json @@ -1,7 +1,7 @@ { "domain": "dsmr", "name": "Dsmr", - "documentation": "https://www.home-assistant.io/components/dsmr", + "documentation": "https://www.home-assistant.io/integrations/dsmr", "requirements": [ "dsmr_parser==0.12" ], diff --git a/homeassistant/components/dte_energy_bridge/manifest.json b/homeassistant/components/dte_energy_bridge/manifest.json index fbf7a00f8e6..f3ccbdeebb2 100644 --- a/homeassistant/components/dte_energy_bridge/manifest.json +++ b/homeassistant/components/dte_energy_bridge/manifest.json @@ -1,7 +1,7 @@ { "domain": "dte_energy_bridge", "name": "Dte energy bridge", - "documentation": "https://www.home-assistant.io/components/dte_energy_bridge", + "documentation": "https://www.home-assistant.io/integrations/dte_energy_bridge", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/dublin_bus_transport/manifest.json b/homeassistant/components/dublin_bus_transport/manifest.json index fc13fddc936..3e377f3a2ea 100644 --- a/homeassistant/components/dublin_bus_transport/manifest.json +++ b/homeassistant/components/dublin_bus_transport/manifest.json @@ -1,7 +1,7 @@ { "domain": "dublin_bus_transport", "name": "Dublin bus transport", - "documentation": "https://www.home-assistant.io/components/dublin_bus_transport", + "documentation": "https://www.home-assistant.io/integrations/dublin_bus_transport", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/duckdns/manifest.json b/homeassistant/components/duckdns/manifest.json index ed38d35346f..9e0ad6c001c 100644 --- a/homeassistant/components/duckdns/manifest.json +++ b/homeassistant/components/duckdns/manifest.json @@ -1,7 +1,7 @@ { "domain": "duckdns", "name": "Duckdns", - "documentation": "https://www.home-assistant.io/components/duckdns", + "documentation": "https://www.home-assistant.io/integrations/duckdns", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/duke_energy/manifest.json b/homeassistant/components/duke_energy/manifest.json index 602dfec801f..131063ad81f 100644 --- a/homeassistant/components/duke_energy/manifest.json +++ b/homeassistant/components/duke_energy/manifest.json @@ -1,7 +1,7 @@ { "domain": "duke_energy", "name": "Duke energy", - "documentation": "https://www.home-assistant.io/components/duke_energy", + "documentation": "https://www.home-assistant.io/integrations/duke_energy", "requirements": [ "pydukeenergy==0.0.6" ], diff --git a/homeassistant/components/dunehd/manifest.json b/homeassistant/components/dunehd/manifest.json index 35e6c4a2449..47250b32cbc 100644 --- a/homeassistant/components/dunehd/manifest.json +++ b/homeassistant/components/dunehd/manifest.json @@ -1,7 +1,7 @@ { "domain": "dunehd", "name": "Dunehd", - "documentation": "https://www.home-assistant.io/components/dunehd", + "documentation": "https://www.home-assistant.io/integrations/dunehd", "requirements": [ "pdunehd==1.3" ], diff --git a/homeassistant/components/dwd_weather_warnings/manifest.json b/homeassistant/components/dwd_weather_warnings/manifest.json index a2b21a9e0bf..a35fcbcdf68 100644 --- a/homeassistant/components/dwd_weather_warnings/manifest.json +++ b/homeassistant/components/dwd_weather_warnings/manifest.json @@ -1,7 +1,7 @@ { "domain": "dwd_weather_warnings", "name": "Dwd weather warnings", - "documentation": "https://www.home-assistant.io/components/dwd_weather_warnings", + "documentation": "https://www.home-assistant.io/integrations/dwd_weather_warnings", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/dweet/manifest.json b/homeassistant/components/dweet/manifest.json index e0a00620210..75d8cfb6054 100644 --- a/homeassistant/components/dweet/manifest.json +++ b/homeassistant/components/dweet/manifest.json @@ -1,7 +1,7 @@ { "domain": "dweet", "name": "Dweet", - "documentation": "https://www.home-assistant.io/components/dweet", + "documentation": "https://www.home-assistant.io/integrations/dweet", "requirements": [ "dweepy==0.3.0" ], diff --git a/homeassistant/components/dyson/manifest.json b/homeassistant/components/dyson/manifest.json index 7b956dd96c8..92940c8c1e1 100644 --- a/homeassistant/components/dyson/manifest.json +++ b/homeassistant/components/dyson/manifest.json @@ -1,7 +1,7 @@ { "domain": "dyson", "name": "Dyson", - "documentation": "https://www.home-assistant.io/components/dyson", + "documentation": "https://www.home-assistant.io/integrations/dyson", "requirements": [ "libpurecool==0.5.0" ], diff --git a/homeassistant/components/ebox/manifest.json b/homeassistant/components/ebox/manifest.json index 16b033df8fd..d32206da726 100644 --- a/homeassistant/components/ebox/manifest.json +++ b/homeassistant/components/ebox/manifest.json @@ -1,7 +1,7 @@ { "domain": "ebox", "name": "Ebox", - "documentation": "https://www.home-assistant.io/components/ebox", + "documentation": "https://www.home-assistant.io/integrations/ebox", "requirements": [ "pyebox==1.1.4" ], diff --git a/homeassistant/components/ebusd/const.py b/homeassistant/components/ebusd/const.py index 7587d0cd42d..db79d81736e 100644 --- a/homeassistant/components/ebusd/const.py +++ b/homeassistant/components/ebusd/const.py @@ -1,40 +1,41 @@ """Constants for ebus component.""" -from homeassistant.const import ENERGY_KILO_WATT_HOUR +from homeassistant.const import ENERGY_KILO_WATT_HOUR, TEMP_CELSIUS, PRESSURE_BAR DOMAIN = "ebusd" +TIME_SECONDS = "seconds" -# SensorTypes: +# SensorTypes from ebusdpy module : # 0='decimal', 1='time-schedule', 2='switch', 3='string', 4='value;status' SENSOR_TYPES = { "700": { "ActualFlowTemperatureDesired": [ "Hc1ActualFlowTempDesired", - "°C", + TEMP_CELSIUS, "mdi:thermometer", 0, ], "MaxFlowTemperatureDesired": [ "Hc1MaxFlowTempDesired", - "°C", + TEMP_CELSIUS, "mdi:thermometer", 0, ], "MinFlowTemperatureDesired": [ "Hc1MinFlowTempDesired", - "°C", + TEMP_CELSIUS, "mdi:thermometer", 0, ], "PumpStatus": ["Hc1PumpStatus", None, "mdi:toggle-switch", 2], "HCSummerTemperatureLimit": [ "Hc1SummerTempLimit", - "°C", + TEMP_CELSIUS, "mdi:weather-sunny", 0, ], - "HolidayTemperature": ["HolidayTemp", "°C", "mdi:thermometer", 0], - "HWTemperatureDesired": ["HwcTempDesired", "°C", "mdi:thermometer", 0], + "HolidayTemperature": ["HolidayTemp", TEMP_CELSIUS, "mdi:thermometer", 0], + "HWTemperatureDesired": ["HwcTempDesired", TEMP_CELSIUS, "mdi:thermometer", 0], "HWTimerMonday": ["hwcTimer.Monday", None, "mdi:timer", 1], "HWTimerTuesday": ["hwcTimer.Tuesday", None, "mdi:timer", 1], "HWTimerWednesday": ["hwcTimer.Wednesday", None, "mdi:timer", 1], @@ -42,15 +43,20 @@ SENSOR_TYPES = { "HWTimerFriday": ["hwcTimer.Friday", None, "mdi:timer", 1], "HWTimerSaturday": ["hwcTimer.Saturday", None, "mdi:timer", 1], "HWTimerSunday": ["hwcTimer.Sunday", None, "mdi:timer", 1], - "WaterPressure": ["WaterPressure", "bar", "mdi:water-pump", 0], + "WaterPressure": ["WaterPressure", PRESSURE_BAR, "mdi:water-pump", 0], "Zone1RoomZoneMapping": ["z1RoomZoneMapping", None, "mdi:label", 0], - "Zone1NightTemperature": ["z1NightTemp", "°C", "mdi:weather-night", 0], - "Zone1DayTemperature": ["z1DayTemp", "°C", "mdi:weather-sunny", 0], - "Zone1HolidayTemperature": ["z1HolidayTemp", "°C", "mdi:thermometer", 0], - "Zone1RoomTemperature": ["z1RoomTemp", "°C", "mdi:thermometer", 0], + "Zone1NightTemperature": ["z1NightTemp", TEMP_CELSIUS, "mdi:weather-night", 0], + "Zone1DayTemperature": ["z1DayTemp", TEMP_CELSIUS, "mdi:weather-sunny", 0], + "Zone1HolidayTemperature": [ + "z1HolidayTemp", + TEMP_CELSIUS, + "mdi:thermometer", + 0, + ], + "Zone1RoomTemperature": ["z1RoomTemp", TEMP_CELSIUS, "mdi:thermometer", 0], "Zone1ActualRoomTemperatureDesired": [ "z1ActualRoomTempDesired", - "°C", + TEMP_CELSIUS, "mdi:thermometer", 0, ], @@ -62,7 +68,7 @@ SENSOR_TYPES = { "Zone1TimerSaturday": ["z1Timer.Saturday", None, "mdi:timer", 1], "Zone1TimerSunday": ["z1Timer.Sunday", None, "mdi:timer", 1], "Zone1OperativeMode": ["z1OpMode", None, "mdi:math-compass", 3], - "ContinuosHeating": ["ContinuosHeating", "°C", "mdi:weather-snowy", 0], + "ContinuosHeating": ["ContinuosHeating", TEMP_CELSIUS, "mdi:weather-snowy", 0], "PowerEnergyConsumptionLastMonth": [ "PrEnergySumHcLastMonth", ENERGY_KILO_WATT_HOUR, @@ -77,14 +83,38 @@ SENSOR_TYPES = { ], }, "ehp": { - "HWTemperature": ["HwcTemp", "°C", "mdi:thermometer", 4], - "OutsideTemp": ["OutsideTemp", "°C", "mdi:thermometer", 4], + "HWTemperature": ["HwcTemp", TEMP_CELSIUS, "mdi:thermometer", 4], + "OutsideTemp": ["OutsideTemp", TEMP_CELSIUS, "mdi:thermometer", 4], }, "bai": { - "ReturnTemperature": ["ReturnTemp", "°C", "mdi:thermometer", 4], + "HotWaterTemperature": ["HwcTemp", TEMP_CELSIUS, "mdi:thermometer", 4], + "StorageTemperature": ["StorageTemp", TEMP_CELSIUS, "mdi:thermometer", 4], + "DesiredStorageTemperature": [ + "StorageTempDesired", + TEMP_CELSIUS, + "mdi:thermometer", + 0, + ], + "OutdoorsTemperature": [ + "OutdoorstempSensor", + TEMP_CELSIUS, + "mdi:thermometer", + 4, + ], + "WaterPreasure": ["WaterPressure", PRESSURE_BAR, "mdi:pipe", 4], + "AverageIgnitionTime": ["averageIgnitiontime", TIME_SECONDS, "mdi:av-timer", 0], + "MaximumIgnitionTime": ["maxIgnitiontime", TIME_SECONDS, "mdi:av-timer", 0], + "MinimumIgnitionTime": ["minIgnitiontime", TIME_SECONDS, "mdi:av-timer", 0], + "ReturnTemperature": ["ReturnTemp", TEMP_CELSIUS, "mdi:thermometer", 4], "CentralHeatingPump": ["WP", None, "mdi:toggle-switch", 2], "HeatingSwitch": ["HeatingSwitch", None, "mdi:toggle-switch", 2], - "FlowTemperature": ["FlowTemp", "°C", "mdi:thermometer", 4], + "DesiredFlowTemperature": [ + "FlowTempDesired", + TEMP_CELSIUS, + "mdi:thermometer", + 0, + ], + "FlowTemperature": ["FlowTemp", TEMP_CELSIUS, "mdi:thermometer", 4], "Flame": ["Flame", None, "mdi:toggle-switch", 2], "PowerEnergyConsumptionHeatingCircuit": [ "PrEnergySumHc1", diff --git a/homeassistant/components/ebusd/manifest.json b/homeassistant/components/ebusd/manifest.json index 46b8fb761dc..b9be062d3e2 100644 --- a/homeassistant/components/ebusd/manifest.json +++ b/homeassistant/components/ebusd/manifest.json @@ -1,7 +1,7 @@ { "domain": "ebusd", "name": "Ebusd", - "documentation": "https://www.home-assistant.io/components/ebusd", + "documentation": "https://www.home-assistant.io/integrations/ebusd", "requirements": [ "ebusdpy==0.0.16" ], diff --git a/homeassistant/components/ebusd/sensor.py b/homeassistant/components/ebusd/sensor.py index ac156e040d7..4bc79e7bd39 100644 --- a/homeassistant/components/ebusd/sensor.py +++ b/homeassistant/components/ebusd/sensor.py @@ -3,6 +3,7 @@ import logging import datetime from homeassistant.helpers.entity import Entity +import homeassistant.util.dt as dt_util from .const import DOMAIN @@ -68,9 +69,7 @@ class EbusdSensor(Entity): if index < len(time_frame): parsed = datetime.datetime.strptime(time_frame[index], "%H:%M") parsed = parsed.replace( - datetime.datetime.now().year, - datetime.datetime.now().month, - datetime.datetime.now().day, + dt_util.now().year, dt_util.now().month, dt_util.now().day ) schedule[item[0]] = parsed.isoformat() return schedule diff --git a/homeassistant/components/ecoal_boiler/manifest.json b/homeassistant/components/ecoal_boiler/manifest.json index 5bd488e0ff4..c1bf938dc34 100644 --- a/homeassistant/components/ecoal_boiler/manifest.json +++ b/homeassistant/components/ecoal_boiler/manifest.json @@ -1,7 +1,7 @@ { "domain": "ecoal_boiler", "name": "Ecoal boiler", - "documentation": "https://www.home-assistant.io/components/ecoal_boiler", + "documentation": "https://www.home-assistant.io/integrations/ecoal_boiler", "requirements": [ "ecoaliface==0.4.0" ], diff --git a/homeassistant/components/ecobee/.translations/bg.json b/homeassistant/components/ecobee/.translations/bg.json new file mode 100644 index 00000000000..bd8503fabd8 --- /dev/null +++ b/homeassistant/components/ecobee/.translations/bg.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "one_instance_only": "\u0422\u0430\u0437\u0438 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \u043f\u043e\u0434\u0434\u044a\u0440\u0436\u0430 \u0441\u0430\u043c\u043e \u0435\u0434\u043d\u043e \u043a\u043e\u043f\u0438\u0435 \u043d\u0430 ecobee." + }, + "error": { + "pin_request_failed": "\u0413\u0440\u0435\u0448\u043a\u0430 \u043f\u0440\u0438 \u0438\u0441\u043a\u0430\u043d\u0435 \u043d\u0430 \u041f\u0418\u041d \u043e\u0442 ecobee; \u043c\u043e\u043b\u044f, \u043f\u0440\u043e\u0432\u0435\u0440\u0435\u0442\u0435 \u0434\u0430\u043b\u0438 API \u043a\u043b\u044e\u0447\u044a\u0442 \u0435 \u043f\u0440\u0430\u0432\u0438\u043b\u0435\u043d.", + "token_request_failed": "\u0413\u0440\u0435\u0448\u043a\u0430 \u043f\u0440\u0438 \u0438\u0441\u043a\u0430\u043d\u0435 \u043d\u0430 \u043a\u043e\u0434\u043e\u0432\u0435 \u043e\u0442 ecobee; \u043c\u043e\u043b\u044f, \u043e\u043f\u0438\u0442\u0430\u0439\u0442\u0435 \u043e\u0442\u043d\u043e\u0432\u043e." + }, + "step": { + "authorize": { + "description": "\u041c\u043e\u043b\u044f, \u043e\u0442\u043e\u0440\u0438\u0437\u0438\u0440\u0430\u0439\u0442\u0435 \u0442\u043e\u0432\u0430 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435 \u043d\u0430 https://www.ecobee.com/consumerportal/index.html \u0441 \u043f\u0438\u043d \u043a\u043e\u0434: \n\n {pin} \n \n \u0421\u043b\u0435\u0434 \u0442\u043e\u0432\u0430 \u043d\u0430\u0442\u0438\u0441\u043d\u0435\u0442\u0435 \u0418\u0437\u043f\u0440\u0430\u0449\u0430\u043d\u0435.", + "title": "\u041e\u0442\u043e\u0440\u0438\u0437\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435 \u043d\u0430 ecobee.com" + }, + "user": { + "data": { + "api_key": "API \u043a\u043b\u044e\u0447" + }, + "description": "\u041c\u043e\u043b\u044f, \u0432\u044a\u0432\u0435\u0434\u0435\u0442\u0435 API \u043a\u043b\u044e\u0447\u0430, \u043f\u043e\u043b\u0443\u0447\u0435\u043d \u043e\u0442 ecobee.com.", + "title": "ecobee API \u043a\u043b\u044e\u0447" + } + }, + "title": "ecobee" + } +} \ No newline at end of file diff --git a/homeassistant/components/ecobee/.translations/ca.json b/homeassistant/components/ecobee/.translations/ca.json new file mode 100644 index 00000000000..2c4d16b5787 --- /dev/null +++ b/homeassistant/components/ecobee/.translations/ca.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "one_instance_only": "Aquesta integraci\u00f3 nom\u00e9s admet una sola inst\u00e0ncia ecobee." + }, + "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." + }, + "step": { + "authorize": { + "description": "Autoritza aquesta aplicaci\u00f3 a https://www.ecobee.com/consumerportal/index.html amb el codi pin seg\u00fcent: \n\n {pin} \n \n A continuaci\u00f3, prem Enviar.", + "title": "Autoritzaci\u00f3 de l'aplicaci\u00f3 a ecobee.com" + }, + "user": { + "data": { + "api_key": "Clau API" + }, + "description": "Introdueix la clau API obteinguda a trav\u00e9s del lloc web ecobee.com.", + "title": "Clau API d'ecobee" + } + }, + "title": "ecobee" + } +} \ No newline at end of file diff --git a/homeassistant/components/ecobee/.translations/da.json b/homeassistant/components/ecobee/.translations/da.json new file mode 100644 index 00000000000..7a42a9470db --- /dev/null +++ b/homeassistant/components/ecobee/.translations/da.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "one_instance_only": "Integrationen underst\u00f8tter kun \u00e9n ecobee forekomst" + }, + "error": { + "pin_request_failed": "Fejl ved anmodning om pinkode fra ecobee. Kontroller at API-n\u00f8glen er korrekt.", + "token_request_failed": "Fejl ved anmodning om tokens fra ecobee. Pr\u00f8v igen." + }, + "step": { + "authorize": { + "title": "Autoriser app p\u00e5 ecobee.com" + }, + "user": { + "data": { + "api_key": "API-n\u00f8gle" + }, + "description": "Indtast API-n\u00f8glen, du har f\u00e5et fra ecobee.com.", + "title": "ecobee API-n\u00f8gle" + } + }, + "title": "ecobee" + } +} \ No newline at end of file diff --git a/homeassistant/components/ecobee/.translations/de.json b/homeassistant/components/ecobee/.translations/de.json new file mode 100644 index 00000000000..1959f769d3a --- /dev/null +++ b/homeassistant/components/ecobee/.translations/de.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "api_key": "API Key" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ecobee/.translations/en.json b/homeassistant/components/ecobee/.translations/en.json new file mode 100644 index 00000000000..39072f70d82 --- /dev/null +++ b/homeassistant/components/ecobee/.translations/en.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "one_instance_only": "This integration currently supports only one ecobee instance." + }, + "error": { + "pin_request_failed": "Error requesting PIN from ecobee; please verify API key is correct.", + "token_request_failed": "Error requesting tokens from ecobee; please try again." + }, + "step": { + "authorize": { + "description": "Please authorize this app at https://www.ecobee.com/consumerportal/index.html with pin code:\n\n{pin}\n\nThen, press Submit.", + "title": "Authorize app on ecobee.com" + }, + "user": { + "data": { + "api_key": "API Key" + }, + "description": "Please enter the API key obtained from ecobee.com.", + "title": "ecobee API key" + } + }, + "title": "ecobee" + } +} \ No newline at end of file diff --git a/homeassistant/components/ecobee/.translations/es.json b/homeassistant/components/ecobee/.translations/es.json new file mode 100644 index 00000000000..5544d2e7f7b --- /dev/null +++ b/homeassistant/components/ecobee/.translations/es.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "one_instance_only": "Esta integraci\u00f3n actualmente solo admite una instancia de ecobee." + }, + "error": { + "pin_request_failed": "Error al solicitar el PIN de ecobee; verifique que la clave API sea correcta.", + "token_request_failed": "Error al solicitar tokens de ecobee; Int\u00e9ntalo de nuevo." + }, + "step": { + "authorize": { + "description": "Por favor, autorizar esta aplicaci\u00f3n en https://www.ecobee.com/consumerportal/index.html con c\u00f3digo pin:\n\n{pin}\n\nA continuaci\u00f3n, pulse Enviar.", + "title": "Autorizar aplicaci\u00f3n en ecobee.com" + }, + "user": { + "data": { + "api_key": "Clave API" + }, + "description": "Introduzca la clave de API obtenida de ecobee.com.", + "title": "Clave API de ecobee" + } + }, + "title": "ecobee" + } +} \ No newline at end of file diff --git a/homeassistant/components/ecobee/.translations/fr.json b/homeassistant/components/ecobee/.translations/fr.json new file mode 100644 index 00000000000..85da5b3a4ec --- /dev/null +++ b/homeassistant/components/ecobee/.translations/fr.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "one_instance_only": "Cette int\u00e9gration ne prend actuellement en charge qu'une seule instance ecobee." + }, + "error": { + "pin_request_failed": "Erreur lors de la demande du code PIN \u00e0 ecobee; veuillez v\u00e9rifier que la cl\u00e9 API est correcte.", + "token_request_failed": "Erreur lors de la demande de jetons \u00e0 ecobee; Veuillez r\u00e9essayer." + }, + "step": { + "authorize": { + "title": "Autoriser l'application sur ecobee.com" + }, + "user": { + "data": { + "api_key": "Cl\u00e9 API" + }, + "description": "Veuillez entrer la cl\u00e9 API obtenue aupr\u00e8s d'ecobee.com.", + "title": "Cl\u00e9 API ecobee" + } + }, + "title": "ecobee" + } +} \ No newline at end of file diff --git a/homeassistant/components/ecobee/.translations/it.json b/homeassistant/components/ecobee/.translations/it.json new file mode 100644 index 00000000000..2ecb587f19e --- /dev/null +++ b/homeassistant/components/ecobee/.translations/it.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "one_instance_only": "Questa integrazione supporta attualmente una sola istanza ecobee." + }, + "error": { + "pin_request_failed": "Errore durante la richiesta del PIN da ecobee; verificare che la chiave API sia corretta.", + "token_request_failed": "Errore durante la richiesta di token da ecobee; per favore riprova." + }, + "step": { + "authorize": { + "description": "Autorizza questa app su https://www.ecobee.com/consumerportal/index.html con il codice PIN: \n\n {pin} \n \n Quindi, premi Invia.", + "title": "Autorizza l'app su ecobee.com" + }, + "user": { + "data": { + "api_key": "API Key" + }, + "description": "Inserisci la chiave API ottenuta da ecobee.com.", + "title": "chiave API ecobee" + } + }, + "title": "ecobee" + } +} \ No newline at end of file diff --git a/homeassistant/components/ecobee/.translations/lb.json b/homeassistant/components/ecobee/.translations/lb.json new file mode 100644 index 00000000000..ee1fd5246c0 --- /dev/null +++ b/homeassistant/components/ecobee/.translations/lb.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "one_instance_only": "D\u00ebs Integratioun \u00ebnnerst\u00ebtzt n\u00ebmmen eng ecobee Instanz." + }, + "error": { + "pin_request_failed": "Feeler beim ufroe vum PIN vun ecobee; iwwerpr\u00e9ift op den API Schl\u00ebssel korrekt ass.", + "token_request_failed": "Feeler beim ufroe vum Jeton vun ecobee; prob\u00e9iert nach emol." + }, + "step": { + "authorize": { + "description": "Autoris\u00e9iert d\u00ebs App op https://www.ecobee.com/consumerportal/index.html mam Pin Code:\n\n{pin}\n\nKlickt dann op ofsch\u00e9cken.", + "title": "App autoris\u00e9ieren op ecobee.com" + }, + "user": { + "data": { + "api_key": "API Schl\u00ebssel" + }, + "description": "Gitt den API Schl\u00ebssel vun ecobee.com an:", + "title": "ecobee API Schl\u00ebssel" + } + }, + "title": "ecobee" + } +} \ No newline at end of file diff --git a/homeassistant/components/ecobee/.translations/nn.json b/homeassistant/components/ecobee/.translations/nn.json new file mode 100644 index 00000000000..301239cf31a --- /dev/null +++ b/homeassistant/components/ecobee/.translations/nn.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "ecobee" + } +} \ No newline at end of file diff --git a/homeassistant/components/ecobee/.translations/no.json b/homeassistant/components/ecobee/.translations/no.json new file mode 100644 index 00000000000..2bf141f6489 --- /dev/null +++ b/homeassistant/components/ecobee/.translations/no.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "one_instance_only": "Denne integrasjonen st\u00f8tter forel\u00f8pig bare en ecobee-forekomst." + }, + "error": { + "pin_request_failed": "Feil under foresp\u00f8rsel om PIN-kode fra ecobee. Kontroller at API-n\u00f8kkelen er riktig.", + "token_request_failed": "Feil ved foresp\u00f8rsel om tokener fra ecobee; Pr\u00f8v p\u00e5 nytt." + }, + "step": { + "authorize": { + "description": "Vennligst autoriser denne appen p\u00e5 https://www.ecobee.com/consumerportal/index.html med pin-kode:\n\n{pin}\n\nDeretter, trykk p\u00e5 Send.", + "title": "Autoriser 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.", + "title": "ecobee API-n\u00f8kkel" + } + }, + "title": "ecobee" + } +} \ No newline at end of file diff --git a/homeassistant/components/ecobee/.translations/pl.json b/homeassistant/components/ecobee/.translations/pl.json new file mode 100644 index 00000000000..5c51d86fee4 --- /dev/null +++ b/homeassistant/components/ecobee/.translations/pl.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "one_instance_only": "Komponent obs\u0142uguje tylko jedn\u0105 instancj\u0119 ecobee" + }, + "error": { + "pin_request_failed": "B\u0142\u0105d podczas \u017c\u0105dania kodu PIN od ecobee; sprawd\u017a, czy klucz API jest poprawny.", + "token_request_failed": "B\u0142\u0105d podczas \u017c\u0105dania token\u00f3w od ecobee; prosz\u0119 spr\u00f3buj ponownie." + }, + "step": { + "authorize": { + "description": "Autoryzuj t\u0119 aplikacj\u0119 na https://www.ecobee.com/consumerportal/index.html za pomoc\u0105 kodu PIN: \n\n {pin} \n \n Nast\u0119pnie naci\u015bnij przycisk Prze\u015blij.", + "title": "Autoryzuj aplikacj\u0119 na ecobee.com" + }, + "user": { + "data": { + "api_key": "Klucz API" + }, + "description": "Prosz\u0119 wprowadzi\u0107 klucz API uzyskany na ecobee.com.", + "title": "Klucz API" + } + }, + "title": "ecobee" + } +} \ No newline at end of file diff --git a/homeassistant/components/ecobee/.translations/pt-BR.json b/homeassistant/components/ecobee/.translations/pt-BR.json new file mode 100644 index 00000000000..65394faba17 --- /dev/null +++ b/homeassistant/components/ecobee/.translations/pt-BR.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "one_instance_only": "Essa integra\u00e7\u00e3o atualmente suporta apenas uma inst\u00e2ncia ecobee." + }, + "error": { + "token_request_failed": "Erro ao solicitar tokens da ecobee; Por favor, tente novamente." + }, + "step": { + "authorize": { + "description": "Por favor, autorize este aplicativo em https://www.ecobee.com/consumerportal/index.html com c\u00f3digo PIN:\n\n{pin}\n\nEm seguida, pressione Submit.", + "title": "Autorizar aplicativo em ecobee.com" + }, + "user": { + "data": { + "api_key": "Chave API" + }, + "description": "Por favor, insira a chave de API obtida em ecobee.com.", + "title": "chave da API ecobee" + } + }, + "title": "ecobee" + } +} \ No newline at end of file diff --git a/homeassistant/components/ecobee/.translations/ru.json b/homeassistant/components/ecobee/.translations/ru.json new file mode 100644 index 00000000000..660e0064bb6 --- /dev/null +++ b/homeassistant/components/ecobee/.translations/ru.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "one_instance_only": "\u042d\u0442\u0430 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \u0432 \u043d\u0430\u0441\u0442\u043e\u044f\u0449\u0435\u0435 \u0432\u0440\u0435\u043c\u044f \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442 \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u0438\u043d \u044d\u043a\u0437\u0435\u043c\u043f\u043b\u044f\u0440 ecobee." + }, + "error": { + "pin_request_failed": "\u041f\u0440\u043e\u0438\u0437\u043e\u0448\u043b\u0430 \u043e\u0448\u0438\u0431\u043a\u0430 \u0432\u043e \u0432\u0440\u0435\u043c\u044f \u0437\u0430\u043f\u0440\u043e\u0441\u0430 PIN-\u043a\u043e\u0434\u0430 \u0443 ecobee; \u043f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u043e\u0441\u0442\u044c \u043a\u043b\u044e\u0447\u0430 API.", + "token_request_failed": "\u041f\u0440\u043e\u0438\u0437\u043e\u0448\u043b\u0430 \u043e\u0448\u0438\u0431\u043a\u0430 \u0432\u043e \u0432\u0440\u0435\u043c\u044f \u0437\u0430\u043f\u0440\u043e\u0441\u0430 \u0442\u043e\u043a\u0435\u043d\u043e\u0432 \u0443 ecobee; \u043f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0435\u0449\u0435 \u0440\u0430\u0437." + }, + "step": { + "authorize": { + "description": "\u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043f\u0440\u043e\u0439\u0434\u0438\u0442\u0435 \u043f\u043e \u0430\u0434\u0440\u0435\u0441\u0443 https://www.ecobee.com/consumerportal/index.html \u0438 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0443\u0439\u0442\u0435\u0441\u044c \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e PIN-\u043a\u043e\u0434\u0430: \n\n {pin} \n \n \u0417\u0430\u0442\u0435\u043c \u043d\u0430\u0436\u043c\u0438\u0442\u0435 \u041e\u0442\u043f\u0440\u0430\u0432\u0438\u0442\u044c.", + "title": "\u0410\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u044f \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u043d\u0430 ecobee.com" + }, + "user": { + "data": { + "api_key": "\u041a\u043b\u044e\u0447 API" + }, + "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u043a\u043b\u044e\u0447 API, \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u043d\u044b\u0439 \u043e\u0442 ecobee.com.", + "title": "ecobee" + } + }, + "title": "ecobee" + } +} \ No newline at end of file diff --git a/homeassistant/components/ecobee/.translations/sl.json b/homeassistant/components/ecobee/.translations/sl.json new file mode 100644 index 00000000000..d70be59afb5 --- /dev/null +++ b/homeassistant/components/ecobee/.translations/sl.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "one_instance_only": "Ta integracija trenutno podpira samo en primerek ecobee." + }, + "error": { + "pin_request_failed": "Napaka pri zahtevi PIN-a od ecobee; preverite, ali je klju\u010d API pravilen.", + "token_request_failed": "Napaka pri zahtevanju \u017eetonov od ecobeeja; prosim poskusite ponovno." + }, + "step": { + "authorize": { + "description": "Prosimo, pooblastite to aplikacijo na https://www.ecobee.com/consumerportal/index.html s kodo PIN:\n\n{pin}\n\nNato pritisnite Po\u0161lji.", + "title": "Pooblasti aplikacijo na ecobee.com" + }, + "user": { + "data": { + "api_key": "API Klju\u010d" + }, + "description": "Prosimo vnesite API klju\u010d, pridobljen iz ecobee.com.", + "title": "ecobee API klju\u010d" + } + }, + "title": "ecobee" + } +} \ No newline at end of file diff --git a/homeassistant/components/ecobee/.translations/sv.json b/homeassistant/components/ecobee/.translations/sv.json new file mode 100644 index 00000000000..f4a63bb449d --- /dev/null +++ b/homeassistant/components/ecobee/.translations/sv.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "api_key": "API-nyckel" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ecobee/.translations/zh-Hant.json b/homeassistant/components/ecobee/.translations/zh-Hant.json new file mode 100644 index 00000000000..e1eb6ebd357 --- /dev/null +++ b/homeassistant/components/ecobee/.translations/zh-Hant.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "one_instance_only": "\u6b64\u6574\u5408\u76ee\u524d\u50c5\u652f\u63f4\u4e00\u7d44 ecobee \u7269\u4ef6" + }, + "error": { + "pin_request_failed": "ecobee \u6240\u9700\u4ee3\u78bc\u932f\u8aa4\uff0c\u8acb\u78ba\u8a8d\u5bc6\u9470\u6b63\u78ba\u6027\u3002", + "token_request_failed": "ecobee \u6240\u9700\u5bc6\u9470\u932f\u8aa4\uff0c\u8acb\u518d\u8a66\u4e00\u6b21\u3002" + }, + "step": { + "authorize": { + "description": "\u8acb\u65bc https://www.ecobee.com/consumerportal/index.html \u8f38\u5165\u4e0b\u65b9\u4ee3\u78bc\uff0c\u8a8d\u8b49\u6b64 App\uff1a\n\n{pin}\n\n\u7136\u5f8c\u6309\u4e0b\u300cSubmit\u300d\u3002", + "title": "\u65bc ecobee.com \u4e0a\u8a8d\u8b49 App" + }, + "user": { + "data": { + "api_key": "API \u5bc6\u9470" + }, + "description": "\u8acb\u8f38\u5165\u7531 ecobee.com \u6240\u7372\u5f97\u7684 API \u5bc6\u9470\u3002", + "title": "ecobee API \u5bc6\u9470" + } + }, + "title": "ecobee" + } +} \ No newline at end of file diff --git a/homeassistant/components/ecobee/__init__.py b/homeassistant/components/ecobee/__init__.py index cb8b7436b51..eb65a7ed426 100644 --- a/homeassistant/components/ecobee/__init__.py +++ b/homeassistant/components/ecobee/__init__.py @@ -1,123 +1,130 @@ -"""Support for Ecobee devices.""" -import logging -import os +"""Support for ecobee.""" +import asyncio from datetime import timedelta - import voluptuous as vol -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers import discovery +from pyecobee import Ecobee, ECOBEE_API_KEY, ECOBEE_REFRESH_TOKEN, ExpiredTokenError + +from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.const import CONF_API_KEY +from homeassistant.helpers import config_validation as cv from homeassistant.util import Throttle -from homeassistant.util.json import save_json -_CONFIGURING = {} -_LOGGER = logging.getLogger(__name__) - -CONF_HOLD_TEMP = "hold_temp" - -DOMAIN = "ecobee" - -ECOBEE_CONFIG_FILE = "ecobee.conf" +from .const import ( + CONF_REFRESH_TOKEN, + DATA_ECOBEE_CONFIG, + DOMAIN, + ECOBEE_PLATFORMS, + _LOGGER, +) MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=180) -NETWORK = None - CONFIG_SCHEMA = vol.Schema( - { - DOMAIN: vol.Schema( - { - vol.Optional(CONF_API_KEY): cv.string, - vol.Optional(CONF_HOLD_TEMP, default=False): cv.boolean, - } - ) - }, - extra=vol.ALLOW_EXTRA, + {DOMAIN: vol.Schema({vol.Optional(CONF_API_KEY): cv.string})}, extra=vol.ALLOW_EXTRA ) -def request_configuration(network, hass, config): - """Request configuration steps from the user.""" - configurator = hass.components.configurator - if "ecobee" in _CONFIGURING: - configurator.notify_errors( - _CONFIGURING["ecobee"], "Failed to register, please try again." +async def async_setup(hass, config): + """ + Ecobee uses config flow for configuration. + + But, an "ecobee:" entry in configuration.yaml will trigger an import flow + if a config entry doesn't already exist. If ecobee.conf exists, the import + flow will attempt to import it and create a config entry, to assist users + migrating from the old ecobee component. Otherwise, the user will have to + continue setting up the integration via the config flow. + """ + hass.data[DATA_ECOBEE_CONFIG] = config.get(DOMAIN, {}) + + if not hass.config_entries.async_entries(DOMAIN) and hass.data[DATA_ECOBEE_CONFIG]: + # No config entry exists and configuration.yaml config exists, trigger the import flow. + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT} + ) ) - return - - def ecobee_configuration_callback(callback_data): - """Handle configuration callbacks.""" - network.request_tokens() - network.update() - setup_ecobee(hass, network, config) - - _CONFIGURING["ecobee"] = configurator.request_config( - "Ecobee", - ecobee_configuration_callback, - description=( - "Please authorize this app at https://www.ecobee.com/consumer" - "portal/index.html with pin code: " + network.pin - ), - description_image="/static/images/config_ecobee_thermostat.png", - submit_caption="I have authorized the app.", - ) + return True -def setup_ecobee(hass, network, config): - """Set up the Ecobee thermostat.""" - # If ecobee has a PIN then it needs to be configured. - if network.pin is not None: - request_configuration(network, hass, config) - return +async def async_setup_entry(hass, entry): + """Set up ecobee via a config entry.""" + api_key = entry.data[CONF_API_KEY] + refresh_token = entry.data[CONF_REFRESH_TOKEN] - if "ecobee" in _CONFIGURING: - configurator = hass.components.configurator - configurator.request_done(_CONFIGURING.pop("ecobee")) + data = EcobeeData(hass, entry, api_key=api_key, refresh_token=refresh_token) - hold_temp = config[DOMAIN].get(CONF_HOLD_TEMP) + if not await data.refresh(): + return False - discovery.load_platform(hass, "climate", DOMAIN, {"hold_temp": hold_temp}, config) - discovery.load_platform(hass, "sensor", DOMAIN, {}, config) - discovery.load_platform(hass, "binary_sensor", DOMAIN, {}, config) - discovery.load_platform(hass, "weather", DOMAIN, {}, config) + await data.update() + + if data.ecobee.thermostats is None: + _LOGGER.error("No ecobee devices found to set up") + return False + + hass.data[DOMAIN] = data + + for component in ECOBEE_PLATFORMS: + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, component) + ) + + return True class EcobeeData: - """Get the latest data and update the states.""" + """ + Handle getting the latest data from ecobee.com so platforms can use it. - def __init__(self, config_file): - """Init the Ecobee data object.""" - from pyecobee import Ecobee + Also handle refreshing tokens and updating config entry with refreshed tokens. + """ - self.ecobee = Ecobee(config_file) + def __init__(self, hass, entry, api_key, refresh_token): + """Initialize the Ecobee data object.""" + self._hass = hass + self._entry = entry + self.ecobee = Ecobee( + config={ECOBEE_API_KEY: api_key, ECOBEE_REFRESH_TOKEN: refresh_token} + ) @Throttle(MIN_TIME_BETWEEN_UPDATES) - def update(self): - """Get the latest data from pyecobee.""" - self.ecobee.update() - _LOGGER.debug("Ecobee data updated successfully") + async def update(self): + """Get the latest data from ecobee.com.""" + try: + await self._hass.async_add_executor_job(self.ecobee.update) + _LOGGER.debug("Updating ecobee") + except ExpiredTokenError: + _LOGGER.warning( + "Ecobee update failed; attempting to refresh expired tokens" + ) + await self.refresh() + + async def refresh(self) -> bool: + """Refresh ecobee tokens and update config entry.""" + _LOGGER.debug("Refreshing ecobee tokens and updating config entry") + if await self._hass.async_add_executor_job(self.ecobee.refresh_tokens): + self._hass.config_entries.async_update_entry( + self._entry, + data={ + CONF_API_KEY: self.ecobee.config[ECOBEE_API_KEY], + CONF_REFRESH_TOKEN: self.ecobee.config[ECOBEE_REFRESH_TOKEN], + }, + ) + return True + _LOGGER.error("Error updating ecobee tokens") + return False -def setup(hass, config): - """Set up the Ecobee. +async def async_unload_entry(hass, config_entry): + """Unload the config entry and platforms.""" + hass.data.pop(DOMAIN) - Will automatically load thermostat and sensor components to support - devices discovered on the network. - """ - global NETWORK + tasks = [] + for platform in ECOBEE_PLATFORMS: + tasks.append( + hass.config_entries.async_forward_entry_unload(config_entry, platform) + ) - if "ecobee" in _CONFIGURING: - return - - # Create ecobee.conf if it doesn't exist - if not os.path.isfile(hass.config.path(ECOBEE_CONFIG_FILE)): - jsonconfig = {"API_KEY": config[DOMAIN].get(CONF_API_KEY)} - save_json(hass.config.path(ECOBEE_CONFIG_FILE), jsonconfig) - - NETWORK = EcobeeData(hass.config.path(ECOBEE_CONFIG_FILE)) - - setup_ecobee(hass, NETWORK.ecobee, config) - - return True + return all(await asyncio.gather(*tasks)) diff --git a/homeassistant/components/ecobee/binary_sensor.py b/homeassistant/components/ecobee/binary_sensor.py index a3cd49ff458..7afdbae5a28 100644 --- a/homeassistant/components/ecobee/binary_sensor.py +++ b/homeassistant/components/ecobee/binary_sensor.py @@ -1,15 +1,20 @@ """Support for Ecobee binary sensors.""" -from homeassistant.components import ecobee -from homeassistant.components.binary_sensor import BinarySensorDevice +from homeassistant.components.binary_sensor import ( + BinarySensorDevice, + DEVICE_CLASS_OCCUPANCY, +) -ECOBEE_CONFIG_FILE = "ecobee.conf" +from .const import DOMAIN -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the Ecobee sensors.""" - if discovery_info is None: - return - data = ecobee.NETWORK +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Old way of setting up ecobee binary sensors.""" + pass + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up ecobee binary (occupancy) sensors.""" + data = hass.data[DOMAIN] dev = list() for index in range(len(data.ecobee.thermostats)): for sensor in data.ecobee.get_remote_sensors(index): @@ -17,27 +22,37 @@ def setup_platform(hass, config, add_entities, discovery_info=None): if item["type"] != "occupancy": continue - dev.append(EcobeeBinarySensor(sensor["name"], index)) + dev.append(EcobeeBinarySensor(data, sensor["name"], index)) - add_entities(dev, True) + async_add_entities(dev, True) class EcobeeBinarySensor(BinarySensorDevice): """Representation of an Ecobee sensor.""" - def __init__(self, sensor_name, sensor_index): + def __init__(self, data, sensor_name, sensor_index): """Initialize the Ecobee sensor.""" + self.data = data self._name = sensor_name + " Occupancy" self.sensor_name = sensor_name self.index = sensor_index self._state = None - self._device_class = "occupancy" @property def name(self): """Return the name of the Ecobee sensor.""" return self._name.rstrip() + @property + def unique_id(self): + """Return a unique identifier for this sensor.""" + for sensor in self.data.ecobee.get_remote_sensors(self.index): + if sensor["name"] == self.sensor_name: + if "code" in sensor: + return f"{sensor['code']}-{self.device_class}" + thermostat = self.data.ecobee.get_thermostat(self.index) + return f"{thermostat['identifier']}-{sensor['id']}-{self.device_class}" + @property def is_on(self): """Return the status of the sensor.""" @@ -46,13 +61,12 @@ class EcobeeBinarySensor(BinarySensorDevice): @property def device_class(self): """Return the class of this sensor, from DEVICE_CLASSES.""" - return self._device_class + return DEVICE_CLASS_OCCUPANCY - def update(self): + async def async_update(self): """Get the latest state of the sensor.""" - data = ecobee.NETWORK - data.update() - for sensor in data.ecobee.get_remote_sensors(self.index): + await self.data.update() + for sensor in self.data.ecobee.get_remote_sensors(self.index): for item in sensor["capability"]: if item["type"] == "occupancy" and self.sensor_name == sensor["name"]: self._state = item["value"] diff --git a/homeassistant/components/ecobee/climate.py b/homeassistant/components/ecobee/climate.py index 181f1561eba..f930282ba7b 100644 --- a/homeassistant/components/ecobee/climate.py +++ b/homeassistant/components/ecobee/climate.py @@ -1,14 +1,11 @@ """Support for Ecobee Thermostats.""" import collections -import logging from typing import Optional import voluptuous as vol -from homeassistant.components import ecobee from homeassistant.components.climate import ClimateDevice from homeassistant.components.climate.const import ( - DOMAIN, HVAC_MODE_COOL, HVAC_MODE_HEAT, HVAC_MODE_AUTO, @@ -36,13 +33,22 @@ from homeassistant.const import ( ATTR_TEMPERATURE, TEMP_FAHRENHEIT, ) +from homeassistant.util.temperature import convert import homeassistant.helpers.config_validation as cv -_CONFIGURING = {} -_LOGGER = logging.getLogger(__name__) +from .const import DOMAIN, _LOGGER +from .util import ecobee_date, ecobee_time +ATTR_COOL_TEMP = "cool_temp" +ATTR_END_DATE = "end_date" +ATTR_END_TIME = "end_time" ATTR_FAN_MIN_ON_TIME = "fan_min_on_time" +ATTR_FAN_MODE = "fan_mode" +ATTR_HEAT_TEMP = "heat_temp" ATTR_RESUME_ALL = "resume_all" +ATTR_START_DATE = "start_date" +ATTR_START_TIME = "start_time" +ATTR_VACATION_NAME = "vacation_name" DEFAULT_RESUME_ALL = False PRESET_TEMPERATURE = "temp" @@ -88,13 +94,41 @@ PRESET_TO_ECOBEE_HOLD = { PRESET_HOLD_INDEFINITE: "indefinite", } -SERVICE_SET_FAN_MIN_ON_TIME = "ecobee_set_fan_min_on_time" -SERVICE_RESUME_PROGRAM = "ecobee_resume_program" +SERVICE_CREATE_VACATION = "create_vacation" +SERVICE_DELETE_VACATION = "delete_vacation" +SERVICE_RESUME_PROGRAM = "resume_program" +SERVICE_SET_FAN_MIN_ON_TIME = "set_fan_min_on_time" -SET_FAN_MIN_ON_TIME_SCHEMA = vol.Schema( +DTGROUP_INCLUSIVE_MSG = ( + f"{ATTR_START_DATE}, {ATTR_START_TIME}, {ATTR_END_DATE}, " + f"and {ATTR_END_TIME} must be specified together" +) + +CREATE_VACATION_SCHEMA = vol.Schema( { - vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, - vol.Required(ATTR_FAN_MIN_ON_TIME): vol.Coerce(int), + vol.Required(ATTR_ENTITY_ID): cv.entity_id, + vol.Required(ATTR_VACATION_NAME): vol.All(cv.string, vol.Length(max=12)), + vol.Required(ATTR_COOL_TEMP): vol.Coerce(float), + vol.Required(ATTR_HEAT_TEMP): vol.Coerce(float), + vol.Inclusive( + ATTR_START_DATE, "dtgroup", msg=DTGROUP_INCLUSIVE_MSG + ): ecobee_date, + vol.Inclusive( + ATTR_START_TIME, "dtgroup", msg=DTGROUP_INCLUSIVE_MSG + ): ecobee_time, + vol.Inclusive(ATTR_END_DATE, "dtgroup", msg=DTGROUP_INCLUSIVE_MSG): ecobee_date, + vol.Inclusive(ATTR_END_TIME, "dtgroup", msg=DTGROUP_INCLUSIVE_MSG): ecobee_time, + vol.Optional(ATTR_FAN_MODE, default="auto"): vol.Any("auto", "on"), + vol.Optional(ATTR_FAN_MIN_ON_TIME, default=0): vol.All( + int, vol.Range(min=0, max=60) + ), + } +) + +DELETE_VACATION_SCHEMA = vol.Schema( + { + vol.Required(ATTR_ENTITY_ID): cv.entity_id, + vol.Required(ATTR_VACATION_NAME): vol.All(cv.string, vol.Length(max=12)), } ) @@ -105,6 +139,14 @@ RESUME_PROGRAM_SCHEMA = vol.Schema( } ) +SET_FAN_MIN_ON_TIME_SCHEMA = vol.Schema( + { + vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, + vol.Required(ATTR_FAN_MIN_ON_TIME): vol.Coerce(int), + } +) + + SUPPORT_FLAGS = ( SUPPORT_TARGET_TEMPERATURE | SUPPORT_PRESET_MODE @@ -114,20 +156,40 @@ SUPPORT_FLAGS = ( ) -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the Ecobee Thermostat Platform.""" - if discovery_info is None: - return - data = ecobee.NETWORK - hold_temp = discovery_info["hold_temp"] - _LOGGER.info( - "Loading ecobee thermostat component with hold_temp set to %s", hold_temp - ) - devices = [ - Thermostat(data, index, hold_temp) - for index in range(len(data.ecobee.thermostats)) - ] - add_entities(devices) +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Old way of setting up ecobee thermostat.""" + pass + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the ecobee thermostat.""" + + data = hass.data[DOMAIN] + + devices = [Thermostat(data, index) for index in range(len(data.ecobee.thermostats))] + + async_add_entities(devices, True) + + def create_vacation_service(service): + """Create a vacation on the target thermostat.""" + entity_id = service.data[ATTR_ENTITY_ID] + + for thermostat in devices: + if thermostat.entity_id == entity_id: + thermostat.create_vacation(service.data) + thermostat.schedule_update_ha_state(True) + break + + def delete_vacation_service(service): + """Delete a vacation on the target thermostat.""" + entity_id = service.data[ATTR_ENTITY_ID] + vacation_name = service.data[ATTR_VACATION_NAME] + + for thermostat in devices: + if thermostat.entity_id == entity_id: + thermostat.delete_vacation(vacation_name) + thermostat.schedule_update_ha_state(True) + break def fan_min_on_time_set_service(service): """Set the minimum fan on time on the target thermostats.""" @@ -163,14 +225,28 @@ def setup_platform(hass, config, add_entities, discovery_info=None): thermostat.schedule_update_ha_state(True) - hass.services.register( + hass.services.async_register( + DOMAIN, + SERVICE_CREATE_VACATION, + create_vacation_service, + schema=CREATE_VACATION_SCHEMA, + ) + + hass.services.async_register( + DOMAIN, + SERVICE_DELETE_VACATION, + delete_vacation_service, + schema=DELETE_VACATION_SCHEMA, + ) + + hass.services.async_register( DOMAIN, SERVICE_SET_FAN_MIN_ON_TIME, fan_min_on_time_set_service, schema=SET_FAN_MIN_ON_TIME_SCHEMA, ) - hass.services.register( + hass.services.async_register( DOMAIN, SERVICE_RESUME_PROGRAM, resume_program_set_service, @@ -181,13 +257,12 @@ def setup_platform(hass, config, add_entities, discovery_info=None): class Thermostat(ClimateDevice): """A thermostat class for Ecobee.""" - def __init__(self, data, thermostat_index, hold_temp): + def __init__(self, data, thermostat_index): """Initialize the thermostat.""" self.data = data self.thermostat_index = thermostat_index self.thermostat = self.data.ecobee.get_thermostat(self.thermostat_index) self._name = self.thermostat["name"] - self.hold_temp = hold_temp self.vacation = None self._operation_list = [] @@ -206,14 +281,13 @@ class Thermostat(ClimateDevice): self._fan_modes = [FAN_AUTO, FAN_ON] self.update_without_throttle = False - def update(self): + async def async_update(self): """Get the latest state from the thermostat.""" if self.update_without_throttle: - self.data.update(no_throttle=True) + await self.data.update(no_throttle=True) self.update_without_throttle = False else: - self.data.update() - + await self.data.update() self.thermostat = self.data.ecobee.get_thermostat(self.thermostat_index) @property @@ -231,6 +305,11 @@ class Thermostat(ClimateDevice): """Return the name of the Ecobee Thermostat.""" return self.thermostat["name"] + @property + def unique_id(self): + """Return a unique identifier for this ecobee thermostat.""" + return self.thermostat["identifier"] + @property def temperature_unit(self): """Return the unit of measurement.""" @@ -543,3 +622,58 @@ class Thermostat(ClimateDevice): # supported; note that this should not include 'indefinite' # as an indefinite away hold is interpreted as away_mode return "nextTransition" + + def create_vacation(self, service_data): + """Create a vacation with user-specified parameters.""" + vacation_name = service_data[ATTR_VACATION_NAME] + cool_temp = convert( + service_data[ATTR_COOL_TEMP], + self.hass.config.units.temperature_unit, + TEMP_FAHRENHEIT, + ) + heat_temp = convert( + service_data[ATTR_HEAT_TEMP], + self.hass.config.units.temperature_unit, + TEMP_FAHRENHEIT, + ) + start_date = service_data.get(ATTR_START_DATE) + start_time = service_data.get(ATTR_START_TIME) + end_date = service_data.get(ATTR_END_DATE) + end_time = service_data.get(ATTR_END_TIME) + fan_mode = service_data[ATTR_FAN_MODE] + fan_min_on_time = service_data[ATTR_FAN_MIN_ON_TIME] + + kwargs = { + key: value + for key, value in { + "start_date": start_date, + "start_time": start_time, + "end_date": end_date, + "end_time": end_time, + "fan_mode": fan_mode, + "fan_min_on_time": fan_min_on_time, + }.items() + if value is not None + } + + _LOGGER.debug( + "Creating a vacation on thermostat %s with name %s, cool temp %s, heat temp %s, " + "and the following other parameters: %s", + self.name, + vacation_name, + cool_temp, + heat_temp, + kwargs, + ) + self.data.ecobee.create_vacation( + self.thermostat_index, vacation_name, cool_temp, heat_temp, **kwargs + ) + + def delete_vacation(self, vacation_name): + """Delete a vacation with the specified name.""" + _LOGGER.debug( + "Deleting a vacation on thermostat %s with name %s", + self.name, + vacation_name, + ) + self.data.ecobee.delete_vacation(self.thermostat_index, vacation_name) diff --git a/homeassistant/components/ecobee/config_flow.py b/homeassistant/components/ecobee/config_flow.py new file mode 100644 index 00000000000..56ce13f7701 --- /dev/null +++ b/homeassistant/components/ecobee/config_flow.py @@ -0,0 +1,124 @@ +"""Config flow to configure ecobee.""" +import voluptuous as vol + +from pyecobee import ( + Ecobee, + ECOBEE_CONFIG_FILENAME, + ECOBEE_API_KEY, + ECOBEE_REFRESH_TOKEN, +) + +from homeassistant import config_entries +from homeassistant.const import CONF_API_KEY +from homeassistant.core import HomeAssistantError +from homeassistant.util.json import load_json + +from .const import CONF_REFRESH_TOKEN, DATA_ECOBEE_CONFIG, DOMAIN, _LOGGER + + +class EcobeeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): + """Handle an ecobee config flow.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL + + def __init__(self): + """Initialize the ecobee flow.""" + self._ecobee = None + + async def async_step_user(self, user_input=None): + """Handle a flow initiated by the user.""" + if self._async_current_entries(): + # Config entry already exists, only one allowed. + return self.async_abort(reason="one_instance_only") + + errors = {} + stored_api_key = ( + self.hass.data[DATA_ECOBEE_CONFIG].get(CONF_API_KEY) + if DATA_ECOBEE_CONFIG in self.hass.data + else "" + ) + + if user_input is not None: + # Use the user-supplied API key to attempt to obtain a PIN from ecobee. + self._ecobee = Ecobee(config={ECOBEE_API_KEY: user_input[CONF_API_KEY]}) + + if await self.hass.async_add_executor_job(self._ecobee.request_pin): + # We have a PIN; move to the next step of the flow. + return await self.async_step_authorize() + errors["base"] = "pin_request_failed" + + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + {vol.Required(CONF_API_KEY, default=stored_api_key): str} + ), + errors=errors, + ) + + async def async_step_authorize(self, user_input=None): + """Present the user with the PIN so that the app can be authorized on ecobee.com.""" + errors = {} + + if user_input is not None: + # Attempt to obtain tokens from ecobee and finish the flow. + if await self.hass.async_add_executor_job(self._ecobee.request_tokens): + # Refresh token obtained; create the config entry. + config = { + CONF_API_KEY: self._ecobee.api_key, + CONF_REFRESH_TOKEN: self._ecobee.refresh_token, + } + return self.async_create_entry(title=DOMAIN, data=config) + errors["base"] = "token_request_failed" + + return self.async_show_form( + step_id="authorize", + errors=errors, + description_placeholders={"pin": self._ecobee.pin}, + ) + + async def async_step_import(self, import_data): + """ + Import ecobee config from configuration.yaml. + + Triggered by async_setup only if a config entry doesn't already exist. + If ecobee.conf exists, we will attempt to validate the credentials + and create an entry if valid. Otherwise, we will delegate to the user + step so that the user can continue the config flow. + """ + try: + legacy_config = await self.hass.async_add_executor_job( + load_json, self.hass.config.path(ECOBEE_CONFIG_FILENAME) + ) + config = { + ECOBEE_API_KEY: legacy_config[ECOBEE_API_KEY], + ECOBEE_REFRESH_TOKEN: legacy_config[ECOBEE_REFRESH_TOKEN], + } + except (HomeAssistantError, KeyError): + _LOGGER.debug( + "No valid ecobee.conf configuration found for import, delegating to user step" + ) + return await self.async_step_user( + user_input={ + CONF_API_KEY: self.hass.data[DATA_ECOBEE_CONFIG].get(CONF_API_KEY) + } + ) + + ecobee = Ecobee(config=config) + if await self.hass.async_add_executor_job(ecobee.refresh_tokens): + # Credentials found and validated; create the entry. + _LOGGER.debug( + "Valid ecobee configuration found for import, creating config entry" + ) + return self.async_create_entry( + title=DOMAIN, + data={ + CONF_API_KEY: ecobee.api_key, + CONF_REFRESH_TOKEN: ecobee.refresh_token, + }, + ) + return await self.async_step_user( + user_input={ + CONF_API_KEY: self.hass.data[DATA_ECOBEE_CONFIG].get(CONF_API_KEY) + } + ) diff --git a/homeassistant/components/ecobee/const.py b/homeassistant/components/ecobee/const.py new file mode 100644 index 00000000000..c3a23099b8a --- /dev/null +++ b/homeassistant/components/ecobee/const.py @@ -0,0 +1,12 @@ +"""Constants for the ecobee integration.""" +import logging + +_LOGGER = logging.getLogger(__package__) + +DOMAIN = "ecobee" +DATA_ECOBEE_CONFIG = "ecobee_config" + +CONF_INDEX = "index" +CONF_REFRESH_TOKEN = "refresh_token" + +ECOBEE_PLATFORMS = ["binary_sensor", "climate", "sensor", "weather"] diff --git a/homeassistant/components/ecobee/manifest.json b/homeassistant/components/ecobee/manifest.json index 31cca1e676f..bc87b3cd64e 100644 --- a/homeassistant/components/ecobee/manifest.json +++ b/homeassistant/components/ecobee/manifest.json @@ -1,10 +1,9 @@ { - "domain": "ecobee", - "name": "Ecobee", - "documentation": "https://www.home-assistant.io/components/ecobee", - "requirements": [ - "python-ecobee-api==0.0.21" - ], - "dependencies": ["configurator"], - "codeowners": [] + "domain": "ecobee", + "name": "Ecobee", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/ecobee", + "dependencies": [], + "requirements": ["python-ecobee-api==0.1.4"], + "codeowners": ["@marthoc"] } diff --git a/homeassistant/components/ecobee/notify.py b/homeassistant/components/ecobee/notify.py index bb6861a1492..c7b3f47d29c 100644 --- a/homeassistant/components/ecobee/notify.py +++ b/homeassistant/components/ecobee/notify.py @@ -1,15 +1,10 @@ """Support for Ecobee Send Message service.""" -import logging - import voluptuous as vol import homeassistant.helpers.config_validation as cv -from homeassistant.components import ecobee from homeassistant.components.notify import BaseNotificationService, PLATFORM_SCHEMA -_LOGGER = logging.getLogger(__name__) - -CONF_INDEX = "index" +from .const import CONF_INDEX, DOMAIN PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( {vol.Optional(CONF_INDEX, default=0): cv.positive_int} @@ -18,17 +13,19 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def get_service(hass, config, discovery_info=None): """Get the Ecobee notification service.""" + data = hass.data[DOMAIN] index = config.get(CONF_INDEX) - return EcobeeNotificationService(index) + return EcobeeNotificationService(data, index) class EcobeeNotificationService(BaseNotificationService): """Implement the notification service for the Ecobee thermostat.""" - def __init__(self, thermostat_index): + def __init__(self, data, thermostat_index): """Initialize the service.""" + self.data = data self.thermostat_index = thermostat_index def send_message(self, message="", **kwargs): - """Send a message to a command line.""" - ecobee.NETWORK.ecobee.send_message(self.thermostat_index, message) + """Send a message.""" + self.data.ecobee.send_message(self.thermostat_index, message) diff --git a/homeassistant/components/ecobee/sensor.py b/homeassistant/components/ecobee/sensor.py index d21f937dd20..24ea3d281bc 100644 --- a/homeassistant/components/ecobee/sensor.py +++ b/homeassistant/components/ecobee/sensor.py @@ -1,5 +1,6 @@ """Support for Ecobee sensors.""" -from homeassistant.components import ecobee +from pyecobee.const import ECOBEE_STATE_CALIBRATING, ECOBEE_STATE_UNKNOWN + from homeassistant.const import ( DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_TEMPERATURE, @@ -7,7 +8,7 @@ from homeassistant.const import ( ) from homeassistant.helpers.entity import Entity -ECOBEE_CONFIG_FILE = "ecobee.conf" +from .const import DOMAIN SENSOR_TYPES = { "temperature": ["Temperature", TEMP_FAHRENHEIT], @@ -15,11 +16,14 @@ SENSOR_TYPES = { } -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the Ecobee sensors.""" - if discovery_info is None: - return - data = ecobee.NETWORK +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Old way of setting up ecobee sensors.""" + pass + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up ecobee (temperature and humidity) sensors.""" + data = hass.data[DOMAIN] dev = list() for index in range(len(data.ecobee.thermostats)): for sensor in data.ecobee.get_remote_sensors(index): @@ -27,16 +31,17 @@ def setup_platform(hass, config, add_entities, discovery_info=None): if item["type"] not in ("temperature", "humidity"): continue - dev.append(EcobeeSensor(sensor["name"], item["type"], index)) + dev.append(EcobeeSensor(data, sensor["name"], item["type"], index)) - add_entities(dev, True) + async_add_entities(dev, True) class EcobeeSensor(Entity): """Representation of an Ecobee sensor.""" - def __init__(self, sensor_name, sensor_type, sensor_index): + def __init__(self, data, sensor_name, sensor_type, sensor_index): """Initialize the sensor.""" + self.data = data self._name = "{} {}".format(sensor_name, SENSOR_TYPES[sensor_type][0]) self.sensor_name = sensor_name self.type = sensor_type @@ -49,6 +54,16 @@ class EcobeeSensor(Entity): """Return the name of the Ecobee sensor.""" return self._name + @property + def unique_id(self): + """Return a unique identifier for this sensor.""" + for sensor in self.data.ecobee.get_remote_sensors(self.index): + if sensor["name"] == self.sensor_name: + if "code" in sensor: + return f"{sensor['code']}-{self.device_class}" + thermostat = self.data.ecobee.get_thermostat(self.index) + return f"{thermostat['identifier']}-{sensor['id']}-{self.device_class}" + @property def device_class(self): """Return the device class of the sensor.""" @@ -59,6 +74,12 @@ class EcobeeSensor(Entity): @property def state(self): """Return the state of the sensor.""" + if self._state in [ECOBEE_STATE_CALIBRATING, ECOBEE_STATE_UNKNOWN]: + return None + + if self.type == "temperature": + return float(self._state) / 10 + return self._state @property @@ -66,14 +87,10 @@ class EcobeeSensor(Entity): """Return the unit of measurement this sensor expresses itself in.""" return self._unit_of_measurement - def update(self): + async def async_update(self): """Get the latest state of the sensor.""" - data = ecobee.NETWORK - data.update() - for sensor in data.ecobee.get_remote_sensors(self.index): + await self.data.update() + for sensor in self.data.ecobee.get_remote_sensors(self.index): for item in sensor["capability"]: if item["type"] == self.type and self.sensor_name == sensor["name"]: - if self.type == "temperature" and item["value"] != "unknown": - self._state = float(item["value"]) / 10 - else: - self._state = item["value"] + self._state = item["value"] diff --git a/homeassistant/components/ecobee/services.yaml b/homeassistant/components/ecobee/services.yaml index e69de29bb2d..2155d3cf7d2 100644 --- a/homeassistant/components/ecobee/services.yaml +++ b/homeassistant/components/ecobee/services.yaml @@ -0,0 +1,71 @@ +create_vacation: + description: >- + Create a vacation on the selected thermostat. Note: start/end date and time must all be specified + together for these parameters to have an effect. If start/end date and time are not specified, the + vacation will start immediately and last 14 days (unless deleted earlier). + fields: + entity_id: + description: ecobee thermostat on which to create the vacation (required). + example: "climate.kitchen" + vacation_name: + description: Name of the vacation to create; must be unique on the thermostat (required). + example: "Skiing" + cool_temp: + description: Cooling temperature during the vacation (required). + example: 23 + heat_temp: + description: Heating temperature during the vacation (required). + example: 25 + start_date: + description: >- + Date the vacation starts in the YYYY-MM-DD format (optional, immediately if not provided along with + start_time, end_date, and end_time). + example: "2019-03-15" + start_time: + description: Time the vacation starts, in the local time of the thermostat, in the 24-hour format "HH:MM:SS" + example: "20:00:00" + end_date: + description: >- + Date the vacation ends in the YYYY-MM-DD format (optional, 14 days from now if not provided along with + start_date, start_time, and end_time). + example: "2019-03-20" + end_time: + description: Time the vacation ends, in the local time of the thermostat, in the 24-hour format "HH:MM:SS" + example: "20:00:00" + fan_mode: + description: Fan mode of the thermostat during the vacation (auto or on) (optional, auto if not provided). + example: "on" + fan_min_on_time: + description: Minimum number of minutes to run the fan each hour (0 to 60) during the vacation (optional, 0 if not provided). + example: 30 + +delete_vacation: + description: >- + Delete a vacation on the selected thermostat. + fields: + entity_id: + description: ecobee thermostat on which to delete the vacation (required). + example: "climate.kitchen" + vacation_name: + description: Name of the vacation to delete (required). + example: "Skiing" + +resume_program: + description: Resume the programmed schedule. + fields: + entity_id: + description: Name(s) of entities to change. + example: 'climate.kitchen' + resume_all: + description: Resume all events and return to the scheduled program. This default to false which removes only the top event. + example: true + +set_fan_min_on_time: + description: Set the minimum fan on time. + fields: + entity_id: + description: Name(s) of entities to change. + example: 'climate.kitchen' + fan_min_on_time: + description: New value of fan min on time. + example: 5 diff --git a/homeassistant/components/ecobee/strings.json b/homeassistant/components/ecobee/strings.json new file mode 100644 index 00000000000..9e7e9fed396 --- /dev/null +++ b/homeassistant/components/ecobee/strings.json @@ -0,0 +1,23 @@ +{ + "config": { + "title": "ecobee", + "step": { + "user": { + "title": "ecobee API key", + "description": "Please enter the API key obtained from ecobee.com.", + "data": {"api_key": "API Key"} + }, + "authorize": { + "title": "Authorize app on ecobee.com", + "description": "Please authorize this app at https://www.ecobee.com/consumerportal/index.html with pin code:\n\n{pin}\n\nThen, press Submit." + } + }, + "error": { + "pin_request_failed": "Error requesting PIN from ecobee; please verify API key is correct.", + "token_request_failed": "Error requesting tokens from ecobee; please try again." + }, + "abort": { + "one_instance_only": "This integration currently supports only one ecobee instance." + } + } +} diff --git a/homeassistant/components/ecobee/util.py b/homeassistant/components/ecobee/util.py new file mode 100644 index 00000000000..3acc3e5676d --- /dev/null +++ b/homeassistant/components/ecobee/util.py @@ -0,0 +1,21 @@ +"""Validation utility functions for ecobee services.""" +from datetime import datetime +import voluptuous as vol + + +def ecobee_date(date_string): + """Validate a date_string as valid for the ecobee API.""" + try: + datetime.strptime(date_string, "%Y-%m-%d") + except ValueError: + raise vol.Invalid("Date does not match ecobee date format YYYY-MM-DD") + return date_string + + +def ecobee_time(time_string): + """Validate a time_string as valid for the ecobee API.""" + try: + datetime.strptime(time_string, "%H:%M:%S") + except ValueError: + raise vol.Invalid("Time does not match ecobee 24-hour time format HH:MM:SS") + return time_string diff --git a/homeassistant/components/ecobee/weather.py b/homeassistant/components/ecobee/weather.py index b09e06bd822..6175405638e 100644 --- a/homeassistant/components/ecobee/weather.py +++ b/homeassistant/components/ecobee/weather.py @@ -1,7 +1,8 @@ """Support for displaying weather info from Ecobee API.""" from datetime import datetime -from homeassistant.components import ecobee +from pyecobee.const import ECOBEE_STATE_UNKNOWN + from homeassistant.components.weather import ( ATTR_FORECAST_CONDITION, ATTR_FORECAST_TEMP, @@ -12,33 +13,37 @@ from homeassistant.components.weather import ( ) from homeassistant.const import TEMP_FAHRENHEIT +from .const import DOMAIN + ATTR_FORECAST_TEMP_HIGH = "temphigh" ATTR_FORECAST_PRESSURE = "pressure" ATTR_FORECAST_VISIBILITY = "visibility" ATTR_FORECAST_HUMIDITY = "humidity" -MISSING_DATA = -5002 + +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Old way of setting up the ecobee weather platform.""" + pass -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the Ecobee weather platform.""" - if discovery_info is None: - return +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the ecobee weather platform.""" + data = hass.data[DOMAIN] dev = list() - data = ecobee.NETWORK for index in range(len(data.ecobee.thermostats)): thermostat = data.ecobee.get_thermostat(index) if "weather" in thermostat: - dev.append(EcobeeWeather(thermostat["name"], index)) + dev.append(EcobeeWeather(data, thermostat["name"], index)) - add_entities(dev, True) + async_add_entities(dev, True) class EcobeeWeather(WeatherEntity): """Representation of Ecobee weather data.""" - def __init__(self, name, index): + def __init__(self, data, name, index): """Initialize the Ecobee weather platform.""" + self.data = data self._name = name self._index = index self.weather = None @@ -56,6 +61,11 @@ class EcobeeWeather(WeatherEntity): """Return the name of the sensor.""" return self._name + @property + def unique_id(self): + """Return a unique identifier for the weather platform.""" + return self.data.ecobee.get_thermostat(self._index)["identifier"] + @property def condition(self): """Return the current condition.""" @@ -140,26 +150,25 @@ class EcobeeWeather(WeatherEntity): ATTR_FORECAST_CONDITION: day["condition"], ATTR_FORECAST_TEMP: float(day["tempHigh"]) / 10, } - if day["tempHigh"] == MISSING_DATA: + if day["tempHigh"] == ECOBEE_STATE_UNKNOWN: break - if day["tempLow"] != MISSING_DATA: + if day["tempLow"] != ECOBEE_STATE_UNKNOWN: forecast[ATTR_FORECAST_TEMP_LOW] = float(day["tempLow"]) / 10 - if day["pressure"] != MISSING_DATA: + if day["pressure"] != ECOBEE_STATE_UNKNOWN: forecast[ATTR_FORECAST_PRESSURE] = int(day["pressure"]) - if day["windSpeed"] != MISSING_DATA: + if day["windSpeed"] != ECOBEE_STATE_UNKNOWN: forecast[ATTR_FORECAST_WIND_SPEED] = int(day["windSpeed"]) - if day["visibility"] != MISSING_DATA: + if day["visibility"] != ECOBEE_STATE_UNKNOWN: forecast[ATTR_FORECAST_WIND_SPEED] = int(day["visibility"]) - if day["relativeHumidity"] != MISSING_DATA: + if day["relativeHumidity"] != ECOBEE_STATE_UNKNOWN: forecast[ATTR_FORECAST_HUMIDITY] = int(day["relativeHumidity"]) forecasts.append(forecast) return forecasts except (ValueError, IndexError, KeyError): return None - def update(self): - """Get the latest state of the sensor.""" - data = ecobee.NETWORK - data.update() - thermostat = data.ecobee.get_thermostat(self._index) + async def async_update(self): + """Get the latest weather data.""" + await self.data.update() + thermostat = self.data.ecobee.get_thermostat(self._index) self.weather = thermostat.get("weather", None) diff --git a/homeassistant/components/econet/manifest.json b/homeassistant/components/econet/manifest.json index 3ae6b1eac35..7ce52c021a1 100644 --- a/homeassistant/components/econet/manifest.json +++ b/homeassistant/components/econet/manifest.json @@ -1,7 +1,7 @@ { "domain": "econet", "name": "Econet", - "documentation": "https://www.home-assistant.io/components/econet", + "documentation": "https://www.home-assistant.io/integrations/econet", "requirements": [ "pyeconet==0.0.11" ], diff --git a/homeassistant/components/ecovacs/manifest.json b/homeassistant/components/ecovacs/manifest.json index 4495cb3c2f9..5de2390dd30 100644 --- a/homeassistant/components/ecovacs/manifest.json +++ b/homeassistant/components/ecovacs/manifest.json @@ -1,7 +1,7 @@ { "domain": "ecovacs", "name": "Ecovacs", - "documentation": "https://www.home-assistant.io/components/ecovacs", + "documentation": "https://www.home-assistant.io/integrations/ecovacs", "requirements": [ "sucks==0.9.4" ], diff --git a/homeassistant/components/eddystone_temperature/manifest.json b/homeassistant/components/eddystone_temperature/manifest.json index 4684655aa37..36918fa5ee5 100644 --- a/homeassistant/components/eddystone_temperature/manifest.json +++ b/homeassistant/components/eddystone_temperature/manifest.json @@ -1,7 +1,7 @@ { "domain": "eddystone_temperature", "name": "Eddystone temperature", - "documentation": "https://www.home-assistant.io/components/eddystone_temperature", + "documentation": "https://www.home-assistant.io/integrations/eddystone_temperature", "requirements": [ "beacontools[scan]==1.2.3", "construct==2.9.45" diff --git a/homeassistant/components/eddystone_temperature/sensor.py b/homeassistant/components/eddystone_temperature/sensor.py index 5492582ebed..67724e9fcf3 100644 --- a/homeassistant/components/eddystone_temperature/sensor.py +++ b/homeassistant/components/eddystone_temperature/sensor.py @@ -60,8 +60,8 @@ def setup_platform(hass, config, add_entities, discovery_info=None): if instance is None or namespace is None: _LOGGER.error("Skipping %s", dev_name) continue - else: - devices.append(EddystoneTemp(name, namespace, instance)) + + devices.append(EddystoneTemp(name, namespace, instance)) if devices: mon = Monitor(hass, devices, bt_device_id) diff --git a/homeassistant/components/edimax/manifest.json b/homeassistant/components/edimax/manifest.json index 9fe0e4c50c9..6d1d444d2f4 100644 --- a/homeassistant/components/edimax/manifest.json +++ b/homeassistant/components/edimax/manifest.json @@ -1,7 +1,7 @@ { "domain": "edimax", "name": "Edimax", - "documentation": "https://www.home-assistant.io/components/edimax", + "documentation": "https://www.home-assistant.io/integrations/edimax", "requirements": [ "pyedimax==0.1" ], diff --git a/homeassistant/components/ee_brightbox/manifest.json b/homeassistant/components/ee_brightbox/manifest.json index 967f04228a8..d4ae0c9d6df 100644 --- a/homeassistant/components/ee_brightbox/manifest.json +++ b/homeassistant/components/ee_brightbox/manifest.json @@ -1,7 +1,7 @@ { "domain": "ee_brightbox", "name": "Ee brightbox", - "documentation": "https://www.home-assistant.io/components/ee_brightbox", + "documentation": "https://www.home-assistant.io/integrations/ee_brightbox", "requirements": [ "eebrightbox==0.0.4" ], diff --git a/homeassistant/components/efergy/manifest.json b/homeassistant/components/efergy/manifest.json index f4ca116a647..99b966c6c50 100644 --- a/homeassistant/components/efergy/manifest.json +++ b/homeassistant/components/efergy/manifest.json @@ -1,7 +1,7 @@ { "domain": "efergy", "name": "Efergy", - "documentation": "https://www.home-assistant.io/components/efergy", + "documentation": "https://www.home-assistant.io/integrations/efergy", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/egardia/manifest.json b/homeassistant/components/egardia/manifest.json index 3a95b90db99..b0dfc63d929 100644 --- a/homeassistant/components/egardia/manifest.json +++ b/homeassistant/components/egardia/manifest.json @@ -1,12 +1,8 @@ { "domain": "egardia", "name": "Egardia", - "documentation": "https://www.home-assistant.io/components/egardia", - "requirements": [ - "pythonegardia==1.0.39" - ], + "documentation": "https://www.home-assistant.io/integrations/egardia", + "requirements": ["pythonegardia==1.0.40"], "dependencies": [], - "codeowners": [ - "@jeroenterheerdt" - ] + "codeowners": ["@jeroenterheerdt"] } diff --git a/homeassistant/components/eight_sleep/manifest.json b/homeassistant/components/eight_sleep/manifest.json index 2b008c3c370..3353f1fa4d9 100644 --- a/homeassistant/components/eight_sleep/manifest.json +++ b/homeassistant/components/eight_sleep/manifest.json @@ -1,7 +1,7 @@ { "domain": "eight_sleep", "name": "Eight sleep", - "documentation": "https://www.home-assistant.io/components/eight_sleep", + "documentation": "https://www.home-assistant.io/integrations/eight_sleep", "requirements": [ "pyeight==0.1.1" ], diff --git a/homeassistant/components/eliqonline/manifest.json b/homeassistant/components/eliqonline/manifest.json index 98d94fd009e..0bc242509c3 100644 --- a/homeassistant/components/eliqonline/manifest.json +++ b/homeassistant/components/eliqonline/manifest.json @@ -1,7 +1,7 @@ { "domain": "eliqonline", "name": "Eliqonline", - "documentation": "https://www.home-assistant.io/components/eliqonline", + "documentation": "https://www.home-assistant.io/integrations/eliqonline", "requirements": [ "eliqonline==1.2.2" ], diff --git a/homeassistant/components/elkm1/manifest.json b/homeassistant/components/elkm1/manifest.json index 466f9da7e90..75acab5860d 100644 --- a/homeassistant/components/elkm1/manifest.json +++ b/homeassistant/components/elkm1/manifest.json @@ -1,7 +1,7 @@ { "domain": "elkm1", "name": "Elkm1", - "documentation": "https://www.home-assistant.io/components/elkm1", + "documentation": "https://www.home-assistant.io/integrations/elkm1", "requirements": [ "elkm1-lib==0.7.15" ], diff --git a/homeassistant/components/elv/__init__.py b/homeassistant/components/elv/__init__.py index 13ade253ff6..b6097737414 100644 --- a/homeassistant/components/elv/__init__.py +++ b/homeassistant/components/elv/__init__.py @@ -1 +1,37 @@ """The Elv integration.""" + +import logging + +import voluptuous as vol + +from homeassistant.helpers import discovery +from homeassistant.const import CONF_DEVICE +import homeassistant.helpers.config_validation as cv + +_LOGGER = logging.getLogger(__name__) + +DOMAIN = "elv" + +DEFAULT_DEVICE = "/dev/ttyUSB0" + +ELV_PLATFORMS = ["switch"] + +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: vol.Schema( + {vol.Optional(CONF_DEVICE, default=DEFAULT_DEVICE): cv.string} + ) + }, + extra=vol.ALLOW_EXTRA, +) + + +def setup(hass, config): + """Set up the PCA switch platform.""" + + for platform in ELV_PLATFORMS: + discovery.load_platform( + hass, platform, DOMAIN, {"device": config[DOMAIN][CONF_DEVICE]}, config + ) + + return True diff --git a/homeassistant/components/elv/manifest.json b/homeassistant/components/elv/manifest.json index 4c9ed56352e..b4871a805d2 100644 --- a/homeassistant/components/elv/manifest.json +++ b/homeassistant/components/elv/manifest.json @@ -1,8 +1,8 @@ { "domain": "elv", "name": "ELV PCA", - "documentation": "https://www.home-assistant.io/components/pca", + "documentation": "https://www.home-assistant.io/integrations/pca", "dependencies": [], "codeowners": ["@majuss"], - "requirements": ["pypca==0.0.4"] - } \ No newline at end of file + "requirements": ["pypca==0.0.5"] + } diff --git a/homeassistant/components/elv/switch.py b/homeassistant/components/elv/switch.py index c6258e244e9..362424c7fac 100644 --- a/homeassistant/components/elv/switch.py +++ b/homeassistant/components/elv/switch.py @@ -1,15 +1,11 @@ """Support for PCA 301 smart switch.""" import logging -import voluptuous as vol +import pypca +from serial import SerialException -from homeassistant.components.switch import ( - SwitchDevice, - PLATFORM_SCHEMA, - ATTR_CURRENT_POWER_W, -) -from homeassistant.const import CONF_NAME, CONF_DEVICE, EVENT_HOMEASSISTANT_STOP -import homeassistant.helpers.config_validation as cv +from homeassistant.components.switch import SwitchDevice, ATTR_CURRENT_POWER_W +from homeassistant.const import EVENT_HOMEASSISTANT_STOP _LOGGER = logging.getLogger(__name__) @@ -17,26 +13,20 @@ ATTR_TOTAL_ENERGY_KWH = "total_energy_kwh" DEFAULT_NAME = "PCA 301" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Required(CONF_DEVICE): cv.string, - } -) - def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the PCA switch platform.""" - import pypca - from serial import SerialException - name = config[CONF_NAME] - usb_device = config[CONF_DEVICE] + if discovery_info is None: + return + + serial_device = discovery_info["device"] try: - pca = pypca.PCA(usb_device) + pca = pypca.PCA(serial_device) pca.open() - entities = [SmartPlugSwitch(pca, device, name) for device in pca.get_devices()] + + entities = [SmartPlugSwitch(pca, device) for device in pca.get_devices()] add_entities(entities, True) except SerialException as exc: @@ -51,10 +41,10 @@ def setup_platform(hass, config, add_entities, discovery_info=None): class SmartPlugSwitch(SwitchDevice): """Representation of a PCA Smart Plug switch.""" - def __init__(self, pca, device_id, name): + def __init__(self, pca, device_id): """Initialize the switch.""" self._device_id = device_id - self._name = name + self._name = "PCA 301" self._state = None self._available = True self._emeter_params = {} diff --git a/homeassistant/components/emby/manifest.json b/homeassistant/components/emby/manifest.json index 87688733e59..12dbb152206 100644 --- a/homeassistant/components/emby/manifest.json +++ b/homeassistant/components/emby/manifest.json @@ -1,7 +1,7 @@ { "domain": "emby", "name": "Emby", - "documentation": "https://www.home-assistant.io/components/emby", + "documentation": "https://www.home-assistant.io/integrations/emby", "requirements": [ "pyemby==1.6" ], diff --git a/homeassistant/components/emoncms/manifest.json b/homeassistant/components/emoncms/manifest.json index 90623c01d1b..83833d4f79b 100644 --- a/homeassistant/components/emoncms/manifest.json +++ b/homeassistant/components/emoncms/manifest.json @@ -1,7 +1,7 @@ { "domain": "emoncms", "name": "Emoncms", - "documentation": "https://www.home-assistant.io/components/emoncms", + "documentation": "https://www.home-assistant.io/integrations/emoncms", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/emoncms_history/manifest.json b/homeassistant/components/emoncms_history/manifest.json index 0cb09e3fb73..80d946f4868 100644 --- a/homeassistant/components/emoncms_history/manifest.json +++ b/homeassistant/components/emoncms_history/manifest.json @@ -1,7 +1,7 @@ { "domain": "emoncms_history", "name": "Emoncms history", - "documentation": "https://www.home-assistant.io/components/emoncms_history", + "documentation": "https://www.home-assistant.io/integrations/emoncms_history", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/emulated_hue/manifest.json b/homeassistant/components/emulated_hue/manifest.json index 75fcbc4c555..9b3b00d20b2 100644 --- a/homeassistant/components/emulated_hue/manifest.json +++ b/homeassistant/components/emulated_hue/manifest.json @@ -1,7 +1,7 @@ { "domain": "emulated_hue", "name": "Emulated hue", - "documentation": "https://www.home-assistant.io/components/emulated_hue", + "documentation": "https://www.home-assistant.io/integrations/emulated_hue", "requirements": [ "aiohttp_cors==0.7.0" ], diff --git a/homeassistant/components/emulated_roku/.translations/nn.json b/homeassistant/components/emulated_roku/.translations/nn.json new file mode 100644 index 00000000000..fc349a0d9de --- /dev/null +++ b/homeassistant/components/emulated_roku/.translations/nn.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "EmulatedRoku" + } +} \ No newline at end of file diff --git a/homeassistant/components/emulated_roku/.translations/ru.json b/homeassistant/components/emulated_roku/.translations/ru.json index c7b85c19592..32bf473ac38 100644 --- a/homeassistant/components/emulated_roku/.translations/ru.json +++ b/homeassistant/components/emulated_roku/.translations/ru.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "name_exists": "\u0418\u043c\u044f \u0443\u0436\u0435 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u0435\u0442" + "name_exists": "\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." }, "step": { "user": { diff --git a/homeassistant/components/emulated_roku/manifest.json b/homeassistant/components/emulated_roku/manifest.json index ba68ce94951..824e5bef7c8 100644 --- a/homeassistant/components/emulated_roku/manifest.json +++ b/homeassistant/components/emulated_roku/manifest.json @@ -2,7 +2,7 @@ "domain": "emulated_roku", "name": "Emulated roku", "config_flow": true, - "documentation": "https://www.home-assistant.io/components/emulated_roku", + "documentation": "https://www.home-assistant.io/integrations/emulated_roku", "requirements": [ "emulated_roku==0.1.8" ], diff --git a/homeassistant/components/enigma2/manifest.json b/homeassistant/components/enigma2/manifest.json index d523bd72b72..870681fd4a5 100644 --- a/homeassistant/components/enigma2/manifest.json +++ b/homeassistant/components/enigma2/manifest.json @@ -1,7 +1,7 @@ { "domain": "enigma2", "name": "Enigma2", - "documentation": "https://www.home-assistant.io/components/enigma2", + "documentation": "https://www.home-assistant.io/integrations/enigma2", "requirements": [ "openwebifpy==3.1.1" ], diff --git a/homeassistant/components/enocean/manifest.json b/homeassistant/components/enocean/manifest.json index e6f1c5d7826..4dffbabd219 100644 --- a/homeassistant/components/enocean/manifest.json +++ b/homeassistant/components/enocean/manifest.json @@ -1,7 +1,7 @@ { "domain": "enocean", "name": "Enocean", - "documentation": "https://www.home-assistant.io/components/enocean", + "documentation": "https://www.home-assistant.io/integrations/enocean", "requirements": [ "enocean==0.50" ], diff --git a/homeassistant/components/enphase_envoy/manifest.json b/homeassistant/components/enphase_envoy/manifest.json index 86d2d69cf9b..5b5f94f7c8c 100644 --- a/homeassistant/components/enphase_envoy/manifest.json +++ b/homeassistant/components/enphase_envoy/manifest.json @@ -1,7 +1,7 @@ { "domain": "enphase_envoy", "name": "Enphase envoy", - "documentation": "https://www.home-assistant.io/components/enphase_envoy", + "documentation": "https://www.home-assistant.io/integrations/enphase_envoy", "requirements": [ "envoy_reader==0.8.6" ], diff --git a/homeassistant/components/enphase_envoy/sensor.py b/homeassistant/components/enphase_envoy/sensor.py index 2cc46632dda..13784e24d77 100644 --- a/homeassistant/components/enphase_envoy/sensor.py +++ b/homeassistant/components/enphase_envoy/sensor.py @@ -9,6 +9,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.const import ( CONF_IP_ADDRESS, CONF_MONITORED_CONDITIONS, + CONF_NAME, POWER_WATT, ENERGY_WATT_HOUR, ) @@ -44,6 +45,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( vol.Optional(CONF_MONITORED_CONDITIONS, default=list(SENSORS)): vol.All( cv.ensure_list, [vol.In(list(SENSORS))] ), + vol.Optional(CONF_NAME, default=""): cv.string, } ) @@ -54,6 +56,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= ip_address = config[CONF_IP_ADDRESS] monitored_conditions = config[CONF_MONITORED_CONDITIONS] + name = config[CONF_NAME] entities = [] # Iterate through the list of sensors @@ -66,14 +69,17 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= Envoy( ip_address, condition, - "{} {}".format(SENSORS[condition][0], inverter), + f"{name}{SENSORS[condition][0]} {inverter}", SENSORS[condition][1], ) ) else: entities.append( Envoy( - ip_address, condition, SENSORS[condition][0], SENSORS[condition][1] + ip_address, + condition, + f"{name}{SENSORS[condition][0]}", + SENSORS[condition][1], ) ) async_add_entities(entities) diff --git a/homeassistant/components/entur_public_transport/manifest.json b/homeassistant/components/entur_public_transport/manifest.json index b2b60cff95a..b0910f16536 100644 --- a/homeassistant/components/entur_public_transport/manifest.json +++ b/homeassistant/components/entur_public_transport/manifest.json @@ -1,7 +1,7 @@ { "domain": "entur_public_transport", "name": "Entur public transport", - "documentation": "https://www.home-assistant.io/components/entur_public_transport", + "documentation": "https://www.home-assistant.io/integrations/entur_public_transport", "requirements": [ "enturclient==0.2.0" ], diff --git a/homeassistant/components/environment_canada/manifest.json b/homeassistant/components/environment_canada/manifest.json index 0625fd4c27f..c62e1e356b6 100644 --- a/homeassistant/components/environment_canada/manifest.json +++ b/homeassistant/components/environment_canada/manifest.json @@ -1,9 +1,9 @@ { "domain": "environment_canada", "name": "Environment Canada", - "documentation": "https://www.home-assistant.io/components/environment_canada", + "documentation": "https://www.home-assistant.io/integrations/environment_canada", "requirements": [ - "env_canada==0.0.24" + "env_canada==0.0.25" ], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/environment_canada/sensor.py b/homeassistant/components/environment_canada/sensor.py index 2413edaebce..244fda61656 100644 --- a/homeassistant/components/environment_canada/sensor.py +++ b/homeassistant/components/environment_canada/sensor.py @@ -68,7 +68,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): ec_data = ECData(coordinates=(lat, lon), language=config.get(CONF_LANGUAGE)) sensor_list = list(ec_data.conditions.keys()) + list(ec_data.alerts.keys()) - sensor_list.remove("icon_code") add_entities([ECSensor(sensor_type, ec_data) for sensor_type in sensor_list], True) diff --git a/homeassistant/components/envirophat/manifest.json b/homeassistant/components/envirophat/manifest.json index c69a66d43f8..ddf69d0d417 100644 --- a/homeassistant/components/envirophat/manifest.json +++ b/homeassistant/components/envirophat/manifest.json @@ -1,7 +1,7 @@ { "domain": "envirophat", "name": "Envirophat", - "documentation": "https://www.home-assistant.io/components/envirophat", + "documentation": "https://www.home-assistant.io/integrations/envirophat", "requirements": [ "envirophat==0.0.6", "smbus-cffi==0.5.1" diff --git a/homeassistant/components/envisalink/manifest.json b/homeassistant/components/envisalink/manifest.json index b34aa08951c..6c5405c75ea 100644 --- a/homeassistant/components/envisalink/manifest.json +++ b/homeassistant/components/envisalink/manifest.json @@ -1,7 +1,7 @@ { "domain": "envisalink", "name": "Envisalink", - "documentation": "https://www.home-assistant.io/components/envisalink", + "documentation": "https://www.home-assistant.io/integrations/envisalink", "requirements": [ "pyenvisalink==3.8" ], diff --git a/homeassistant/components/ephember/manifest.json b/homeassistant/components/ephember/manifest.json index 3fed307aed5..7509e627621 100644 --- a/homeassistant/components/ephember/manifest.json +++ b/homeassistant/components/ephember/manifest.json @@ -1,7 +1,7 @@ { "domain": "ephember", "name": "Ephember", - "documentation": "https://www.home-assistant.io/components/ephember", + "documentation": "https://www.home-assistant.io/integrations/ephember", "requirements": [ "pyephember==0.2.0" ], diff --git a/homeassistant/components/epson/manifest.json b/homeassistant/components/epson/manifest.json index e6623b83013..22055e347af 100644 --- a/homeassistant/components/epson/manifest.json +++ b/homeassistant/components/epson/manifest.json @@ -1,7 +1,7 @@ { "domain": "epson", "name": "Epson", - "documentation": "https://www.home-assistant.io/components/epson", + "documentation": "https://www.home-assistant.io/integrations/epson", "requirements": [ "epson-projector==0.1.3" ], diff --git a/homeassistant/components/epsonworkforce/manifest.json b/homeassistant/components/epsonworkforce/manifest.json index 21f76c3a31f..d73b331d5f0 100644 --- a/homeassistant/components/epsonworkforce/manifest.json +++ b/homeassistant/components/epsonworkforce/manifest.json @@ -1,7 +1,7 @@ { "domain": "epsonworkforce", "name": "Epson Workforce", - "documentation": "https://www.home-assistant.io/components/epsonworkforce", + "documentation": "https://www.home-assistant.io/integrations/epsonworkforce", "dependencies": [], "codeowners": ["@ThaStealth"], "requirements": ["epsonprinter==0.0.9"] diff --git a/homeassistant/components/eq3btsmart/manifest.json b/homeassistant/components/eq3btsmart/manifest.json index 6d13c79bcec..1065b94c12a 100644 --- a/homeassistant/components/eq3btsmart/manifest.json +++ b/homeassistant/components/eq3btsmart/manifest.json @@ -1,7 +1,7 @@ { "domain": "eq3btsmart", "name": "Eq3btsmart", - "documentation": "https://www.home-assistant.io/components/eq3btsmart", + "documentation": "https://www.home-assistant.io/integrations/eq3btsmart", "requirements": [ "construct==2.9.45", "python-eq3bt==0.1.9" diff --git a/homeassistant/components/esphome/.translations/es.json b/homeassistant/components/esphome/.translations/es.json index 70d766cf4c0..be8033f316a 100644 --- a/homeassistant/components/esphome/.translations/es.json +++ b/homeassistant/components/esphome/.translations/es.json @@ -8,7 +8,7 @@ "invalid_password": "\u00a1Contrase\u00f1a incorrecta!", "resolve_error": "No se puede resolver la direcci\u00f3n de ESP. Si el error persiste, configura una direcci\u00f3n IP est\u00e1tica: https://esphomelib.com/esphomeyaml/components/wifi.html#manual-ips" }, - "flow_title": "Desplom\u00e9: {name}", + "flow_title": "ESPHome: {name}", "step": { "authenticate": { "data": { diff --git a/homeassistant/components/esphome/.translations/nn.json b/homeassistant/components/esphome/.translations/nn.json index 830391f58f6..5e40c8ec5e5 100644 --- a/homeassistant/components/esphome/.translations/nn.json +++ b/homeassistant/components/esphome/.translations/nn.json @@ -1,9 +1,14 @@ { "config": { + "flow_title": "ESPHome: {name}", "step": { "discovery_confirm": { "title": "Fann ESPhome node" + }, + "user": { + "title": "ESPHome" } - } + }, + "title": "ESPHome" } } \ No newline at end of file diff --git a/homeassistant/components/esphome/.translations/ru.json b/homeassistant/components/esphome/.translations/ru.json index 1405112c070..62d24662ab6 100644 --- a/homeassistant/components/esphome/.translations/ru.json +++ b/homeassistant/components/esphome/.translations/ru.json @@ -1,7 +1,7 @@ { "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." }, "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/cover.py b/homeassistant/components/esphome/cover.py index 7da2fcee380..31b895b4eb2 100644 --- a/homeassistant/components/esphome/cover.py +++ b/homeassistant/components/esphome/cover.py @@ -91,11 +91,11 @@ class EsphomeCover(EsphomeEntity, CoverDevice): return self._state.current_operation == CoverOperation.IS_CLOSING @esphome_state_property - def current_cover_position(self) -> Optional[float]: + def current_cover_position(self) -> Optional[int]: """Return current position of cover. 0 is closed, 100 is open.""" if not self._static_info.supports_position: return None - return self._state.position * 100.0 + return round(self._state.position * 100.0) @esphome_state_property def current_cover_tilt_position(self) -> Optional[float]: diff --git a/homeassistant/components/esphome/light.py b/homeassistant/components/esphome/light.py index e455d5581d1..1205521706e 100644 --- a/homeassistant/components/esphome/light.py +++ b/homeassistant/components/esphome/light.py @@ -74,7 +74,7 @@ class EsphomeLight(EsphomeEntity, Light): red, green, blue = color_util.color_hsv_to_RGB(hue, sat, 100) data["rgb"] = (red / 255, green / 255, blue / 255) if ATTR_FLASH in kwargs: - data["flash"] = FLASH_LENGTHS[kwargs[ATTR_FLASH]] + data["flash_length"] = FLASH_LENGTHS[kwargs[ATTR_FLASH]] if ATTR_TRANSITION in kwargs: data["transition_length"] = kwargs[ATTR_TRANSITION] if ATTR_BRIGHTNESS in kwargs: diff --git a/homeassistant/components/esphome/manifest.json b/homeassistant/components/esphome/manifest.json index 43987cce2c9..bde64762121 100644 --- a/homeassistant/components/esphome/manifest.json +++ b/homeassistant/components/esphome/manifest.json @@ -2,7 +2,7 @@ "domain": "esphome", "name": "ESPHome", "config_flow": true, - "documentation": "https://www.home-assistant.io/components/esphome", + "documentation": "https://www.home-assistant.io/integrations/esphome", "requirements": [ "aioesphomeapi==2.2.0" ], diff --git a/homeassistant/components/essent/manifest.json b/homeassistant/components/essent/manifest.json index aeb3b48311e..914c8f1556f 100644 --- a/homeassistant/components/essent/manifest.json +++ b/homeassistant/components/essent/manifest.json @@ -1,7 +1,7 @@ { "domain": "essent", "name": "Essent", - "documentation": "https://www.home-assistant.io/components/essent", + "documentation": "https://www.home-assistant.io/integrations/essent", "requirements": ["PyEssent==0.13"], "dependencies": [], "codeowners": ["@TheLastProject"] diff --git a/homeassistant/components/etherscan/manifest.json b/homeassistant/components/etherscan/manifest.json index 452d1c4c475..f0abf8c7de0 100644 --- a/homeassistant/components/etherscan/manifest.json +++ b/homeassistant/components/etherscan/manifest.json @@ -1,7 +1,7 @@ { "domain": "etherscan", "name": "Etherscan", - "documentation": "https://www.home-assistant.io/components/etherscan", + "documentation": "https://www.home-assistant.io/integrations/etherscan", "requirements": [ "python-etherscan-api==0.0.3" ], diff --git a/homeassistant/components/eufy/manifest.json b/homeassistant/components/eufy/manifest.json index ec7f1fe7072..92a91976500 100644 --- a/homeassistant/components/eufy/manifest.json +++ b/homeassistant/components/eufy/manifest.json @@ -1,7 +1,7 @@ { "domain": "eufy", "name": "Eufy", - "documentation": "https://www.home-assistant.io/components/eufy", + "documentation": "https://www.home-assistant.io/integrations/eufy", "requirements": [ "lakeside==0.12" ], diff --git a/homeassistant/components/everlights/manifest.json b/homeassistant/components/everlights/manifest.json index 9c2e1b2ae4f..53e2dbf2cb4 100644 --- a/homeassistant/components/everlights/manifest.json +++ b/homeassistant/components/everlights/manifest.json @@ -1,7 +1,7 @@ { "domain": "everlights", "name": "Everlights", - "documentation": "https://www.home-assistant.io/components/everlights", + "documentation": "https://www.home-assistant.io/integrations/everlights", "requirements": [ "pyeverlights==0.1.0" ], diff --git a/homeassistant/components/evohome/__init__.py b/homeassistant/components/evohome/__init__.py index ba7a72024ed..14bf1223953 100644 --- a/homeassistant/components/evohome/__init__.py +++ b/homeassistant/components/evohome/__init__.py @@ -4,6 +4,7 @@ Such systems include evohome (multi-zone), and Round Thermostat (single zone). """ from datetime import datetime, timedelta import logging +import re from typing import Any, Dict, Optional, Tuple import aiohttp.client_exceptions @@ -25,9 +26,9 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.discovery import async_load_platform from homeassistant.helpers.entity import Entity from homeassistant.helpers.typing import ConfigType, HomeAssistantType -from homeassistant.util.dt import parse_datetime, utcnow +import homeassistant.util.dt as dt_util -from .const import DOMAIN, STORAGE_VERSION, STORAGE_KEY, GWS, TCS +from .const import DOMAIN, EVO_FOLLOW, STORAGE_VERSION, STORAGE_KEY, GWS, TCS _LOGGER = logging.getLogger(__name__) @@ -55,20 +56,45 @@ CONFIG_SCHEMA = vol.Schema( ) -def _local_dt_to_utc(dt_naive: datetime) -> datetime: - dt_aware = utcnow() + (dt_naive - datetime.now()) +def _local_dt_to_aware(dt_naive: datetime) -> datetime: + dt_aware = dt_util.now() + (dt_naive - datetime.now()) if dt_aware.microsecond >= 500000: dt_aware += timedelta(seconds=1) return dt_aware.replace(microsecond=0) -def _utc_to_local_dt(dt_aware: datetime) -> datetime: - dt_naive = datetime.now() + (dt_aware - utcnow()) +def _dt_to_local_naive(dt_aware: datetime) -> datetime: + dt_naive = datetime.now() + (dt_aware - dt_util.now()) if dt_naive.microsecond >= 500000: dt_naive += timedelta(seconds=1) return dt_naive.replace(microsecond=0) +def convert_until(status_dict, until_key) -> str: + """Convert datetime string from "%Y-%m-%dT%H:%M:%SZ" to local/aware/isoformat.""" + if until_key in status_dict: # only present for certain modes + dt_utc_naive = dt_util.parse_datetime(status_dict[until_key]) + status_dict[until_key] = dt_util.as_local(dt_utc_naive).isoformat() + + +def convert_dict(dictionary: Dict[str, Any]) -> Dict[str, Any]: + """Recursively convert a dict's keys to snake_case.""" + + def convert_key(key: str) -> str: + """Convert a string to snake_case.""" + string = re.sub(r"[\-\.\s]", "_", str(key)) + return (string[0]).lower() + re.sub( + r"[A-Z]", lambda matched: "_" + matched.group(0).lower(), string[1:] + ) + + return { + (convert_key(k) if isinstance(k, str) else k): ( + convert_dict(v) if isinstance(v, dict) else v + ) + for k, v in dictionary.items() + } + + def _handle_exception(err) -> bool: try: raise err @@ -135,7 +161,7 @@ class EvoBroker: """Container for evohome client and data.""" def __init__(self, hass, params) -> None: - """Initialize the evohome client and data structure.""" + """Initialize the evohome client and its data structure.""" self.hass = hass self.params = params self.config = {} @@ -157,7 +183,7 @@ class EvoBroker: # evohomeasync2 uses naive/local datetimes if access_token_expires is not None: - access_token_expires = _utc_to_local_dt(access_token_expires) + access_token_expires = _dt_to_local_naive(access_token_expires) client = self.client = evohomeasync2.EvohomeClient( self.params[CONF_USERNAME], @@ -220,7 +246,7 @@ class EvoBroker: access_token = app_storage.get(CONF_ACCESS_TOKEN) at_expires_str = app_storage.get(CONF_ACCESS_TOKEN_EXPIRES) if at_expires_str: - at_expires_dt = parse_datetime(at_expires_str) + at_expires_dt = dt_util.parse_datetime(at_expires_str) else: at_expires_dt = None @@ -230,7 +256,7 @@ class EvoBroker: async def _save_auth_tokens(self, *args) -> None: # evohomeasync2 uses naive/local datetimes - access_token_expires = _local_dt_to_utc(self.client.access_token_expires) + access_token_expires = _local_dt_to_aware(self.client.access_token_expires) self._app_storage[CONF_USERNAME] = self.params[CONF_USERNAME] self._app_storage[CONF_REFRESH_TOKEN] = self.client.refresh_token @@ -246,11 +272,11 @@ class EvoBroker: ) async def update(self, *args, **kwargs) -> None: - """Get the latest state data of the entire evohome Location. + """Get the latest state data of an entire evohome Location. - This includes state data for the Controller and all its child devices, - such as the operating mode of the Controller and the current temp of - its children (e.g. Zones, DHW controller). + This includes state data for a Controller and all its child devices, such as the + operating mode of the Controller and the current temp of its children (e.g. + Zones, DHW controller). """ loc_idx = self.params[CONF_LOCATION_IDX] @@ -260,9 +286,7 @@ class EvoBroker: _handle_exception(err) else: # inform the evohome devices that state data has been updated - self.hass.helpers.dispatcher.async_dispatcher_send( - DOMAIN, {"signal": "refresh"} - ) + self.hass.helpers.dispatcher.async_dispatcher_send(DOMAIN) _LOGGER.debug("Status = %s", status[GWS][0][TCS][0]) @@ -270,8 +294,8 @@ class EvoBroker: class EvoDevice(Entity): """Base for any evohome device. - This includes the Controller, (up to 12) Heating Zones and - (optionally) a DHW controller. + This includes the Controller, (up to 12) Heating Zones and (optionally) a + DHW controller. """ def __init__(self, evo_broker, evo_device) -> None: @@ -280,72 +304,26 @@ class EvoDevice(Entity): self._evo_broker = evo_broker self._evo_tcs = evo_broker.tcs - self._name = self._icon = self._precision = None - self._state_attributes = [] + self._unique_id = self._name = self._icon = self._precision = None + self._device_state_attrs = {} + self._state_attributes = [] self._supported_features = None - self._schedule = {} @callback - def _refresh(self, packet): - if packet["signal"] == "refresh": - self.async_schedule_update_ha_state(force_refresh=True) - - @property - def setpoints(self) -> Dict[str, Any]: - """Return the current/next setpoints from the schedule. - - Only Zones & DHW controllers (but not the TCS) can have schedules. - """ - if not self._schedule["DailySchedules"]: - return {} - - switchpoints = {} - - day_time = datetime.now() - day_of_week = int(day_time.strftime("%w")) # 0 is Sunday - - # Iterate today's switchpoints until past the current time of day... - day = self._schedule["DailySchedules"][day_of_week] - sp_idx = -1 # last switchpoint of the day before - for i, tmp in enumerate(day["Switchpoints"]): - if day_time.strftime("%H:%M:%S") > tmp["TimeOfDay"]: - sp_idx = i # current setpoint - else: - break - - # Did the current SP start yesterday? Does the next start SP tomorrow? - current_sp_day = -1 if sp_idx == -1 else 0 - next_sp_day = 1 if sp_idx + 1 == len(day["Switchpoints"]) else 0 - - for key, offset, idx in [ - ("current", current_sp_day, sp_idx), - ("next", next_sp_day, (sp_idx + 1) * (1 - next_sp_day)), - ]: - - spt = switchpoints[key] = {} - - sp_date = (day_time + timedelta(days=offset)).strftime("%Y-%m-%d") - day = self._schedule["DailySchedules"][(day_of_week + offset) % 7] - switchpoint = day["Switchpoints"][idx] - - dt_naive = datetime.strptime( - f"{sp_date}T{switchpoint['TimeOfDay']}", "%Y-%m-%dT%H:%M:%S" - ) - - spt["from"] = _local_dt_to_utc(dt_naive).isoformat() - try: - spt["temperature"] = switchpoint["heatSetpoint"] - except KeyError: - spt["state"] = switchpoint["DhwState"] - - return switchpoints + def _refresh(self) -> None: + self.async_schedule_update_ha_state(force_refresh=True) @property def should_poll(self) -> bool: """Evohome entities should not be polled.""" return False + @property + def unique_id(self) -> Optional[str]: + """Return a unique ID.""" + return self._unique_id + @property def name(self) -> str: """Return the name of the Evohome entity.""" @@ -354,15 +332,15 @@ class EvoDevice(Entity): @property def device_state_attributes(self) -> Dict[str, Any]: """Return the Evohome-specific state attributes.""" - status = {} - for attr in self._state_attributes: - if attr != "setpoints": - status[attr] = getattr(self._evo_device, attr) + status = self._device_state_attrs + if "systemModeStatus" in status: + convert_until(status["systemModeStatus"], "timeUntil") + if "setpointStatus" in status: + convert_until(status["setpointStatus"], "until") + if "stateStatus" in status: + convert_until(status["stateStatus"], "until") - if "setpoints" in self._state_attributes: - status["setpoints"] = self.setpoints - - return {"status": status} + return {"status": convert_dict(status)} @property def icon(self) -> str: @@ -388,27 +366,98 @@ class EvoDevice(Entity): """Return the temperature unit to use in the frontend UI.""" return TEMP_CELSIUS - async def _call_client_api(self, api_function) -> None: + async def _call_client_api(self, api_function, refresh=True) -> Any: try: - await api_function + result = await api_function except (aiohttp.ClientError, evohomeasync2.AuthenticationError) as err: - _handle_exception(err) + if not _handle_exception(err): + return - self.hass.helpers.event.async_call_later( - 2, self._evo_broker.update() - ) # call update() in 2 seconds + if refresh is True: + self.hass.helpers.event.async_call_later(1, self._evo_broker.update()) + + return result + + +class EvoChild(EvoDevice): + """Base for any evohome child. + + This includes (up to 12) Heating Zones and (optionally) a DHW controller. + """ + + def __init__(self, evo_broker, evo_device) -> None: + """Initialize a evohome Controller (hub).""" + super().__init__(evo_broker, evo_device) + self._schedule = {} + self._setpoints = {} + + @property + def current_temperature(self) -> Optional[float]: + """Return the current temperature of a Zone.""" + if self._evo_device.temperatureStatus["isAvailable"]: + return self._evo_device.temperatureStatus["temperature"] + return None + + @property + def setpoints(self) -> Dict[str, Any]: + """Return the current/next setpoints from the schedule. + + Only Zones & DHW controllers (but not the TCS) can have schedules. + """ + if not self._schedule["DailySchedules"]: + return {} # no schedule {'DailySchedules': []}, so no scheduled setpoints + + day_time = dt_util.now() + day_of_week = int(day_time.strftime("%w")) # 0 is Sunday + time_of_day = day_time.strftime("%H:%M:%S") + + # Iterate today's switchpoints until past the current time of day... + day = self._schedule["DailySchedules"][day_of_week] + sp_idx = -1 # last switchpoint of the day before + for i, tmp in enumerate(day["Switchpoints"]): + if time_of_day > tmp["TimeOfDay"]: + sp_idx = i # current setpoint + else: + break + + # Did the current SP start yesterday? Does the next start SP tomorrow? + this_sp_day = -1 if sp_idx == -1 else 0 + next_sp_day = 1 if sp_idx + 1 == len(day["Switchpoints"]) else 0 + + for key, offset, idx in [ + ("this", this_sp_day, sp_idx), + ("next", next_sp_day, (sp_idx + 1) * (1 - next_sp_day)), + ]: + sp_date = (day_time + timedelta(days=offset)).strftime("%Y-%m-%d") + day = self._schedule["DailySchedules"][(day_of_week + offset) % 7] + switchpoint = day["Switchpoints"][idx] + + dt_local_aware = _local_dt_to_aware( + dt_util.parse_datetime(f"{sp_date}T{switchpoint['TimeOfDay']}") + ) + + self._setpoints[f"{key}_sp_from"] = dt_local_aware.isoformat() + try: + self._setpoints[f"{key}_sp_temp"] = switchpoint["heatSetpoint"] + except KeyError: + self._setpoints[f"{key}_sp_state"] = switchpoint["DhwState"] + + return self._setpoints async def _update_schedule(self) -> None: - """Get the latest state data.""" - if ( - not self._schedule.get("DailySchedules") - or parse_datetime(self.setpoints["next"]["from"]) < utcnow() - ): - try: - self._schedule = await self._evo_device.schedule() - except (aiohttp.ClientError, evohomeasync2.AuthenticationError) as err: - _handle_exception(err) + """Get the latest schedule.""" + if "DailySchedules" in self._schedule and not self._schedule["DailySchedules"]: + if not self._evo_device.setpointStatus["setpointMode"] == EVO_FOLLOW: + return # avoid unnecessary I/O - there's nothing to update + + self._schedule = await self._call_client_api( + self._evo_device.schedule(), refresh=False + ) async def async_update(self) -> None: """Get the latest state data.""" - await self._update_schedule() + next_sp_from = self._setpoints.get("next_sp_from", "2000-01-01T00:00:00+00:00") + if dt_util.now() >= dt_util.parse_datetime(next_sp_from): + await self._update_schedule() # no schedule, or it's out-of-date + + self._device_state_attrs = {"setpoints": self.setpoints} diff --git a/homeassistant/components/evohome/climate.py b/homeassistant/components/evohome/climate.py index 0264f76f38f..7df2db1b17e 100644 --- a/homeassistant/components/evohome/climate.py +++ b/homeassistant/components/evohome/climate.py @@ -1,7 +1,6 @@ """Support for Climate devices of (EMEA/EU-based) Honeywell TCC systems.""" -from datetime import datetime import logging -from typing import Any, Dict, Optional, List +from typing import Optional, List from homeassistant.components.climate import ClimateDevice from homeassistant.components.climate.const import ( @@ -22,7 +21,7 @@ from homeassistant.const import PRECISION_TENTHS from homeassistant.helpers.typing import ConfigType, HomeAssistantType from homeassistant.util.dt import parse_datetime -from . import CONF_LOCATION_IDX, EvoDevice +from . import CONF_LOCATION_IDX, EvoDevice, EvoChild from .const import ( DOMAIN, EVO_RESET, @@ -61,6 +60,9 @@ EVO_PRESET_TO_HA = { } HA_PRESET_TO_EVO = {v: k for k, v in EVO_PRESET_TO_HA.items()} +STATE_ATTRS_TCS = ["systemId", "activeFaults", "systemModeStatus"] +STATE_ATTRS_ZONES = ["zoneId", "activeFaults", "setpointStatus", "temperatureStatus"] + async def async_setup_platform( hass: HomeAssistantType, config: ConfigType, async_add_entities, discovery_info=None @@ -73,21 +75,20 @@ async def async_setup_platform( loc_idx = broker.params[CONF_LOCATION_IDX] _LOGGER.debug( - "Found Location/Controller, id=%s [%s], name=%s (location_idx=%s)", - broker.tcs.systemId, + "Found the Location/Controller (%s), id=%s, name=%s (location_idx=%s)", broker.tcs.modelType, + broker.tcs.systemId, broker.tcs.location.name, loc_idx, ) - # special case of RoundThermostat (is single zone) - if broker.config["zones"][0]["modelType"] == "RoundModulation": + # special case of RoundModulation/RoundWireless (is a single zone system) + if broker.config["zones"][0]["zoneType"] == "Thermostat": zone = list(broker.tcs.zones.values())[0] _LOGGER.debug( - "Found %s, id=%s [%s], name=%s", - zone.zoneType, - zone.zoneId, + "Found the Thermostat (%s), id=%s, name=%s", zone.modelType, + zone.zoneId, zone.name, ) @@ -99,10 +100,10 @@ async def async_setup_platform( zones = [] for zone in broker.tcs.zones.values(): _LOGGER.debug( - "Found %s, id=%s [%s], name=%s", + "Found a %s (%s), id=%s, name=%s", zone.zoneType, - zone.zoneId, zone.modelType, + zone.zoneId, zone.name, ) zones.append(EvoZone(broker, zone)) @@ -114,63 +115,20 @@ class EvoClimateDevice(EvoDevice, ClimateDevice): """Base for a Honeywell evohome Climate device.""" def __init__(self, evo_broker, evo_device) -> None: - """Initialize the evohome Climate device.""" + """Initialize a Climate device.""" super().__init__(evo_broker, evo_device) self._preset_modes = None - async def _set_temperature( - self, temperature: float, until: Optional[datetime] = None - ) -> None: - """Set a new target temperature for the Zone. - - until == None means indefinitely (i.e. PermanentOverride) - """ - await self._call_client_api( - self._evo_device.set_temperature(temperature, until) - ) - - async def _set_zone_mode(self, op_mode: str) -> None: - """Set a Zone to one of its native EVO_* operating modes. - - Zones inherit their _effective_ operating mode from the Controller. - - Usually, Zones are in 'FollowSchedule' mode, where their setpoints are - a function of their own schedule and the Controller's operating mode, - e.g. 'AutoWithEco' mode means their setpoint is (by default) 3C less - than scheduled. - - However, Zones can _override_ these setpoints, either indefinitely, - 'PermanentOverride' mode, or for a period of time, 'TemporaryOverride', - after which they will revert back to 'FollowSchedule'. - - Finally, some of the Controller's operating modes are _forced_ upon the - Zones, regardless of any override mode, e.g. 'HeatingOff', Zones to - (by default) 5C, and 'Away', Zones to (by default) 12C. - """ - if op_mode == EVO_FOLLOW: - await self._call_client_api(self._evo_device.cancel_temp_override()) - return - - temperature = self._evo_device.setpointStatus["targetHeatTemperature"] - until = None # EVO_PERMOVER - - if op_mode == EVO_TEMPOVER and self._schedule["DailySchedules"]: - await self._update_schedule() - if self._schedule["DailySchedules"]: - until = parse_datetime(self.setpoints["next"]["from"]) - - await self._set_temperature(temperature, until=until) - async def _set_tcs_mode(self, op_mode: str) -> None: - """Set the Controller to any of its native EVO_* operating modes.""" + """Set a Controller to any of its native EVO_* operating modes.""" await self._call_client_api( self._evo_tcs._set_status(op_mode) # pylint: disable=protected-access ) @property def hvac_modes(self) -> List[str]: - """Return the list of available hvac operation modes.""" + """Return a list of available hvac operation modes.""" return list(HA_HVAC_TO_TCS) @property @@ -179,36 +137,24 @@ class EvoClimateDevice(EvoDevice, ClimateDevice): return self._preset_modes -class EvoZone(EvoClimateDevice): +class EvoZone(EvoChild, EvoClimateDevice): """Base for a Honeywell evohome Zone.""" def __init__(self, evo_broker, evo_device) -> None: - """Initialize the evohome Zone.""" + """Initialize a Zone.""" super().__init__(evo_broker, evo_device) + self._unique_id = evo_device.zoneId self._name = evo_device.name self._icon = "mdi:radiator" self._precision = self._evo_device.setpointCapabilities["valueResolution"] - self._state_attributes = [ - "zoneId", - "activeFaults", - "setpointStatus", - "temperatureStatus", - "setpoints", - ] - self._supported_features = SUPPORT_PRESET_MODE | SUPPORT_TARGET_TEMPERATURE self._preset_modes = list(HA_PRESET_TO_EVO) - @property - def available(self) -> bool: - """Return True if entity is available.""" - return self._evo_device.temperatureStatus["isAvailable"] - @property def hvac_mode(self) -> str: - """Return the current operating mode of the evohome Zone.""" + """Return the current operating mode of a Zone.""" if self._evo_tcs.systemModeStatus["mode"] in [EVO_AWAY, EVO_HEATOFF]: return HVAC_MODE_AUTO is_off = self.target_temperature <= self.min_temp @@ -221,24 +167,15 @@ class EvoZone(EvoClimateDevice): return CURRENT_HVAC_OFF if self.target_temperature <= self.min_temp: return CURRENT_HVAC_OFF - if self.target_temperature < self.current_temperature: + if not self._evo_device.temperatureStatus["isAvailable"]: + return None + if self.target_temperature <= self.current_temperature: return CURRENT_HVAC_IDLE return CURRENT_HVAC_HEAT - @property - def current_temperature(self) -> Optional[float]: - """Return the current temperature of the evohome Zone.""" - return ( - self._evo_device.temperatureStatus["temperature"] - if self._evo_device.temperatureStatus["isAvailable"] - else None - ) - @property def target_temperature(self) -> float: - """Return the target temperature of the evohome Zone.""" - if self._evo_tcs.systemModeStatus["mode"] == EVO_HEATOFF: - return self._evo_device.setpointCapabilities["minHeatSetpoint"] + """Return the target temperature of a Zone.""" return self._evo_device.setpointStatus["targetHeatTemperature"] @property @@ -252,7 +189,7 @@ class EvoZone(EvoClimateDevice): @property def min_temp(self) -> float: - """Return the minimum target temperature of a evohome Zone. + """Return the minimum target temperature of a Zone. The default is 5, but is user-configurable within 5-35 (in Celsius). """ @@ -260,7 +197,7 @@ class EvoZone(EvoClimateDevice): @property def max_temp(self) -> float: - """Return the maximum target temperature of a evohome Zone. + """Return the maximum target temperature of a Zone. The default is 35, but is user-configurable within 5-35 (in Celsius). """ @@ -268,26 +205,70 @@ class EvoZone(EvoClimateDevice): async def async_set_temperature(self, **kwargs) -> None: """Set a new target temperature.""" - until = kwargs.get("until") - if until: - until = parse_datetime(until) + temperature = kwargs["temperature"] - await self._set_temperature(kwargs["temperature"], until) + if self._evo_device.setpointStatus["setpointMode"] == EVO_FOLLOW: + await self._update_schedule() + until = parse_datetime(str(self.setpoints.get("next_sp_from"))) + elif self._evo_device.setpointStatus["setpointMode"] == EVO_TEMPOVER: + until = parse_datetime(self._evo_device.setpointStatus["until"]) + else: # EVO_PERMOVER + until = None + + await self._call_client_api( + self._evo_device.set_temperature(temperature, until) + ) async def async_set_hvac_mode(self, hvac_mode: str) -> None: - """Set an operating mode for the Zone.""" - if hvac_mode == HVAC_MODE_OFF: - await self._set_temperature(self.min_temp, until=None) + """Set a Zone to one of its native EVO_* operating modes. + Zones inherit their _effective_ operating mode from their Controller. + + Usually, Zones are in 'FollowSchedule' mode, where their setpoints are a + function of their own schedule and the Controller's operating mode, e.g. + 'AutoWithEco' mode means their setpoint is (by default) 3C less than scheduled. + + However, Zones can _override_ these setpoints, either indefinitely, + 'PermanentOverride' mode, or for a set period of time, 'TemporaryOverride' mode + (after which they will revert back to 'FollowSchedule' mode). + + Finally, some of the Controller's operating modes are _forced_ upon the Zones, + regardless of any override mode, e.g. 'HeatingOff', Zones to (by default) 5C, + and 'Away', Zones to (by default) 12C. + """ + if hvac_mode == HVAC_MODE_OFF: + await self._call_client_api( + self._evo_device.set_temperature(self.min_temp, until=None) + ) else: # HVAC_MODE_HEAT - await self._set_zone_mode(EVO_FOLLOW) + await self._call_client_api(self._evo_device.cancel_temp_override()) async def async_set_preset_mode(self, preset_mode: Optional[str]) -> None: - """Set a new preset mode. + """Set the preset mode; if None, then revert to following the schedule.""" + evo_preset_mode = HA_PRESET_TO_EVO.get(preset_mode, EVO_FOLLOW) - If preset_mode is None, then revert to following the schedule. - """ - await self._set_zone_mode(HA_PRESET_TO_EVO.get(preset_mode, EVO_FOLLOW)) + if evo_preset_mode == EVO_FOLLOW: + await self._call_client_api(self._evo_device.cancel_temp_override()) + return + + temperature = self._evo_device.setpointStatus["targetHeatTemperature"] + + if evo_preset_mode == EVO_TEMPOVER: + await self._update_schedule() + until = parse_datetime(str(self.setpoints.get("next_sp_from"))) + else: # EVO_PERMOVER + until = None + + await self._call_client_api( + self._evo_device.set_temperature(temperature, until) + ) + + async def async_update(self) -> None: + """Get the latest state data for a Zone.""" + await super().async_update() + + for attr in STATE_ATTRS_ZONES: + self._device_state_attrs[attr] = getattr(self._evo_device, attr) class EvoController(EvoClimateDevice): @@ -298,21 +279,20 @@ class EvoController(EvoClimateDevice): """ def __init__(self, evo_broker, evo_device) -> None: - """Initialize the evohome Controller (hub).""" + """Initialize a evohome Controller (hub).""" super().__init__(evo_broker, evo_device) + self._unique_id = evo_device.systemId self._name = evo_device.location.name self._icon = "mdi:thermostat" self._precision = PRECISION_TENTHS - self._state_attributes = ["systemId", "activeFaults", "systemModeStatus"] - self._supported_features = SUPPORT_PRESET_MODE self._preset_modes = list(HA_PRESET_TO_TCS) @property def hvac_mode(self) -> str: - """Return the current operating mode of the evohome Controller.""" + """Return the current operating mode of a Controller.""" tcs_mode = self._evo_tcs.systemModeStatus["mode"] return HVAC_MODE_OFF if tcs_mode == EVO_HEATOFF else HVAC_MODE_HEAT @@ -334,52 +314,53 @@ class EvoController(EvoClimateDevice): """Return the current preset mode, e.g., home, away, temp.""" return TCS_PRESET_TO_HA.get(self._evo_tcs.systemModeStatus["mode"]) - async def async_set_temperature(self, **kwargs) -> None: - """Do nothing. + @property + def min_temp(self) -> float: + """Return None as Controllers don't have a target temperature.""" + return None - The evohome Controller doesn't have a target temperature. - """ - return + @property + def max_temp(self) -> float: + """Return None as Controllers don't have a target temperature.""" + return None + + async def async_set_temperature(self, **kwargs) -> None: + """Raise exception as Controllers don't have a target temperature.""" + raise NotImplementedError("Evohome Controllers don't have target temperatures.") async def async_set_hvac_mode(self, hvac_mode: str) -> None: - """Set an operating mode for the Controller.""" + """Set an operating mode for a Controller.""" await self._set_tcs_mode(HA_HVAC_TO_TCS.get(hvac_mode)) async def async_set_preset_mode(self, preset_mode: Optional[str]) -> None: - """Set a new preset mode. - - If preset_mode is None, then revert to 'Auto' mode. - """ + """Set the preset mode; if None, then revert to 'Auto' mode.""" await self._set_tcs_mode(HA_PRESET_TO_TCS.get(preset_mode, EVO_AUTO)) async def async_update(self) -> None: - """Get the latest state data.""" - return + """Get the latest state data for a Controller.""" + self._device_state_attrs = {} + + attrs = self._device_state_attrs + for attr in STATE_ATTRS_TCS: + if attr == "activeFaults": + attrs["activeSystemFaults"] = getattr(self._evo_tcs, attr) + else: + attrs[attr] = getattr(self._evo_tcs, attr) class EvoThermostat(EvoZone): """Base for a Honeywell Round Thermostat. - Implemented as a combined Controller/Zone. + These are implemented as a combined Controller/Zone. """ def __init__(self, evo_broker, evo_device) -> None: - """Initialize the Round Thermostat.""" + """Initialize the Thermostat.""" super().__init__(evo_broker, evo_device) self._name = evo_broker.tcs.location.name self._preset_modes = [PRESET_AWAY, PRESET_ECO] - @property - def device_state_attributes(self) -> Dict[str, Any]: - """Return the device-specific state attributes.""" - status = super().device_state_attributes["status"] - - status["systemModeStatus"] = self._evo_tcs.systemModeStatus - status["activeFaults"] += self._evo_tcs.activeFaults - - return {"status": status} - @property def hvac_mode(self) -> str: """Return the current operating mode.""" @@ -404,11 +385,19 @@ class EvoThermostat(EvoZone): await self._set_tcs_mode(HA_HVAC_TO_TCS.get(hvac_mode)) async def async_set_preset_mode(self, preset_mode: Optional[str]) -> None: - """Set a new preset mode. - - If preset_mode is None, then revert to following the schedule. - """ + """Set the preset mode; if None, then revert to following the schedule.""" if preset_mode in list(HA_PRESET_TO_TCS): await self._set_tcs_mode(HA_PRESET_TO_TCS.get(preset_mode)) else: - await self._set_zone_mode(HA_PRESET_TO_EVO.get(preset_mode, EVO_FOLLOW)) + await super().async_set_hvac_mode(preset_mode) + + async def async_update(self) -> None: + """Get the latest state data for the Thermostat.""" + await super().async_update() + + attrs = self._device_state_attrs + for attr in STATE_ATTRS_TCS: + if attr == "activeFaults": # self._evo_device also has "activeFaults" + attrs["activeSystemFaults"] = getattr(self._evo_tcs, attr) + else: + attrs[attr] = getattr(self._evo_tcs, attr) diff --git a/homeassistant/components/evohome/const.py b/homeassistant/components/evohome/const.py index a0480d62a10..444671cf82a 100644 --- a/homeassistant/components/evohome/const.py +++ b/homeassistant/components/evohome/const.py @@ -21,5 +21,3 @@ EVO_PERMOVER = "PermanentOverride" # These are used only to help prevent E501 (line too long) violations GWS = "gateways" TCS = "temperatureControlSystems" - -EVO_STRFTIME = "%Y-%m-%dT%H:%M:%SZ" diff --git a/homeassistant/components/evohome/manifest.json b/homeassistant/components/evohome/manifest.json index 32a57cf20b1..5633880be35 100644 --- a/homeassistant/components/evohome/manifest.json +++ b/homeassistant/components/evohome/manifest.json @@ -1,7 +1,7 @@ { "domain": "evohome", "name": "Evohome", - "documentation": "https://www.home-assistant.io/components/evohome", + "documentation": "https://www.home-assistant.io/integrations/evohome", "requirements": [ "evohome-async==0.3.3b4" ], diff --git a/homeassistant/components/evohome/water_heater.py b/homeassistant/components/evohome/water_heater.py index 1b37bc3b2b5..37bdcd82afc 100644 --- a/homeassistant/components/evohome/water_heater.py +++ b/homeassistant/components/evohome/water_heater.py @@ -3,34 +3,40 @@ import logging from typing import List from homeassistant.components.water_heater import ( + SUPPORT_AWAY_MODE, SUPPORT_OPERATION_MODE, WaterHeaterDevice, ) from homeassistant.const import PRECISION_WHOLE, STATE_OFF, STATE_ON +from homeassistant.helpers.typing import ConfigType, HomeAssistantType from homeassistant.util.dt import parse_datetime -from . import EvoDevice -from .const import DOMAIN, EVO_STRFTIME, EVO_FOLLOW, EVO_TEMPOVER, EVO_PERMOVER +from . import EvoChild +from .const import DOMAIN, EVO_FOLLOW, EVO_PERMOVER _LOGGER = logging.getLogger(__name__) -HA_STATE_TO_EVO = {STATE_ON: "On", STATE_OFF: "Off"} -EVO_STATE_TO_HA = {v: k for k, v in HA_STATE_TO_EVO.items()} +STATE_AUTO = "auto" -HA_OPMODE_TO_DHW = {STATE_ON: EVO_FOLLOW, STATE_OFF: EVO_PERMOVER} +HA_STATE_TO_EVO = {STATE_AUTO: "", STATE_ON: "On", STATE_OFF: "Off"} +EVO_STATE_TO_HA = {v: k for k, v in HA_STATE_TO_EVO.items() if k != ""} + +STATE_ATTRS_DHW = ["dhwId", "activeFaults", "stateStatus", "temperatureStatus"] async def async_setup_platform( - hass, config, async_add_entities, discovery_info=None + hass: HomeAssistantType, config: ConfigType, async_add_entities, discovery_info=None ) -> None: - """Create the DHW controller.""" + """Create a DHW controller.""" if discovery_info is None: return broker = hass.data[DOMAIN]["broker"] _LOGGER.debug( - "Found %s, id: %s", broker.tcs.hotwater.zone_type, broker.tcs.hotwater.zoneId + "Found the DHW Controller (%s), id: %s", + broker.tcs.hotwater.zone_type, + broker.tcs.hotwater.zoneId, ) evo_dhw = EvoDHW(broker, broker.tcs.hotwater) @@ -38,63 +44,71 @@ async def async_setup_platform( async_add_entities([evo_dhw], update_before_add=True) -class EvoDHW(EvoDevice, WaterHeaterDevice): +class EvoDHW(EvoChild, WaterHeaterDevice): """Base for a Honeywell evohome DHW controller (aka boiler).""" def __init__(self, evo_broker, evo_device) -> None: - """Initialize the evohome DHW controller.""" + """Initialize a evohome DHW controller.""" super().__init__(evo_broker, evo_device) + self._unique_id = evo_device.dhwId self._name = "DHW controller" self._icon = "mdi:thermometer-lines" self._precision = PRECISION_WHOLE - self._state_attributes = [ - "dhwId", - "activeFaults", - "stateStatus", - "temperatureStatus", - "setpoints", - ] - - self._supported_features = SUPPORT_OPERATION_MODE - self._operation_list = list(HA_OPMODE_TO_DHW) + self._supported_features = SUPPORT_AWAY_MODE | SUPPORT_OPERATION_MODE @property - def available(self) -> bool: - """Return True if entity is available.""" - return self._evo_device.temperatureStatus.get("isAvailable", False) + def state(self): + """Return the current state.""" + return EVO_STATE_TO_HA[self._evo_device.stateStatus["state"]] @property def current_operation(self) -> str: - """Return the current operating mode (On, or Off).""" + """Return the current operating mode (Auto, On, or Off).""" + if self._evo_device.stateStatus["mode"] == EVO_FOLLOW: + return STATE_AUTO return EVO_STATE_TO_HA[self._evo_device.stateStatus["state"]] @property def operation_list(self) -> List[str]: """Return the list of available operations.""" - return self._operation_list + return list(HA_STATE_TO_EVO) @property - def current_temperature(self) -> float: - """Return the current temperature.""" - return self._evo_device.temperatureStatus["temperature"] + def is_away_mode_on(self): + """Return True if away mode is on.""" + is_off = EVO_STATE_TO_HA[self._evo_device.stateStatus["state"]] == STATE_OFF + is_permanent = self._evo_device.stateStatus["mode"] == EVO_PERMOVER + return is_off and is_permanent async def async_set_operation_mode(self, operation_mode: str) -> None: - """Set new operation mode for a DHW controller.""" - op_mode = HA_OPMODE_TO_DHW[operation_mode] + """Set new operation mode for a DHW controller. - state = "" if op_mode == EVO_FOLLOW else HA_STATE_TO_EVO[STATE_OFF] - until = None # EVO_FOLLOW, EVO_PERMOVER - - if op_mode == EVO_TEMPOVER and self._schedule["DailySchedules"]: + Except for Auto, the mode is only until the next SetPoint. + """ + if operation_mode == STATE_AUTO: + await self._call_client_api(self._evo_device.set_dhw_auto()) + else: await self._update_schedule() - if self._schedule["DailySchedules"]: - until = parse_datetime(self.setpoints["next"]["from"]) - until = until.strftime(EVO_STRFTIME) + until = parse_datetime(str(self.setpoints.get("next_sp_from"))) - data = {"Mode": op_mode, "State": state, "UntilTime": until} + if operation_mode == STATE_ON: + await self._call_client_api(self._evo_device.set_dhw_on(until)) + else: # STATE_OFF + await self._call_client_api(self._evo_device.set_dhw_off(until)) - await self._call_client_api( - self._evo_device._set_dhw(data) # pylint: disable=protected-access - ) + async def async_turn_away_mode_on(self): + """Turn away mode on.""" + await self._call_client_api(self._evo_device.set_dhw_off()) + + async def async_turn_away_mode_off(self): + """Turn away mode off.""" + await self._call_client_api(self._evo_device.set_dhw_auto()) + + async def async_update(self) -> None: + """Get the latest state data for a DHW controller.""" + await super().async_update() + + for attr in STATE_ATTRS_DHW: + self._device_state_attrs[attr] = getattr(self._evo_device, attr) diff --git a/homeassistant/components/facebook/manifest.json b/homeassistant/components/facebook/manifest.json index 9632906a25a..930047065c5 100644 --- a/homeassistant/components/facebook/manifest.json +++ b/homeassistant/components/facebook/manifest.json @@ -1,7 +1,7 @@ { "domain": "facebook", "name": "Facebook", - "documentation": "https://www.home-assistant.io/components/facebook", + "documentation": "https://www.home-assistant.io/integrations/facebook", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/facebox/manifest.json b/homeassistant/components/facebox/manifest.json index 4a3eefc135c..2c911eb04ef 100644 --- a/homeassistant/components/facebox/manifest.json +++ b/homeassistant/components/facebox/manifest.json @@ -1,7 +1,7 @@ { "domain": "facebox", "name": "Facebox", - "documentation": "https://www.home-assistant.io/components/facebox", + "documentation": "https://www.home-assistant.io/integrations/facebox", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/fail2ban/manifest.json b/homeassistant/components/fail2ban/manifest.json index fc60658a3f2..6ff0c7be0e4 100644 --- a/homeassistant/components/fail2ban/manifest.json +++ b/homeassistant/components/fail2ban/manifest.json @@ -1,7 +1,7 @@ { "domain": "fail2ban", "name": "Fail2ban", - "documentation": "https://www.home-assistant.io/components/fail2ban", + "documentation": "https://www.home-assistant.io/integrations/fail2ban", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/familyhub/manifest.json b/homeassistant/components/familyhub/manifest.json index 48a73dfb030..e8aa3ab51b3 100644 --- a/homeassistant/components/familyhub/manifest.json +++ b/homeassistant/components/familyhub/manifest.json @@ -1,7 +1,7 @@ { "domain": "familyhub", "name": "Familyhub", - "documentation": "https://www.home-assistant.io/components/familyhub", + "documentation": "https://www.home-assistant.io/integrations/familyhub", "requirements": [ "python-family-hub-local==0.0.2" ], diff --git a/homeassistant/components/fan/manifest.json b/homeassistant/components/fan/manifest.json index 85bb982d2d1..0df3b7a850e 100644 --- a/homeassistant/components/fan/manifest.json +++ b/homeassistant/components/fan/manifest.json @@ -1,7 +1,7 @@ { "domain": "fan", "name": "Fan", - "documentation": "https://www.home-assistant.io/components/fan", + "documentation": "https://www.home-assistant.io/integrations/fan", "requirements": [], "dependencies": [ "group" diff --git a/homeassistant/components/fastdotcom/manifest.json b/homeassistant/components/fastdotcom/manifest.json index f4bf021380c..3655ce22ba7 100644 --- a/homeassistant/components/fastdotcom/manifest.json +++ b/homeassistant/components/fastdotcom/manifest.json @@ -1,7 +1,7 @@ { "domain": "fastdotcom", "name": "Fastdotcom", - "documentation": "https://www.home-assistant.io/components/fastdotcom", + "documentation": "https://www.home-assistant.io/integrations/fastdotcom", "requirements": [ "fastdotcom==0.0.3" ], diff --git a/homeassistant/components/fedex/__init__.py b/homeassistant/components/fedex/__init__.py deleted file mode 100644 index d685ab50372..00000000000 --- a/homeassistant/components/fedex/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""The fedex component.""" diff --git a/homeassistant/components/fedex/manifest.json b/homeassistant/components/fedex/manifest.json deleted file mode 100644 index b34a8b8383e..00000000000 --- a/homeassistant/components/fedex/manifest.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "domain": "fedex", - "name": "Fedex", - "documentation": "https://www.home-assistant.io/components/fedex", - "requirements": [ - "fedexdeliverymanager==1.0.6" - ], - "dependencies": [], - "codeowners": [] -} diff --git a/homeassistant/components/fedex/sensor.py b/homeassistant/components/fedex/sensor.py deleted file mode 100644 index 2f499e52e23..00000000000 --- a/homeassistant/components/fedex/sensor.py +++ /dev/null @@ -1,120 +0,0 @@ -"""Sensor for Fedex packages.""" -import logging -from collections import defaultdict -from datetime import timedelta - -import voluptuous as vol - -import homeassistant.helpers.config_validation as cv -from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import ( - CONF_NAME, - CONF_USERNAME, - CONF_PASSWORD, - ATTR_ATTRIBUTION, - CONF_SCAN_INTERVAL, -) -from homeassistant.helpers.entity import Entity -from homeassistant.util import Throttle -from homeassistant.util import slugify -from homeassistant.util.dt import now, parse_date - -_LOGGER = logging.getLogger(__name__) - -COOKIE = "fedexdeliverymanager_cookies.pickle" - -DOMAIN = "fedex" - -ICON = "mdi:package-variant-closed" - -STATUS_DELIVERED = "delivered" - -SCAN_INTERVAL = timedelta(seconds=1800) - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Optional(CONF_NAME): cv.string, - } -) - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the Fedex platform.""" - import fedexdeliverymanager - - _LOGGER.warning( - "The fedex integration is deprecated and will be removed " - "in Home Assistant 0.100.0. For more information see ADR-0004:" - "https://github.com/home-assistant/architecture/blob/master/adr/0004-webscraping.md" - ) - - name = config.get(CONF_NAME) - update_interval = config.get(CONF_SCAN_INTERVAL, SCAN_INTERVAL) - - try: - cookie = hass.config.path(COOKIE) - session = fedexdeliverymanager.get_session( - config.get(CONF_USERNAME), config.get(CONF_PASSWORD), cookie_path=cookie - ) - except fedexdeliverymanager.FedexError: - _LOGGER.exception("Could not connect to Fedex Delivery Manager") - return False - - add_entities([FedexSensor(session, name, update_interval)], True) - - -class FedexSensor(Entity): - """Fedex Sensor.""" - - def __init__(self, session, name, interval): - """Initialize the sensor.""" - self._session = session - self._name = name - self._attributes = None - self._state = None - self.update = Throttle(interval)(self._update) - - @property - def name(self): - """Return the name of the sensor.""" - return self._name or DOMAIN - - @property - def state(self): - """Return the state of the sensor.""" - return self._state - - @property - def unit_of_measurement(self): - """Return the unit of measurement of this entity, if any.""" - return "packages" - - def _update(self): - """Update device state.""" - import fedexdeliverymanager - - status_counts = defaultdict(int) - for package in fedexdeliverymanager.get_packages(self._session): - status = slugify(package["primary_status"]) - skip = ( - status == STATUS_DELIVERED - and parse_date(package["delivery_date"]) < now().date() - ) - if skip: - continue - status_counts[status] += 1 - self._attributes = {ATTR_ATTRIBUTION: fedexdeliverymanager.ATTRIBUTION} - self._attributes.update(status_counts) - self._state = sum(status_counts.values()) - - @property - def device_state_attributes(self): - """Return the state attributes.""" - return self._attributes - - @property - def icon(self): - """Icon to use in the frontend.""" - return ICON diff --git a/homeassistant/components/feedreader/manifest.json b/homeassistant/components/feedreader/manifest.json index e458d30073e..6ecc9efffb8 100644 --- a/homeassistant/components/feedreader/manifest.json +++ b/homeassistant/components/feedreader/manifest.json @@ -1,7 +1,7 @@ { "domain": "feedreader", "name": "Feedreader", - "documentation": "https://www.home-assistant.io/components/feedreader", + "documentation": "https://www.home-assistant.io/integrations/feedreader", "requirements": [ "feedparser-homeassistant==5.2.2.dev1" ], diff --git a/homeassistant/components/ffmpeg/manifest.json b/homeassistant/components/ffmpeg/manifest.json index 4a3695e7dcc..438891eca92 100644 --- a/homeassistant/components/ffmpeg/manifest.json +++ b/homeassistant/components/ffmpeg/manifest.json @@ -1,7 +1,7 @@ { "domain": "ffmpeg", "name": "Ffmpeg", - "documentation": "https://www.home-assistant.io/components/ffmpeg", + "documentation": "https://www.home-assistant.io/integrations/ffmpeg", "requirements": [ "ha-ffmpeg==2.0" ], diff --git a/homeassistant/components/ffmpeg_motion/manifest.json b/homeassistant/components/ffmpeg_motion/manifest.json index e9a0e7b1014..5b445dd3094 100644 --- a/homeassistant/components/ffmpeg_motion/manifest.json +++ b/homeassistant/components/ffmpeg_motion/manifest.json @@ -1,7 +1,7 @@ { "domain": "ffmpeg_motion", "name": "Ffmpeg motion", - "documentation": "https://www.home-assistant.io/components/ffmpeg_motion", + "documentation": "https://www.home-assistant.io/integrations/ffmpeg_motion", "requirements": [], "dependencies": ["ffmpeg"], "codeowners": [] diff --git a/homeassistant/components/ffmpeg_noise/manifest.json b/homeassistant/components/ffmpeg_noise/manifest.json index 71600b31117..1bb8e7353dc 100644 --- a/homeassistant/components/ffmpeg_noise/manifest.json +++ b/homeassistant/components/ffmpeg_noise/manifest.json @@ -1,7 +1,7 @@ { "domain": "ffmpeg_noise", "name": "Ffmpeg noise", - "documentation": "https://www.home-assistant.io/components/ffmpeg_noise", + "documentation": "https://www.home-assistant.io/integrations/ffmpeg_noise", "requirements": [], "dependencies": ["ffmpeg"], "codeowners": [] diff --git a/homeassistant/components/fibaro/manifest.json b/homeassistant/components/fibaro/manifest.json index 3574e6254de..0a5d1316561 100644 --- a/homeassistant/components/fibaro/manifest.json +++ b/homeassistant/components/fibaro/manifest.json @@ -1,7 +1,7 @@ { "domain": "fibaro", "name": "Fibaro", - "documentation": "https://www.home-assistant.io/components/fibaro", + "documentation": "https://www.home-assistant.io/integrations/fibaro", "requirements": [ "fiblary3==0.1.7" ], diff --git a/homeassistant/components/fido/manifest.json b/homeassistant/components/fido/manifest.json index 343a21ff072..638505d5dd9 100644 --- a/homeassistant/components/fido/manifest.json +++ b/homeassistant/components/fido/manifest.json @@ -1,7 +1,7 @@ { "domain": "fido", "name": "Fido", - "documentation": "https://www.home-assistant.io/components/fido", + "documentation": "https://www.home-assistant.io/integrations/fido", "requirements": [ "pyfido==2.1.1" ], diff --git a/homeassistant/components/file/manifest.json b/homeassistant/components/file/manifest.json index 581b0e14156..07a9cde900f 100644 --- a/homeassistant/components/file/manifest.json +++ b/homeassistant/components/file/manifest.json @@ -1,7 +1,7 @@ { "domain": "file", "name": "File", - "documentation": "https://www.home-assistant.io/components/file", + "documentation": "https://www.home-assistant.io/integrations/file", "requirements": [], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/filesize/manifest.json b/homeassistant/components/filesize/manifest.json index f76bcd27466..14e0a6a487c 100644 --- a/homeassistant/components/filesize/manifest.json +++ b/homeassistant/components/filesize/manifest.json @@ -1,7 +1,7 @@ { "domain": "filesize", "name": "Filesize", - "documentation": "https://www.home-assistant.io/components/filesize", + "documentation": "https://www.home-assistant.io/integrations/filesize", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/filesize/sensor.py b/homeassistant/components/filesize/sensor.py index a4b9bc5cd76..af9375aad05 100644 --- a/homeassistant/components/filesize/sensor.py +++ b/homeassistant/components/filesize/sensor.py @@ -27,8 +27,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): if not hass.config.is_allowed_path(path): _LOGGER.error("Filepath %s is not valid or allowed", path) continue - else: - sensors.append(Filesize(path)) + sensors.append(Filesize(path)) if sensors: add_entities(sensors, True) diff --git a/homeassistant/components/filter/manifest.json b/homeassistant/components/filter/manifest.json index 28f061d26f7..f28007ba552 100644 --- a/homeassistant/components/filter/manifest.json +++ b/homeassistant/components/filter/manifest.json @@ -1,7 +1,7 @@ { "domain": "filter", "name": "Filter", - "documentation": "https://www.home-assistant.io/components/filter", + "documentation": "https://www.home-assistant.io/integrations/filter", "requirements": [], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/fints/manifest.json b/homeassistant/components/fints/manifest.json index e3580676290..1d0879c291a 100644 --- a/homeassistant/components/fints/manifest.json +++ b/homeassistant/components/fints/manifest.json @@ -1,7 +1,7 @@ { "domain": "fints", "name": "Fints", - "documentation": "https://www.home-assistant.io/components/fints", + "documentation": "https://www.home-assistant.io/integrations/fints", "requirements": [ "fints==1.0.1" ], diff --git a/homeassistant/components/fitbit/manifest.json b/homeassistant/components/fitbit/manifest.json index 6a6316d80a3..f550cb75c5d 100644 --- a/homeassistant/components/fitbit/manifest.json +++ b/homeassistant/components/fitbit/manifest.json @@ -1,7 +1,7 @@ { "domain": "fitbit", "name": "Fitbit", - "documentation": "https://www.home-assistant.io/components/fitbit", + "documentation": "https://www.home-assistant.io/integrations/fitbit", "requirements": [ "fitbit==0.3.1" ], diff --git a/homeassistant/components/fixer/manifest.json b/homeassistant/components/fixer/manifest.json index 1e010bb06ed..e6661ca6ce4 100644 --- a/homeassistant/components/fixer/manifest.json +++ b/homeassistant/components/fixer/manifest.json @@ -1,7 +1,7 @@ { "domain": "fixer", "name": "Fixer", - "documentation": "https://www.home-assistant.io/components/fixer", + "documentation": "https://www.home-assistant.io/integrations/fixer", "requirements": [ "fixerio==1.0.0a0" ], diff --git a/homeassistant/components/fleetgo/manifest.json b/homeassistant/components/fleetgo/manifest.json index c37ece4ebd8..eece29e167c 100644 --- a/homeassistant/components/fleetgo/manifest.json +++ b/homeassistant/components/fleetgo/manifest.json @@ -1,7 +1,7 @@ { "domain": "fleetgo", "name": "FleetGO", - "documentation": "https://www.home-assistant.io/components/fleetgo", + "documentation": "https://www.home-assistant.io/integrations/fleetgo", "requirements": [ "ritassist==0.9.2" ], diff --git a/homeassistant/components/flexit/manifest.json b/homeassistant/components/flexit/manifest.json index 0ee0e81143c..311904166d5 100644 --- a/homeassistant/components/flexit/manifest.json +++ b/homeassistant/components/flexit/manifest.json @@ -1,7 +1,7 @@ { "domain": "flexit", "name": "Flexit", - "documentation": "https://www.home-assistant.io/components/flexit", + "documentation": "https://www.home-assistant.io/integrations/flexit", "requirements": [ "pyflexit==0.3" ], diff --git a/homeassistant/components/flic/manifest.json b/homeassistant/components/flic/manifest.json index 827bcb167c3..d0651c7fbe9 100644 --- a/homeassistant/components/flic/manifest.json +++ b/homeassistant/components/flic/manifest.json @@ -1,7 +1,7 @@ { "domain": "flic", "name": "Flic", - "documentation": "https://www.home-assistant.io/components/flic", + "documentation": "https://www.home-assistant.io/integrations/flic", "requirements": [ "pyflic-homeassistant==0.4.dev0" ], diff --git a/homeassistant/components/flock/manifest.json b/homeassistant/components/flock/manifest.json index a5af541eeee..ba6f4b1f43f 100644 --- a/homeassistant/components/flock/manifest.json +++ b/homeassistant/components/flock/manifest.json @@ -1,7 +1,7 @@ { "domain": "flock", "name": "Flock", - "documentation": "https://www.home-assistant.io/components/flock", + "documentation": "https://www.home-assistant.io/integrations/flock", "requirements": [], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/flunearyou/manifest.json b/homeassistant/components/flunearyou/manifest.json index 76053f75081..a5dfaf4027f 100644 --- a/homeassistant/components/flunearyou/manifest.json +++ b/homeassistant/components/flunearyou/manifest.json @@ -1,7 +1,7 @@ { "domain": "flunearyou", "name": "Flunearyou", - "documentation": "https://www.home-assistant.io/components/flunearyou", + "documentation": "https://www.home-assistant.io/integrations/flunearyou", "requirements": [ "pyflunearyou==1.0.3" ], diff --git a/homeassistant/components/flux/manifest.json b/homeassistant/components/flux/manifest.json index 9bf3ba09ce7..e7b0387698d 100644 --- a/homeassistant/components/flux/manifest.json +++ b/homeassistant/components/flux/manifest.json @@ -1,7 +1,7 @@ { "domain": "flux", "name": "Flux", - "documentation": "https://www.home-assistant.io/components/flux", + "documentation": "https://www.home-assistant.io/integrations/flux", "requirements": [], "dependencies": [], "after_dependencies": ["light"], diff --git a/homeassistant/components/flux_led/light.py b/homeassistant/components/flux_led/light.py index 23fdb38aa05..0a95de783fa 100644 --- a/homeassistant/components/flux_led/light.py +++ b/homeassistant/components/flux_led/light.py @@ -5,7 +5,7 @@ import random import voluptuous as vol -from homeassistant.const import CONF_DEVICES, CONF_NAME, CONF_PROTOCOL +from homeassistant.const import CONF_DEVICES, CONF_NAME, CONF_PROTOCOL, ATTR_MODE from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_HS_COLOR, @@ -30,7 +30,6 @@ CONF_CUSTOM_EFFECT = "custom_effect" CONF_COLORS = "colors" CONF_SPEED_PCT = "speed_pct" CONF_TRANSITION = "transition" -ATTR_MODE = "mode" DOMAIN = "flux_led" diff --git a/homeassistant/components/flux_led/manifest.json b/homeassistant/components/flux_led/manifest.json index 0d00275200c..4e5531ab4e3 100644 --- a/homeassistant/components/flux_led/manifest.json +++ b/homeassistant/components/flux_led/manifest.json @@ -1,7 +1,7 @@ { "domain": "flux_led", "name": "Flux led", - "documentation": "https://www.home-assistant.io/components/flux_led", + "documentation": "https://www.home-assistant.io/integrations/flux_led", "requirements": [ "flux_led==0.22" ], diff --git a/homeassistant/components/folder/manifest.json b/homeassistant/components/folder/manifest.json index 7a0bf76e0aa..d4026e7689d 100644 --- a/homeassistant/components/folder/manifest.json +++ b/homeassistant/components/folder/manifest.json @@ -1,7 +1,7 @@ { "domain": "folder", "name": "Folder", - "documentation": "https://www.home-assistant.io/components/folder", + "documentation": "https://www.home-assistant.io/integrations/folder", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/folder_watcher/manifest.json b/homeassistant/components/folder_watcher/manifest.json index 1a5b547e5ff..17c54a39763 100644 --- a/homeassistant/components/folder_watcher/manifest.json +++ b/homeassistant/components/folder_watcher/manifest.json @@ -1,7 +1,7 @@ { "domain": "folder_watcher", "name": "Folder watcher", - "documentation": "https://www.home-assistant.io/components/folder_watcher", + "documentation": "https://www.home-assistant.io/integrations/folder_watcher", "requirements": [ "watchdog==0.8.3" ], diff --git a/homeassistant/components/foobot/manifest.json b/homeassistant/components/foobot/manifest.json index 9ed95597e41..b02149d2bcd 100644 --- a/homeassistant/components/foobot/manifest.json +++ b/homeassistant/components/foobot/manifest.json @@ -1,7 +1,7 @@ { "domain": "foobot", "name": "Foobot", - "documentation": "https://www.home-assistant.io/components/foobot", + "documentation": "https://www.home-assistant.io/integrations/foobot", "requirements": [ "foobot_async==0.3.1" ], diff --git a/homeassistant/components/fortigate/manifest.json b/homeassistant/components/fortigate/manifest.json index 544ea860778..ff063d6b7e2 100644 --- a/homeassistant/components/fortigate/manifest.json +++ b/homeassistant/components/fortigate/manifest.json @@ -1,7 +1,7 @@ { "domain": "fortigate", "name": "Fortigate", - "documentation": "https://www.home-assistant.io/components/fortigate", + "documentation": "https://www.home-assistant.io/integrations/fortigate", "dependencies": [], "codeowners": [ "@kifeo" diff --git a/homeassistant/components/fortios/manifest.json b/homeassistant/components/fortios/manifest.json index a63d4b292ad..4ec5a4fcb2a 100644 --- a/homeassistant/components/fortios/manifest.json +++ b/homeassistant/components/fortios/manifest.json @@ -1,7 +1,7 @@ { "domain": "fortios", "name": "Home Assistant Device Tracker to support FortiOS", - "documentation": "https://www.home-assistant.io/components/fortios/", + "documentation": "https://www.home-assistant.io/integrations/fortios/", "requirements": [ "fortiosapi==0.10.8" ], diff --git a/homeassistant/components/foscam/camera.py b/homeassistant/components/foscam/camera.py index 37f792cec45..63e9956d0df 100644 --- a/homeassistant/components/foscam/camera.py +++ b/homeassistant/components/foscam/camera.py @@ -41,7 +41,7 @@ class FoscamCam(Camera): """Initialize a Foscam camera.""" from libpyfoscam import FoscamCamera - super(FoscamCam, self).__init__() + super().__init__() ip_address = device_info.get(CONF_IP) port = device_info.get(CONF_PORT) diff --git a/homeassistant/components/foscam/manifest.json b/homeassistant/components/foscam/manifest.json index b05aa956b42..b2c44c113ee 100644 --- a/homeassistant/components/foscam/manifest.json +++ b/homeassistant/components/foscam/manifest.json @@ -1,7 +1,7 @@ { "domain": "foscam", "name": "Foscam", - "documentation": "https://www.home-assistant.io/components/foscam", + "documentation": "https://www.home-assistant.io/integrations/foscam", "requirements": [ "libpyfoscam==1.0" ], diff --git a/homeassistant/components/foursquare/manifest.json b/homeassistant/components/foursquare/manifest.json index 84a98ca0336..c488bf790d5 100644 --- a/homeassistant/components/foursquare/manifest.json +++ b/homeassistant/components/foursquare/manifest.json @@ -1,7 +1,7 @@ { "domain": "foursquare", "name": "Foursquare", - "documentation": "https://www.home-assistant.io/components/foursquare", + "documentation": "https://www.home-assistant.io/integrations/foursquare", "requirements": [], "dependencies": [ "http" diff --git a/homeassistant/components/free_mobile/manifest.json b/homeassistant/components/free_mobile/manifest.json index b8a40c3fc1d..19d1ce0aff7 100644 --- a/homeassistant/components/free_mobile/manifest.json +++ b/homeassistant/components/free_mobile/manifest.json @@ -1,7 +1,7 @@ { "domain": "free_mobile", "name": "Free mobile", - "documentation": "https://www.home-assistant.io/components/free_mobile", + "documentation": "https://www.home-assistant.io/integrations/free_mobile", "requirements": [ "freesms==0.1.2" ], diff --git a/homeassistant/components/freebox/manifest.json b/homeassistant/components/freebox/manifest.json index 9ee134d4170..ac507f59c7f 100644 --- a/homeassistant/components/freebox/manifest.json +++ b/homeassistant/components/freebox/manifest.json @@ -1,7 +1,7 @@ { "domain": "freebox", "name": "Freebox", - "documentation": "https://www.home-assistant.io/components/freebox", + "documentation": "https://www.home-assistant.io/integrations/freebox", "requirements": [ "aiofreepybox==0.0.8" ], diff --git a/homeassistant/components/freedns/manifest.json b/homeassistant/components/freedns/manifest.json index 63f929754db..02332c9fb10 100644 --- a/homeassistant/components/freedns/manifest.json +++ b/homeassistant/components/freedns/manifest.json @@ -1,7 +1,7 @@ { "domain": "freedns", "name": "Freedns", - "documentation": "https://www.home-assistant.io/components/freedns", + "documentation": "https://www.home-assistant.io/integrations/freedns", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/fritz/manifest.json b/homeassistant/components/fritz/manifest.json index b2aacbd48ad..e6c1fee2c95 100644 --- a/homeassistant/components/fritz/manifest.json +++ b/homeassistant/components/fritz/manifest.json @@ -1,7 +1,7 @@ { "domain": "fritz", "name": "Fritz", - "documentation": "https://www.home-assistant.io/components/fritz", + "documentation": "https://www.home-assistant.io/integrations/fritz", "requirements": [ "fritzconnection==0.6.5" ], diff --git a/homeassistant/components/fritzbox/manifest.json b/homeassistant/components/fritzbox/manifest.json index 1ed18140bd2..a1e28966568 100644 --- a/homeassistant/components/fritzbox/manifest.json +++ b/homeassistant/components/fritzbox/manifest.json @@ -1,7 +1,7 @@ { "domain": "fritzbox", "name": "Fritzbox", - "documentation": "https://www.home-assistant.io/components/fritzbox", + "documentation": "https://www.home-assistant.io/integrations/fritzbox", "requirements": [ "pyfritzhome==0.4.0" ], diff --git a/homeassistant/components/fritzbox_callmonitor/manifest.json b/homeassistant/components/fritzbox_callmonitor/manifest.json index 19f232ed667..35c27b7ca84 100644 --- a/homeassistant/components/fritzbox_callmonitor/manifest.json +++ b/homeassistant/components/fritzbox_callmonitor/manifest.json @@ -1,7 +1,7 @@ { "domain": "fritzbox_callmonitor", "name": "Fritzbox callmonitor", - "documentation": "https://www.home-assistant.io/components/fritzbox_callmonitor", + "documentation": "https://www.home-assistant.io/integrations/fritzbox_callmonitor", "requirements": [ "fritzconnection==0.6.5" ], diff --git a/homeassistant/components/fritzbox_netmonitor/manifest.json b/homeassistant/components/fritzbox_netmonitor/manifest.json index ac1ce2893e4..88a7ab5a338 100644 --- a/homeassistant/components/fritzbox_netmonitor/manifest.json +++ b/homeassistant/components/fritzbox_netmonitor/manifest.json @@ -1,7 +1,7 @@ { "domain": "fritzbox_netmonitor", "name": "Fritzbox netmonitor", - "documentation": "https://www.home-assistant.io/components/fritzbox_netmonitor", + "documentation": "https://www.home-assistant.io/integrations/fritzbox_netmonitor", "requirements": [ "fritzconnection==0.6.5" ], diff --git a/homeassistant/components/fritzdect/manifest.json b/homeassistant/components/fritzdect/manifest.json index 98d628fe078..e7b59b07138 100644 --- a/homeassistant/components/fritzdect/manifest.json +++ b/homeassistant/components/fritzdect/manifest.json @@ -1,7 +1,7 @@ { "domain": "fritzdect", "name": "Fritzdect", - "documentation": "https://www.home-assistant.io/components/fritzdect", + "documentation": "https://www.home-assistant.io/integrations/fritzdect", "requirements": [ "fritzhome==1.0.4" ], diff --git a/homeassistant/components/fronius/manifest.json b/homeassistant/components/fronius/manifest.json index 8f737e2e1ff..c7e919c95e5 100644 --- a/homeassistant/components/fronius/manifest.json +++ b/homeassistant/components/fronius/manifest.json @@ -1,7 +1,7 @@ { "domain": "fronius", "name": "Fronius", - "documentation": "https://www.home-assistant.io/components/fronius", + "documentation": "https://www.home-assistant.io/integrations/fronius", "requirements": ["pyfronius==0.4.6"], "dependencies": [], "codeowners": ["@nielstron"] diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index 7298ce8c1d0..e46423c8271 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -4,6 +4,7 @@ import logging import mimetypes import os import pathlib +from typing import Any, Dict, Optional, Set, Tuple from aiohttp import web, web_urldispatcher, hdrs import voluptuous as vol @@ -22,7 +23,7 @@ from homeassistant.loader import bind_hass from .storage import async_setup_frontend_storage -# mypy: allow-incomplete-defs, allow-untyped-defs, no-check-untyped-defs +# mypy: allow-untyped-defs, no-check-untyped-defs # Fix mimetypes for borked Windows machines # https://github.com/home-assistant/home-assistant-polymer/issues/3336 @@ -121,19 +122,19 @@ class Panel: """Abstract class for panels.""" # Name of the webcomponent - component_name = None + component_name: Optional[str] = None - # Icon to show in the sidebar (optional) - sidebar_icon = None + # Icon to show in the sidebar + sidebar_icon: Optional[str] = None - # Title to show in the sidebar (optional) - sidebar_title = None + # Title to show in the sidebar + sidebar_title: Optional[str] = None # Url to show the panel in the frontend - frontend_url_path = None + frontend_url_path: Optional[str] = None # Config to pass to the webcomponent - config = None + config: Optional[Dict[str, Any]] = None # If the panel should only be visible to admins require_admin = False @@ -400,7 +401,9 @@ class IndexView(web_urldispatcher.AbstractResource): """Construct url for resource with additional params.""" return URL("/") - async def resolve(self, request: web.Request): + async def resolve( + self, request: web.Request + ) -> Tuple[Optional[web_urldispatcher.UrlMappingMatchInfo], Set[str]]: """Resolve resource. Return (UrlMappingMatchInfo, allowed_methods) pair. @@ -447,7 +450,7 @@ class IndexView(web_urldispatcher.AbstractResource): return tpl - async def get(self, request: web.Request): + async def get(self, request: web.Request) -> web.Response: """Serve the index page for panel pages.""" hass = request.app["hass"] diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index e50989a15df..67a66bc9612 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -1,8 +1,10 @@ { "domain": "frontend", "name": "Home Assistant Frontend", - "documentation": "https://www.home-assistant.io/components/frontend", - "requirements": ["home-assistant-frontend==20190919.1"], + "documentation": "https://www.home-assistant.io/integrations/frontend", + "requirements": [ + "home-assistant-frontend==20191002.2" + ], "dependencies": [ "api", "auth", @@ -12,5 +14,7 @@ "system_log", "websocket_api" ], - "codeowners": ["@home-assistant/frontend"] -} + "codeowners": [ + "@home-assistant/frontend" + ] +} \ No newline at end of file diff --git a/homeassistant/components/frontier_silicon/manifest.json b/homeassistant/components/frontier_silicon/manifest.json index 0e20a509d1f..17e9f973fd6 100644 --- a/homeassistant/components/frontier_silicon/manifest.json +++ b/homeassistant/components/frontier_silicon/manifest.json @@ -1,7 +1,7 @@ { "domain": "frontier_silicon", "name": "Frontier silicon", - "documentation": "https://www.home-assistant.io/components/frontier_silicon", + "documentation": "https://www.home-assistant.io/integrations/frontier_silicon", "requirements": [ "afsapi==0.0.4" ], diff --git a/homeassistant/components/futurenow/manifest.json b/homeassistant/components/futurenow/manifest.json index 5191ab611ac..c1b0cd2c0ff 100644 --- a/homeassistant/components/futurenow/manifest.json +++ b/homeassistant/components/futurenow/manifest.json @@ -1,7 +1,7 @@ { "domain": "futurenow", "name": "Futurenow", - "documentation": "https://www.home-assistant.io/components/futurenow", + "documentation": "https://www.home-assistant.io/integrations/futurenow", "requirements": [ "pyfnip==0.2" ], diff --git a/homeassistant/components/garadget/manifest.json b/homeassistant/components/garadget/manifest.json index d3781f81d04..b86f4e26b11 100644 --- a/homeassistant/components/garadget/manifest.json +++ b/homeassistant/components/garadget/manifest.json @@ -1,7 +1,7 @@ { "domain": "garadget", "name": "Garadget", - "documentation": "https://www.home-assistant.io/components/garadget", + "documentation": "https://www.home-assistant.io/integrations/garadget", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/gc100/manifest.json b/homeassistant/components/gc100/manifest.json index 96d792196ce..5ea7cb2fb41 100644 --- a/homeassistant/components/gc100/manifest.json +++ b/homeassistant/components/gc100/manifest.json @@ -1,7 +1,7 @@ { "domain": "gc100", "name": "Gc100", - "documentation": "https://www.home-assistant.io/components/gc100", + "documentation": "https://www.home-assistant.io/integrations/gc100", "requirements": [ "python-gc100==1.0.3a" ], diff --git a/homeassistant/components/gearbest/manifest.json b/homeassistant/components/gearbest/manifest.json index 39ceca41d08..c8bb89c71a9 100644 --- a/homeassistant/components/gearbest/manifest.json +++ b/homeassistant/components/gearbest/manifest.json @@ -1,7 +1,7 @@ { "domain": "gearbest", "name": "Gearbest", - "documentation": "https://www.home-assistant.io/components/gearbest", + "documentation": "https://www.home-assistant.io/integrations/gearbest", "requirements": [ "gearbest_parser==1.0.7" ], diff --git a/homeassistant/components/geizhals/manifest.json b/homeassistant/components/geizhals/manifest.json index d53bceaa145..0f81ecbf1be 100644 --- a/homeassistant/components/geizhals/manifest.json +++ b/homeassistant/components/geizhals/manifest.json @@ -1,7 +1,7 @@ { "domain": "geizhals", "name": "Geizhals", - "documentation": "https://www.home-assistant.io/components/geizhals", + "documentation": "https://www.home-assistant.io/integrations/geizhals", "requirements": [ "geizhals==0.0.9" ], diff --git a/homeassistant/components/generic/camera.py b/homeassistant/components/generic/camera.py index 307142ed989..01d2fb948ed 100644 --- a/homeassistant/components/generic/camera.py +++ b/homeassistant/components/generic/camera.py @@ -26,7 +26,6 @@ from homeassistant.components.camera import ( ) from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers import config_validation as cv -from homeassistant.util.async_ import run_coroutine_threadsafe _LOGGER = logging.getLogger(__name__) @@ -105,7 +104,7 @@ class GenericCamera(Camera): def camera_image(self): """Return bytes of camera image.""" - return run_coroutine_threadsafe( + return asyncio.run_coroutine_threadsafe( self.async_camera_image(), self.hass.loop ).result() diff --git a/homeassistant/components/generic/manifest.json b/homeassistant/components/generic/manifest.json index e4d3622a562..9d59d5b9919 100644 --- a/homeassistant/components/generic/manifest.json +++ b/homeassistant/components/generic/manifest.json @@ -1,7 +1,7 @@ { "domain": "generic", "name": "Generic", - "documentation": "https://www.home-assistant.io/components/generic", + "documentation": "https://www.home-assistant.io/integrations/generic", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/generic_thermostat/manifest.json b/homeassistant/components/generic_thermostat/manifest.json index 41fb04c8456..283ea3c45fa 100644 --- a/homeassistant/components/generic_thermostat/manifest.json +++ b/homeassistant/components/generic_thermostat/manifest.json @@ -1,7 +1,7 @@ { "domain": "generic_thermostat", "name": "Generic thermostat", - "documentation": "https://www.home-assistant.io/components/generic_thermostat", + "documentation": "https://www.home-assistant.io/integrations/generic_thermostat", "requirements": [], "dependencies": [ "sensor", diff --git a/homeassistant/components/geniushub/__init__.py b/homeassistant/components/geniushub/__init__.py index 45f3f91cd6d..d9f6c877cbc 100644 --- a/homeassistant/components/geniushub/__init__.py +++ b/homeassistant/components/geniushub/__init__.py @@ -1,14 +1,22 @@ """Support for a Genius Hub system.""" from datetime import timedelta import logging -from typing import Awaitable +from typing import Any, Dict, Optional import aiohttp import voluptuous as vol from geniushubclient import GeniusHub -from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_TOKEN, CONF_USERNAME +from homeassistant.const import ( + ATTR_TEMPERATURE, + CONF_HOST, + CONF_MAC, + CONF_PASSWORD, + CONF_TOKEN, + CONF_USERNAME, + TEMP_CELSIUS, +) from homeassistant.core import callback from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -19,39 +27,66 @@ from homeassistant.helpers.dispatcher import ( ) from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import async_track_time_interval +from homeassistant.helpers.typing import ConfigType, HomeAssistantType +import homeassistant.util.dt as dt_util + +ATTR_DURATION = "duration" _LOGGER = logging.getLogger(__name__) DOMAIN = "geniushub" +# temperature is repeated here, as it gives access to high-precision temps +GH_ZONE_ATTRS = ["mode", "temperature", "type", "occupied", "override"] +GH_DEVICE_ATTRS = { + "luminance": "luminance", + "measuredTemperature": "measured_temperature", + "occupancyTrigger": "occupancy_trigger", + "setback": "setback", + "setTemperature": "set_temperature", + "wakeupInterval": "wakeup_interval", +} + SCAN_INTERVAL = timedelta(seconds=60) -_V1_API_SCHEMA = vol.Schema({vol.Required(CONF_TOKEN): cv.string}) -_V3_API_SCHEMA = vol.Schema( +MAC_ADDRESS_REGEXP = r"^([0-9A-F]{2}:){5}([0-9A-F]{2})$" + +V1_API_SCHEMA = vol.Schema( + { + vol.Required(CONF_TOKEN): cv.string, + vol.Required(CONF_MAC): vol.Match(MAC_ADDRESS_REGEXP), + } +) +V3_API_SCHEMA = vol.Schema( { vol.Required(CONF_HOST): cv.string, vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_PASSWORD): cv.string, + vol.Optional(CONF_MAC): vol.Match(MAC_ADDRESS_REGEXP), } ) CONFIG_SCHEMA = vol.Schema( - {DOMAIN: vol.Any(_V3_API_SCHEMA, _V1_API_SCHEMA)}, extra=vol.ALLOW_EXTRA + {DOMAIN: vol.Any(V3_API_SCHEMA, V1_API_SCHEMA)}, extra=vol.ALLOW_EXTRA ) -async def async_setup(hass, hass_config): +async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: """Create a Genius Hub system.""" - kwargs = dict(hass_config[DOMAIN]) + hass.data[DOMAIN] = {} + + kwargs = dict(config[DOMAIN]) if CONF_HOST in kwargs: args = (kwargs.pop(CONF_HOST),) else: args = (kwargs.pop(CONF_TOKEN),) + hub_uid = kwargs.pop(CONF_MAC, None) - hass.data[DOMAIN] = {} - broker = GeniusBroker(hass, args, kwargs) + client = GeniusHub(*args, **kwargs, session=async_get_clientsession(hass)) + + broker = hass.data[DOMAIN]["broker"] = GeniusBroker(hass, client, hub_uid) try: - await broker.client.update() + await client.update() except aiohttp.ClientResponseError as err: _LOGGER.error("Setup failed, check your configuration, %s", err) return False @@ -59,16 +94,8 @@ async def async_setup(hass, hass_config): async_track_time_interval(hass, broker.async_update, SCAN_INTERVAL) - for platform in ["climate", "water_heater"]: - hass.async_create_task( - async_load_platform(hass, platform, DOMAIN, {}, hass_config) - ) - - if broker.client.api_version == 3: # pylint: disable=no-member - for platform in ["sensor", "binary_sensor"]: - hass.async_create_task( - async_load_platform(hass, platform, DOMAIN, {}, hass_config) - ) + for platform in ["climate", "water_heater", "sensor", "binary_sensor"]: + hass.async_create_task(async_load_platform(hass, platform, DOMAIN, {}, config)) return True @@ -76,25 +103,30 @@ async def async_setup(hass, hass_config): class GeniusBroker: """Container for geniushub client and data.""" - def __init__(self, hass, args, kwargs): + def __init__(self, hass, client, hub_uid) -> None: """Initialize the geniushub client.""" self.hass = hass - self.client = hass.data[DOMAIN]["client"] = GeniusHub( - *args, **kwargs, session=async_get_clientsession(hass) - ) + self.client = client + self._hub_uid = hub_uid - async def async_update(self, now, **kwargs): + @property + def hub_uid(self) -> int: + """Return the Hub UID (MAC address).""" + # pylint: disable=no-member + return self._hub_uid if self._hub_uid is not None else self.client.uid + + async def async_update(self, now, **kwargs) -> None: """Update the geniushub client's data.""" try: await self.client.update() except aiohttp.ClientResponseError as err: - _LOGGER.warning("Update failed, %s", err) + _LOGGER.warning("Update failed, message is: %s", err) return self.make_debug_log_entries() async_dispatcher_send(self.hass, DOMAIN) - def make_debug_log_entries(self): + def make_debug_log_entries(self) -> None: """Make any useful debug log entries.""" # pylint: disable=protected-access _LOGGER.debug( @@ -105,13 +137,13 @@ class GeniusBroker: class GeniusEntity(Entity): - """Base for all Genius Hub endtities.""" + """Base for all Genius Hub entities.""" - def __init__(self): + def __init__(self) -> None: """Initialize the entity.""" - self._name = None + self._unique_id = self._name = None - async def async_added_to_hass(self) -> Awaitable[None]: + async def async_added_to_hass(self) -> None: """Set up a listener when this entity is added to HA.""" async_dispatcher_connect(self.hass, DOMAIN, self._refresh) @@ -119,6 +151,11 @@ class GeniusEntity(Entity): def _refresh(self) -> None: self.async_schedule_update_ha_state(force_refresh=True) + @property + def unique_id(self) -> Optional[str]: + """Return a unique ID.""" + return self._unique_id + @property def name(self) -> str: """Return the name of the geniushub entity.""" @@ -128,3 +165,102 @@ class GeniusEntity(Entity): def should_poll(self) -> bool: """Return False as geniushub entities should not be polled.""" return False + + +class GeniusDevice(GeniusEntity): + """Base for all Genius Hub devices.""" + + def __init__(self, broker, device) -> None: + """Initialize the Device.""" + super().__init__() + + self._device = device + self._unique_id = f"{broker.hub_uid}_device_{device.id}" + + self._last_comms = self._state_attr = None + + @property + def device_state_attributes(self) -> Dict[str, Any]: + """Return the device state attributes.""" + + attrs = {} + attrs["assigned_zone"] = self._device.data["assignedZones"][0]["name"] + if self._last_comms: + attrs["last_comms"] = self._last_comms.isoformat() + + state = dict(self._device.data["state"]) + if "_state" in self._device.data: # only for v3 API + state.update(self._device.data["_state"]) + + attrs["state"] = { + GH_DEVICE_ATTRS[k]: v for k, v in state.items() if k in GH_DEVICE_ATTRS + } + + return attrs + + async def async_update(self) -> None: + """Update an entity's state data.""" + if "_state" in self._device.data: # only for v3 API + self._last_comms = dt_util.utc_from_timestamp( + self._device.data["_state"]["lastComms"] + ) + + +class GeniusZone(GeniusEntity): + """Base for all Genius Hub zones.""" + + def __init__(self, broker, zone) -> None: + """Initialize the Zone.""" + super().__init__() + + self._zone = zone + self._unique_id = f"{broker.hub_uid}_device_{zone.id}" + + self._max_temp = self._min_temp = self._supported_features = None + + @property + def name(self) -> str: + """Return the name of the climate device.""" + return self._zone.name + + @property + def device_state_attributes(self) -> Dict[str, Any]: + """Return the device state attributes.""" + status = {k: v for k, v in self._zone.data.items() if k in GH_ZONE_ATTRS} + return {"status": status} + + @property + def current_temperature(self) -> Optional[float]: + """Return the current temperature.""" + return self._zone.data.get("temperature") + + @property + def target_temperature(self) -> float: + """Return the temperature we try to reach.""" + return self._zone.data["setpoint"] + + @property + def min_temp(self) -> float: + """Return max valid temperature that can be set.""" + return self._min_temp + + @property + def max_temp(self) -> float: + """Return max valid temperature that can be set.""" + return self._max_temp + + @property + def temperature_unit(self) -> str: + """Return the unit of measurement.""" + return TEMP_CELSIUS + + @property + def supported_features(self) -> int: + """Return the bitmask of supported features.""" + return self._supported_features + + async def async_set_temperature(self, **kwargs) -> None: + """Set a new target temperature for this zone.""" + await self._zone.set_override( + kwargs[ATTR_TEMPERATURE], kwargs.get(ATTR_DURATION, 3600) + ) diff --git a/homeassistant/components/geniushub/binary_sensor.py b/homeassistant/components/geniushub/binary_sensor.py index 105a03bf757..33458d049a2 100644 --- a/homeassistant/components/geniushub/binary_sensor.py +++ b/homeassistant/components/geniushub/binary_sensor.py @@ -1,52 +1,45 @@ """Support for Genius Hub binary_sensor devices.""" -from typing import Any, Dict - from homeassistant.components.binary_sensor import BinarySensorDevice -from homeassistant.util.dt import utc_from_timestamp +from homeassistant.helpers.typing import ConfigType, HomeAssistantType -from . import DOMAIN, GeniusEntity +from . import DOMAIN, GeniusDevice -GH_IS_SWITCH = ["Dual Channel Receiver", "Electric Switch", "Smart Plug"] +GH_STATE_ATTR = "outputOnOff" -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): +async def async_setup_platform( + hass: HomeAssistantType, config: ConfigType, async_add_entities, discovery_info=None +) -> None: """Set up the Genius Hub sensor entities.""" - client = hass.data[DOMAIN]["client"] + if discovery_info is None: + return + + broker = hass.data[DOMAIN]["broker"] switches = [ - GeniusBinarySensor(d) for d in client.device_objs if d.type[:21] in GH_IS_SWITCH + GeniusBinarySensor(broker, d, GH_STATE_ATTR) + for d in broker.client.device_objs + if GH_STATE_ATTR in d.data["state"] ] - async_add_entities(switches) + async_add_entities(switches, update_before_add=True) -class GeniusBinarySensor(GeniusEntity, BinarySensorDevice): +class GeniusBinarySensor(GeniusDevice, BinarySensorDevice): """Representation of a Genius Hub binary_sensor.""" - def __init__(self, device) -> None: + def __init__(self, broker, device, state_attr) -> None: """Initialize the binary sensor.""" - super().__init__() + super().__init__(broker, device) + + self._state_attr = state_attr - self._device = device if device.type[:21] == "Dual Channel Receiver": - self._name = f"Dual Channel Receiver {device.id}" + self._name = f"{device.type[:21]} {device.id}" else: self._name = f"{device.type} {device.id}" @property def is_on(self) -> bool: """Return the status of the sensor.""" - return self._device.data["state"]["outputOnOff"] - - @property - def device_state_attributes(self) -> Dict[str, Any]: - """Return the device state attributes.""" - attrs = {} - attrs["assigned_zone"] = self._device.data["assignedZones"][0]["name"] - - # pylint: disable=protected-access - last_comms = self._device._raw["childValues"]["lastComms"]["val"] - if last_comms != 0: - attrs["last_comms"] = utc_from_timestamp(last_comms).isoformat() - - return {**attrs} + return self._device.data["state"][self._state_attr] diff --git a/homeassistant/components/geniushub/climate.py b/homeassistant/components/geniushub/climate.py index a856e48438f..f27b1cc7f1a 100644 --- a/homeassistant/components/geniushub/climate.py +++ b/homeassistant/components/geniushub/climate.py @@ -1,5 +1,5 @@ """Support for Genius Hub climate devices.""" -from typing import Any, Awaitable, Dict, Optional, List +from typing import Optional, List from homeassistant.components.climate import ClimateDevice from homeassistant.components.climate.const import ( @@ -10,16 +10,9 @@ from homeassistant.components.climate.const import ( SUPPORT_TARGET_TEMPERATURE, SUPPORT_PRESET_MODE, ) -from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS +from homeassistant.helpers.typing import ConfigType, HomeAssistantType -from . import DOMAIN, GeniusEntity - -ATTR_DURATION = "duration" - -GH_ZONES = ["radiator"] - -# temperature is repeated here, as it gives access to high-precision temps -GH_STATE_ATTRS = ["mode", "temperature", "type", "occupied", "override"] +from . import DOMAIN, GeniusZone # GeniusHub Zones support: Off, Timer, Override/Boost, Footprint & Linked modes HA_HVAC_TO_GH = {HVAC_MODE_OFF: "off", HVAC_MODE_HEAT: "timer"} @@ -28,78 +21,43 @@ GH_HVAC_TO_HA = {v: k for k, v in HA_HVAC_TO_GH.items()} HA_PRESET_TO_GH = {PRESET_ACTIVITY: "footprint", PRESET_BOOST: "override"} GH_PRESET_TO_HA = {v: k for k, v in HA_PRESET_TO_GH.items()} +GH_ZONES = ["radiator", "wet underfloor"] + async def async_setup_platform( - hass, hass_config, async_add_entities, discovery_info=None -): + hass: HomeAssistantType, config: ConfigType, async_add_entities, discovery_info=None +) -> None: """Set up the Genius Hub climate entities.""" - client = hass.data[DOMAIN]["client"] + if discovery_info is None: + return - entities = [ - GeniusClimateZone(z) for z in client.zone_objs if z.data["type"] in GH_ZONES - ] - async_add_entities(entities) + broker = hass.data[DOMAIN]["broker"] + + async_add_entities( + [ + GeniusClimateZone(broker, z) + for z in broker.client.zone_objs + if z.data["type"] in GH_ZONES + ] + ) -class GeniusClimateZone(GeniusEntity, ClimateDevice): +class GeniusClimateZone(GeniusZone, ClimateDevice): """Representation of a Genius Hub climate device.""" - def __init__(self, zone) -> None: + def __init__(self, broker, zone) -> None: """Initialize the climate device.""" - super().__init__() + super().__init__(broker, zone) - self._zone = zone - if hasattr(self._zone, "occupied"): # has a movement sensor - self._preset_modes = list(HA_PRESET_TO_GH) - else: - self._preset_modes = [PRESET_BOOST] - - @property - def name(self) -> str: - """Return the name of the climate device.""" - return self._zone.name - - @property - def device_state_attributes(self) -> Dict[str, Any]: - """Return the device state attributes.""" - tmp = self._zone.data.items() - return {"status": {k: v for k, v in tmp if k in GH_STATE_ATTRS}} + self._max_temp = 28.0 + self._min_temp = 4.0 + self._supported_features = SUPPORT_TARGET_TEMPERATURE | SUPPORT_PRESET_MODE @property def icon(self) -> str: """Return the icon to use in the frontend UI.""" return "mdi:radiator" - @property - def current_temperature(self) -> Optional[float]: - """Return the current temperature.""" - return self._zone.data["temperature"] - - @property - def target_temperature(self) -> float: - """Return the temperature we try to reach.""" - return self._zone.data["setpoint"] - - @property - def min_temp(self) -> float: - """Return max valid temperature that can be set.""" - return 4.0 - - @property - def max_temp(self) -> float: - """Return max valid temperature that can be set.""" - return 28.0 - - @property - def temperature_unit(self) -> str: - """Return the unit of measurement.""" - return TEMP_CELSIUS - - @property - def supported_features(self) -> int: - """Return the list of supported features.""" - return SUPPORT_TARGET_TEMPERATURE | SUPPORT_PRESET_MODE - @property def hvac_mode(self) -> str: """Return hvac operation ie. heat, cool mode.""" @@ -118,18 +76,14 @@ class GeniusClimateZone(GeniusEntity, ClimateDevice): @property def preset_modes(self) -> Optional[List[str]]: """Return a list of available preset modes.""" - return self._preset_modes + if "occupied" in self._zone.data: # if has a movement sensor + return [PRESET_ACTIVITY, PRESET_BOOST] + return [PRESET_BOOST] - async def async_set_temperature(self, **kwargs) -> Awaitable[None]: - """Set a new target temperature for this zone.""" - await self._zone.set_override( - kwargs[ATTR_TEMPERATURE], kwargs.get(ATTR_DURATION, 3600) - ) - - async def async_set_hvac_mode(self, hvac_mode: str) -> Awaitable[None]: + async def async_set_hvac_mode(self, hvac_mode: str) -> None: """Set a new hvac mode.""" await self._zone.set_mode(HA_HVAC_TO_GH.get(hvac_mode)) - async def async_set_preset_mode(self, preset_mode: str) -> Awaitable[None]: + async def async_set_preset_mode(self, preset_mode: str) -> None: """Set a new preset mode.""" await self._zone.set_mode(HA_PRESET_TO_GH.get(preset_mode, "timer")) diff --git a/homeassistant/components/geniushub/manifest.json b/homeassistant/components/geniushub/manifest.json index f2110ffb2f0..96497388a48 100644 --- a/homeassistant/components/geniushub/manifest.json +++ b/homeassistant/components/geniushub/manifest.json @@ -1,9 +1,9 @@ { "domain": "geniushub", "name": "Genius Hub", - "documentation": "https://www.home-assistant.io/components/geniushub", + "documentation": "https://www.home-assistant.io/integrations/geniushub", "requirements": [ - "geniushub-client==0.6.13" + "geniushub-client==0.6.26" ], "dependencies": [], "codeowners": ["@zxdavb"] diff --git a/homeassistant/components/geniushub/sensor.py b/homeassistant/components/geniushub/sensor.py index 82db3d4224e..2f5d9bceb8b 100644 --- a/homeassistant/components/geniushub/sensor.py +++ b/homeassistant/components/geniushub/sensor.py @@ -1,13 +1,14 @@ """Support for Genius Hub sensor devices.""" from datetime import timedelta -from typing import Any, Awaitable, Dict +from typing import Any, Dict from homeassistant.const import DEVICE_CLASS_BATTERY -from homeassistant.util.dt import utc_from_timestamp, utcnow +from homeassistant.helpers.typing import ConfigType, HomeAssistantType +import homeassistant.util.dt as dt_util -from . import DOMAIN, GeniusEntity +from . import DOMAIN, GeniusDevice, GeniusEntity -GH_HAS_BATTERY = ["Room Thermostat", "Genius Valve", "Room Sensor", "Radiator Valve"] +GH_STATE_ATTR = "batteryLevel" GH_LEVEL_MAPPING = { "error": "Errors", @@ -16,42 +17,47 @@ GH_LEVEL_MAPPING = { } -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): +async def async_setup_platform( + hass: HomeAssistantType, config: ConfigType, async_add_entities, discovery_info=None +) -> None: """Set up the Genius Hub sensor entities.""" - client = hass.data[DOMAIN]["client"] + if discovery_info is None: + return - sensors = [GeniusBattery(d) for d in client.device_objs if d.type in GH_HAS_BATTERY] - issues = [GeniusIssue(client, i) for i in list(GH_LEVEL_MAPPING)] + broker = hass.data[DOMAIN]["broker"] + + sensors = [ + GeniusBattery(broker, d, GH_STATE_ATTR) + for d in broker.client.device_objs + if GH_STATE_ATTR in d.data["state"] + ] + issues = [GeniusIssue(broker, i) for i in list(GH_LEVEL_MAPPING)] async_add_entities(sensors + issues, update_before_add=True) -class GeniusBattery(GeniusEntity): +class GeniusBattery(GeniusDevice): """Representation of a Genius Hub sensor.""" - def __init__(self, device) -> None: + def __init__(self, broker, device, state_attr) -> None: """Initialize the sensor.""" - super().__init__() + super().__init__(broker, device) + + self._state_attr = state_attr - self._device = device self._name = f"{device.type} {device.id}" @property def icon(self) -> str: """Return the icon of the sensor.""" + if "_state" in self._device.data: # only for v3 API + interval = timedelta( + seconds=self._device.data["_state"].get("wakeupInterval", 30 * 60) + ) + if self._last_comms < dt_util.utcnow() - interval * 3: + return "mdi:battery-unknown" - values = self._device._raw["childValues"] # pylint: disable=protected-access - - last_comms = utc_from_timestamp(values["lastComms"]["val"]) - if "WakeUp_Interval" in values: - interval = timedelta(seconds=values["WakeUp_Interval"]["val"]) - else: - interval = timedelta(minutes=20) - - if last_comms < utcnow() - interval * 3: - return "mdi:battery-unknown" - - battery_level = self._device.data["state"]["batteryLevel"] + battery_level = self._device.data["state"][self._state_attr] if battery_level == 255: return "mdi:battery-unknown" if battery_level < 40: @@ -76,31 +82,19 @@ class GeniusBattery(GeniusEntity): @property def state(self) -> str: """Return the state of the sensor.""" - level = self._device.data["state"].get("batteryLevel", 255) + level = self._device.data["state"][self._state_attr] return level if level != 255 else 0 - @property - def device_state_attributes(self) -> Dict[str, Any]: - """Return the device state attributes.""" - attrs = {} - attrs["assigned_zone"] = self._device.data["assignedZones"][0]["name"] - - # pylint: disable=protected-access - last_comms = self._device._raw["childValues"]["lastComms"]["val"] - attrs["last_comms"] = utc_from_timestamp(last_comms).isoformat() - - return {**attrs} - class GeniusIssue(GeniusEntity): """Representation of a Genius Hub sensor.""" - def __init__(self, hub, level) -> None: + def __init__(self, broker, level) -> None: """Initialize the sensor.""" super().__init__() - self._hub = hub - self._name = GH_LEVEL_MAPPING[level] + self._hub = broker.client + self._name = f"GeniusHub {GH_LEVEL_MAPPING[level]}" self._level = level self._issues = [] @@ -114,7 +108,7 @@ class GeniusIssue(GeniusEntity): """Return the device state attributes.""" return {f"{self._level}_list": self._issues} - async def async_update(self) -> Awaitable[None]: + async def async_update(self) -> None: """Process the sensor's state data.""" self._issues = [ i["description"] for i in self._hub.issues if i["level"] == self._level diff --git a/homeassistant/components/geniushub/water_heater.py b/homeassistant/components/geniushub/water_heater.py index 1086160e77c..cd4f536e14f 100644 --- a/homeassistant/components/geniushub/water_heater.py +++ b/homeassistant/components/geniushub/water_heater.py @@ -1,27 +1,20 @@ """Support for Genius Hub water_heater devices.""" -from typing import Any, Awaitable, Dict, Optional, List +from typing import List from homeassistant.components.water_heater import ( WaterHeaterDevice, SUPPORT_TARGET_TEMPERATURE, SUPPORT_OPERATION_MODE, ) -from homeassistant.const import ATTR_TEMPERATURE, STATE_OFF, TEMP_CELSIUS +from homeassistant.const import STATE_OFF +from homeassistant.helpers.typing import ConfigType, HomeAssistantType -from . import DOMAIN, GeniusEntity +from . import DOMAIN, GeniusZone STATE_AUTO = "auto" STATE_MANUAL = "manual" -GH_HEATERS = ["hot water temperature"] - -GH_SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE | SUPPORT_OPERATION_MODE -# HA does not have SUPPORT_ON_OFF for water_heater - -GH_MAX_TEMP = 80.0 -GH_MIN_TEMP = 30.0 - -# Genius Hub HW supports only Off, Override/Boost & Timer modes +# Genius Hub HW zones support only Off, Override/Boost & Timer modes HA_OPMODE_TO_GH = {STATE_OFF: "off", STATE_AUTO: "timer", STATE_MANUAL: "override"} GH_STATE_TO_HA = { "off": STATE_OFF, @@ -34,91 +27,49 @@ GH_STATE_TO_HA = { "linked": None, "other": None, } -GH_STATE_ATTRS = ["type", "override"] + +GH_HEATERS = ["hot water temperature"] async def async_setup_platform( - hass, hass_config, async_add_entities, discovery_info=None -): + hass: HomeAssistantType, config: ConfigType, async_add_entities, discovery_info=None +) -> None: """Set up the Genius Hub water_heater entities.""" - client = hass.data[DOMAIN]["client"] + if discovery_info is None: + return - entities = [ - GeniusWaterHeater(z) for z in client.zone_objs if z.data["type"] in GH_HEATERS - ] + broker = hass.data[DOMAIN]["broker"] - async_add_entities(entities) + async_add_entities( + [ + GeniusWaterHeater(broker, z) + for z in broker.client.zone_objs + if z.data["type"] in GH_HEATERS + ] + ) -class GeniusWaterHeater(GeniusEntity, WaterHeaterDevice): +class GeniusWaterHeater(GeniusZone, WaterHeaterDevice): """Representation of a Genius Hub water_heater device.""" - def __init__(self, boiler) -> None: + def __init__(self, broker, zone) -> None: """Initialize the water_heater device.""" - super().__init__() + super().__init__(broker, zone) - self._boiler = boiler - self._operation_list = list(HA_OPMODE_TO_GH) - - @property - def name(self) -> str: - """Return the name of the water_heater device.""" - return self._boiler.name - - @property - def device_state_attributes(self) -> Dict[str, Any]: - """Return the device state attributes.""" - return { - "status": { - k: v for k, v in self._boiler.data.items() if k in GH_STATE_ATTRS - } - } - - @property - def current_temperature(self) -> Optional[float]: - """Return the current temperature.""" - return self._boiler.data.get("temperature") - - @property - def target_temperature(self) -> float: - """Return the temperature we try to reach.""" - return self._boiler.data["setpoint"] - - @property - def min_temp(self) -> float: - """Return max valid temperature that can be set.""" - return GH_MIN_TEMP - - @property - def max_temp(self) -> float: - """Return max valid temperature that can be set.""" - return GH_MAX_TEMP - - @property - def temperature_unit(self) -> str: - """Return the unit of measurement.""" - return TEMP_CELSIUS - - @property - def supported_features(self) -> int: - """Return the list of supported features.""" - return GH_SUPPORT_FLAGS + self._max_temp = 80.0 + self._min_temp = 30.0 + self._supported_features = SUPPORT_TARGET_TEMPERATURE | SUPPORT_OPERATION_MODE @property def operation_list(self) -> List[str]: """Return the list of available operation modes.""" - return self._operation_list + return list(HA_OPMODE_TO_GH) @property def current_operation(self) -> str: """Return the current operation mode.""" - return GH_STATE_TO_HA[self._boiler.data["mode"]] + return GH_STATE_TO_HA[self._zone.data["mode"]] - async def async_set_operation_mode(self, operation_mode) -> Awaitable[None]: + async def async_set_operation_mode(self, operation_mode) -> None: """Set a new operation mode for this boiler.""" - await self._boiler.set_mode(HA_OPMODE_TO_GH[operation_mode]) - - async def async_set_temperature(self, **kwargs) -> Awaitable[None]: - """Set a new target temperature for this boiler.""" - temperature = kwargs[ATTR_TEMPERATURE] - await self._boiler.set_override(temperature, 3600) # 1 hour + await self._zone.set_mode(HA_OPMODE_TO_GH[operation_mode]) diff --git a/homeassistant/components/geo_json_events/manifest.json b/homeassistant/components/geo_json_events/manifest.json index 6ee78fec562..c4c892e88f4 100644 --- a/homeassistant/components/geo_json_events/manifest.json +++ b/homeassistant/components/geo_json_events/manifest.json @@ -1,7 +1,7 @@ { "domain": "geo_json_events", "name": "Geo json events", - "documentation": "https://www.home-assistant.io/components/geo_json_events", + "documentation": "https://www.home-assistant.io/integrations/geo_json_events", "requirements": [ "geojson_client==0.4" ], diff --git a/homeassistant/components/geo_location/manifest.json b/homeassistant/components/geo_location/manifest.json index 83b4241284e..74dd7cbbf87 100644 --- a/homeassistant/components/geo_location/manifest.json +++ b/homeassistant/components/geo_location/manifest.json @@ -1,7 +1,7 @@ { "domain": "geo_location", "name": "Geo location", - "documentation": "https://www.home-assistant.io/components/geo_location", + "documentation": "https://www.home-assistant.io/integrations/geo_location", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/geo_rss_events/manifest.json b/homeassistant/components/geo_rss_events/manifest.json index bce6758b0fe..8fd19f6b034 100644 --- a/homeassistant/components/geo_rss_events/manifest.json +++ b/homeassistant/components/geo_rss_events/manifest.json @@ -1,7 +1,7 @@ { "domain": "geo_rss_events", "name": "Geo rss events", - "documentation": "https://www.home-assistant.io/components/geo_rss_events", + "documentation": "https://www.home-assistant.io/integrations/geo_rss_events", "requirements": [ "georss_generic_client==0.2" ], diff --git a/homeassistant/components/geofency/.translations/no.json b/homeassistant/components/geofency/.translations/no.json index 4409616cef4..1956c453a9f 100644 --- a/homeassistant/components/geofency/.translations/no.json +++ b/homeassistant/components/geofency/.translations/no.json @@ -2,7 +2,7 @@ "config": { "abort": { "not_internet_accessible": "Home Assistant m\u00e5 v\u00e6re tilgjengelig fra internett for \u00e5 kunne motta meldinger fra Geofency.", - "one_instance_allowed": "Kun en enkelt forekomst er n\u00f8dvendig." + "one_instance_allowed": "Kun en forekomst er n\u00f8dvendig." }, "create_entry": { "default": "For \u00e5 kunne sende hendelser til Home Assistant, m\u00e5 du sette opp webhook-funksjonen i Geofency. \n\n Fyll ut f\u00f8lgende informasjon: \n\n - URL: `{webhook_url}` \n - Metode: POST \n\n Se [dokumentasjonen]({docs_url}) for ytterligere detaljer." diff --git a/homeassistant/components/geofency/.translations/zh-Hant.json b/homeassistant/components/geofency/.translations/zh-Hant.json index bec33c26d10..003a3db8bf1 100644 --- a/homeassistant/components/geofency/.translations/zh-Hant.json +++ b/homeassistant/components/geofency/.translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "not_internet_accessible": "Home Assistant \u5be6\u4f8b\u5fc5\u9808\u80fd\u5920\u7531\u7db2\u969b\u7db2\u8def\u5b58\u53d6\uff0c\u65b9\u80fd\u63a5\u53d7 Geofency \u8a0a\u606f\u3002", + "not_internet_accessible": "Home Assistant \u7269\u4ef6\u5fc5\u9808\u80fd\u5920\u7531\u7db2\u969b\u7db2\u8def\u5b58\u53d6\uff0c\u65b9\u80fd\u63a5\u53d7 Geofency \u8a0a\u606f\u3002", "one_instance_allowed": "\u50c5\u9700\u8a2d\u5b9a\u4e00\u7d44\u7269\u4ef6\u5373\u53ef\u3002" }, "create_entry": { diff --git a/homeassistant/components/geofency/config_flow.py b/homeassistant/components/geofency/config_flow.py index d354e6e245d..1a87502df2a 100644 --- a/homeassistant/components/geofency/config_flow.py +++ b/homeassistant/components/geofency/config_flow.py @@ -6,5 +6,5 @@ from .const import DOMAIN config_entry_flow.register_webhook_flow( DOMAIN, "Geofency Webhook", - {"docs_url": "https://www.home-assistant.io/components/geofency/"}, + {"docs_url": "https://www.home-assistant.io/integrations/geofency/"}, ) diff --git a/homeassistant/components/geofency/manifest.json b/homeassistant/components/geofency/manifest.json index d593aec46a4..f7939e2b025 100644 --- a/homeassistant/components/geofency/manifest.json +++ b/homeassistant/components/geofency/manifest.json @@ -2,7 +2,7 @@ "domain": "geofency", "name": "Geofency", "config_flow": true, - "documentation": "https://www.home-assistant.io/components/geofency", + "documentation": "https://www.home-assistant.io/integrations/geofency", "requirements": [], "dependencies": [ "webhook" diff --git a/homeassistant/components/geonetnz_quakes/.translations/bg.json b/homeassistant/components/geonetnz_quakes/.translations/bg.json new file mode 100644 index 00000000000..48d6eacda91 --- /dev/null +++ b/homeassistant/components/geonetnz_quakes/.translations/bg.json @@ -0,0 +1,17 @@ +{ + "config": { + "error": { + "identifier_exists": "\u041c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u0430\u043d\u043e" + }, + "step": { + "user": { + "data": { + "mmi": "MMI", + "radius": "Radius" + }, + "title": "\u041f\u043e\u043f\u044a\u043b\u043d\u0435\u0442\u0435 \u0434\u0430\u043d\u043d\u0438\u0442\u0435 \u0437\u0430 \u0444\u0438\u043b\u0442\u044a\u0440\u0430 \u0441\u0438." + } + }, + "title": "GeoNet NZ \u0417\u0435\u043c\u0435\u0442\u0440\u0435\u0441\u0435\u043d\u0438\u044f" + } +} \ No newline at end of file diff --git a/homeassistant/components/geonetnz_quakes/.translations/lb.json b/homeassistant/components/geonetnz_quakes/.translations/lb.json new file mode 100644 index 00000000000..2499befecbb --- /dev/null +++ b/homeassistant/components/geonetnz_quakes/.translations/lb.json @@ -0,0 +1,17 @@ +{ + "config": { + "error": { + "identifier_exists": "Standuert ass scho registr\u00e9iert" + }, + "step": { + "user": { + "data": { + "mmi": "MMI", + "radius": "Radius" + }, + "title": "F\u00ebllt \u00e4r Filter D\u00e9tailer aus." + } + }, + "title": "GeoNet NZ Quakes" + } +} \ No newline at end of file diff --git a/homeassistant/components/geonetnz_quakes/.translations/nn.json b/homeassistant/components/geonetnz_quakes/.translations/nn.json new file mode 100644 index 00000000000..d8afb1e7aae --- /dev/null +++ b/homeassistant/components/geonetnz_quakes/.translations/nn.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "mmi": "MMI", + "radius": "Radius" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/geonetnz_quakes/.translations/pt-BR.json b/homeassistant/components/geonetnz_quakes/.translations/pt-BR.json new file mode 100644 index 00000000000..7e3ee3b24da --- /dev/null +++ b/homeassistant/components/geonetnz_quakes/.translations/pt-BR.json @@ -0,0 +1,17 @@ +{ + "config": { + "error": { + "identifier_exists": "Localiza\u00e7\u00e3o j\u00e1 registrada" + }, + "step": { + "user": { + "data": { + "mmi": "MMI", + "radius": "Radius" + }, + "title": "Preencha os detalhes do filtro." + } + }, + "title": "GeoNet NZ Quakes" + } +} \ No newline at end of file diff --git a/homeassistant/components/geonetnz_quakes/.translations/ru.json b/homeassistant/components/geonetnz_quakes/.translations/ru.json index 7d6583bc1d5..d6763d17e2d 100644 --- a/homeassistant/components/geonetnz_quakes/.translations/ru.json +++ b/homeassistant/components/geonetnz_quakes/.translations/ru.json @@ -1,7 +1,7 @@ { "config": { "error": { - "identifier_exists": "\u041c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435 \u0443\u0436\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u043e" + "identifier_exists": "\u041c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435 \u0443\u0436\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u043e." }, "step": { "user": { diff --git a/homeassistant/components/geonetnz_quakes/.translations/zh-Hans.json b/homeassistant/components/geonetnz_quakes/.translations/zh-Hans.json new file mode 100644 index 00000000000..3786b03f41f --- /dev/null +++ b/homeassistant/components/geonetnz_quakes/.translations/zh-Hans.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "user": { + "title": "\u586b\u5199\u60a8\u7684filter\u8be6\u7ec6\u4fe1\u606f\u3002" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/geonetnz_quakes/manifest.json b/homeassistant/components/geonetnz_quakes/manifest.json index c84a4152582..f7aa53b0a3a 100644 --- a/homeassistant/components/geonetnz_quakes/manifest.json +++ b/homeassistant/components/geonetnz_quakes/manifest.json @@ -2,9 +2,9 @@ "domain": "geonetnz_quakes", "name": "GeoNet NZ Quakes", "config_flow": true, - "documentation": "https://www.home-assistant.io/components/geonetnz_quakes", + "documentation": "https://www.home-assistant.io/integrations/geonetnz_quakes", "requirements": [ - "aio_geojson_geonetnz_quakes==0.9" + "aio_geojson_geonetnz_quakes==0.10" ], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/github/manifest.json b/homeassistant/components/github/manifest.json index a2c2ae04376..0b5e3c0df9f 100644 --- a/homeassistant/components/github/manifest.json +++ b/homeassistant/components/github/manifest.json @@ -1,7 +1,7 @@ { "domain": "github", "name": "Github", - "documentation": "https://www.home-assistant.io/components/github", + "documentation": "https://www.home-assistant.io/integrations/github", "requirements": [ "PyGithub==1.43.5" ], diff --git a/homeassistant/components/gitlab_ci/manifest.json b/homeassistant/components/gitlab_ci/manifest.json index 4ea04de9e02..e439e8d7eda 100644 --- a/homeassistant/components/gitlab_ci/manifest.json +++ b/homeassistant/components/gitlab_ci/manifest.json @@ -1,7 +1,7 @@ { "domain": "gitlab_ci", "name": "Gitlab ci", - "documentation": "https://www.home-assistant.io/components/gitlab_ci", + "documentation": "https://www.home-assistant.io/integrations/gitlab_ci", "requirements": [ "python-gitlab==1.6.0" ], diff --git a/homeassistant/components/gitter/manifest.json b/homeassistant/components/gitter/manifest.json index 6600e46a4ce..96df8c4e083 100644 --- a/homeassistant/components/gitter/manifest.json +++ b/homeassistant/components/gitter/manifest.json @@ -1,7 +1,7 @@ { "domain": "gitter", "name": "Gitter", - "documentation": "https://www.home-assistant.io/components/gitter", + "documentation": "https://www.home-assistant.io/integrations/gitter", "requirements": [ "gitterpy==0.1.7" ], diff --git a/homeassistant/components/glances/manifest.json b/homeassistant/components/glances/manifest.json index 621bca8c430..775d208c1c4 100644 --- a/homeassistant/components/glances/manifest.json +++ b/homeassistant/components/glances/manifest.json @@ -1,7 +1,7 @@ { "domain": "glances", "name": "Glances", - "documentation": "https://www.home-assistant.io/components/glances", + "documentation": "https://www.home-assistant.io/integrations/glances", "requirements": [ "glances_api==0.2.0" ], diff --git a/homeassistant/components/glances/sensor.py b/homeassistant/components/glances/sensor.py index 1385d5e59a7..90b4b386f37 100644 --- a/homeassistant/components/glances/sensor.py +++ b/homeassistant/components/glances/sensor.py @@ -197,18 +197,20 @@ class GlancesSensor(Entity): elif self.type == "cpu_temp": for sensor in value["sensors"]: if sensor["label"] in [ - "CPU", - "CPU Temperature", - "Package id 0", - "Physical id 0", - "cpu_thermal 1", - "cpu-thermal 1", - "exynos-therm 1", - "soc_thermal 1", - "soc-thermal 1", + "amdgpu 1", "aml_thermal", "Core 0", "Core 1", + "CPU Temperature", + "CPU", + "cpu-thermal 1", + "cpu_thermal 1", + "exynos-therm 1", + "Package id 0", + "Physical id 0", + "radeon 1", + "soc-thermal 1", + "soc_thermal 1", ]: self._state = sensor["value"] elif self.type == "docker_active": diff --git a/homeassistant/components/gntp/manifest.json b/homeassistant/components/gntp/manifest.json index 7315e3c7c84..f1c030125ac 100644 --- a/homeassistant/components/gntp/manifest.json +++ b/homeassistant/components/gntp/manifest.json @@ -1,7 +1,7 @@ { "domain": "gntp", "name": "Gntp", - "documentation": "https://www.home-assistant.io/components/gntp", + "documentation": "https://www.home-assistant.io/integrations/gntp", "requirements": [ "gntp==1.0.3" ], diff --git a/homeassistant/components/goalfeed/manifest.json b/homeassistant/components/goalfeed/manifest.json index 861abe0b462..a4d7cd50686 100644 --- a/homeassistant/components/goalfeed/manifest.json +++ b/homeassistant/components/goalfeed/manifest.json @@ -1,7 +1,7 @@ { "domain": "goalfeed", "name": "Goalfeed", - "documentation": "https://www.home-assistant.io/components/goalfeed", + "documentation": "https://www.home-assistant.io/integrations/goalfeed", "requirements": [ "pysher==1.0.1" ], diff --git a/homeassistant/components/gogogate2/manifest.json b/homeassistant/components/gogogate2/manifest.json index 3f3f2c25d0c..d8878c8b351 100644 --- a/homeassistant/components/gogogate2/manifest.json +++ b/homeassistant/components/gogogate2/manifest.json @@ -1,7 +1,7 @@ { "domain": "gogogate2", "name": "Gogogate2", - "documentation": "https://www.home-assistant.io/components/gogogate2", + "documentation": "https://www.home-assistant.io/integrations/gogogate2", "requirements": [ "pygogogate2==0.1.1" ], diff --git a/homeassistant/components/google/manifest.json b/homeassistant/components/google/manifest.json index 4c7e82ecfef..d72cc992f6d 100644 --- a/homeassistant/components/google/manifest.json +++ b/homeassistant/components/google/manifest.json @@ -1,7 +1,7 @@ { "domain": "google", "name": "Google", - "documentation": "https://www.home-assistant.io/components/google", + "documentation": "https://www.home-assistant.io/integrations/google", "requirements": [ "google-api-python-client==1.6.4", "httplib2==0.10.3", diff --git a/homeassistant/components/google_assistant/__init__.py b/homeassistant/components/google_assistant/__init__.py index 61e0c70b6b3..a1252d67fff 100644 --- a/homeassistant/components/google_assistant/__init__.py +++ b/homeassistant/components/google_assistant/__init__.py @@ -83,6 +83,13 @@ async def async_setup(hass: HomeAssistant, yaml_config: Dict[str, Any]): try: with async_timeout.timeout(15): agent_user_id = call.data.get("agent_user_id") or call.context.user_id + + if agent_user_id is None: + _LOGGER.warning( + "No agent_user_id supplied for request_sync. Call as a user or pass in user id as agent_user_id." + ) + return + res = await websession.post( REQUEST_SYNC_BASE_URL, params={"key": api_key}, diff --git a/homeassistant/components/google_assistant/const.py b/homeassistant/components/google_assistant/const.py index 1d266d23d3f..54abd54caaf 100644 --- a/homeassistant/components/google_assistant/const.py +++ b/homeassistant/components/google_assistant/const.py @@ -15,6 +15,7 @@ from homeassistant.components import ( sensor, switch, vacuum, + alarm_control_panel, ) DOMAIN = "google_assistant" @@ -48,6 +49,7 @@ DEFAULT_EXPOSED_DOMAINS = [ "lock", "binary_sensor", "sensor", + "alarm_control_panel", ] PREFIX_TYPES = "action.devices.types." @@ -66,6 +68,7 @@ TYPE_SENSOR = PREFIX_TYPES + "SENSOR" TYPE_DOOR = PREFIX_TYPES + "DOOR" TYPE_TV = PREFIX_TYPES + "TV" TYPE_SPEAKER = PREFIX_TYPES + "SPEAKER" +TYPE_ALARM = PREFIX_TYPES + "SECURITYSYSTEM" SERVICE_REQUEST_SYNC = "request_sync" HOMEGRAPH_URL = "https://homegraph.googleapis.com/" @@ -81,6 +84,9 @@ ERR_PROTOCOL_ERROR = "protocolError" ERR_UNKNOWN_ERROR = "unknownError" ERR_FUNCTION_NOT_SUPPORTED = "functionNotSupported" +ERR_ALREADY_DISARMED = "alreadyDisarmed" +ERR_ALREADY_ARMED = "alreadyArmed" + ERR_CHALLENGE_NEEDED = "challengeNeeded" ERR_CHALLENGE_NOT_SETUP = "challengeFailedNotSetup" ERR_TOO_MANY_FAILED_ATTEMPTS = "tooManyFailedAttempts" @@ -106,6 +112,7 @@ DOMAIN_TO_GOOGLE_TYPES = { script.DOMAIN: TYPE_SCENE, switch.DOMAIN: TYPE_SWITCH, vacuum.DOMAIN: TYPE_VACUUM, + alarm_control_panel.DOMAIN: TYPE_ALARM, } DEVICE_CLASS_TO_GOOGLE_TYPES = { diff --git a/homeassistant/components/google_assistant/helpers.py b/homeassistant/components/google_assistant/helpers.py index daaf790a0c1..933f0c07999 100644 --- a/homeassistant/components/google_assistant/helpers.py +++ b/homeassistant/components/google_assistant/helpers.py @@ -3,7 +3,8 @@ from asyncio import gather from collections.abc import Mapping from typing import List -from homeassistant.core import Context, callback +from homeassistant.core import Context, callback, HomeAssistant, State +from homeassistant.helpers.event import async_call_later from homeassistant.const import ( CONF_NAME, STATE_UNAVAILABLE, @@ -22,10 +23,24 @@ from .const import ( ) from .error import SmartHomeError +SYNC_DELAY = 15 + class AbstractConfig: """Hold the configuration for Google Assistant.""" + _unsub_report_state = None + + def __init__(self, hass): + """Initialize abstract config.""" + self.hass = hass + self._google_sync_unsub = None + + @property + def enabled(self): + """Return if Google is enabled.""" + return False + @property def agent_user_id(self): """Return Agent User Id to use for query responses.""" @@ -41,6 +56,17 @@ class AbstractConfig: """Return entity config.""" return None + @property + def is_reporting_state(self): + """Return if we're actively reporting states.""" + return self._unsub_report_state is not None + + @property + def should_report_state(self): + """Return if states should be proactively reported.""" + # pylint: disable=no-self-use + return False + def should_expose(self, state) -> bool: """Return if entity should be exposed.""" raise NotImplementedError @@ -50,11 +76,66 @@ class AbstractConfig: # pylint: disable=no-self-use return True + async def async_report_state(self, message): + """Send a state report to Google.""" + raise NotImplementedError + + def async_enable_report_state(self): + """Enable proactive mode.""" + # Circular dep + from .report_state import async_enable_report_state + + if self._unsub_report_state is None: + self._unsub_report_state = async_enable_report_state(self.hass, self) + + def async_disable_report_state(self): + """Disable report state.""" + if self._unsub_report_state is not None: + self._unsub_report_state() + self._unsub_report_state = None + + async def async_sync_entities(self): + """Sync all entities to Google.""" + # Remove any pending sync + if self._google_sync_unsub: + self._google_sync_unsub() + self._google_sync_unsub = None + + return await self._async_request_sync_devices() + + async def _schedule_callback(self, _now): + """Handle a scheduled sync callback.""" + self._google_sync_unsub = None + await self.async_sync_entities() + + @callback + def async_schedule_google_sync(self): + """Schedule a sync.""" + if self._google_sync_unsub: + self._google_sync_unsub() + + self._google_sync_unsub = async_call_later( + self.hass, SYNC_DELAY, self._schedule_callback + ) + + async def _async_request_sync_devices(self) -> int: + """Trigger a sync with Google. + + Return value is the HTTP status code of the sync request. + """ + raise NotImplementedError + + async def async_deactivate_report_state(self): + """Turn off report state and disable further state reporting. + + Called when the user disconnects their account from Google. + """ + class RequestData: """Hold data associated with a particular request.""" - def __init__(self, config, user_id, request_id): + def __init__(self, config: AbstractConfig, user_id: str, request_id: str): """Initialize the request data.""" self.config = config self.request_id = request_id @@ -71,7 +152,7 @@ def get_google_type(domain, device_class): class GoogleEntity: """Adaptation of Entity expressed in Google's terms.""" - def __init__(self, hass, config, state): + def __init__(self, hass: HomeAssistant, config: AbstractConfig, state: State): """Initialize a Google entity.""" self.hass = hass self.config = config @@ -101,6 +182,11 @@ class GoogleEntity: ] return self._traits + @callback + def should_expose(self): + """If entity should be exposed.""" + return self.config.should_expose(self.state) + @callback def is_supported(self) -> bool: """Return if the entity is supported by Google.""" @@ -139,7 +225,7 @@ class GoogleEntity: "name": {"name": name}, "attributes": {}, "traits": [trait.name for trait in traits], - "willReportState": False, + "willReportState": self.config.should_report_state, "type": device_type, } diff --git a/homeassistant/components/google_assistant/http.py b/homeassistant/components/google_assistant/http.py index d68650fb638..aea226348b8 100644 --- a/homeassistant/components/google_assistant/http.py +++ b/homeassistant/components/google_assistant/http.py @@ -25,10 +25,16 @@ _LOGGER = logging.getLogger(__name__) class GoogleConfig(AbstractConfig): """Config for manual setup of Google.""" - def __init__(self, config): + def __init__(self, hass, config): """Initialize the config.""" + super().__init__(hass) self._config = config + @property + def enabled(self): + """Return if Google is enabled.""" + return True + @property def agent_user_id(self): """Return Agent User Id to use for query responses.""" @@ -77,7 +83,7 @@ class GoogleConfig(AbstractConfig): @callback def async_register_http(hass, cfg): """Register HTTP views for Google Assistant.""" - hass.http.register_view(GoogleAssistantView(GoogleConfig(cfg))) + hass.http.register_view(GoogleAssistantView(GoogleConfig(hass, cfg))) class GoogleAssistantView(HomeAssistantView): diff --git a/homeassistant/components/google_assistant/manifest.json b/homeassistant/components/google_assistant/manifest.json index ff916930216..f97977a7400 100644 --- a/homeassistant/components/google_assistant/manifest.json +++ b/homeassistant/components/google_assistant/manifest.json @@ -1,10 +1,8 @@ { "domain": "google_assistant", "name": "Google assistant", - "documentation": "https://www.home-assistant.io/components/google_assistant", + "documentation": "https://www.home-assistant.io/integrations/google_assistant", "requirements": [], - "dependencies": [ - "http" - ], - "codeowners": [] + "dependencies": ["http"], + "codeowners": ["@home-assistant/cloud"] } diff --git a/homeassistant/components/google_assistant/report_state.py b/homeassistant/components/google_assistant/report_state.py new file mode 100644 index 00000000000..869bc61d7a3 --- /dev/null +++ b/homeassistant/components/google_assistant/report_state.py @@ -0,0 +1,61 @@ +"""Google Report State implementation.""" +from homeassistant.core import HomeAssistant, callback +from homeassistant.const import MATCH_ALL +from homeassistant.helpers.event import async_call_later + +from .helpers import AbstractConfig, GoogleEntity, async_get_entities + +# Time to wait until the homegraph updates +# https://github.com/actions-on-google/smart-home-nodejs/issues/196#issuecomment-439156639 +INITIAL_REPORT_DELAY = 60 + + +@callback +def async_enable_report_state(hass: HomeAssistant, google_config: AbstractConfig): + """Enable state reporting.""" + + async def async_entity_state_listener(changed_entity, old_state, new_state): + if not new_state: + return + + if not google_config.should_expose(new_state): + return + + entity = GoogleEntity(hass, google_config, new_state) + + if not entity.is_supported(): + return + + entity_data = entity.query_serialize() + + if old_state: + old_entity = GoogleEntity(hass, google_config, old_state) + + # Only report to Google if data that Google cares about has changed + if entity_data == old_entity.query_serialize(): + return + + await google_config.async_report_state( + {"devices": {"states": {changed_entity: entity_data}}} + ) + + async_call_later( + hass, INITIAL_REPORT_DELAY, _async_report_all_states(hass, google_config) + ) + + return hass.helpers.event.async_track_state_change( + MATCH_ALL, async_entity_state_listener + ) + + +async def _async_report_all_states(hass: HomeAssistant, google_config: AbstractConfig): + """Report all states.""" + entities = {} + + for entity in async_get_entities(hass, google_config): + if not entity.should_expose(): + continue + + entities[entity.entity_id] = entity.query_serialize() + + await google_config.async_report_state({"devices": {"states": entities}}) diff --git a/homeassistant/components/google_assistant/smart_home.py b/homeassistant/components/google_assistant/smart_home.py index 6ab6d937b51..f9b311a3880 100644 --- a/homeassistant/components/google_assistant/smart_home.py +++ b/homeassistant/components/google_assistant/smart_home.py @@ -193,11 +193,12 @@ async def handle_devices_execute(hass, data, payload): @HANDLERS.register("action.devices.DISCONNECT") -async def async_devices_disconnect(hass, data, payload): +async def async_devices_disconnect(hass, data: RequestData, payload): """Handle action.devices.DISCONNECT request. https://developers.google.com/actions/smarthome/create#actiondevicesdisconnect """ + await data.config.async_deactivate_report_state() return None diff --git a/homeassistant/components/google_assistant/trait.py b/homeassistant/components/google_assistant/trait.py index 5fa7d49b885..7d6e79a8237 100644 --- a/homeassistant/components/google_assistant/trait.py +++ b/homeassistant/components/google_assistant/trait.py @@ -16,6 +16,7 @@ from homeassistant.components import ( sensor, switch, vacuum, + alarm_control_panel, ) from homeassistant.components.climate import const as climate from homeassistant.const import ( @@ -31,6 +32,20 @@ from homeassistant.const import ( ATTR_SUPPORTED_FEATURES, ATTR_TEMPERATURE, ATTR_ASSUMED_STATE, + SERVICE_ALARM_DISARM, + SERVICE_ALARM_ARM_HOME, + SERVICE_ALARM_ARM_AWAY, + SERVICE_ALARM_ARM_NIGHT, + SERVICE_ALARM_ARM_CUSTOM_BYPASS, + SERVICE_ALARM_TRIGGER, + STATE_ALARM_ARMED_HOME, + STATE_ALARM_ARMED_AWAY, + STATE_ALARM_ARMED_NIGHT, + STATE_ALARM_ARMED_CUSTOM_BYPASS, + STATE_ALARM_DISARMED, + STATE_ALARM_TRIGGERED, + STATE_ALARM_PENDING, + ATTR_CODE, STATE_UNKNOWN, ) from homeassistant.core import DOMAIN as HA_DOMAIN @@ -43,6 +58,8 @@ from .const import ( CHALLENGE_ACK_NEEDED, CHALLENGE_PIN_NEEDED, CHALLENGE_FAILED_PIN_NEEDED, + ERR_ALREADY_DISARMED, + ERR_ALREADY_ARMED, ) from .error import SmartHomeError, ChallengeNeeded @@ -62,6 +79,7 @@ TRAIT_FANSPEED = PREFIX_TRAITS + "FanSpeed" TRAIT_MODES = PREFIX_TRAITS + "Modes" TRAIT_OPENCLOSE = PREFIX_TRAITS + "OpenClose" TRAIT_VOLUME = PREFIX_TRAITS + "Volume" +TRAIT_ARMDISARM = PREFIX_TRAITS + "ArmDisarm" PREFIX_COMMANDS = "action.devices.commands." COMMAND_ONOFF = PREFIX_COMMANDS + "OnOff" @@ -85,6 +103,7 @@ COMMAND_MODES = PREFIX_COMMANDS + "SetModes" COMMAND_OPENCLOSE = PREFIX_COMMANDS + "OpenClose" COMMAND_SET_VOLUME = PREFIX_COMMANDS + "setVolume" COMMAND_VOLUME_RELATIVE = PREFIX_COMMANDS + "volumeRelative" +COMMAND_ARMDISARM = PREFIX_COMMANDS + "ArmDisarm" TRAITS = [] @@ -308,7 +327,7 @@ class ColorSettingTrait(_Trait): if features & light.SUPPORT_COLOR_TEMP: # Max Kelvin is Min Mireds K = 1000000 / mireds - # Min Kevin is Max Mireds K = 1000000 / mireds + # Min Kelvin is Max Mireds K = 1000000 / mireds response["colorTemperatureRange"] = { "temperatureMaxK": color_util.color_temperature_mired_to_kelvin( attrs.get(light.ATTR_MIN_MIREDS) @@ -873,6 +892,98 @@ class LockUnlockTrait(_Trait): ) +@register_trait +class ArmDisArmTrait(_Trait): + """Trait to Arm or Disarm a Security System. + + https://developers.google.com/actions/smarthome/traits/armdisarm + """ + + name = TRAIT_ARMDISARM + commands = [COMMAND_ARMDISARM] + + state_to_service = { + STATE_ALARM_ARMED_HOME: SERVICE_ALARM_ARM_HOME, + STATE_ALARM_ARMED_AWAY: SERVICE_ALARM_ARM_AWAY, + STATE_ALARM_ARMED_NIGHT: SERVICE_ALARM_ARM_NIGHT, + STATE_ALARM_ARMED_CUSTOM_BYPASS: SERVICE_ALARM_ARM_CUSTOM_BYPASS, + STATE_ALARM_TRIGGERED: SERVICE_ALARM_TRIGGER, + } + + @staticmethod + def supported(domain, features, device_class): + """Test if state is supported.""" + return domain == alarm_control_panel.DOMAIN + + @staticmethod + def might_2fa(domain, features, device_class): + """Return if the trait might ask for 2FA.""" + return True + + def sync_attributes(self): + """Return ArmDisarm attributes for a sync request.""" + response = {} + levels = [] + for state in self.state_to_service: + # level synonyms are generated from state names + # 'armed_away' becomes 'armed away' or 'away' + level_synonym = [state.replace("_", " ")] + if state != STATE_ALARM_TRIGGERED: + level_synonym.append(state.split("_")[1]) + + level = { + "level_name": state, + "level_values": [{"level_synonym": level_synonym, "lang": "en"}], + } + levels.append(level) + response["availableArmLevels"] = {"levels": levels, "ordered": False} + return response + + def query_attributes(self): + """Return ArmDisarm query attributes.""" + if "post_pending_state" in self.state.attributes: + armed_state = self.state.attributes["post_pending_state"] + else: + armed_state = self.state.state + response = {"isArmed": armed_state in self.state_to_service} + if response["isArmed"]: + response.update({"currentArmLevel": armed_state}) + return response + + async def execute(self, command, data, params, challenge): + """Execute an ArmDisarm command.""" + if params["arm"] and not params.get("cancel"): + if self.state.state == params["armLevel"]: + raise SmartHomeError(ERR_ALREADY_ARMED, "System is already armed") + if self.state.attributes["code_arm_required"]: + _verify_pin_challenge(data, self.state, challenge) + service = self.state_to_service[params["armLevel"]] + # disarm the system without asking for code when + # 'cancel' arming action is received while current status is pending + elif ( + params["arm"] + and params.get("cancel") + and self.state.state == STATE_ALARM_PENDING + ): + service = SERVICE_ALARM_DISARM + else: + if self.state.state == STATE_ALARM_DISARMED: + raise SmartHomeError(ERR_ALREADY_DISARMED, "System is already disarmed") + _verify_pin_challenge(data, self.state, challenge) + service = SERVICE_ALARM_DISARM + + await self.hass.services.async_call( + alarm_control_panel.DOMAIN, + service, + { + ATTR_ENTITY_ID: self.state.entity_id, + ATTR_CODE: data.config.secure_devices_pin, + }, + blocking=True, + context=data.context, + ) + + @register_trait class FanSpeedTrait(_Trait): """Trait to control speed of Fan. @@ -1343,7 +1454,6 @@ def _verify_pin_challenge(data, state, challenge): """Verify a pin challenge.""" if not data.config.should_2fa(state): return - if not data.config.secure_devices_pin: raise SmartHomeError(ERR_CHALLENGE_NOT_SETUP, "Challenge is not set up") diff --git a/homeassistant/components/google_cloud/manifest.json b/homeassistant/components/google_cloud/manifest.json index c8ac0d2e81e..bc9c71c5a61 100644 --- a/homeassistant/components/google_cloud/manifest.json +++ b/homeassistant/components/google_cloud/manifest.json @@ -1,7 +1,7 @@ { "domain": "google_cloud", "name": "Google Cloud Platform", - "documentation": "https://www.home-assistant.io/components/google_cloud", + "documentation": "https://www.home-assistant.io/integrations/google_cloud", "requirements": [ "google-cloud-texttospeech==0.4.0" ], diff --git a/homeassistant/components/google_domains/manifest.json b/homeassistant/components/google_domains/manifest.json index 190e5860ee6..64076434aa5 100644 --- a/homeassistant/components/google_domains/manifest.json +++ b/homeassistant/components/google_domains/manifest.json @@ -1,7 +1,7 @@ { "domain": "google_domains", "name": "Google domains", - "documentation": "https://www.home-assistant.io/components/google_domains", + "documentation": "https://www.home-assistant.io/integrations/google_domains", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/google_maps/manifest.json b/homeassistant/components/google_maps/manifest.json index ec48e5252a8..30571c33865 100644 --- a/homeassistant/components/google_maps/manifest.json +++ b/homeassistant/components/google_maps/manifest.json @@ -1,7 +1,7 @@ { "domain": "google_maps", "name": "Google maps", - "documentation": "https://www.home-assistant.io/components/google_maps", + "documentation": "https://www.home-assistant.io/integrations/google_maps", "requirements": [ "locationsharinglib==4.1.0" ], diff --git a/homeassistant/components/google_pubsub/manifest.json b/homeassistant/components/google_pubsub/manifest.json index ff61ad0e05d..b23a101ca46 100644 --- a/homeassistant/components/google_pubsub/manifest.json +++ b/homeassistant/components/google_pubsub/manifest.json @@ -1,7 +1,7 @@ { "domain": "google_pubsub", "name": "Google pubsub", - "documentation": "https://www.home-assistant.io/components/google_pubsub", + "documentation": "https://www.home-assistant.io/integrations/google_pubsub", "requirements": [ "google-cloud-pubsub==0.39.1" ], diff --git a/homeassistant/components/google_translate/manifest.json b/homeassistant/components/google_translate/manifest.json index cb3cd350c04..8b9621b4236 100644 --- a/homeassistant/components/google_translate/manifest.json +++ b/homeassistant/components/google_translate/manifest.json @@ -1,7 +1,7 @@ { "domain": "google_translate", "name": "Google Translate", - "documentation": "https://www.home-assistant.io/components/google_translate", + "documentation": "https://www.home-assistant.io/integrations/google_translate", "requirements": [ "gTTS-token==1.1.3" ], diff --git a/homeassistant/components/google_travel_time/manifest.json b/homeassistant/components/google_travel_time/manifest.json index eaa168332a6..f4113f85a31 100644 --- a/homeassistant/components/google_travel_time/manifest.json +++ b/homeassistant/components/google_travel_time/manifest.json @@ -1,7 +1,7 @@ { "domain": "google_travel_time", "name": "Google travel time", - "documentation": "https://www.home-assistant.io/components/google_travel_time", + "documentation": "https://www.home-assistant.io/integrations/google_travel_time", "requirements": [ "googlemaps==2.5.1" ], diff --git a/homeassistant/components/google_wifi/manifest.json b/homeassistant/components/google_wifi/manifest.json index 6e840458207..77062cbdd26 100644 --- a/homeassistant/components/google_wifi/manifest.json +++ b/homeassistant/components/google_wifi/manifest.json @@ -1,7 +1,7 @@ { "domain": "google_wifi", "name": "Google wifi", - "documentation": "https://www.home-assistant.io/components/google_wifi", + "documentation": "https://www.home-assistant.io/integrations/google_wifi", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/gpmdp/manifest.json b/homeassistant/components/gpmdp/manifest.json index 98ab8035023..a3c2389478e 100644 --- a/homeassistant/components/gpmdp/manifest.json +++ b/homeassistant/components/gpmdp/manifest.json @@ -1,7 +1,7 @@ { "domain": "gpmdp", "name": "Gpmdp", - "documentation": "https://www.home-assistant.io/components/gpmdp", + "documentation": "https://www.home-assistant.io/integrations/gpmdp", "requirements": [ "websocket-client==0.54.0" ], diff --git a/homeassistant/components/gpsd/manifest.json b/homeassistant/components/gpsd/manifest.json index b35d5cb1850..7bb828cacf9 100644 --- a/homeassistant/components/gpsd/manifest.json +++ b/homeassistant/components/gpsd/manifest.json @@ -1,7 +1,7 @@ { "domain": "gpsd", "name": "Gpsd", - "documentation": "https://www.home-assistant.io/components/gpsd", + "documentation": "https://www.home-assistant.io/integrations/gpsd", "requirements": [ "gps3==0.33.3" ], diff --git a/homeassistant/components/gpsd/sensor.py b/homeassistant/components/gpsd/sensor.py index ab4545256ae..197e424ce86 100644 --- a/homeassistant/components/gpsd/sensor.py +++ b/homeassistant/components/gpsd/sensor.py @@ -7,6 +7,7 @@ from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( ATTR_LATITUDE, ATTR_LONGITUDE, + ATTR_MODE, CONF_HOST, CONF_PORT, CONF_NAME, @@ -19,7 +20,6 @@ _LOGGER = logging.getLogger(__name__) ATTR_CLIMB = "climb" ATTR_ELEVATION = "elevation" ATTR_GPS_TIME = "gps_time" -ATTR_MODE = "mode" ATTR_SPEED = "speed" DEFAULT_HOST = "localhost" diff --git a/homeassistant/components/gpslogger/.translations/no.json b/homeassistant/components/gpslogger/.translations/no.json index 836b5c8bc68..488a09c3768 100644 --- a/homeassistant/components/gpslogger/.translations/no.json +++ b/homeassistant/components/gpslogger/.translations/no.json @@ -2,7 +2,7 @@ "config": { "abort": { "not_internet_accessible": "Home Assistant m\u00e5 v\u00e6re tilgjengelig fra internett for \u00e5 kunne motta meldinger fra GPSLogger.", - "one_instance_allowed": "Kun en enkelt forekomst er n\u00f8dvendig." + "one_instance_allowed": "Kun en forekomst er n\u00f8dvendig." }, "create_entry": { "default": "For \u00e5 sende hendelser til Home Assistant, m\u00e5 du sette opp webhook-funksjonen i GPSLogger. \n\n Fyll ut f\u00f8lgende informasjon: \n\n - URL: `{webhook_url}` \n - Metode: POST \n\n Se [dokumentasjonen]({docs_url}) for ytterligere detaljer." diff --git a/homeassistant/components/gpslogger/.translations/zh-Hant.json b/homeassistant/components/gpslogger/.translations/zh-Hant.json index c9d98da1afc..c21e76a6eee 100644 --- a/homeassistant/components/gpslogger/.translations/zh-Hant.json +++ b/homeassistant/components/gpslogger/.translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "not_internet_accessible": "Home Assistant \u5be6\u4f8b\u5fc5\u9808\u80fd\u5920\u7531\u7db2\u969b\u7db2\u8def\u5b58\u53d6\uff0c\u65b9\u80fd\u63a5\u53d7 GPSLogger \u8a0a\u606f\u3002", + "not_internet_accessible": "Home Assistant \u7269\u4ef6\u5fc5\u9808\u80fd\u5920\u7531\u7db2\u969b\u7db2\u8def\u5b58\u53d6\uff0c\u65b9\u80fd\u63a5\u53d7 GPSLogger \u8a0a\u606f\u3002", "one_instance_allowed": "\u50c5\u9700\u8a2d\u5b9a\u4e00\u7d44\u7269\u4ef6\u5373\u53ef\u3002" }, "create_entry": { diff --git a/homeassistant/components/gpslogger/config_flow.py b/homeassistant/components/gpslogger/config_flow.py index a572bddd0e9..5173c02e7ff 100644 --- a/homeassistant/components/gpslogger/config_flow.py +++ b/homeassistant/components/gpslogger/config_flow.py @@ -6,5 +6,5 @@ from .const import DOMAIN config_entry_flow.register_webhook_flow( DOMAIN, "GPSLogger Webhook", - {"docs_url": "https://www.home-assistant.io/components/gpslogger/"}, + {"docs_url": "https://www.home-assistant.io/integrations/gpslogger/"}, ) diff --git a/homeassistant/components/gpslogger/manifest.json b/homeassistant/components/gpslogger/manifest.json index f039e50914b..cbfd79671eb 100644 --- a/homeassistant/components/gpslogger/manifest.json +++ b/homeassistant/components/gpslogger/manifest.json @@ -2,7 +2,7 @@ "domain": "gpslogger", "name": "Gpslogger", "config_flow": true, - "documentation": "https://www.home-assistant.io/components/gpslogger", + "documentation": "https://www.home-assistant.io/integrations/gpslogger", "requirements": [], "dependencies": [ "webhook" diff --git a/homeassistant/components/graphite/__init__.py b/homeassistant/components/graphite/__init__.py index 550a0ce1d13..3809249bea6 100644 --- a/homeassistant/components/graphite/__init__.py +++ b/homeassistant/components/graphite/__init__.py @@ -64,7 +64,7 @@ class GraphiteFeeder(threading.Thread): def __init__(self, hass, host, port, prefix): """Initialize the feeder.""" - super(GraphiteFeeder, self).__init__(daemon=True) + super().__init__(daemon=True) self._hass = hass self._host = host self._port = port diff --git a/homeassistant/components/graphite/manifest.json b/homeassistant/components/graphite/manifest.json index a5eefc5af04..49748128258 100644 --- a/homeassistant/components/graphite/manifest.json +++ b/homeassistant/components/graphite/manifest.json @@ -1,7 +1,7 @@ { "domain": "graphite", "name": "Graphite", - "documentation": "https://www.home-assistant.io/components/graphite", + "documentation": "https://www.home-assistant.io/integrations/graphite", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/greeneye_monitor/manifest.json b/homeassistant/components/greeneye_monitor/manifest.json index 7bfb87ede47..1e9569e8509 100644 --- a/homeassistant/components/greeneye_monitor/manifest.json +++ b/homeassistant/components/greeneye_monitor/manifest.json @@ -1,7 +1,7 @@ { "domain": "greeneye_monitor", "name": "Greeneye monitor", - "documentation": "https://www.home-assistant.io/components/greeneye_monitor", + "documentation": "https://www.home-assistant.io/integrations/greeneye_monitor", "requirements": [ "greeneye_monitor==1.0" ], diff --git a/homeassistant/components/greenwave/manifest.json b/homeassistant/components/greenwave/manifest.json index 1032b5eaf2a..20a49e834b0 100644 --- a/homeassistant/components/greenwave/manifest.json +++ b/homeassistant/components/greenwave/manifest.json @@ -1,7 +1,7 @@ { "domain": "greenwave", "name": "Greenwave", - "documentation": "https://www.home-assistant.io/components/greenwave", + "documentation": "https://www.home-assistant.io/integrations/greenwave", "requirements": [ "greenwavereality==0.5.1" ], diff --git a/homeassistant/components/group/__init__.py b/homeassistant/components/group/__init__.py index 75b45471982..39574a2b03b 100644 --- a/homeassistant/components/group/__init__.py +++ b/homeassistant/components/group/__init__.py @@ -1,6 +1,7 @@ """Provide the functionality to group entities.""" import asyncio import logging +from typing import Any, Iterable, List, Optional, cast import voluptuous as vol @@ -32,9 +33,11 @@ from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.event import async_track_state_change import homeassistant.helpers.config_validation as cv from homeassistant.helpers.config_validation import ENTITY_SERVICE_SCHEMA -from homeassistant.util.async_ import run_coroutine_threadsafe +from homeassistant.helpers.typing import HomeAssistantType +# mypy: allow-untyped-calls, allow-untyped-defs + DOMAIN = "group" ENTITY_ID_FORMAT = DOMAIN + ".{}" @@ -143,12 +146,12 @@ def is_on(hass, entity_id): @bind_hass -def expand_entity_ids(hass, entity_ids): +def expand_entity_ids(hass: HomeAssistantType, entity_ids: Iterable[Any]) -> List[str]: """Return entity_ids with group entity ids replaced by their members. Async friendly. """ - found_ids = [] + found_ids: List[str] = [] for entity_id in entity_ids: if not isinstance(entity_id, str): continue @@ -182,7 +185,9 @@ def expand_entity_ids(hass, entity_ids): @bind_hass -def get_entity_ids(hass, entity_id, domain_filter=None): +def get_entity_ids( + hass: HomeAssistantType, entity_id: str, domain_filter: Optional[str] = None +) -> List[str]: """Get members of this group. Async friendly. @@ -194,7 +199,7 @@ def get_entity_ids(hass, entity_id, domain_filter=None): entity_ids = group.attributes[ATTR_ENTITY_ID] if not domain_filter: - return entity_ids + return cast(List[str], entity_ids) domain_filter = domain_filter.lower() + "." @@ -424,7 +429,7 @@ class Group(Entity): mode=None, ): """Initialize a group.""" - return run_coroutine_threadsafe( + return asyncio.run_coroutine_threadsafe( Group.async_create_group( hass, name, @@ -540,7 +545,7 @@ class Group(Entity): def update_tracked_entity_ids(self, entity_ids): """Update the member entity IDs.""" - run_coroutine_threadsafe( + asyncio.run_coroutine_threadsafe( self.async_update_tracked_entity_ids(entity_ids), self.hass.loop ).result() diff --git a/homeassistant/components/group/cover.py b/homeassistant/components/group/cover.py index faa4ddfc87d..c5200082f2f 100644 --- a/homeassistant/components/group/cover.py +++ b/homeassistant/components/group/cover.py @@ -1,5 +1,6 @@ """This platform allows several cover to be grouped into one cover.""" import logging +from typing import Dict, Optional, Set import voluptuous as vol @@ -11,7 +12,7 @@ from homeassistant.const import ( CONF_NAME, STATE_CLOSED, ) -from homeassistant.core import callback +from homeassistant.core import callback, State import homeassistant.helpers.config_validation as cv from homeassistant.helpers.event import async_track_state_change @@ -41,6 +42,9 @@ from homeassistant.components.cover import ( CoverDevice, ) + +# mypy: allow-incomplete-defs, allow-untyped-calls, allow-untyped-defs + _LOGGER = logging.getLogger(__name__) KEY_OPEN_CLOSE = "open_close" @@ -76,13 +80,25 @@ class CoverGroup(CoverDevice): self._assumed_state = True self._entities = entities - self._covers = {KEY_OPEN_CLOSE: set(), KEY_STOP: set(), KEY_POSITION: set()} - self._tilts = {KEY_OPEN_CLOSE: set(), KEY_STOP: set(), KEY_POSITION: set()} + self._covers: Dict[str, Set[str]] = { + KEY_OPEN_CLOSE: set(), + KEY_STOP: set(), + KEY_POSITION: set(), + } + self._tilts: Dict[str, Set[str]] = { + KEY_OPEN_CLOSE: set(), + KEY_STOP: set(), + KEY_POSITION: set(), + } @callback def update_supported_features( - self, entity_id, old_state, new_state, update_state=True - ): + self, + entity_id: str, + old_state: Optional[State], + new_state: Optional[State], + update_state: bool = True, + ) -> None: """Update dictionaries with supported features.""" if not new_state: for values in self._covers.values(): diff --git a/homeassistant/components/group/light.py b/homeassistant/components/group/light.py index 87d8134ccbf..e77c858fc02 100644 --- a/homeassistant/components/group/light.py +++ b/homeassistant/components/group/light.py @@ -1,8 +1,9 @@ """This platform allows several lights to be grouped into one light.""" +import asyncio from collections import Counter import itertools import logging -from typing import Any, Callable, Iterator, List, Optional, Tuple +from typing import Any, Callable, Iterator, List, Optional, Tuple, cast import voluptuous as vol @@ -19,6 +20,7 @@ from homeassistant.core import State, callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.event import async_track_state_change from homeassistant.helpers.typing import ConfigType, HomeAssistantType +from homeassistant.util import color as color_util from homeassistant.components.light import ( ATTR_BRIGHTNESS, @@ -41,6 +43,9 @@ from homeassistant.components.light import ( SUPPORT_WHITE_VALUE, ) + +# mypy: allow-incomplete-defs, allow-untyped-calls, allow-untyped-defs + _LOGGER = logging.getLogger(__name__) DEFAULT_NAME = "Light Group" @@ -67,7 +72,9 @@ async def async_setup_platform( hass: HomeAssistantType, config: ConfigType, async_add_entities, discovery_info=None ) -> None: """Initialize light.group platform.""" - async_add_entities([LightGroup(config.get(CONF_NAME), config[CONF_ENTITIES])]) + async_add_entities( + [LightGroup(cast(str, config.get(CONF_NAME)), config[CONF_ENTITIES])] + ) class LightGroup(light.Light): @@ -179,6 +186,7 @@ class LightGroup(light.Light): async def async_turn_on(self, **kwargs): """Forward the turn_on command to all lights in the light group.""" data = {ATTR_ENTITY_ID: self._entity_ids} + emulate_color_temp_entity_ids = [] if ATTR_BRIGHTNESS in kwargs: data[ATTR_BRIGHTNESS] = kwargs[ATTR_BRIGHTNESS] @@ -189,6 +197,23 @@ class LightGroup(light.Light): if ATTR_COLOR_TEMP in kwargs: data[ATTR_COLOR_TEMP] = kwargs[ATTR_COLOR_TEMP] + # Create a new entity list to mutate + updated_entities = list(self._entity_ids) + + # Walk through initial entity ids, split entity lists by support + for entity_id in self._entity_ids: + state = self.hass.states.get(entity_id) + if not state: + continue + support = state.attributes.get(ATTR_SUPPORTED_FEATURES) + # Only pass color temperature to supported entity_ids + if bool(support & SUPPORT_COLOR) and not bool( + support & SUPPORT_COLOR_TEMP + ): + emulate_color_temp_entity_ids.append(entity_id) + updated_entities.remove(entity_id) + data[ATTR_ENTITY_ID] = updated_entities + if ATTR_WHITE_VALUE in kwargs: data[ATTR_WHITE_VALUE] = kwargs[ATTR_WHITE_VALUE] @@ -201,8 +226,32 @@ class LightGroup(light.Light): if ATTR_FLASH in kwargs: data[ATTR_FLASH] = kwargs[ATTR_FLASH] - await self.hass.services.async_call( - light.DOMAIN, light.SERVICE_TURN_ON, data, blocking=True + if not emulate_color_temp_entity_ids: + await self.hass.services.async_call( + light.DOMAIN, light.SERVICE_TURN_ON, data, blocking=True + ) + return + + emulate_color_temp_data = data.copy() + temp_k = color_util.color_temperature_mired_to_kelvin( + emulate_color_temp_data[ATTR_COLOR_TEMP] + ) + hs_color = color_util.color_temperature_to_hs(temp_k) + emulate_color_temp_data[ATTR_HS_COLOR] = hs_color + del emulate_color_temp_data[ATTR_COLOR_TEMP] + + emulate_color_temp_data[ATTR_ENTITY_ID] = emulate_color_temp_entity_ids + + await asyncio.gather( + self.hass.services.async_call( + light.DOMAIN, light.SERVICE_TURN_ON, data, blocking=True + ), + self.hass.services.async_call( + light.DOMAIN, + light.SERVICE_TURN_ON, + emulate_color_temp_data, + blocking=True, + ), ) async def async_turn_off(self, **kwargs): @@ -219,7 +268,7 @@ class LightGroup(light.Light): async def async_update(self): """Query all members and determine the light group state.""" all_states = [self.hass.states.get(x) for x in self._entity_ids] - states = list(filter(None, all_states)) + states: List[State] = list(filter(None, all_states)) on_states = [state for state in states if state.state == STATE_ON] self._is_on = len(on_states) > 0 diff --git a/homeassistant/components/group/manifest.json b/homeassistant/components/group/manifest.json index aa99e20a4df..195227ca242 100644 --- a/homeassistant/components/group/manifest.json +++ b/homeassistant/components/group/manifest.json @@ -1,7 +1,7 @@ { "domain": "group", "name": "Group", - "documentation": "https://www.home-assistant.io/components/group", + "documentation": "https://www.home-assistant.io/integrations/group", "requirements": [], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/group/notify.py b/homeassistant/components/group/notify.py index 3d3c644fea9..2ffb7fea049 100644 --- a/homeassistant/components/group/notify.py +++ b/homeassistant/components/group/notify.py @@ -17,6 +17,9 @@ from homeassistant.components.notify import ( BaseNotificationService, ) + +# mypy: allow-untyped-calls, allow-untyped-defs + _LOGGER = logging.getLogger(__name__) CONF_SERVICES = "services" diff --git a/homeassistant/components/growatt_server/manifest.json b/homeassistant/components/growatt_server/manifest.json index a6a1d2b8aeb..0d4508c26dc 100644 --- a/homeassistant/components/growatt_server/manifest.json +++ b/homeassistant/components/growatt_server/manifest.json @@ -1,7 +1,7 @@ { "domain": "growatt_server", "name": "Growatt Server", - "documentation": "https://www.home-assistant.io/components/growatt_server/", + "documentation": "https://www.home-assistant.io/integrations/growatt_server/", "requirements": [ "growattServer==0.0.1" ], diff --git a/homeassistant/components/gstreamer/manifest.json b/homeassistant/components/gstreamer/manifest.json index 6bfb8abbe0b..66cae733d9c 100644 --- a/homeassistant/components/gstreamer/manifest.json +++ b/homeassistant/components/gstreamer/manifest.json @@ -1,7 +1,7 @@ { "domain": "gstreamer", "name": "Gstreamer", - "documentation": "https://www.home-assistant.io/components/gstreamer", + "documentation": "https://www.home-assistant.io/integrations/gstreamer", "requirements": [ "gstreamer-player==1.1.2" ], diff --git a/homeassistant/components/gtfs/manifest.json b/homeassistant/components/gtfs/manifest.json index 1c7ddbd65ee..b25134bb79e 100644 --- a/homeassistant/components/gtfs/manifest.json +++ b/homeassistant/components/gtfs/manifest.json @@ -1,7 +1,7 @@ { "domain": "gtfs", "name": "Gtfs", - "documentation": "https://www.home-assistant.io/components/gtfs", + "documentation": "https://www.home-assistant.io/integrations/gtfs", "requirements": [ "pygtfs==0.1.5" ], diff --git a/homeassistant/components/gtt/manifest.json b/homeassistant/components/gtt/manifest.json index 142261fe155..217b1755554 100644 --- a/homeassistant/components/gtt/manifest.json +++ b/homeassistant/components/gtt/manifest.json @@ -1,7 +1,7 @@ { "domain": "gtt", "name": "Gtt", - "documentation": "https://www.home-assistant.io/components/gtt", + "documentation": "https://www.home-assistant.io/integrations/gtt", "requirements": [ "pygtt==1.1.2" ], diff --git a/homeassistant/components/habitica/manifest.json b/homeassistant/components/habitica/manifest.json index b8e622823d3..a3ac10a1c6d 100644 --- a/homeassistant/components/habitica/manifest.json +++ b/homeassistant/components/habitica/manifest.json @@ -1,7 +1,7 @@ { "domain": "habitica", "name": "Habitica", - "documentation": "https://www.home-assistant.io/components/habitica", + "documentation": "https://www.home-assistant.io/integrations/habitica", "requirements": [ "habitipy==0.2.0" ], diff --git a/homeassistant/components/hangouts/.translations/ru.json b/homeassistant/components/hangouts/.translations/ru.json index 52b8798c0f4..6942f683fa6 100644 --- a/homeassistant/components/hangouts/.translations/ru.json +++ b/homeassistant/components/hangouts/.translations/ru.json @@ -1,7 +1,7 @@ { "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.", "unknown": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430" }, "error": { diff --git a/homeassistant/components/hangouts/manifest.json b/homeassistant/components/hangouts/manifest.json index 4a90e9c977e..2f222b3c16e 100644 --- a/homeassistant/components/hangouts/manifest.json +++ b/homeassistant/components/hangouts/manifest.json @@ -2,7 +2,7 @@ "domain": "hangouts", "name": "Hangouts", "config_flow": true, - "documentation": "https://www.home-assistant.io/components/hangouts", + "documentation": "https://www.home-assistant.io/integrations/hangouts", "requirements": [ "hangups==0.4.9" ], diff --git a/homeassistant/components/harman_kardon_avr/manifest.json b/homeassistant/components/harman_kardon_avr/manifest.json index eecbf0edd63..6bd64942619 100644 --- a/homeassistant/components/harman_kardon_avr/manifest.json +++ b/homeassistant/components/harman_kardon_avr/manifest.json @@ -1,7 +1,7 @@ { "domain": "harman_kardon_avr", "name": "Harman kardon avr", - "documentation": "https://www.home-assistant.io/components/harman_kardon_avr", + "documentation": "https://www.home-assistant.io/integrations/harman_kardon_avr", "requirements": [ "hkavr==0.0.5" ], diff --git a/homeassistant/components/harmony/manifest.json b/homeassistant/components/harmony/manifest.json index a957db0675f..af4427c5db3 100644 --- a/homeassistant/components/harmony/manifest.json +++ b/homeassistant/components/harmony/manifest.json @@ -1,7 +1,7 @@ { "domain": "harmony", "name": "Harmony", - "documentation": "https://www.home-assistant.io/components/harmony", + "documentation": "https://www.home-assistant.io/integrations/harmony", "requirements": [ "aioharmony==0.1.13" ], diff --git a/homeassistant/components/haveibeenpwned/manifest.json b/homeassistant/components/haveibeenpwned/manifest.json index 40572f82ea8..00c7b7a19b8 100644 --- a/homeassistant/components/haveibeenpwned/manifest.json +++ b/homeassistant/components/haveibeenpwned/manifest.json @@ -1,7 +1,7 @@ { "domain": "haveibeenpwned", "name": "Haveibeenpwned", - "documentation": "https://www.home-assistant.io/components/haveibeenpwned", + "documentation": "https://www.home-assistant.io/integrations/haveibeenpwned", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/hddtemp/manifest.json b/homeassistant/components/hddtemp/manifest.json index 2d34d3b4e7b..484886aff21 100644 --- a/homeassistant/components/hddtemp/manifest.json +++ b/homeassistant/components/hddtemp/manifest.json @@ -1,7 +1,7 @@ { "domain": "hddtemp", "name": "Hddtemp", - "documentation": "https://www.home-assistant.io/components/hddtemp", + "documentation": "https://www.home-assistant.io/integrations/hddtemp", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/hdmi_cec/manifest.json b/homeassistant/components/hdmi_cec/manifest.json index b59d5622821..7c877f5c2a7 100644 --- a/homeassistant/components/hdmi_cec/manifest.json +++ b/homeassistant/components/hdmi_cec/manifest.json @@ -1,7 +1,7 @@ { "domain": "hdmi_cec", "name": "Hdmi cec", - "documentation": "https://www.home-assistant.io/components/hdmi_cec", + "documentation": "https://www.home-assistant.io/integrations/hdmi_cec", "requirements": [ "pyCEC==0.4.13" ], diff --git a/homeassistant/components/heatmiser/manifest.json b/homeassistant/components/heatmiser/manifest.json index 0a11aecd079..b3882c63c51 100644 --- a/homeassistant/components/heatmiser/manifest.json +++ b/homeassistant/components/heatmiser/manifest.json @@ -1,7 +1,7 @@ { "domain": "heatmiser", "name": "Heatmiser", - "documentation": "https://www.home-assistant.io/components/heatmiser", + "documentation": "https://www.home-assistant.io/integrations/heatmiser", "requirements": [ "heatmiserV3==0.9.1" ], diff --git a/homeassistant/components/heos/.translations/de.json b/homeassistant/components/heos/.translations/de.json index e8f4df930db..e98df7466ff 100644 --- a/homeassistant/components/heos/.translations/de.json +++ b/homeassistant/components/heos/.translations/de.json @@ -16,6 +16,6 @@ "title": "Mit Heos verbinden" } }, - "title": "Heos" + "title": "HEOS" } } \ No newline at end of file diff --git a/homeassistant/components/heos/.translations/es-419.json b/homeassistant/components/heos/.translations/es-419.json index 66c02884a7e..4d442a4543b 100644 --- a/homeassistant/components/heos/.translations/es-419.json +++ b/homeassistant/components/heos/.translations/es-419.json @@ -3,6 +3,11 @@ "abort": { "already_setup": "Solo puede configurar una sola conexi\u00f3n Heos, ya que ser\u00e1 compatible con todos los dispositivos de la red." }, + "step": { + "user": { + "title": "Con\u00e9ctate a Heos" + } + }, "title": "Heos" } } \ No newline at end of file diff --git a/homeassistant/components/heos/.translations/no.json b/homeassistant/components/heos/.translations/no.json index dd4cb48a090..d41051b6674 100644 --- a/homeassistant/components/heos/.translations/no.json +++ b/homeassistant/components/heos/.translations/no.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_setup": "Du kan kun konfigurere en enkelt Heos tilkobling, da den st\u00f8tter alle enhetene p\u00e5 nettverket." + "already_setup": "Du kan kun konfigurere en Heos tilkobling, da den st\u00f8tter alle enhetene p\u00e5 nettverket." }, "error": { "connection_failure": "Kan ikke koble til den angitte verten." diff --git a/homeassistant/components/heos/.translations/zh-Hant.json b/homeassistant/components/heos/.translations/zh-Hant.json index c45f9c467e4..9efacaf163f 100644 --- a/homeassistant/components/heos/.translations/zh-Hant.json +++ b/homeassistant/components/heos/.translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_setup": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44 Heos \u9023\u7dda\uff0c\u5c07\u652f\u63f4\u7db2\u8def\u4e2d\u6240\u6709\u5c0d\u61c9\u88dd\u7f6e\u3002" + "already_setup": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44 Heos \u9023\u7dda\uff0c\u5c07\u652f\u63f4\u7db2\u8def\u4e2d\u6240\u6709\u5c0d\u61c9\u8a2d\u5099\u3002" }, "error": { "connection_failure": "\u7121\u6cd5\u9023\u7dda\u81f3\u6307\u5b9a\u4e3b\u6a5f\u7aef\u3002" @@ -12,7 +12,7 @@ "access_token": "\u4e3b\u6a5f\u7aef", "host": "\u4e3b\u6a5f\u7aef" }, - "description": "\u8acb\u8f38\u5165\u4e3b\u6a5f\u6bb5\u540d\u7a31\u6216 Heos \u88dd\u7f6e IP \u4f4d\u5740\uff08\u5df2\u900f\u904e\u6709\u7dda\u7db2\u8def\u9023\u7dda\uff09\u3002", + "description": "\u8acb\u8f38\u5165\u4e3b\u6a5f\u6bb5\u540d\u7a31\u6216 Heos \u8a2d\u5099 IP \u4f4d\u5740\uff08\u5df2\u900f\u904e\u6709\u7dda\u7db2\u8def\u9023\u7dda\uff09\u3002", "title": "\u9023\u7dda\u81f3 Heos" } }, diff --git a/homeassistant/components/heos/manifest.json b/homeassistant/components/heos/manifest.json index eb9ef258a3c..fb21a43356f 100644 --- a/homeassistant/components/heos/manifest.json +++ b/homeassistant/components/heos/manifest.json @@ -2,7 +2,7 @@ "domain": "heos", "name": "HEOS", "config_flow": true, - "documentation": "https://www.home-assistant.io/components/heos", + "documentation": "https://www.home-assistant.io/integrations/heos", "requirements": [ "pyheos==0.6.0" ], diff --git a/homeassistant/components/here_travel_time/__init__.py b/homeassistant/components/here_travel_time/__init__.py new file mode 100755 index 00000000000..9a5c8ec32ac --- /dev/null +++ b/homeassistant/components/here_travel_time/__init__.py @@ -0,0 +1 @@ +"""The here_travel_time component.""" diff --git a/homeassistant/components/here_travel_time/manifest.json b/homeassistant/components/here_travel_time/manifest.json new file mode 100755 index 00000000000..0f2bde253de --- /dev/null +++ b/homeassistant/components/here_travel_time/manifest.json @@ -0,0 +1,12 @@ +{ + "domain": "here_travel_time", + "name": "HERE travel time", + "documentation": "https://www.home-assistant.io/integrations/here_travel_time", + "requirements": [ + "herepy==0.6.3.1" + ], + "dependencies": [], + "codeowners": [ + "@eifinger" + ] + } diff --git a/homeassistant/components/here_travel_time/sensor.py b/homeassistant/components/here_travel_time/sensor.py new file mode 100755 index 00000000000..b752b82d087 --- /dev/null +++ b/homeassistant/components/here_travel_time/sensor.py @@ -0,0 +1,424 @@ +"""Support for HERE travel time sensors.""" +from datetime import timedelta +import logging +from typing import Callable, Dict, Optional, Union + +import herepy +import voluptuous as vol + +from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import ( + ATTR_ATTRIBUTION, + ATTR_LATITUDE, + ATTR_LONGITUDE, + ATTR_MODE, + CONF_MODE, + CONF_NAME, + CONF_UNIT_SYSTEM, + CONF_UNIT_SYSTEM_IMPERIAL, + CONF_UNIT_SYSTEM_METRIC, +) +from homeassistant.core import HomeAssistant, State +from homeassistant.helpers import location +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity + +_LOGGER = logging.getLogger(__name__) + +CONF_DESTINATION_LATITUDE = "destination_latitude" +CONF_DESTINATION_LONGITUDE = "destination_longitude" +CONF_DESTINATION_ENTITY_ID = "destination_entity_id" +CONF_ORIGIN_LATITUDE = "origin_latitude" +CONF_ORIGIN_LONGITUDE = "origin_longitude" +CONF_ORIGIN_ENTITY_ID = "origin_entity_id" +CONF_APP_ID = "app_id" +CONF_APP_CODE = "app_code" +CONF_TRAFFIC_MODE = "traffic_mode" +CONF_ROUTE_MODE = "route_mode" + +DEFAULT_NAME = "HERE Travel Time" + +TRAVEL_MODE_BICYCLE = "bicycle" +TRAVEL_MODE_CAR = "car" +TRAVEL_MODE_PEDESTRIAN = "pedestrian" +TRAVEL_MODE_PUBLIC = "publicTransport" +TRAVEL_MODE_PUBLIC_TIME_TABLE = "publicTransportTimeTable" +TRAVEL_MODE_TRUCK = "truck" +TRAVEL_MODE = [ + TRAVEL_MODE_BICYCLE, + TRAVEL_MODE_CAR, + TRAVEL_MODE_PEDESTRIAN, + TRAVEL_MODE_PUBLIC, + TRAVEL_MODE_PUBLIC_TIME_TABLE, + TRAVEL_MODE_TRUCK, +] + +TRAVEL_MODES_PUBLIC = [TRAVEL_MODE_PUBLIC, TRAVEL_MODE_PUBLIC_TIME_TABLE] +TRAVEL_MODES_VEHICLE = [TRAVEL_MODE_CAR, TRAVEL_MODE_TRUCK] +TRAVEL_MODES_NON_VEHICLE = [TRAVEL_MODE_BICYCLE, TRAVEL_MODE_PEDESTRIAN] + +TRAFFIC_MODE_ENABLED = "traffic_enabled" +TRAFFIC_MODE_DISABLED = "traffic_disabled" + +ROUTE_MODE_FASTEST = "fastest" +ROUTE_MODE_SHORTEST = "shortest" +ROUTE_MODE = [ROUTE_MODE_FASTEST, ROUTE_MODE_SHORTEST] + +ICON_BICYCLE = "mdi:bike" +ICON_CAR = "mdi:car" +ICON_PEDESTRIAN = "mdi:walk" +ICON_PUBLIC = "mdi:bus" +ICON_TRUCK = "mdi:truck" + +UNITS = [CONF_UNIT_SYSTEM_METRIC, CONF_UNIT_SYSTEM_IMPERIAL] + +ATTR_DURATION = "duration" +ATTR_DISTANCE = "distance" +ATTR_ROUTE = "route" +ATTR_ORIGIN = "origin" +ATTR_DESTINATION = "destination" + +ATTR_UNIT_SYSTEM = CONF_UNIT_SYSTEM +ATTR_TRAFFIC_MODE = CONF_TRAFFIC_MODE + +ATTR_DURATION_IN_TRAFFIC = "duration_in_traffic" +ATTR_ORIGIN_NAME = "origin_name" +ATTR_DESTINATION_NAME = "destination_name" + +UNIT_OF_MEASUREMENT = "min" + +SCAN_INTERVAL = timedelta(minutes=5) + +TRACKABLE_DOMAINS = ["device_tracker", "sensor", "zone", "person"] + +NO_ROUTE_ERROR_MESSAGE = "HERE could not find a route based on the input" + +PLATFORM_SCHEMA = vol.All( + cv.has_at_least_one_key(CONF_DESTINATION_LATITUDE, CONF_DESTINATION_ENTITY_ID), + cv.has_at_least_one_key(CONF_ORIGIN_LATITUDE, CONF_ORIGIN_ENTITY_ID), + PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_APP_ID): cv.string, + vol.Required(CONF_APP_CODE): cv.string, + vol.Inclusive( + CONF_DESTINATION_LATITUDE, "destination_coordinates" + ): cv.latitude, + vol.Inclusive( + CONF_DESTINATION_LONGITUDE, "destination_coordinates" + ): cv.longitude, + vol.Exclusive(CONF_DESTINATION_LATITUDE, "destination"): cv.latitude, + vol.Exclusive(CONF_DESTINATION_ENTITY_ID, "destination"): cv.entity_id, + vol.Inclusive(CONF_ORIGIN_LATITUDE, "origin_coordinates"): cv.latitude, + vol.Inclusive(CONF_ORIGIN_LONGITUDE, "origin_coordinates"): cv.longitude, + vol.Exclusive(CONF_ORIGIN_LATITUDE, "origin"): cv.latitude, + vol.Exclusive(CONF_ORIGIN_ENTITY_ID, "origin"): cv.entity_id, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_MODE, default=TRAVEL_MODE_CAR): vol.In(TRAVEL_MODE), + vol.Optional(CONF_ROUTE_MODE, default=ROUTE_MODE_FASTEST): vol.In( + ROUTE_MODE + ), + vol.Optional(CONF_TRAFFIC_MODE, default=False): cv.boolean, + vol.Optional(CONF_UNIT_SYSTEM): vol.In(UNITS), + } + ), +) + + +async def async_setup_platform( + hass: HomeAssistant, + config: Dict[str, Union[str, bool]], + async_add_entities: Callable, + discovery_info: None = None, +) -> None: + """Set up the HERE travel time platform.""" + + app_id = config[CONF_APP_ID] + app_code = config[CONF_APP_CODE] + here_client = herepy.RoutingApi(app_id, app_code) + + if not await hass.async_add_executor_job( + _are_valid_client_credentials, here_client + ): + _LOGGER.error( + "Invalid credentials. This error is returned if the specified token was invalid or no contract could be found for this token." + ) + return + + if config.get(CONF_ORIGIN_LATITUDE) is not None: + origin = f"{config[CONF_ORIGIN_LATITUDE]},{config[CONF_ORIGIN_LONGITUDE]}" + else: + origin = config[CONF_ORIGIN_ENTITY_ID] + + if config.get(CONF_DESTINATION_LATITUDE) is not None: + destination = ( + f"{config[CONF_DESTINATION_LATITUDE]},{config[CONF_DESTINATION_LONGITUDE]}" + ) + else: + destination = config[CONF_DESTINATION_ENTITY_ID] + + travel_mode = config[CONF_MODE] + traffic_mode = config[CONF_TRAFFIC_MODE] + route_mode = config[CONF_ROUTE_MODE] + name = config[CONF_NAME] + units = config.get(CONF_UNIT_SYSTEM, hass.config.units.name) + + here_data = HERETravelTimeData( + here_client, travel_mode, traffic_mode, route_mode, units + ) + + sensor = HERETravelTimeSensor(name, origin, destination, here_data) + + async_add_entities([sensor], True) + + +def _are_valid_client_credentials(here_client: herepy.RoutingApi) -> bool: + """Check if the provided credentials are correct using defaults.""" + known_working_origin = [38.9, -77.04833] + known_working_destination = [39.0, -77.1] + try: + here_client.car_route( + known_working_origin, + known_working_destination, + [ + herepy.RouteMode[ROUTE_MODE_FASTEST], + herepy.RouteMode[TRAVEL_MODE_CAR], + herepy.RouteMode[TRAFFIC_MODE_DISABLED], + ], + ) + except herepy.InvalidCredentialsError: + return False + return True + + +class HERETravelTimeSensor(Entity): + """Representation of a HERE travel time sensor.""" + + def __init__( + self, name: str, origin: str, destination: str, here_data: "HERETravelTimeData" + ) -> None: + """Initialize the sensor.""" + self._name = name + self._here_data = here_data + self._unit_of_measurement = UNIT_OF_MEASUREMENT + self._origin_entity_id = None + self._destination_entity_id = None + self._attrs = { + ATTR_UNIT_SYSTEM: self._here_data.units, + ATTR_MODE: self._here_data.travel_mode, + ATTR_TRAFFIC_MODE: self._here_data.traffic_mode, + } + + # Check if location is a trackable entity + if origin.split(".", 1)[0] in TRACKABLE_DOMAINS: + self._origin_entity_id = origin + else: + self._here_data.origin = origin + + if destination.split(".", 1)[0] in TRACKABLE_DOMAINS: + self._destination_entity_id = destination + else: + self._here_data.destination = destination + + @property + def state(self) -> Optional[str]: + """Return the state of the sensor.""" + if self._here_data.traffic_mode: + if self._here_data.traffic_time is not None: + return str(round(self._here_data.traffic_time / 60)) + if self._here_data.base_time is not None: + return str(round(self._here_data.base_time / 60)) + + return None + + @property + def name(self) -> str: + """Get the name of the sensor.""" + return self._name + + @property + def device_state_attributes( + self + ) -> Optional[Dict[str, Union[None, float, str, bool]]]: + """Return the state attributes.""" + if self._here_data.base_time is None: + return None + + res = self._attrs + if self._here_data.attribution is not None: + res[ATTR_ATTRIBUTION] = self._here_data.attribution + res[ATTR_DURATION] = self._here_data.base_time / 60 + res[ATTR_DISTANCE] = self._here_data.distance + res[ATTR_ROUTE] = self._here_data.route + res[ATTR_DURATION_IN_TRAFFIC] = self._here_data.traffic_time / 60 + res[ATTR_ORIGIN] = self._here_data.origin + res[ATTR_DESTINATION] = self._here_data.destination + res[ATTR_ORIGIN_NAME] = self._here_data.origin_name + res[ATTR_DESTINATION_NAME] = self._here_data.destination_name + return res + + @property + def unit_of_measurement(self) -> str: + """Return the unit this state is expressed in.""" + return self._unit_of_measurement + + @property + def icon(self) -> str: + """Icon to use in the frontend depending on travel_mode.""" + if self._here_data.travel_mode == TRAVEL_MODE_BICYCLE: + return ICON_BICYCLE + if self._here_data.travel_mode == TRAVEL_MODE_PEDESTRIAN: + return ICON_PEDESTRIAN + if self._here_data.travel_mode in TRAVEL_MODES_PUBLIC: + return ICON_PUBLIC + if self._here_data.travel_mode == TRAVEL_MODE_TRUCK: + return ICON_TRUCK + return ICON_CAR + + async def async_update(self) -> None: + """Update Sensor Information.""" + # Convert device_trackers to HERE friendly location + if self._origin_entity_id is not None: + self._here_data.origin = await self._get_location_from_entity( + self._origin_entity_id + ) + + if self._destination_entity_id is not None: + self._here_data.destination = await self._get_location_from_entity( + self._destination_entity_id + ) + + await self.hass.async_add_executor_job(self._here_data.update) + + async def _get_location_from_entity(self, entity_id: str) -> Optional[str]: + """Get the location from the entity state or attributes.""" + entity = self.hass.states.get(entity_id) + + if entity is None: + _LOGGER.error("Unable to find entity %s", entity_id) + return None + + # Check if the entity has location attributes + if location.has_location(entity): + return self._get_location_from_attributes(entity) + + # Check if device is in a zone + zone_entity = self.hass.states.get("zone.{}".format(entity.state)) + if location.has_location(zone_entity): + _LOGGER.debug( + "%s is in %s, getting zone location", entity_id, zone_entity.entity_id + ) + return self._get_location_from_attributes(zone_entity) + + # If zone was not found in state then use the state as the location + if entity_id.startswith("sensor."): + return entity.state + + @staticmethod + def _get_location_from_attributes(entity: State) -> str: + """Get the lat/long string from an entities attributes.""" + attr = entity.attributes + return "{},{}".format(attr.get(ATTR_LATITUDE), attr.get(ATTR_LONGITUDE)) + + +class HERETravelTimeData: + """HERETravelTime data object.""" + + def __init__( + self, + here_client: herepy.RoutingApi, + travel_mode: str, + traffic_mode: bool, + route_mode: str, + units: str, + ) -> None: + """Initialize herepy.""" + self.origin = None + self.destination = None + self.travel_mode = travel_mode + self.traffic_mode = traffic_mode + self.route_mode = route_mode + self.attribution = None + self.traffic_time = None + self.distance = None + self.route = None + self.base_time = None + self.origin_name = None + self.destination_name = None + self.units = units + self._client = here_client + + def update(self) -> None: + """Get the latest data from HERE.""" + if self.traffic_mode: + traffic_mode = TRAFFIC_MODE_ENABLED + else: + traffic_mode = TRAFFIC_MODE_DISABLED + + if self.destination is not None and self.origin is not None: + # Convert location to HERE friendly location + destination = self.destination.split(",") + origin = self.origin.split(",") + + _LOGGER.debug( + "Requesting route for origin: %s, destination: %s, route_mode: %s, mode: %s, traffic_mode: %s", + origin, + destination, + herepy.RouteMode[self.route_mode], + herepy.RouteMode[self.travel_mode], + herepy.RouteMode[traffic_mode], + ) + try: + response = self._client.car_route( + origin, + destination, + [ + herepy.RouteMode[self.route_mode], + herepy.RouteMode[self.travel_mode], + herepy.RouteMode[traffic_mode], + ], + ) + except herepy.NoRouteFoundError: + # Better error message for cryptic no route error codes + _LOGGER.error(NO_ROUTE_ERROR_MESSAGE) + return + + _LOGGER.debug("Raw response is: %s", response.response) + + # pylint: disable=no-member + source_attribution = response.response.get("sourceAttribution") + if source_attribution is not None: + self.attribution = self._build_hass_attribution(source_attribution) + # pylint: disable=no-member + route = response.response["route"] + summary = route[0]["summary"] + waypoint = route[0]["waypoint"] + self.base_time = summary["baseTime"] + if self.travel_mode in TRAVEL_MODES_VEHICLE: + self.traffic_time = summary["trafficTime"] + else: + self.traffic_time = self.base_time + distance = summary["distance"] + if self.units == CONF_UNIT_SYSTEM_IMPERIAL: + # Convert to miles. + self.distance = distance / 1609.344 + else: + # Convert to kilometers + self.distance = distance / 1000 + # pylint: disable=no-member + self.route = response.route_short + self.origin_name = waypoint[0]["mappedRoadName"] + self.destination_name = waypoint[1]["mappedRoadName"] + + @staticmethod + def _build_hass_attribution(source_attribution: Dict) -> Optional[str]: + """Build a hass frontend ready string out of the sourceAttribution.""" + suppliers = source_attribution.get("supplier") + if suppliers is not None: + supplier_titles = [] + for supplier in suppliers: + title = supplier.get("title") + if title is not None: + supplier_titles.append(title) + joined_supplier_titles = ",".join(supplier_titles) + attribution = f"With the support of {joined_supplier_titles}. All information is provided without warranty of any kind." + return attribution diff --git a/homeassistant/components/hikvision/manifest.json b/homeassistant/components/hikvision/manifest.json index bee53c89cdf..78917a5351b 100644 --- a/homeassistant/components/hikvision/manifest.json +++ b/homeassistant/components/hikvision/manifest.json @@ -1,7 +1,7 @@ { "domain": "hikvision", "name": "Hikvision", - "documentation": "https://www.home-assistant.io/components/hikvision", + "documentation": "https://www.home-assistant.io/integrations/hikvision", "requirements": [ "pyhik==0.2.3" ], diff --git a/homeassistant/components/hikvisioncam/manifest.json b/homeassistant/components/hikvisioncam/manifest.json index f2bb0822d17..8dcef17fad6 100644 --- a/homeassistant/components/hikvisioncam/manifest.json +++ b/homeassistant/components/hikvisioncam/manifest.json @@ -1,7 +1,7 @@ { "domain": "hikvisioncam", "name": "Hikvisioncam", - "documentation": "https://www.home-assistant.io/components/hikvisioncam", + "documentation": "https://www.home-assistant.io/integrations/hikvisioncam", "requirements": [ "hikvision==0.4" ], diff --git a/homeassistant/components/hipchat/manifest.json b/homeassistant/components/hipchat/manifest.json index d49e05a5416..9d563719a2e 100644 --- a/homeassistant/components/hipchat/manifest.json +++ b/homeassistant/components/hipchat/manifest.json @@ -1,7 +1,7 @@ { "domain": "hipchat", "name": "Hipchat", - "documentation": "https://www.home-assistant.io/components/hipchat", + "documentation": "https://www.home-assistant.io/integrations/hipchat", "requirements": [ "hipnotify==1.0.8" ], diff --git a/homeassistant/components/history/manifest.json b/homeassistant/components/history/manifest.json index e0989958626..00789c905c2 100644 --- a/homeassistant/components/history/manifest.json +++ b/homeassistant/components/history/manifest.json @@ -1,7 +1,7 @@ { "domain": "history", "name": "History", - "documentation": "https://www.home-assistant.io/components/history", + "documentation": "https://www.home-assistant.io/integrations/history", "requirements": [], "dependencies": [ "http", diff --git a/homeassistant/components/history_graph/manifest.json b/homeassistant/components/history_graph/manifest.json index fa0d437a700..a4a0eb4d3e9 100644 --- a/homeassistant/components/history_graph/manifest.json +++ b/homeassistant/components/history_graph/manifest.json @@ -1,7 +1,7 @@ { "domain": "history_graph", "name": "History graph", - "documentation": "https://www.home-assistant.io/components/history_graph", + "documentation": "https://www.home-assistant.io/integrations/history_graph", "requirements": [], "dependencies": [ "history" diff --git a/homeassistant/components/history_stats/manifest.json b/homeassistant/components/history_stats/manifest.json index ea0abd87c28..55a3449f4d6 100644 --- a/homeassistant/components/history_stats/manifest.json +++ b/homeassistant/components/history_stats/manifest.json @@ -1,7 +1,7 @@ { "domain": "history_stats", "name": "History stats", - "documentation": "https://www.home-assistant.io/components/history_stats", + "documentation": "https://www.home-assistant.io/integrations/history_stats", "requirements": [], "dependencies": [ "history" diff --git a/homeassistant/components/hitron_coda/manifest.json b/homeassistant/components/hitron_coda/manifest.json index 9f3c20fcca5..6b0492881fb 100644 --- a/homeassistant/components/hitron_coda/manifest.json +++ b/homeassistant/components/hitron_coda/manifest.json @@ -1,7 +1,7 @@ { "domain": "hitron_coda", "name": "Hitron coda", - "documentation": "https://www.home-assistant.io/components/hitron_coda", + "documentation": "https://www.home-assistant.io/integrations/hitron_coda", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/hive/__init__.py b/homeassistant/components/hive/__init__.py index fc96f2d8c96..3301097bab7 100644 --- a/homeassistant/components/hive/__init__.py +++ b/homeassistant/components/hive/__init__.py @@ -1,17 +1,32 @@ -"""Support for the Hive devices.""" +"""Support for the Hive devices and services.""" +from functools import wraps import logging from pyhiveapi import Pyhiveapi import voluptuous as vol -from homeassistant.const import CONF_PASSWORD, CONF_SCAN_INTERVAL, CONF_USERNAME +from homeassistant.const import ( + ATTR_ENTITY_ID, + ATTR_TEMPERATURE, + CONF_PASSWORD, + CONF_SCAN_INTERVAL, + CONF_USERNAME, +) +from homeassistant.core import callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.discovery import load_platform +from homeassistant.helpers.dispatcher import async_dispatcher_connect, dispatcher_send +from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) DOMAIN = "hive" DATA_HIVE = "data_hive" +SERVICES = ["Heating", "HotWater"] +SERVICE_BOOST_HOT_WATER = "boost_hot_water" +SERVICE_BOOST_HEATING = "boost_heating" +ATTR_TIME_PERIOD = "time_period" +ATTR_MODE = "on_off" DEVICETYPES = { "binary_sensor": "device_list_binary_sensor", "climate": "device_list_climate", @@ -34,11 +49,31 @@ CONFIG_SCHEMA = vol.Schema( extra=vol.ALLOW_EXTRA, ) +BOOST_HEATING_SCHEMA = vol.Schema( + { + vol.Required(ATTR_ENTITY_ID): cv.entity_id, + vol.Required(ATTR_TIME_PERIOD): vol.All( + cv.time_period, cv.positive_timedelta, lambda td: td.total_seconds() // 60 + ), + vol.Optional(ATTR_TEMPERATURE, default="25.0"): vol.Coerce(float), + } +) + +BOOST_HOT_WATER_SCHEMA = vol.Schema( + { + vol.Required(ATTR_ENTITY_ID): cv.entity_id, + vol.Optional(ATTR_TIME_PERIOD, default="00:30:00"): vol.All( + cv.time_period, cv.positive_timedelta, lambda td: td.total_seconds() // 60 + ), + vol.Required(ATTR_MODE): cv.string, + } +) + class HiveSession: """Initiate Hive Session Class.""" - entities = [] + entity_lookup = {} core = None heating = None hotwater = None @@ -51,6 +86,35 @@ class HiveSession: def setup(hass, config): """Set up the Hive Component.""" + + def heating_boost(service): + """Handle the service call.""" + node_id = HiveSession.entity_lookup.get(service.data[ATTR_ENTITY_ID]) + if not node_id: + # log or raise error + _LOGGER.error("Cannot boost entity id entered") + return + + minutes = service.data[ATTR_TIME_PERIOD] + temperature = service.data[ATTR_TEMPERATURE] + + session.heating.turn_boost_on(node_id, minutes, temperature) + + def hot_water_boost(service): + """Handle the service call.""" + node_id = HiveSession.entity_lookup.get(service.data[ATTR_ENTITY_ID]) + if not node_id: + # log or raise error + _LOGGER.error("Cannot boost entity id entered") + return + minutes = service.data[ATTR_TIME_PERIOD] + mode = service.data[ATTR_MODE] + + if mode == "on": + session.hotwater.turn_boost_on(node_id, minutes) + elif mode == "off": + session.hotwater.turn_boost_off(node_id) + session = HiveSession() session.core = Pyhiveapi() @@ -58,9 +122,9 @@ def setup(hass, config): password = config[DOMAIN][CONF_PASSWORD] update_interval = config[DOMAIN][CONF_SCAN_INTERVAL] - devicelist = session.core.initialise_api(username, password, update_interval) + devices = session.core.initialise_api(username, password, update_interval) - if devicelist is None: + if devices is None: _LOGGER.error("Hive API initialization failed") return False @@ -73,9 +137,59 @@ def setup(hass, config): session.attributes = Pyhiveapi.Attributes() hass.data[DATA_HIVE] = session - for ha_type, hive_type in DEVICETYPES.items(): - for key, devices in devicelist.items(): - if key == hive_type: - for hivedevice in devices: - load_platform(hass, ha_type, DOMAIN, hivedevice, config) + for ha_type in DEVICETYPES: + devicelist = devices.get(DEVICETYPES[ha_type]) + if devicelist: + load_platform(hass, ha_type, DOMAIN, devicelist, config) + if ha_type == "climate": + hass.services.register( + DOMAIN, + SERVICE_BOOST_HEATING, + heating_boost, + schema=BOOST_HEATING_SCHEMA, + ) + if ha_type == "water_heater": + hass.services.register( + DOMAIN, + SERVICE_BOOST_HOT_WATER, + hot_water_boost, + schema=BOOST_HOT_WATER_SCHEMA, + ) + return True + + +def refresh_system(func): + """Force update all entities after state change.""" + + @wraps(func) + def wrapper(self, *args, **kwargs): + func(self, *args, **kwargs) + dispatcher_send(self.hass, DOMAIN) + + return wrapper + + +class HiveEntity(Entity): + """Initiate Hive Base Class.""" + + def __init__(self, session, hive_device): + """Initialize the instance.""" + self.node_id = hive_device["Hive_NodeID"] + self.node_name = hive_device["Hive_NodeName"] + self.device_type = hive_device["HA_DeviceType"] + self.node_device_type = hive_device["Hive_DeviceType"] + self.session = session + self.attributes = {} + self._unique_id = f"{self.node_id}-{self.device_type}" + + async def async_added_to_hass(self): + """When entity is added to Home Assistant.""" + async_dispatcher_connect(self.hass, DOMAIN, self._update_callback) + if self.device_type in SERVICES: + self.session.entity_lookup[self.entity_id] = self.node_id + + @callback + def _update_callback(self): + """Call update method.""" + self.async_schedule_update_ha_state() diff --git a/homeassistant/components/hive/binary_sensor.py b/homeassistant/components/hive/binary_sensor.py index 50c8277302f..ce7e53b77a5 100644 --- a/homeassistant/components/hive/binary_sensor.py +++ b/homeassistant/components/hive/binary_sensor.py @@ -1,7 +1,7 @@ """Support for the Hive binary sensors.""" from homeassistant.components.binary_sensor import BinarySensorDevice -from . import DATA_HIVE, DOMAIN +from . import DOMAIN, DATA_HIVE, HiveEntity DEVICETYPE_DEVICE_CLASS = {"motionsensor": "motion", "contactsensor": "opening"} @@ -10,26 +10,17 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up Hive sensor devices.""" if discovery_info is None: return + session = hass.data.get(DATA_HIVE) - - add_entities([HiveBinarySensorEntity(session, discovery_info)]) + devs = [] + for dev in discovery_info: + devs.append(HiveBinarySensorEntity(session, dev)) + add_entities(devs) -class HiveBinarySensorEntity(BinarySensorDevice): +class HiveBinarySensorEntity(HiveEntity, BinarySensorDevice): """Representation of a Hive binary sensor.""" - def __init__(self, hivesession, hivedevice): - """Initialize the hive sensor.""" - self.node_id = hivedevice["Hive_NodeID"] - self.node_name = hivedevice["Hive_NodeName"] - self.device_type = hivedevice["HA_DeviceType"] - self.node_device_type = hivedevice["Hive_DeviceType"] - self.session = hivesession - self.attributes = {} - self.data_updatesource = f"{self.device_type}.{self.node_id}" - self._unique_id = f"{self.node_id}-{self.device_type}" - self.session.entities.append(self) - @property def unique_id(self): """Return unique ID of entity.""" @@ -40,11 +31,6 @@ class HiveBinarySensorEntity(BinarySensorDevice): """Return device information.""" return {"identifiers": {(DOMAIN, self.unique_id)}, "name": self.name} - def handle_update(self, updatesource): - """Handle the new update request.""" - if f"{self.device_type}.{self.node_id}" not in updatesource: - self.schedule_update_ha_state() - @property def device_class(self): """Return the class of this sensor.""" diff --git a/homeassistant/components/hive/climate.py b/homeassistant/components/hive/climate.py index 861957e6ef0..1fb77ce6cb9 100644 --- a/homeassistant/components/hive/climate.py +++ b/homeassistant/components/hive/climate.py @@ -5,13 +5,14 @@ from homeassistant.components.climate.const import ( HVAC_MODE_HEAT, HVAC_MODE_OFF, PRESET_BOOST, + PRESET_NONE, SUPPORT_PRESET_MODE, SUPPORT_TARGET_TEMPERATURE, - PRESET_NONE, ) from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS -from . import DATA_HIVE, DOMAIN + +from . import DOMAIN, DATA_HIVE, HiveEntity, refresh_system HIVE_TO_HASS_STATE = { "SCHEDULE": HVAC_MODE_AUTO, @@ -34,28 +35,21 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up Hive climate devices.""" if discovery_info is None: return - if discovery_info["HA_DeviceType"] != "Heating": - return session = hass.data.get(DATA_HIVE) - climate = HiveClimateEntity(session, discovery_info) - - add_entities([climate]) + devs = [] + for dev in discovery_info: + devs.append(HiveClimateEntity(session, dev)) + add_entities(devs) -class HiveClimateEntity(ClimateDevice): +class HiveClimateEntity(HiveEntity, ClimateDevice): """Hive Climate Device.""" - def __init__(self, hivesession, hivedevice): + def __init__(self, hive_session, hive_device): """Initialize the Climate device.""" - self.node_id = hivedevice["Hive_NodeID"] - self.node_name = hivedevice["Hive_NodeName"] - self.device_type = hivedevice["HA_DeviceType"] - self.thermostat_node_id = hivedevice["Thermostat_NodeID"] - self.session = hivesession - self.attributes = {} - self.data_updatesource = f"{self.device_type}.{self.node_id}" - self._unique_id = f"{self.node_id}-{self.device_type}" + super().__init__(hive_session, hive_device) + self.thermostat_node_id = hive_device["Thermostat_NodeID"] @property def unique_id(self): @@ -72,11 +66,6 @@ class HiveClimateEntity(ClimateDevice): """Return the list of supported features.""" return SUPPORT_FLAGS - def handle_update(self, updatesource): - """Handle the new update request.""" - if f"{self.device_type}.{self.node_id}" not in updatesource: - self.schedule_update_ha_state() - @property def name(self): """Return the name of the Climate device.""" @@ -99,7 +88,7 @@ class HiveClimateEntity(ClimateDevice): return SUPPORT_HVAC @property - def hvac_mode(self) -> str: + def hvac_mode(self): """Return hvac operation ie. heat, cool mode. Need to be one of HVAC_MODE_*. @@ -143,43 +132,29 @@ class HiveClimateEntity(ClimateDevice): """Return a list of available preset modes.""" return SUPPORT_PRESET - async def async_added_to_hass(self): - """When entity is added to Home Assistant.""" - await super().async_added_to_hass() - self.session.entities.append(self) - + @refresh_system def set_hvac_mode(self, hvac_mode): """Set new target hvac mode.""" new_mode = HASS_TO_HIVE_STATE[hvac_mode] self.session.heating.set_mode(self.node_id, new_mode) - for entity in self.session.entities: - entity.handle_update(self.data_updatesource) - + @refresh_system def set_temperature(self, **kwargs): """Set new target temperature.""" new_temperature = kwargs.get(ATTR_TEMPERATURE) if new_temperature is not None: self.session.heating.set_target_temperature(self.node_id, new_temperature) - for entity in self.session.entities: - entity.handle_update(self.data_updatesource) - - def set_preset_mode(self, preset_mode) -> None: + @refresh_system + def set_preset_mode(self, preset_mode): """Set new preset mode.""" if preset_mode == PRESET_NONE and self.preset_mode == PRESET_BOOST: self.session.heating.turn_boost_off(self.node_id) - elif preset_mode == PRESET_BOOST: - curtemp = self.session.heating.current_temperature(self.node_id) - curtemp = round(curtemp * 2) / 2 + curtemp = round(self.current_temperature * 2) / 2 temperature = curtemp + 0.5 - self.session.heating.turn_boost_on(self.node_id, 30, temperature) - for entity in self.session.entities: - entity.handle_update(self.data_updatesource) - def update(self): """Update all Node data from Hive.""" self.session.core.update_data(self.node_id) diff --git a/homeassistant/components/hive/light.py b/homeassistant/components/hive/light.py index a85c3a43992..41fc286d13b 100644 --- a/homeassistant/components/hive/light.py +++ b/homeassistant/components/hive/light.py @@ -10,32 +10,28 @@ from homeassistant.components.light import ( ) import homeassistant.util.color as color_util -from . import DATA_HIVE, DOMAIN +from . import DOMAIN, DATA_HIVE, HiveEntity, refresh_system def setup_platform(hass, config, add_entities, discovery_info=None): """Set up Hive light devices.""" if discovery_info is None: return + session = hass.data.get(DATA_HIVE) - - add_entities([HiveDeviceLight(session, discovery_info)]) + devs = [] + for dev in discovery_info: + devs.append(HiveDeviceLight(session, dev)) + add_entities(devs) -class HiveDeviceLight(Light): +class HiveDeviceLight(HiveEntity, Light): """Hive Active Light Device.""" - def __init__(self, hivesession, hivedevice): + def __init__(self, hive_session, hive_device): """Initialize the Light device.""" - self.node_id = hivedevice["Hive_NodeID"] - self.node_name = hivedevice["Hive_NodeName"] - self.device_type = hivedevice["HA_DeviceType"] - self.light_device_type = hivedevice["Hive_Light_DeviceType"] - self.session = hivesession - self.attributes = {} - self.data_updatesource = f"{self.device_type}.{self.node_id}" - self._unique_id = f"{self.node_id}-{self.device_type}" - self.session.entities.append(self) + super().__init__(hive_session, hive_device) + self.light_device_type = hive_device["Hive_Light_DeviceType"] @property def unique_id(self): @@ -47,11 +43,6 @@ class HiveDeviceLight(Light): """Return device information.""" return {"identifiers": {(DOMAIN, self.unique_id)}, "name": self.name} - def handle_update(self, updatesource): - """Handle the new update request.""" - if f"{self.device_type}.{self.node_id}" not in updatesource: - self.schedule_update_ha_state() - @property def name(self): """Return the display name of this light.""" @@ -106,6 +97,7 @@ class HiveDeviceLight(Light): """Return true if light is on.""" return self.session.light.get_state(self.node_id) + @refresh_system def turn_on(self, **kwargs): """Instruct the light to turn on.""" new_brightness = None @@ -134,14 +126,10 @@ class HiveDeviceLight(Light): new_color, ) - for entity in self.session.entities: - entity.handle_update(self.data_updatesource) - + @refresh_system def turn_off(self, **kwargs): """Instruct the light to turn off.""" self.session.light.turn_off(self.node_id) - for entity in self.session.entities: - entity.handle_update(self.data_updatesource) @property def supported_features(self): diff --git a/homeassistant/components/hive/manifest.json b/homeassistant/components/hive/manifest.json index 886d6841ebb..4164283f9f8 100644 --- a/homeassistant/components/hive/manifest.json +++ b/homeassistant/components/hive/manifest.json @@ -1,9 +1,9 @@ { "domain": "hive", "name": "Hive", - "documentation": "https://www.home-assistant.io/components/hive", + "documentation": "https://www.home-assistant.io/integrations/hive", "requirements": [ - "pyhiveapi==0.2.18.1" + "pyhiveapi==0.2.19.2" ], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/hive/sensor.py b/homeassistant/components/hive/sensor.py index c43fe461a8e..ccd635015de 100644 --- a/homeassistant/components/hive/sensor.py +++ b/homeassistant/components/hive/sensor.py @@ -2,7 +2,7 @@ from homeassistant.const import TEMP_CELSIUS from homeassistant.helpers.entity import Entity -from . import DATA_HIVE, DOMAIN +from . import DOMAIN, DATA_HIVE, HiveEntity FRIENDLY_NAMES = { "Hub_OnlineStatus": "Hive Hub Status", @@ -19,28 +19,18 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up Hive sensor devices.""" if discovery_info is None: return + session = hass.data.get(DATA_HIVE) - - if ( - discovery_info["HA_DeviceType"] == "Hub_OnlineStatus" - or discovery_info["HA_DeviceType"] == "Hive_OutsideTemperature" - ): - add_entities([HiveSensorEntity(session, discovery_info)]) + devs = [] + for dev in discovery_info: + if dev["HA_DeviceType"] in FRIENDLY_NAMES: + devs.append(HiveSensorEntity(session, dev)) + add_entities(devs) -class HiveSensorEntity(Entity): +class HiveSensorEntity(HiveEntity, Entity): """Hive Sensor Entity.""" - def __init__(self, hivesession, hivedevice): - """Initialize the sensor.""" - self.node_id = hivedevice["Hive_NodeID"] - self.device_type = hivedevice["HA_DeviceType"] - self.node_device_type = hivedevice["Hive_DeviceType"] - self.session = hivesession - self.data_updatesource = f"{self.device_type}.{self.node_id}" - self._unique_id = f"{self.node_id}-{self.device_type}" - self.session.entities.append(self) - @property def unique_id(self): """Return unique ID of entity.""" @@ -51,11 +41,6 @@ class HiveSensorEntity(Entity): """Return device information.""" return {"identifiers": {(DOMAIN, self.unique_id)}, "name": self.name} - def handle_update(self, updatesource): - """Handle the new update request.""" - if f"{self.device_type}.{self.node_id}" not in updatesource: - self.schedule_update_ha_state() - @property def name(self): """Return the name of the sensor.""" @@ -82,6 +67,4 @@ class HiveSensorEntity(Entity): def update(self): """Update all Node data from Hive.""" - if self.session.core.update_data(self.node_id): - for entity in self.session.entities: - entity.handle_update(self.data_updatesource) + self.session.core.update_data(self.node_id) diff --git a/homeassistant/components/hive/services.yaml b/homeassistant/components/hive/services.yaml new file mode 100644 index 00000000000..6513d76ca89 --- /dev/null +++ b/homeassistant/components/hive/services.yaml @@ -0,0 +1,27 @@ +boost_heating: + description: "Set the boost mode ON defining the period of time and the desired target temperature + for the boost." + fields: + entity_id: + { + description: Enter the entity_id for the device required to set the boost mode., + example: "climate.heating", + } + time_period: + { description: Set the time period for the boost., example: "01:30:00" } + temperature: + { + description: Set the target temperature for the boost period., + example: "20.5", + } +boost_hot_water: + description: + "Set the boost mode ON or OFF defining the period of time for the boost." + fields: + entity_id: + { + description: Enter the entity_id for the device reuired to set the boost mode., + example: "water_heater.hot_water", + } + time_period: { description: Set the time period for the boost., example: "01:30:00" } + on_off: { description: Set the boost function on or off., example: "on" } diff --git a/homeassistant/components/hive/switch.py b/homeassistant/components/hive/switch.py index 75efdfe3e5d..1447f5483a4 100644 --- a/homeassistant/components/hive/switch.py +++ b/homeassistant/components/hive/switch.py @@ -1,32 +1,24 @@ """Support for the Hive switches.""" from homeassistant.components.switch import SwitchDevice -from . import DATA_HIVE, DOMAIN +from . import DOMAIN, DATA_HIVE, HiveEntity, refresh_system def setup_platform(hass, config, add_entities, discovery_info=None): """Set up Hive switches.""" if discovery_info is None: return + session = hass.data.get(DATA_HIVE) - - add_entities([HiveDevicePlug(session, discovery_info)]) + devs = [] + for dev in discovery_info: + devs.append(HiveDevicePlug(session, dev)) + add_entities(devs) -class HiveDevicePlug(SwitchDevice): +class HiveDevicePlug(HiveEntity, SwitchDevice): """Hive Active Plug.""" - def __init__(self, hivesession, hivedevice): - """Initialize the Switch device.""" - self.node_id = hivedevice["Hive_NodeID"] - self.node_name = hivedevice["Hive_NodeName"] - self.device_type = hivedevice["HA_DeviceType"] - self.session = hivesession - self.attributes = {} - self.data_updatesource = f"{self.device_type}.{self.node_id}" - self._unique_id = f"{self.node_id}-{self.device_type}" - self.session.entities.append(self) - @property def unique_id(self): """Return unique ID of entity.""" @@ -37,11 +29,6 @@ class HiveDevicePlug(SwitchDevice): """Return device information.""" return {"identifiers": {(DOMAIN, self.unique_id)}, "name": self.name} - def handle_update(self, updatesource): - """Handle the new update request.""" - if f"{self.device_type}.{self.node_id}" not in updatesource: - self.schedule_update_ha_state() - @property def name(self): """Return the name of this Switch device if any.""" @@ -62,17 +49,15 @@ class HiveDevicePlug(SwitchDevice): """Return true if switch is on.""" return self.session.switch.get_state(self.node_id) + @refresh_system def turn_on(self, **kwargs): """Turn the switch on.""" self.session.switch.turn_on(self.node_id) - for entity in self.session.entities: - entity.handle_update(self.data_updatesource) + @refresh_system def turn_off(self, **kwargs): """Turn the device off.""" self.session.switch.turn_off(self.node_id) - for entity in self.session.entities: - entity.handle_update(self.data_updatesource) def update(self): """Update all Node data from Hive.""" diff --git a/homeassistant/components/hive/water_heater.py b/homeassistant/components/hive/water_heater.py index 1b009582c1a..c60a9ec01d1 100644 --- a/homeassistant/components/hive/water_heater.py +++ b/homeassistant/components/hive/water_heater.py @@ -1,51 +1,36 @@ """Support for hive water heaters.""" -from homeassistant.const import TEMP_CELSIUS - from homeassistant.components.water_heater import ( STATE_ECO, - STATE_ON, STATE_OFF, + STATE_ON, SUPPORT_OPERATION_MODE, WaterHeaterDevice, ) - -from . import DATA_HIVE, DOMAIN +from homeassistant.const import TEMP_CELSIUS +from . import DOMAIN, DATA_HIVE, HiveEntity, refresh_system SUPPORT_FLAGS_HEATER = SUPPORT_OPERATION_MODE HIVE_TO_HASS_STATE = {"SCHEDULE": STATE_ECO, "ON": STATE_ON, "OFF": STATE_OFF} - HASS_TO_HIVE_STATE = {STATE_ECO: "SCHEDULE", STATE_ON: "ON", STATE_OFF: "OFF"} - SUPPORT_WATER_HEATER = [STATE_ECO, STATE_ON, STATE_OFF] def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the Wink water heater devices.""" + """Set up the Hive water heater devices.""" if discovery_info is None: return - if discovery_info["HA_DeviceType"] != "HotWater": - return session = hass.data.get(DATA_HIVE) - water_heater = HiveWaterHeater(session, discovery_info) - - add_entities([water_heater]) + devs = [] + for dev in discovery_info: + devs.append(HiveWaterHeater(session, dev)) + add_entities(devs) -class HiveWaterHeater(WaterHeaterDevice): +class HiveWaterHeater(HiveEntity, WaterHeaterDevice): """Hive Water Heater Device.""" - def __init__(self, hivesession, hivedevice): - """Initialize the Water Heater device.""" - self.node_id = hivedevice["Hive_NodeID"] - self.node_name = hivedevice["Hive_NodeName"] - self.device_type = hivedevice["HA_DeviceType"] - self.session = hivesession - self.data_updatesource = f"{self.device_type}.{self.node_id}" - self._unique_id = f"{self.node_id}-{self.device_type}" - self._unit_of_measurement = TEMP_CELSIUS - @property def unique_id(self): """Return unique ID of entity.""" @@ -61,11 +46,6 @@ class HiveWaterHeater(WaterHeaterDevice): """Return the list of supported features.""" return SUPPORT_FLAGS_HEATER - def handle_update(self, updatesource): - """Handle the new update request.""" - if f"{self.device_type}.{self.node_id}" not in updatesource: - self.schedule_update_ha_state() - @property def name(self): """Return the name of the water heater.""" @@ -76,7 +56,7 @@ class HiveWaterHeater(WaterHeaterDevice): @property def temperature_unit(self): """Return the unit of measurement.""" - return self._unit_of_measurement + return TEMP_CELSIUS @property def current_operation(self): @@ -88,19 +68,12 @@ class HiveWaterHeater(WaterHeaterDevice): """List of available operation modes.""" return SUPPORT_WATER_HEATER - async def async_added_to_hass(self): - """When entity is added to Home Assistant.""" - await super().async_added_to_hass() - self.session.entities.append(self) - + @refresh_system def set_operation_mode(self, operation_mode): """Set operation mode.""" new_mode = HASS_TO_HIVE_STATE[operation_mode] self.session.hotwater.set_mode(self.node_id, new_mode) - for entity in self.session.entities: - entity.handle_update(self.data_updatesource) - def update(self): """Update all Node data from Hive.""" self.session.core.update_data(self.node_id) diff --git a/homeassistant/components/hlk_sw16/manifest.json b/homeassistant/components/hlk_sw16/manifest.json index 5266b81ab03..037df20b35d 100644 --- a/homeassistant/components/hlk_sw16/manifest.json +++ b/homeassistant/components/hlk_sw16/manifest.json @@ -1,7 +1,7 @@ { "domain": "hlk_sw16", "name": "Hlk sw16", - "documentation": "https://www.home-assistant.io/components/hlk_sw16", + "documentation": "https://www.home-assistant.io/integrations/hlk_sw16", "requirements": [ "hlk-sw16==0.0.7" ], diff --git a/homeassistant/components/homeassistant/manifest.json b/homeassistant/components/homeassistant/manifest.json index b612c3a9fa6..b4c03047a7a 100644 --- a/homeassistant/components/homeassistant/manifest.json +++ b/homeassistant/components/homeassistant/manifest.json @@ -1,7 +1,7 @@ { "domain": "homeassistant", "name": "Home Assistant Core Integration", - "documentation": "https://www.home-assistant.io/components/homeassistant", + "documentation": "https://www.home-assistant.io/integrations/homeassistant", "requirements": [], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/homekit/manifest.json b/homeassistant/components/homekit/manifest.json index ea3e801ac53..c0ab61d8568 100644 --- a/homeassistant/components/homekit/manifest.json +++ b/homeassistant/components/homekit/manifest.json @@ -1,9 +1,9 @@ { "domain": "homekit", "name": "Homekit", - "documentation": "https://www.home-assistant.io/components/homekit", + "documentation": "https://www.home-assistant.io/integrations/homekit", "requirements": [ - "HAP-python==2.5.0" + "HAP-python==2.6.0" ], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/homekit_controller/.translations/es-419.json b/homeassistant/components/homekit_controller/.translations/es-419.json index 9ddf336c060..67a65f752b4 100644 --- a/homeassistant/components/homekit_controller/.translations/es-419.json +++ b/homeassistant/components/homekit_controller/.translations/es-419.json @@ -4,6 +4,7 @@ "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." }, + "flow_title": "Accesorio HomeKit: {name}", "step": { "pair": { "data": { diff --git a/homeassistant/components/homekit_controller/.translations/zh-Hans.json b/homeassistant/components/homekit_controller/.translations/zh-Hans.json index 8d064622f7e..d9fdc8f91c2 100644 --- a/homeassistant/components/homekit_controller/.translations/zh-Hans.json +++ b/homeassistant/components/homekit_controller/.translations/zh-Hans.json @@ -23,7 +23,7 @@ "data": { "pairing_code": "\u914d\u5bf9\u4ee3\u7801" }, - "description": "\u8f93\u5165\u60a8\u7684 HomeKit \u914d\u5bf9\u4ee3\u7801\u4ee5\u4f7f\u7528\u6b64\u914d\u4ef6", + "description": "\u8f93\u5165\u60a8\u7684HomeKit\u914d\u5bf9\u4ee3\u7801\uff08\u683c\u5f0f\u4e3aXXX-XX-XXX\uff09\u4ee5\u4f7f\u7528\u6b64\u914d\u4ef6", "title": "\u4e0e HomeKit \u914d\u4ef6\u914d\u5bf9" }, "user": { diff --git a/homeassistant/components/homekit_controller/.translations/zh-Hant.json b/homeassistant/components/homekit_controller/.translations/zh-Hant.json index 7340569e64f..68e87e9aea8 100644 --- a/homeassistant/components/homekit_controller/.translations/zh-Hant.json +++ b/homeassistant/components/homekit_controller/.translations/zh-Hant.json @@ -1,20 +1,20 @@ { "config": { "abort": { - "accessory_not_found_error": "\u627e\u4e0d\u5230\u88dd\u7f6e\uff0c\u7121\u6cd5\u65b0\u589e\u914d\u5c0d\u3002", + "accessory_not_found_error": "\u627e\u4e0d\u5230\u8a2d\u5099\uff0c\u7121\u6cd5\u65b0\u589e\u914d\u5c0d\u3002", "already_configured": "\u914d\u4ef6\u5df2\u7d93\u7531\u6b64\u63a7\u5236\u5668\u8a2d\u5b9a\u5b8c\u6210", - "already_in_progress": "\u88dd\u7f6e\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d\u3002", - "already_paired": "\u914d\u4ef6\u5df2\u7d93\u8207\u5176\u4ed6\u88dd\u7f6e\u914d\u5c0d\uff0c\u8acb\u91cd\u7f6e\u914d\u4ef6\u5f8c\u518d\u8a66\u4e00\u6b21\u3002", + "already_in_progress": "\u8a2d\u5099\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d\u3002", + "already_paired": "\u914d\u4ef6\u5df2\u7d93\u8207\u5176\u4ed6\u8a2d\u5099\u914d\u5c0d\uff0c\u8acb\u91cd\u7f6e\u914d\u4ef6\u5f8c\u518d\u8a66\u4e00\u6b21\u3002", "ignored_model": "\u7531\u65bc\u6b64\u578b\u865f\u53ef\u539f\u751f\u652f\u63f4\u66f4\u5b8c\u6574\u529f\u80fd\uff0c\u56e0\u6b64 Homekit \u652f\u63f4\u5df2\u88ab\u7981\u6b62\u3002", - "invalid_config_entry": "\u88dd\u7f6e\u986f\u793a\u7b49\u5f85\u9032\u884c\u914d\u5c0d\uff0c\u4f46 Home Assistant \u986f\u793a\u6709\u76f8\u885d\u7a81\u8a2d\u5b9a\u7269\u4ef6\u5fc5\u9808\u5148\u884c\u79fb\u9664\u3002", - "no_devices": "\u627e\u4e0d\u5230\u4efb\u4f55\u672a\u914d\u5c0d\u88dd\u7f6e" + "invalid_config_entry": "\u8a2d\u5099\u986f\u793a\u7b49\u5f85\u9032\u884c\u914d\u5c0d\uff0c\u4f46 Home Assistant \u986f\u793a\u6709\u76f8\u885d\u7a81\u8a2d\u5b9a\u7269\u4ef6\u5fc5\u9808\u5148\u884c\u79fb\u9664\u3002", + "no_devices": "\u627e\u4e0d\u5230\u4efb\u4f55\u672a\u914d\u5c0d\u8a2d\u5099" }, "error": { "authentication_error": "Homekit \u4ee3\u78bc\u932f\u8aa4\uff0c\u8acb\u78ba\u5b9a\u5f8c\u518d\u8a66\u4e00\u6b21\u3002", - "busy_error": "\u88dd\u7f6e\u5df2\u7d93\u8207\u5176\u4ed6\u63a7\u5236\u5668\u914d\u5c0d\uff0c\u62d2\u7d55\u9032\u884c\u914d\u5c0d\u3002", - "max_peers_error": "\u88dd\u7f6e\u5df2\u7121\u5269\u9918\u914d\u5c0d\u7a7a\u9593\uff0c\u62d2\u7d55\u9032\u884c\u914d\u5c0d\u3002", - "max_tries_error": "\u88dd\u7f6e\u6536\u5230\u8d85\u904e 100 \u6b21\u672a\u6210\u529f\u8a8d\u8b49\u5f8c\uff0c\u62d2\u7d55\u9032\u884c\u914d\u5c0d\u3002", - "pairing_failed": "\u7576\u8a66\u5716\u8207\u88dd\u7f6e\u914d\u5c0d\u6642\u767c\u751f\u7121\u6cd5\u8655\u7406\u932f\u8aa4\uff0c\u53ef\u80fd\u50c5\u70ba\u66ab\u6642\u5931\u6548\u3001\u6216\u8005\u88dd\u7f6e\u76ee\u524d\u4e0d\u652f\u63f4\u3002", + "busy_error": "\u8a2d\u5099\u5df2\u7d93\u8207\u5176\u4ed6\u63a7\u5236\u5668\u914d\u5c0d\uff0c\u62d2\u7d55\u9032\u884c\u914d\u5c0d\u3002", + "max_peers_error": "\u8a2d\u5099\u5df2\u7121\u5269\u9918\u914d\u5c0d\u7a7a\u9593\uff0c\u62d2\u7d55\u9032\u884c\u914d\u5c0d\u3002", + "max_tries_error": "\u8a2d\u5099\u6536\u5230\u8d85\u904e 100 \u6b21\u672a\u6210\u529f\u8a8d\u8b49\u5f8c\uff0c\u62d2\u7d55\u9032\u884c\u914d\u5c0d\u3002", + "pairing_failed": "\u7576\u8a66\u5716\u8207\u8a2d\u5099\u914d\u5c0d\u6642\u767c\u751f\u7121\u6cd5\u8655\u7406\u932f\u8aa4\uff0c\u53ef\u80fd\u50c5\u70ba\u66ab\u6642\u5931\u6548\u3001\u6216\u8005\u8a2d\u5099\u76ee\u524d\u4e0d\u652f\u63f4\u3002", "unable_to_pair": "\u7121\u6cd5\u914d\u5c0d\uff0c\u8acb\u518d\u8a66\u4e00\u6b21\u3002", "unknown_error": "\u88dd\u7f6e\u56de\u5831\u672a\u77e5\u932f\u8aa4\u3002\u914d\u5c0d\u5931\u6557\u3002" }, diff --git a/homeassistant/components/homekit_controller/cover.py b/homeassistant/components/homekit_controller/cover.py index 7f70b0cfac0..0606778acb5 100644 --- a/homeassistant/components/homekit_controller/cover.py +++ b/homeassistant/components/homekit_controller/cover.py @@ -33,7 +33,7 @@ CURRENT_GARAGE_STATE_MAP = { TARGET_GARAGE_STATE_MAP = {STATE_OPEN: 0, STATE_CLOSED: 1, STATE_STOPPED: 2} -CURRENT_WINDOW_STATE_MAP = {0: STATE_OPENING, 1: STATE_CLOSING, 2: STATE_STOPPED} +CURRENT_WINDOW_STATE_MAP = {0: STATE_CLOSING, 1: STATE_OPENING, 2: STATE_STOPPED} async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): diff --git a/homeassistant/components/homekit_controller/manifest.json b/homeassistant/components/homekit_controller/manifest.json index 70f6f6a3ce4..2a660281311 100644 --- a/homeassistant/components/homekit_controller/manifest.json +++ b/homeassistant/components/homekit_controller/manifest.json @@ -2,7 +2,7 @@ "domain": "homekit_controller", "name": "Homekit controller", "config_flow": true, - "documentation": "https://www.home-assistant.io/components/homekit_controller", + "documentation": "https://www.home-assistant.io/integrations/homekit_controller", "requirements": [ "homekit[IP]==0.15.0" ], diff --git a/homeassistant/components/homematic/__init__.py b/homeassistant/components/homematic/__init__.py index 598e3765612..cd791434f90 100644 --- a/homeassistant/components/homematic/__init__.py +++ b/homeassistant/components/homematic/__init__.py @@ -7,6 +7,7 @@ import voluptuous as vol from homeassistant.const import ( ATTR_ENTITY_ID, + ATTR_MODE, ATTR_NAME, CONF_HOST, CONF_HOSTS, @@ -47,7 +48,6 @@ ATTR_VALUE_TYPE = "value_type" ATTR_INTERFACE = "interface" ATTR_ERRORCODE = "error" ATTR_MESSAGE = "message" -ATTR_MODE = "mode" ATTR_TIME = "time" ATTR_UNIQUE_ID = "unique_id" ATTR_PARAMSET_KEY = "paramset_key" diff --git a/homeassistant/components/homematic/manifest.json b/homeassistant/components/homematic/manifest.json index 3c350e75730..260e54e65c4 100644 --- a/homeassistant/components/homematic/manifest.json +++ b/homeassistant/components/homematic/manifest.json @@ -1,7 +1,7 @@ { "domain": "homematic", "name": "Homematic", - "documentation": "https://www.home-assistant.io/components/homematic", + "documentation": "https://www.home-assistant.io/integrations/homematic", "requirements": [ "pyhomematic==0.1.60" ], diff --git a/homeassistant/components/homematicip_cloud/.translations/ru.json b/homeassistant/components/homematicip_cloud/.translations/ru.json index 82ecd4a3250..5155a42c4c3 100644 --- a/homeassistant/components/homematicip_cloud/.translations/ru.json +++ b/homeassistant/components/homematicip_cloud/.translations/ru.json @@ -1,7 +1,7 @@ { "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.", "connection_aborted": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u0441\u0435\u0440\u0432\u0435\u0440\u0443 HMIP", "unknown": "\u0412\u043e\u0437\u043d\u0438\u043a\u043b\u0430 \u043d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, diff --git a/homeassistant/components/homematicip_cloud/.translations/zh-Hant.json b/homeassistant/components/homematicip_cloud/.translations/zh-Hant.json index f95ccf57272..c6a960f1a68 100644 --- a/homeassistant/components/homematicip_cloud/.translations/zh-Hant.json +++ b/homeassistant/components/homematicip_cloud/.translations/zh-Hant.json @@ -15,7 +15,7 @@ "init": { "data": { "hapid": "Access point ID (SGTIN)", - "name": "\u540d\u7a31\uff08\u9078\u9805\uff0c\u7528\u4ee5\u4f5c\u70ba\u6240\u6709\u88dd\u7f6e\u7684\u5b57\u9996\u7528\uff09", + "name": "\u540d\u7a31\uff08\u9078\u9805\uff0c\u7528\u4ee5\u4f5c\u70ba\u6240\u6709\u8a2d\u5099\u7684\u5b57\u9996\u7528\uff09", "pin": "PIN \u78bc\uff08\u9078\u9805\uff09" }, "title": "\u9078\u64c7 HomematicIP Access point" diff --git a/homeassistant/components/homematicip_cloud/binary_sensor.py b/homeassistant/components/homematicip_cloud/binary_sensor.py index 594f4f6c54a..4ac4614379b 100644 --- a/homeassistant/components/homematicip_cloud/binary_sensor.py +++ b/homeassistant/components/homematicip_cloud/binary_sensor.py @@ -38,7 +38,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from . import DOMAIN as HMIPC_DOMAIN, HMIPC_HAPID, HomematicipGenericDevice -from .device import ATTR_GROUP_MEMBER_UNREACHABLE, ATTR_MODEL_TYPE +from .device import ATTR_GROUP_MEMBER_UNREACHABLE, ATTR_IS_GROUP, ATTR_MODEL_TYPE _LOGGER = logging.getLogger(__name__) @@ -312,7 +312,7 @@ class HomematicipSecurityZoneSensorGroup(HomematicipGenericDevice, BinarySensorD @property def device_state_attributes(self): """Return the state attributes of the security zone group.""" - state_attr = {ATTR_MODEL_TYPE: self._device.modelType} + state_attr = {ATTR_MODEL_TYPE: self._device.modelType, ATTR_IS_GROUP: True} for attr, attr_key in GROUP_ATTRIBUTES.items(): attr_value = getattr(self._device, attr, None) diff --git a/homeassistant/components/homematicip_cloud/device.py b/homeassistant/components/homematicip_cloud/device.py index 5eeb14b6359..1273278189d 100644 --- a/homeassistant/components/homematicip_cloud/device.py +++ b/homeassistant/components/homematicip_cloud/device.py @@ -6,12 +6,15 @@ from homematicip.aio.device import AsyncDevice from homematicip.aio.home import AsyncHome from homeassistant.components import homematicip_cloud +from homeassistant.core import callback +from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) ATTR_MODEL_TYPE = "model_type" ATTR_ID = "id" +ATTR_IS_GROUP = "is_group" # RSSI HAP -> Device ATTR_RSSI_DEVICE = "rssi_device" # RSSI Device -> HAP @@ -50,6 +53,8 @@ class HomematicipGenericDevice(Entity): self._home = home self._device = device self.post = post + # Marker showing that the HmIP device hase been removed. + self.hmip_device_removed = False _LOGGER.info("Setting up %s (%s)", self.name, self._device.modelType) @property @@ -73,7 +78,9 @@ class HomematicipGenericDevice(Entity): async def async_added_to_hass(self): """Register callbacks.""" self._device.on_update(self._async_device_changed) + self._device.on_remove(self._async_device_removed) + @callback def _async_device_changed(self, *args, **kwargs): """Handle device state changes.""" # Don't update disabled entities @@ -87,6 +94,47 @@ class HomematicipGenericDevice(Entity): self._device.modelType, ) + async def async_will_remove_from_hass(self) -> None: + """Run when entity will be removed from hass.""" + + # Only go further if the device/entity should be removed from registries + # due to a removal of the HmIP device. + if self.hmip_device_removed: + await self.async_remove_from_registries() + + async def async_remove_from_registries(self) -> None: + """Remove entity/device from registry.""" + + # Remove callback from device. + self._device.remove_callback(self._async_device_changed) + self._device.remove_callback(self._async_device_removed) + + if not self.registry_entry: + return + + device_id = self.registry_entry.device_id + if device_id: + # Remove from device registry. + device_registry = await dr.async_get_registry(self.hass) + if device_id in device_registry.devices: + # This will also remove associated entities from entity registry. + device_registry.async_remove_device(device_id) + else: + # Remove from entity registry. + # Only relevant for entities that do not belong to a device. + entity_id = self.registry_entry.entity_id + if entity_id: + entity_registry = await er.async_get_registry(self.hass) + if entity_id in entity_registry.entities: + entity_registry.async_remove(entity_id) + + @callback + def _async_device_removed(self, *args, **kwargs): + """Handle hmip device removal.""" + # Set marker showing that the HmIP device hase been removed. + self.hmip_device_removed = True + self.hass.async_create_task(self.async_remove()) + @property def name(self) -> str: """Return the name of the generic device.""" @@ -131,4 +179,6 @@ class HomematicipGenericDevice(Entity): if attr_value: state_attr[attr_key] = attr_value + state_attr[ATTR_IS_GROUP] = False + return state_attr diff --git a/homeassistant/components/homematicip_cloud/hap.py b/homeassistant/components/homematicip_cloud/hap.py index 23973efb07b..abba183d339 100644 --- a/homeassistant/components/homematicip_cloud/hap.py +++ b/homeassistant/components/homematicip_cloud/hap.py @@ -5,6 +5,7 @@ import logging from homematicip.aio.auth import AsyncAuth from homematicip.aio.home import AsyncHome from homematicip.base.base_connection import HmipConnectionError +from homematicip.base.enums import EventType from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback @@ -137,6 +138,18 @@ class HomematicipHAP: self.home.update_home_only(args[0]) + @callback + def async_create_entity(self, *args, **kwargs): + """Create a device or a group.""" + is_device = EventType(kwargs["event_type"]) == EventType.DEVICE_ADDED + self.hass.async_create_task(self.async_create_entity_lazy(is_device)) + + async def async_create_entity_lazy(self, is_device=True): + """Delay entity creation to allow the user to enter a device name.""" + if is_device: + await asyncio.sleep(30) + await self.hass.config_entries.async_reload(self.config_entry.entry_id) + async def get_state(self): """Update HMIP state and tell Home Assistant.""" await self.home.get_current_state() @@ -225,6 +238,7 @@ class HomematicipHAP: except HmipConnectionError: raise HmipcConnectionError home.on_update(self.async_update) + home.on_create(self.async_create_entity) hass.loop.create_task(self.async_connect()) return home diff --git a/homeassistant/components/homematicip_cloud/manifest.json b/homeassistant/components/homematicip_cloud/manifest.json index 2a041ce6689..40c8c7c3598 100644 --- a/homeassistant/components/homematicip_cloud/manifest.json +++ b/homeassistant/components/homematicip_cloud/manifest.json @@ -2,9 +2,9 @@ "domain": "homematicip_cloud", "name": "Homematicip cloud", "config_flow": true, - "documentation": "https://www.home-assistant.io/components/homematicip_cloud", + "documentation": "https://www.home-assistant.io/integrations/homematicip_cloud", "requirements": [ - "homematicip==0.10.10" + "homematicip==0.10.12" ], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/homematicip_cloud/sensor.py b/homeassistant/components/homematicip_cloud/sensor.py index 43812df94d2..770921288b9 100644 --- a/homeassistant/components/homematicip_cloud/sensor.py +++ b/homeassistant/components/homematicip_cloud/sensor.py @@ -35,7 +35,7 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from . import DOMAIN as HMIPC_DOMAIN, HMIPC_HAPID, HomematicipGenericDevice -from .device import ATTR_MODEL_TYPE +from .device import ATTR_IS_GROUP, ATTR_MODEL_TYPE _LOGGER = logging.getLogger(__name__) @@ -150,8 +150,8 @@ class HomematicipAccesspointStatus(HomematicipGenericDevice): @property def device_state_attributes(self): - """Return the state attributes of the security zone group.""" - return {ATTR_MODEL_TYPE: self._device.modelType} + """Return the state attributes of the access point.""" + return {ATTR_MODEL_TYPE: self._device.modelType, ATTR_IS_GROUP: False} class HomematicipHeatingThermostat(HomematicipGenericDevice): diff --git a/homeassistant/components/homematicip_cloud/switch.py b/homeassistant/components/homematicip_cloud/switch.py index 058e21262e3..ababf793f0c 100644 --- a/homeassistant/components/homematicip_cloud/switch.py +++ b/homeassistant/components/homematicip_cloud/switch.py @@ -19,7 +19,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from . import DOMAIN as HMIPC_DOMAIN, HMIPC_HAPID, HomematicipGenericDevice -from .device import ATTR_GROUP_MEMBER_UNREACHABLE +from .device import ATTR_GROUP_MEMBER_UNREACHABLE, ATTR_IS_GROUP _LOGGER = logging.getLogger(__name__) @@ -113,7 +113,7 @@ class HomematicipGroupSwitch(HomematicipGenericDevice, SwitchDevice): @property def device_state_attributes(self): """Return the state attributes of the switch-group.""" - state_attr = {} + state_attr = {ATTR_IS_GROUP: True} if self._device.unreach: state_attr[ATTR_GROUP_MEMBER_UNREACHABLE] = True return state_attr diff --git a/homeassistant/components/homematicip_cloud/weather.py b/homeassistant/components/homematicip_cloud/weather.py index 2d0a69d7d06..ed9098559a3 100644 --- a/homeassistant/components/homematicip_cloud/weather.py +++ b/homeassistant/components/homematicip_cloud/weather.py @@ -7,6 +7,7 @@ from homematicip.aio.device import ( AsyncWeatherSensorPro, ) from homematicip.aio.home import AsyncHome +from homematicip.base.enums import WeatherCondition from homeassistant.components.weather import WeatherEntity from homeassistant.config_entries import ConfigEntry @@ -17,6 +18,24 @@ from . import DOMAIN as HMIPC_DOMAIN, HMIPC_HAPID, HomematicipGenericDevice _LOGGER = logging.getLogger(__name__) +HOME_WEATHER_CONDITION = { + WeatherCondition.CLEAR: "sunny", + WeatherCondition.LIGHT_CLOUDY: "partlycloudy", + WeatherCondition.CLOUDY: "cloudy", + WeatherCondition.CLOUDY_WITH_RAIN: "rainy", + WeatherCondition.CLOUDY_WITH_SNOW_RAIN: "snowy-rainy", + WeatherCondition.HEAVILY_CLOUDY: "cloudy", + WeatherCondition.HEAVILY_CLOUDY_WITH_RAIN: "rainy", + WeatherCondition.HEAVILY_CLOUDY_WITH_STRONG_RAIN: "snowy-rainy", + WeatherCondition.HEAVILY_CLOUDY_WITH_SNOW: "snowy", + WeatherCondition.HEAVILY_CLOUDY_WITH_SNOW_RAIN: "snowy-rainy", + WeatherCondition.HEAVILY_CLOUDY_WITH_THUNDER: "lightning", + WeatherCondition.HEAVILY_CLOUDY_WITH_RAIN_AND_THUNDER: "lightning-rainy", + WeatherCondition.FOGGY: "fog", + WeatherCondition.STRONG_WIND: "windy", + WeatherCondition.UNKNOWN: "", +} + async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the HomematicIP Cloud weather sensor.""" @@ -35,6 +54,8 @@ async def async_setup_entry( elif isinstance(device, (AsyncWeatherSensor, AsyncWeatherSensorPlus)): devices.append(HomematicipWeatherSensor(home, device)) + devices.append(HomematicipHomeWeather(home)) + if devices: async_add_entities(devices) @@ -95,3 +116,57 @@ class HomematicipWeatherSensorPro(HomematicipWeatherSensor): def wind_bearing(self) -> float: """Return the wind bearing.""" return self._device.windDirection + + +class HomematicipHomeWeather(HomematicipGenericDevice, WeatherEntity): + """representation of a HomematicIP Cloud home weather.""" + + def __init__(self, home: AsyncHome) -> None: + """Initialize the home weather.""" + home.weather.modelType = "HmIP-Home-Weather" + super().__init__(home, home) + + @property + def available(self) -> bool: + """Device available.""" + return self._home.connected + + @property + def name(self) -> str: + """Return the name of the sensor.""" + return f"Weather {self._home.location.city}" + + @property + def temperature(self) -> float: + """Return the platform temperature.""" + return self._device.weather.temperature + + @property + def temperature_unit(self) -> str: + """Return the unit of measurement.""" + return TEMP_CELSIUS + + @property + def humidity(self) -> int: + """Return the humidity.""" + return self._device.weather.humidity + + @property + def wind_speed(self) -> float: + """Return the wind speed.""" + return round(self._device.weather.windSpeed, 1) + + @property + def wind_bearing(self) -> float: + """Return the wind bearing.""" + return self._device.weather.windDirection + + @property + def attribution(self) -> str: + """Return the attribution.""" + return "Powered by Homematic IP" + + @property + def condition(self) -> str: + """Return the current condition.""" + return HOME_WEATHER_CONDITION.get(self._device.weather.weatherCondition) diff --git a/homeassistant/components/homeworks/manifest.json b/homeassistant/components/homeworks/manifest.json index cdbbffb8d36..f2929fb655e 100644 --- a/homeassistant/components/homeworks/manifest.json +++ b/homeassistant/components/homeworks/manifest.json @@ -1,7 +1,7 @@ { "domain": "homeworks", "name": "Homeworks", - "documentation": "https://www.home-assistant.io/components/homeworks", + "documentation": "https://www.home-assistant.io/integrations/homeworks", "requirements": [ "pyhomeworks==0.0.6" ], diff --git a/homeassistant/components/honeywell/climate.py b/homeassistant/components/honeywell/climate.py index 4b73cf4f2b5..15efac87587 100644 --- a/homeassistant/components/honeywell/climate.py +++ b/homeassistant/components/honeywell/climate.py @@ -141,7 +141,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): _LOGGER.warning( "The honeywell component has been deprecated for EU (i.e. non-US) " "systems. For EU-based systems, use the evohome component, " - "see: https://home-assistant.io/components/evohome" + "see: https://home-assistant.io/integrations/evohome" ) diff --git a/homeassistant/components/honeywell/manifest.json b/homeassistant/components/honeywell/manifest.json index b50c7f61dd5..9d644de4445 100644 --- a/homeassistant/components/honeywell/manifest.json +++ b/homeassistant/components/honeywell/manifest.json @@ -1,7 +1,7 @@ { "domain": "honeywell", "name": "Honeywell", - "documentation": "https://www.home-assistant.io/components/honeywell", + "documentation": "https://www.home-assistant.io/integrations/honeywell", "requirements": [ "somecomfort==0.5.2" ], diff --git a/homeassistant/components/hook/manifest.json b/homeassistant/components/hook/manifest.json index d9898a71f8b..035354c969a 100644 --- a/homeassistant/components/hook/manifest.json +++ b/homeassistant/components/hook/manifest.json @@ -1,7 +1,7 @@ { "domain": "hook", "name": "Hook", - "documentation": "https://www.home-assistant.io/components/hook", + "documentation": "https://www.home-assistant.io/integrations/hook", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/horizon/manifest.json b/homeassistant/components/horizon/manifest.json index 2916e81ce4f..4ba3a61d8b7 100644 --- a/homeassistant/components/horizon/manifest.json +++ b/homeassistant/components/horizon/manifest.json @@ -1,7 +1,7 @@ { "domain": "horizon", "name": "Horizon", - "documentation": "https://www.home-assistant.io/components/horizon", + "documentation": "https://www.home-assistant.io/integrations/horizon", "requirements": [ "horimote==0.4.1" ], diff --git a/homeassistant/components/hp_ilo/manifest.json b/homeassistant/components/hp_ilo/manifest.json index a3d5853541f..3dc591cac4c 100644 --- a/homeassistant/components/hp_ilo/manifest.json +++ b/homeassistant/components/hp_ilo/manifest.json @@ -1,7 +1,7 @@ { "domain": "hp_ilo", "name": "Hp ilo", - "documentation": "https://www.home-assistant.io/components/hp_ilo", + "documentation": "https://www.home-assistant.io/integrations/hp_ilo", "requirements": [ "python-hpilo==4.3" ], diff --git a/homeassistant/components/html5/manifest.json b/homeassistant/components/html5/manifest.json index 7b43ec44ef3..667a5789182 100644 --- a/homeassistant/components/html5/manifest.json +++ b/homeassistant/components/html5/manifest.json @@ -1,7 +1,7 @@ { "domain": "html5", "name": "HTML5 Notifications", - "documentation": "https://www.home-assistant.io/components/html5", + "documentation": "https://www.home-assistant.io/integrations/html5", "requirements": [ "pywebpush==1.9.2" ], diff --git a/homeassistant/components/http/ban.py b/homeassistant/components/http/ban.py index d8fa8853c7f..7d1e24f3698 100644 --- a/homeassistant/components/http/ban.py +++ b/homeassistant/components/http/ban.py @@ -18,7 +18,7 @@ from homeassistant.util.yaml import dump from .const import KEY_REAL_IP -# mypy: allow-incomplete-defs, allow-untyped-defs, no-check-untyped-defs +# mypy: allow-untyped-defs, no-check-untyped-defs _LOGGER = logging.getLogger(__name__) @@ -165,7 +165,7 @@ class IpBan: self.banned_at = banned_at or datetime.utcnow() -async def async_load_ip_bans_config(hass: HomeAssistant, path: str): +async def async_load_ip_bans_config(hass: HomeAssistant, path: str) -> List[IpBan]: """Load list of banned IPs from config file.""" ip_list: List[IpBan] = [] @@ -188,7 +188,7 @@ async def async_load_ip_bans_config(hass: HomeAssistant, path: str): return ip_list -def update_ip_bans_config(path: str, ip_ban: IpBan): +def update_ip_bans_config(path: str, ip_ban: IpBan) -> None: """Update config file with new banned IP address.""" with open(path, "a") as out: ip_ = { diff --git a/homeassistant/components/http/manifest.json b/homeassistant/components/http/manifest.json index 0bc5586445d..6db8b041cfd 100644 --- a/homeassistant/components/http/manifest.json +++ b/homeassistant/components/http/manifest.json @@ -1,7 +1,7 @@ { "domain": "http", "name": "HTTP", - "documentation": "https://www.home-assistant.io/components/http", + "documentation": "https://www.home-assistant.io/integrations/http", "requirements": [ "aiohttp_cors==0.7.0" ], diff --git a/homeassistant/components/http/static.py b/homeassistant/components/http/static.py index 952ca473fdc..e6a70c9f643 100644 --- a/homeassistant/components/http/static.py +++ b/homeassistant/components/http/static.py @@ -42,8 +42,10 @@ class CachingStaticResource(StaticResource): if filepath.is_dir(): return await super()._handle(request) if filepath.is_file(): - # type ignore: https://github.com/aio-libs/aiohttp/pull/3976 - return FileResponse( # type: ignore - filepath, chunk_size=self._chunk_size, headers=CACHE_HEADERS + return FileResponse( + filepath, + chunk_size=self._chunk_size, + # type ignore: https://github.com/aio-libs/aiohttp/pull/3976 + headers=CACHE_HEADERS, # type: ignore ) raise HTTPNotFound diff --git a/homeassistant/components/htu21d/manifest.json b/homeassistant/components/htu21d/manifest.json index 70093df9b55..14b0d7b3f15 100644 --- a/homeassistant/components/htu21d/manifest.json +++ b/homeassistant/components/htu21d/manifest.json @@ -1,7 +1,7 @@ { "domain": "htu21d", "name": "Htu21d", - "documentation": "https://www.home-assistant.io/components/htu21d", + "documentation": "https://www.home-assistant.io/integrations/htu21d", "requirements": [ "i2csense==0.0.4", "smbus-cffi==0.5.1" diff --git a/homeassistant/components/huawei_lte/manifest.json b/homeassistant/components/huawei_lte/manifest.json index 3af23be4f0b..5d559cc60c5 100644 --- a/homeassistant/components/huawei_lte/manifest.json +++ b/homeassistant/components/huawei_lte/manifest.json @@ -1,7 +1,7 @@ { "domain": "huawei_lte", "name": "Huawei LTE", - "documentation": "https://www.home-assistant.io/components/huawei_lte", + "documentation": "https://www.home-assistant.io/integrations/huawei_lte", "requirements": [ "getmac==0.8.1", "huawei-lte-api==1.3.0" diff --git a/homeassistant/components/huawei_router/manifest.json b/homeassistant/components/huawei_router/manifest.json index 54fd155b557..2affcb8ee2e 100644 --- a/homeassistant/components/huawei_router/manifest.json +++ b/homeassistant/components/huawei_router/manifest.json @@ -1,7 +1,7 @@ { "domain": "huawei_router", "name": "Huawei router", - "documentation": "https://www.home-assistant.io/components/huawei_router", + "documentation": "https://www.home-assistant.io/integrations/huawei_router", "requirements": [], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/hue/.translations/ru.json b/homeassistant/components/hue/.translations/ru.json index be5d2b7159d..79a46e1861b 100644 --- a/homeassistant/components/hue/.translations/ru.json +++ b/homeassistant/components/hue/.translations/ru.json @@ -1,8 +1,8 @@ { "config": { "abort": { - "all_configured": "\u0412\u0441\u0435 Philips Hue \u0448\u043b\u044e\u0437\u044b \u0443\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u044b", - "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430", + "all_configured": "\u0412\u0441\u0435 Philips Hue \u0448\u043b\u044e\u0437\u044b \u0443\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u044b.", + "already_configured": "\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_in_progress": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0448\u043b\u044e\u0437\u0430 \u0443\u0436\u0435 \u043d\u0430\u0447\u0430\u0442\u0430.", "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", "discover_timeout": "\u0428\u043b\u044e\u0437 Philips Hue \u043d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d", diff --git a/homeassistant/components/hue/.translations/zh-Hant.json b/homeassistant/components/hue/.translations/zh-Hant.json index 7a08b44f25a..6bbe75a8019 100644 --- a/homeassistant/components/hue/.translations/zh-Hant.json +++ b/homeassistant/components/hue/.translations/zh-Hant.json @@ -7,7 +7,7 @@ "cannot_connect": "\u7121\u6cd5\u9023\u7dda\u81f3 Bridge", "discover_timeout": "\u7121\u6cd5\u641c\u5c0b\u5230 Hue Bridge", "no_bridges": "\u672a\u641c\u5c0b\u5230 Philips Hue Bridge", - "not_hue_bridge": "\u975e Hue Bridge \u88dd\u7f6e", + "not_hue_bridge": "\u975e Hue Bridge \u8a2d\u5099", "unknown": "\u767c\u751f\u672a\u77e5\u932f\u8aa4" }, "error": { diff --git a/homeassistant/components/hue/light.py b/homeassistant/components/hue/light.py index 96c749a3413..5a3379f71ce 100644 --- a/homeassistant/components/hue/light.py +++ b/homeassistant/components/hue/light.py @@ -8,6 +8,9 @@ import random import aiohue import async_timeout +from homeassistant.helpers.entity_registry import async_get_registry as get_ent_reg +from homeassistant.helpers.device_registry import async_get_registry as get_dev_reg + from homeassistant.components import hue from homeassistant.components.light import ( ATTR_BRIGHTNESS, @@ -147,6 +150,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): tasks.append( async_update_items( hass, + config_entry, bridge, async_add_entities, request_update, @@ -160,6 +164,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): tasks.append( async_update_items( hass, + config_entry, bridge, async_add_entities, request_update, @@ -176,6 +181,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async def async_update_items( hass, + config_entry, bridge, async_add_entities, request_bridge_update, @@ -204,9 +210,9 @@ async def async_update_items( _LOGGER.error("Unable to reach bridge %s (%s)", bridge.host, err) bridge.available = False - for light_id, light in current.items(): - if light_id not in progress_waiting: - light.async_schedule_update_ha_state() + for item_id, item in current.items(): + if item_id not in progress_waiting: + item.async_schedule_update_ha_state() return @@ -219,7 +225,8 @@ async def async_update_items( _LOGGER.info("Reconnected to bridge %s", bridge.host) bridge.available = True - new_lights = [] + new_items = [] + removed_items = [] for item_id in api: if item_id not in current: @@ -227,12 +234,34 @@ async def async_update_items( api[item_id], request_bridge_update, bridge, is_group ) - new_lights.append(current[item_id]) + new_items.append(current[item_id]) elif item_id not in progress_waiting: current[item_id].async_schedule_update_ha_state() - if new_lights: - async_add_entities(new_lights) + for item_id in current: + if item_id in api: + continue + + # Device is removed from Hue, so we remove it from Home Assistant + entity = current[item_id] + removed_items.append(item_id) + await entity.async_remove() + ent_registry = await get_ent_reg(hass) + if entity.entity_id in ent_registry.entities: + ent_registry.async_remove(entity.entity_id) + dev_registry = await get_dev_reg(hass) + device = dev_registry.async_get_device( + identifiers={(hue.DOMAIN, entity.unique_id)}, connections=set() + ) + dev_registry.async_update_device( + device.id, remove_config_entry_id=config_entry.entry_id + ) + + if new_items: + async_add_entities(new_items) + + for item_id in removed_items: + del current[item_id] class HueLight(Light): diff --git a/homeassistant/components/hue/manifest.json b/homeassistant/components/hue/manifest.json index c0c7c462f90..9a3e478d108 100644 --- a/homeassistant/components/hue/manifest.json +++ b/homeassistant/components/hue/manifest.json @@ -2,9 +2,9 @@ "domain": "hue", "name": "Philips Hue", "config_flow": true, - "documentation": "https://www.home-assistant.io/components/hue", + "documentation": "https://www.home-assistant.io/integrations/hue", "requirements": [ - "aiohue==1.9.1" + "aiohue==1.9.2" ], "ssdp": { "manufacturer": [ diff --git a/homeassistant/components/hunterdouglas_powerview/manifest.json b/homeassistant/components/hunterdouglas_powerview/manifest.json index c4e1bcc28e8..3aeffd025ee 100644 --- a/homeassistant/components/hunterdouglas_powerview/manifest.json +++ b/homeassistant/components/hunterdouglas_powerview/manifest.json @@ -1,7 +1,7 @@ { "domain": "hunterdouglas_powerview", "name": "Hunterdouglas powerview", - "documentation": "https://www.home-assistant.io/components/hunterdouglas_powerview", + "documentation": "https://www.home-assistant.io/integrations/hunterdouglas_powerview", "requirements": [ "aiopvapi==1.6.14" ], diff --git a/homeassistant/components/hydrawise/manifest.json b/homeassistant/components/hydrawise/manifest.json index 6d332a28bcc..eaa0d10622a 100644 --- a/homeassistant/components/hydrawise/manifest.json +++ b/homeassistant/components/hydrawise/manifest.json @@ -1,7 +1,7 @@ { "domain": "hydrawise", "name": "Hydrawise", - "documentation": "https://www.home-assistant.io/components/hydrawise", + "documentation": "https://www.home-assistant.io/integrations/hydrawise", "requirements": [ "hydrawiser==0.1.1" ], diff --git a/homeassistant/components/hydroquebec/manifest.json b/homeassistant/components/hydroquebec/manifest.json index efea5ce0f2e..dbe8af0b41b 100644 --- a/homeassistant/components/hydroquebec/manifest.json +++ b/homeassistant/components/hydroquebec/manifest.json @@ -1,7 +1,7 @@ { "domain": "hydroquebec", "name": "Hydroquebec", - "documentation": "https://www.home-assistant.io/components/hydroquebec", + "documentation": "https://www.home-assistant.io/integrations/hydroquebec", "requirements": [ "pyhydroquebec==2.2.2" ], diff --git a/homeassistant/components/hyperion/manifest.json b/homeassistant/components/hyperion/manifest.json index 980c227944a..e4ac9c0897b 100644 --- a/homeassistant/components/hyperion/manifest.json +++ b/homeassistant/components/hyperion/manifest.json @@ -1,7 +1,7 @@ { "domain": "hyperion", "name": "Hyperion", - "documentation": "https://www.home-assistant.io/components/hyperion", + "documentation": "https://www.home-assistant.io/integrations/hyperion", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/ialarm/manifest.json b/homeassistant/components/ialarm/manifest.json index df492d136fd..6e575ef17bc 100644 --- a/homeassistant/components/ialarm/manifest.json +++ b/homeassistant/components/ialarm/manifest.json @@ -1,7 +1,7 @@ { "domain": "ialarm", "name": "Ialarm", - "documentation": "https://www.home-assistant.io/components/ialarm", + "documentation": "https://www.home-assistant.io/integrations/ialarm", "requirements": [ "pyialarm==0.3" ], diff --git a/homeassistant/components/iaqualink/.translations/es-419.json b/homeassistant/components/iaqualink/.translations/es-419.json new file mode 100644 index 00000000000..170c2851d08 --- /dev/null +++ b/homeassistant/components/iaqualink/.translations/es-419.json @@ -0,0 +1,13 @@ +{ + "config": { + "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." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/iaqualink/.translations/nn.json b/homeassistant/components/iaqualink/.translations/nn.json new file mode 100644 index 00000000000..ea78f0d0d5d --- /dev/null +++ b/homeassistant/components/iaqualink/.translations/nn.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Jandy iAqualink" + } +} \ No newline at end of file diff --git a/homeassistant/components/iaqualink/__init__.py b/homeassistant/components/iaqualink/__init__.py index 56a39df64c9..dec91186be2 100644 --- a/homeassistant/components/iaqualink/__init__.py +++ b/homeassistant/components/iaqualink/__init__.py @@ -7,7 +7,9 @@ from aiohttp import CookieJar import voluptuous as vol from iaqualink import ( + AqualinkBinarySensor, AqualinkClient, + AqualinkDevice, AqualinkLight, AqualinkLoginException, AqualinkSensor, @@ -16,6 +18,7 @@ from iaqualink import ( ) from homeassistant import config_entries +from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN from homeassistant.components.climate import DOMAIN as CLIMATE_DOMAIN from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN @@ -76,6 +79,7 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> None password = entry.data[CONF_PASSWORD] # These will contain the initialized devices + binary_sensors = hass.data[DOMAIN][BINARY_SENSOR_DOMAIN] = [] climates = hass.data[DOMAIN][CLIMATE_DOMAIN] = [] lights = hass.data[DOMAIN][LIGHT_DOMAIN] = [] sensors = hass.data[DOMAIN][SENSOR_DOMAIN] = [] @@ -103,12 +107,17 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> None climates += [dev] elif isinstance(dev, AqualinkLight): lights += [dev] + elif isinstance(dev, AqualinkBinarySensor): + binary_sensors += [dev] elif isinstance(dev, AqualinkSensor): sensors += [dev] elif isinstance(dev, AqualinkToggle): switches += [dev] forward_setup = hass.config_entries.async_forward_entry_setup + if binary_sensors: + _LOGGER.debug("Got %s binary sensors: %s", len(binary_sensors), binary_sensors) + hass.async_create_task(forward_setup(entry, BINARY_SENSOR_DOMAIN)) if climates: _LOGGER.debug("Got %s climates: %s", len(climates), climates) hass.async_create_task(forward_setup(entry, CLIMATE_DOMAIN)) @@ -138,6 +147,8 @@ async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry) -> boo tasks = [] + if hass.data[DOMAIN][BINARY_SENSOR_DOMAIN]: + tasks += [forward_unload(entry, BINARY_SENSOR_DOMAIN)] if hass.data[DOMAIN][CLIMATE_DOMAIN]: tasks += [forward_unload(entry, CLIMATE_DOMAIN)] if hass.data[DOMAIN][LIGHT_DOMAIN]: @@ -174,6 +185,10 @@ class AqualinkEntity(Entity): class. """ + def __init__(self, dev: AqualinkDevice): + """Initialize the entity.""" + self.dev = dev + async def async_added_to_hass(self) -> None: """Set up a listener when this entity is added to HA.""" async_dispatcher_connect(self.hass, DOMAIN, self._update_callback) @@ -190,3 +205,8 @@ class AqualinkEntity(Entity): updates on a timer. """ return False + + @property + def unique_id(self) -> str: + """Return a unique identifier for this entity.""" + return f"{self.dev.system.serial}_{self.dev.name}" diff --git a/homeassistant/components/iaqualink/binary_sensor.py b/homeassistant/components/iaqualink/binary_sensor.py new file mode 100644 index 00000000000..09c9322a587 --- /dev/null +++ b/homeassistant/components/iaqualink/binary_sensor.py @@ -0,0 +1,48 @@ +"""Support for Aqualink temperature sensors.""" +import logging + +from homeassistant.components.binary_sensor import ( + BinarySensorDevice, + DEVICE_CLASS_COLD, + DOMAIN, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.helpers.typing import HomeAssistantType + +from . import AqualinkEntity +from .const import DOMAIN as AQUALINK_DOMAIN + +_LOGGER = logging.getLogger(__name__) + +PARALLEL_UPDATES = 0 + + +async def async_setup_entry( + hass: HomeAssistantType, config_entry: ConfigEntry, async_add_entities +) -> None: + """Set up discovered binary sensors.""" + devs = [] + for dev in hass.data[AQUALINK_DOMAIN][DOMAIN]: + devs.append(HassAqualinkBinarySensor(dev)) + async_add_entities(devs, True) + + +class HassAqualinkBinarySensor(AqualinkEntity, BinarySensorDevice): + """Representation of a binary sensor.""" + + @property + def name(self) -> str: + """Return the name of the binary sensor.""" + return self.dev.label + + @property + def is_on(self) -> bool: + """Return whether the binary sensor is on or not.""" + return self.dev.is_on + + @property + def device_class(self) -> str: + """Return the class of the binary sensor.""" + if self.name == "Freeze Protection": + return DEVICE_CLASS_COLD + return None diff --git a/homeassistant/components/iaqualink/climate.py b/homeassistant/components/iaqualink/climate.py index 321c54329a2..f41d17837c2 100644 --- a/homeassistant/components/iaqualink/climate.py +++ b/homeassistant/components/iaqualink/climate.py @@ -2,13 +2,7 @@ import logging from typing import List, Optional -from iaqualink import ( - AqualinkState, - AqualinkHeater, - AqualinkPump, - AqualinkSensor, - AqualinkThermostat, -) +from iaqualink import AqualinkHeater, AqualinkPump, AqualinkSensor, AqualinkState from iaqualink.const import ( AQUALINK_TEMP_CELSIUS_HIGH, AQUALINK_TEMP_CELSIUS_LOW, @@ -45,13 +39,9 @@ async def async_setup_entry( async_add_entities(devs, True) -class HassAqualinkThermostat(ClimateDevice, AqualinkEntity): +class HassAqualinkThermostat(AqualinkEntity, ClimateDevice): """Representation of a thermostat.""" - def __init__(self, dev: AqualinkThermostat): - """Initialize the thermostat.""" - self.dev = dev - @property def name(self) -> str: """Return the name of the thermostat.""" diff --git a/homeassistant/components/iaqualink/light.py b/homeassistant/components/iaqualink/light.py index fbfb10783ee..813af7863f1 100644 --- a/homeassistant/components/iaqualink/light.py +++ b/homeassistant/components/iaqualink/light.py @@ -1,7 +1,7 @@ """Support for Aqualink pool lights.""" import logging -from iaqualink import AqualinkLight, AqualinkLightEffect +from iaqualink import AqualinkLightEffect from homeassistant.components.light import ( ATTR_BRIGHTNESS, @@ -32,13 +32,9 @@ async def async_setup_entry( async_add_entities(devs, True) -class HassAqualinkLight(Light, AqualinkEntity): +class HassAqualinkLight(AqualinkEntity, Light): """Representation of a light.""" - def __init__(self, dev: AqualinkLight): - """Initialize the light.""" - self.dev = dev - @property def name(self) -> str: """Return the name of the light.""" diff --git a/homeassistant/components/iaqualink/manifest.json b/homeassistant/components/iaqualink/manifest.json index 25e02536897..e883aec371c 100644 --- a/homeassistant/components/iaqualink/manifest.json +++ b/homeassistant/components/iaqualink/manifest.json @@ -2,7 +2,7 @@ "domain": "iaqualink", "name": "Jandy iAqualink", "config_flow": true, - "documentation": "https://www.home-assistant.io/components/iaqualink/", + "documentation": "https://www.home-assistant.io/integrations/iaqualink/", "dependencies": [], "codeowners": [ "@flz" diff --git a/homeassistant/components/iaqualink/sensor.py b/homeassistant/components/iaqualink/sensor.py index 4a1691e0314..81021d0b447 100644 --- a/homeassistant/components/iaqualink/sensor.py +++ b/homeassistant/components/iaqualink/sensor.py @@ -2,8 +2,6 @@ import logging from typing import Optional -from iaqualink import AqualinkSensor - from homeassistant.components.sensor import DOMAIN from homeassistant.config_entries import ConfigEntry from homeassistant.const import DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT @@ -30,10 +28,6 @@ async def async_setup_entry( class HassAqualinkSensor(AqualinkEntity): """Representation of a sensor.""" - def __init__(self, dev: AqualinkSensor): - """Initialize the sensor.""" - self.dev = dev - @property def name(self) -> str: """Return the name of the sensor.""" diff --git a/homeassistant/components/iaqualink/switch.py b/homeassistant/components/iaqualink/switch.py index f2fc51ce713..8efb473cf54 100644 --- a/homeassistant/components/iaqualink/switch.py +++ b/homeassistant/components/iaqualink/switch.py @@ -1,8 +1,6 @@ """Support for Aqualink pool feature switches.""" import logging -from iaqualink import AqualinkToggle - from homeassistant.components.switch import DOMAIN, SwitchDevice from homeassistant.config_entries import ConfigEntry from homeassistant.helpers.typing import HomeAssistantType @@ -25,13 +23,9 @@ async def async_setup_entry( async_add_entities(devs, True) -class HassAqualinkSwitch(SwitchDevice, AqualinkEntity): +class HassAqualinkSwitch(AqualinkEntity, SwitchDevice): """Representation of a switch.""" - def __init__(self, dev: AqualinkToggle): - """Initialize the switch.""" - self.dev = dev - @property def name(self) -> str: """Return the name of the switch.""" diff --git a/homeassistant/components/icloud/manifest.json b/homeassistant/components/icloud/manifest.json index 5f2075a0fd6..d3924ee61a8 100644 --- a/homeassistant/components/icloud/manifest.json +++ b/homeassistant/components/icloud/manifest.json @@ -1,7 +1,7 @@ { "domain": "icloud", "name": "Icloud", - "documentation": "https://www.home-assistant.io/components/icloud", + "documentation": "https://www.home-assistant.io/integrations/icloud", "requirements": [ "pyicloud==0.9.1" ], diff --git a/homeassistant/components/idteck_prox/manifest.json b/homeassistant/components/idteck_prox/manifest.json index 8df144a0f81..7a8e3955686 100644 --- a/homeassistant/components/idteck_prox/manifest.json +++ b/homeassistant/components/idteck_prox/manifest.json @@ -1,7 +1,7 @@ { "domain": "idteck_prox", "name": "Idteck prox", - "documentation": "https://www.home-assistant.io/components/idteck_prox", + "documentation": "https://www.home-assistant.io/integrations/idteck_prox", "requirements": [ "rfk101py==0.0.1" ], diff --git a/homeassistant/components/ifttt/.translations/no.json b/homeassistant/components/ifttt/.translations/no.json index 481ab372e91..2fe38659fad 100644 --- a/homeassistant/components/ifttt/.translations/no.json +++ b/homeassistant/components/ifttt/.translations/no.json @@ -2,7 +2,7 @@ "config": { "abort": { "not_internet_accessible": "Din Home Assistant enhet m\u00e5 v\u00e6re tilgjengelig fra internett for \u00e5 kunne motta IFTTT-meldinger.", - "one_instance_allowed": "Kun en enkelt forekomst er n\u00f8dvendig." + "one_instance_allowed": "Kun en forekomst er n\u00f8dvendig." }, "create_entry": { "default": "For \u00e5 sende hendelser til Home Assistant, m\u00e5 du bruke \"Make a web request\" handlingen fra [IFTTT Webhook applet]({applet_url}).\n\nFyll ut f\u00f8lgende informasjon:\n\n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/json\n\nSe [dokumentasjonen]({docs_url}) om hvordan du konfigurerer automatiseringer for \u00e5 h\u00e5ndtere innkommende data." diff --git a/homeassistant/components/ifttt/config_flow.py b/homeassistant/components/ifttt/config_flow.py index 4fbd50c1d86..ae9be6b698c 100644 --- a/homeassistant/components/ifttt/config_flow.py +++ b/homeassistant/components/ifttt/config_flow.py @@ -8,6 +8,6 @@ config_entry_flow.register_webhook_flow( "IFTTT Webhook", { "applet_url": "https://ifttt.com/maker_webhooks", - "docs_url": "https://www.home-assistant.io/components/ifttt/", + "docs_url": "https://www.home-assistant.io/integrations/ifttt/", }, ) diff --git a/homeassistant/components/ifttt/manifest.json b/homeassistant/components/ifttt/manifest.json index 58490569e65..975a3128f36 100644 --- a/homeassistant/components/ifttt/manifest.json +++ b/homeassistant/components/ifttt/manifest.json @@ -2,7 +2,7 @@ "domain": "ifttt", "name": "Ifttt", "config_flow": true, - "documentation": "https://www.home-assistant.io/components/ifttt", + "documentation": "https://www.home-assistant.io/integrations/ifttt", "requirements": [ "pyfttt==0.3" ], diff --git a/homeassistant/components/iglo/manifest.json b/homeassistant/components/iglo/manifest.json index 4d84c27cd93..8771ada45e1 100644 --- a/homeassistant/components/iglo/manifest.json +++ b/homeassistant/components/iglo/manifest.json @@ -1,7 +1,7 @@ { "domain": "iglo", "name": "Iglo", - "documentation": "https://www.home-assistant.io/components/iglo", + "documentation": "https://www.home-assistant.io/integrations/iglo", "requirements": [ "iglo==1.2.7" ], diff --git a/homeassistant/components/ign_sismologia/manifest.json b/homeassistant/components/ign_sismologia/manifest.json index d2ab3ad449c..edb77f1dc6d 100644 --- a/homeassistant/components/ign_sismologia/manifest.json +++ b/homeassistant/components/ign_sismologia/manifest.json @@ -1,7 +1,7 @@ { "domain": "ign_sismologia", "name": "IGN Sismologia", - "documentation": "https://www.home-assistant.io/components/ign_sismologia", + "documentation": "https://www.home-assistant.io/integrations/ign_sismologia", "requirements": [ "georss_ign_sismologia_client==0.2" ], diff --git a/homeassistant/components/ihc/manifest.json b/homeassistant/components/ihc/manifest.json index 25d0317078f..a415b0e3103 100644 --- a/homeassistant/components/ihc/manifest.json +++ b/homeassistant/components/ihc/manifest.json @@ -1,7 +1,7 @@ { "domain": "ihc", "name": "Ihc", - "documentation": "https://www.home-assistant.io/components/ihc", + "documentation": "https://www.home-assistant.io/integrations/ihc", "requirements": [ "defusedxml==0.6.0", "ihcsdk==2.3.0" diff --git a/homeassistant/components/image_processing/__init__.py b/homeassistant/components/image_processing/__init__.py index b1c167a4175..e9621fe6bbe 100644 --- a/homeassistant/components/image_processing/__init__.py +++ b/homeassistant/components/image_processing/__init__.py @@ -2,7 +2,9 @@ import asyncio from datetime import timedelta import logging +from typing import Tuple +from PIL import ImageDraw import voluptuous as vol from homeassistant.const import ATTR_ENTITY_ID, ATTR_NAME, CONF_ENTITY_ID, CONF_NAME @@ -14,7 +16,6 @@ from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent from homeassistant.util.async_ import run_callback_threadsafe - # mypy: allow-untyped-defs, no-check-untyped-defs _LOGGER = logging.getLogger(__name__) @@ -64,6 +65,43 @@ PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA.extend( PLATFORM_SCHEMA_BASE = cv.PLATFORM_SCHEMA_BASE.extend(PLATFORM_SCHEMA.schema) +def draw_box( + draw: ImageDraw, + box: Tuple[float, float, float, float], + img_width: int, + img_height: int, + text: str = "", + color: Tuple[int, int, int] = (255, 255, 0), +) -> None: + """ + Draw a bounding box on and image. + + The bounding box is defined by the tuple (y_min, x_min, y_max, x_max) + where the coordinates are floats in the range [0.0, 1.0] and + relative to the width and height of the image. + + For example, if an image is 100 x 200 pixels (height x width) and the bounding + box is `(0.1, 0.2, 0.5, 0.9)`, the upper-left and bottom-right coordinates of + the bounding box will be `(40, 10)` to `(180, 50)` (in (x,y) coordinates). + """ + + line_width = 5 + y_min, x_min, y_max, x_max = box + (left, right, top, bottom) = ( + x_min * img_width, + x_max * img_width, + y_min * img_height, + y_max * img_height, + ) + draw.line( + [(left, top), (left, bottom), (right, bottom), (right, top), (left, top)], + width=line_width, + fill=color, + ) + if text: + draw.text((left + line_width, abs(top - line_width)), text, fill=color) + + async def async_setup(hass, config): """Set up the image processing.""" component = EntityComponent(_LOGGER, DOMAIN, hass, SCAN_INTERVAL) diff --git a/homeassistant/components/image_processing/manifest.json b/homeassistant/components/image_processing/manifest.json index e675d18a00b..4a96e9828cb 100644 --- a/homeassistant/components/image_processing/manifest.json +++ b/homeassistant/components/image_processing/manifest.json @@ -1,8 +1,10 @@ { "domain": "image_processing", "name": "Image processing", - "documentation": "https://www.home-assistant.io/components/image_processing", - "requirements": [], + "documentation": "https://www.home-assistant.io/integrations/image_processing", + "requirements": [ + "pillow==6.1.0" + ], "dependencies": [ "camera" ], diff --git a/homeassistant/components/imap/manifest.json b/homeassistant/components/imap/manifest.json index 9e0f387a7a6..20767a0d49d 100644 --- a/homeassistant/components/imap/manifest.json +++ b/homeassistant/components/imap/manifest.json @@ -1,7 +1,7 @@ { "domain": "imap", "name": "Imap", - "documentation": "https://www.home-assistant.io/components/imap", + "documentation": "https://www.home-assistant.io/integrations/imap", "requirements": [ "aioimaplib==0.7.15" ], diff --git a/homeassistant/components/imap_email_content/manifest.json b/homeassistant/components/imap_email_content/manifest.json index a1e2c616832..e689cb859da 100644 --- a/homeassistant/components/imap_email_content/manifest.json +++ b/homeassistant/components/imap_email_content/manifest.json @@ -1,7 +1,7 @@ { "domain": "imap_email_content", "name": "Imap email content", - "documentation": "https://www.home-assistant.io/components/imap_email_content", + "documentation": "https://www.home-assistant.io/integrations/imap_email_content", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/incomfort/binary_sensor.py b/homeassistant/components/incomfort/binary_sensor.py index 004086ab5c8..39a45429cb1 100644 --- a/homeassistant/components/incomfort/binary_sensor.py +++ b/homeassistant/components/incomfort/binary_sensor.py @@ -1,4 +1,6 @@ -"""Support for an Intergas boiler via an InComfort/InTouch Lan2RF gateway.""" +"""Support for an Intergas heater via an InComfort/InTouch Lan2RF gateway.""" +from typing import Any, Dict, Optional + from homeassistant.components.binary_sensor import BinarySensorDevice from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -8,6 +10,9 @@ from . import DOMAIN async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up an InComfort/InTouch binary_sensor device.""" + if discovery_info is None: + return + async_add_entities( [IncomfortFailed(hass.data[DOMAIN]["client"], hass.data[DOMAIN]["heater"])] ) @@ -16,33 +21,40 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= class IncomfortFailed(BinarySensorDevice): """Representation of an InComfort Failed sensor.""" - def __init__(self, client, boiler): + def __init__(self, client, heater) -> None: """Initialize the binary sensor.""" - self._client = client - self._boiler = boiler + self._unique_id = f"{heater.serial_no}_failed" - async def async_added_to_hass(self): + self._client = client + self._heater = heater + + async def async_added_to_hass(self) -> None: """Set up a listener when this entity is added to HA.""" async_dispatcher_connect(self.hass, DOMAIN, self._refresh) @callback - def _refresh(self): + def _refresh(self) -> None: self.async_schedule_update_ha_state(force_refresh=True) @property - def name(self): + def unique_id(self) -> Optional[str]: + """Return a unique ID.""" + return self._unique_id + + @property + def name(self) -> Optional[str]: """Return the name of the sensor.""" return "Fault state" @property - def is_on(self): + def is_on(self) -> bool: """Return the status of the sensor.""" - return self._boiler.status["is_failed"] + return self._heater.status["is_failed"] @property - def device_state_attributes(self): + def device_state_attributes(self) -> Optional[Dict[str, Any]]: """Return the device state attributes.""" - return {"fault_code": self._boiler.status["fault_code"]} + return {"fault_code": self._heater.status["fault_code"]} @property def should_poll(self) -> bool: diff --git a/homeassistant/components/incomfort/climate.py b/homeassistant/components/incomfort/climate.py index cccb9d25644..3918244d4e8 100644 --- a/homeassistant/components/incomfort/climate.py +++ b/homeassistant/components/incomfort/climate.py @@ -1,5 +1,5 @@ """Support for an Intergas boiler via an InComfort/InTouch Lan2RF gateway.""" -from typing import Any, Dict, Optional, List +from typing import Any, Dict, List, Optional from homeassistant.components.climate import ClimateDevice from homeassistant.components.climate.const import ( @@ -13,21 +13,24 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect from . import DOMAIN -async def async_setup_platform( - hass, hass_config, async_add_entities, discovery_info=None -): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up an InComfort/InTouch climate device.""" + if discovery_info is None: + return + client = hass.data[DOMAIN]["client"] heater = hass.data[DOMAIN]["heater"] - async_add_entities([InComfortClimate(client, r) for r in heater.rooms]) + async_add_entities([InComfortClimate(client, heater, r) for r in heater.rooms]) class InComfortClimate(ClimateDevice): """Representation of an InComfort/InTouch climate device.""" - def __init__(self, client, room): + def __init__(self, client, heater, room) -> None: """Initialize the climate device.""" + self._unique_id = f"{heater.serial_no}_{room.room_no}" + self._client = client self._room = room self._name = f"Room {room.room_no}" @@ -37,7 +40,7 @@ class InComfortClimate(ClimateDevice): async_dispatcher_connect(self.hass, DOMAIN, self._refresh) @callback - def _refresh(self): + def _refresh(self) -> None: self.async_schedule_update_ha_state(force_refresh=True) @property @@ -45,6 +48,11 @@ class InComfortClimate(ClimateDevice): """Return False as this device should never be polled.""" return False + @property + def unique_id(self) -> Optional[str]: + """Return a unique ID.""" + return self._unique_id + @property def name(self) -> str: """Return the name of the climate device.""" @@ -78,7 +86,7 @@ class InComfortClimate(ClimateDevice): @property def target_temperature(self) -> Optional[float]: """Return the temperature we try to reach.""" - return self._room.override + return self._room.setpoint @property def supported_features(self) -> int: diff --git a/homeassistant/components/incomfort/manifest.json b/homeassistant/components/incomfort/manifest.json index 8b5f461c8af..4bdf43f8957 100644 --- a/homeassistant/components/incomfort/manifest.json +++ b/homeassistant/components/incomfort/manifest.json @@ -1,9 +1,9 @@ { "domain": "incomfort", "name": "Intergas InComfort/Intouch Lan2RF gateway", - "documentation": "https://www.home-assistant.io/components/incomfort", + "documentation": "https://www.home-assistant.io/integrations/incomfort", "requirements": [ - "incomfort-client==0.3.1" + "incomfort-client==0.3.5" ], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/incomfort/sensor.py b/homeassistant/components/incomfort/sensor.py index e19bbf42aea..772b5dab183 100644 --- a/homeassistant/components/incomfort/sensor.py +++ b/homeassistant/components/incomfort/sensor.py @@ -1,31 +1,43 @@ -"""Support for an Intergas boiler via an InComfort/InTouch Lan2RF gateway.""" -from homeassistant.const import PRESSURE_BAR, TEMP_CELSIUS, DEVICE_CLASS_TEMPERATURE +"""Support for an Intergas heater via an InComfort/InTouch Lan2RF gateway.""" +from typing import Any, Dict, Optional + +from homeassistant.const import ( + PRESSURE_BAR, + TEMP_CELSIUS, + DEVICE_CLASS_PRESSURE, + DEVICE_CLASS_TEMPERATURE, +) from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import Entity +from homeassistant.util import slugify from . import DOMAIN -INTOUCH_HEATER_TEMP = "CV Temp" -INTOUCH_PRESSURE = "CV Pressure" -INTOUCH_TAP_TEMP = "Tap Temp" +INCOMFORT_HEATER_TEMP = "CV Temp" +INCOMFORT_PRESSURE = "CV Pressure" +INCOMFORT_TAP_TEMP = "Tap Temp" -INTOUCH_MAP_ATTRS = { - INTOUCH_HEATER_TEMP: ["heater_temp", "is_pumping"], - INTOUCH_TAP_TEMP: ["tap_temp", "is_tapping"], +INCOMFORT_MAP_ATTRS = { + INCOMFORT_HEATER_TEMP: ["heater_temp", "is_pumping"], + INCOMFORT_PRESSURE: ["pressure", None], + INCOMFORT_TAP_TEMP: ["tap_temp", "is_tapping"], } async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up an InComfort/InTouch sensor device.""" + if discovery_info is None: + return + client = hass.data[DOMAIN]["client"] heater = hass.data[DOMAIN]["heater"] async_add_entities( [ - IncomfortPressure(client, heater, INTOUCH_PRESSURE), - IncomfortTemperature(client, heater, INTOUCH_HEATER_TEMP), - IncomfortTemperature(client, heater, INTOUCH_TAP_TEMP), + IncomfortPressure(client, heater, INCOMFORT_PRESSURE), + IncomfortTemperature(client, heater, INCOMFORT_HEATER_TEMP), + IncomfortTemperature(client, heater, INCOMFORT_TAP_TEMP), ] ) @@ -33,35 +45,47 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= class IncomfortSensor(Entity): """Representation of an InComfort/InTouch sensor device.""" - def __init__(self, client, boiler): + def __init__(self, client, heater, name) -> None: """Initialize the sensor.""" self._client = client - self._boiler = boiler + self._heater = heater - self._name = None + self._unique_id = f"{heater.serial_no}_{slugify(name)}" + + self._name = name self._device_class = None self._unit_of_measurement = None - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Set up a listener when this entity is added to HA.""" async_dispatcher_connect(self.hass, DOMAIN, self._refresh) @callback - def _refresh(self): + def _refresh(self) -> None: self.async_schedule_update_ha_state(force_refresh=True) @property - def name(self): + def unique_id(self) -> Optional[str]: + """Return a unique ID.""" + return self._unique_id + + @property + def name(self) -> Optional[str]: """Return the name of the sensor.""" return self._name @property - def device_class(self): + def state(self) -> Optional[str]: + """Return the state of the sensor.""" + return self._heater.status[INCOMFORT_MAP_ATTRS[self._name][0]] + + @property + def device_class(self) -> Optional[str]: """Return the device class of the sensor.""" return self._device_class @property - def unit_of_measurement(self): + def unit_of_measurement(self) -> Optional[str]: """Return the unit of measurement of the sensor.""" return self._unit_of_measurement @@ -74,37 +98,26 @@ class IncomfortSensor(Entity): class IncomfortPressure(IncomfortSensor): """Representation of an InTouch CV Pressure sensor.""" - def __init__(self, client, boiler, name): + def __init__(self, client, heater, name) -> None: """Initialize the sensor.""" - super().__init__(client, boiler) + super().__init__(client, heater, name) - self._name = name + self._device_class = DEVICE_CLASS_PRESSURE self._unit_of_measurement = PRESSURE_BAR - @property - def state(self): - """Return the state/value of the sensor.""" - return self._boiler.status["pressure"] - class IncomfortTemperature(IncomfortSensor): """Representation of an InTouch Temperature sensor.""" - def __init__(self, client, boiler, name): + def __init__(self, client, heater, name) -> None: """Initialize the signal strength sensor.""" - super().__init__(client, boiler) + super().__init__(client, heater, name) - self._name = name self._device_class = DEVICE_CLASS_TEMPERATURE self._unit_of_measurement = TEMP_CELSIUS @property - def state(self): - """Return the state of the sensor.""" - return self._boiler.status[INTOUCH_MAP_ATTRS[self._name][0]] - - @property - def device_state_attributes(self): + def device_state_attributes(self) -> Optional[Dict[str, Any]]: """Return the device state attributes.""" - key = INTOUCH_MAP_ATTRS[self._name][1] - return {key: self._boiler.status[key]} + key = INCOMFORT_MAP_ATTRS[self._name][1] + return {key: self._heater.status[key]} diff --git a/homeassistant/components/incomfort/water_heater.py b/homeassistant/components/incomfort/water_heater.py index 2449a1223cc..70423611705 100644 --- a/homeassistant/components/incomfort/water_heater.py +++ b/homeassistant/components/incomfort/water_heater.py @@ -1,6 +1,7 @@ """Support for an Intergas boiler via an InComfort/Intouch Lan2RF gateway.""" import asyncio import logging +from typing import Any, Dict, Optional from aiohttp import ClientResponseError from homeassistant.components.water_heater import WaterHeaterDevice @@ -11,60 +12,52 @@ from . import DOMAIN _LOGGER = logging.getLogger(__name__) -HEATER_SUPPORT_FLAGS = 0 - -HEATER_MAX_TEMP = 80.0 -HEATER_MIN_TEMP = 30.0 - -HEATER_NAME = "Boiler" -HEATER_ATTRS = [ - "display_code", - "display_text", - "is_burning", - "rf_message_rssi", - "nodenr", - "rfstatus_cntr", -] +HEATER_ATTRS = ["display_code", "display_text", "is_burning"] -async def async_setup_platform( - hass, hass_config, async_add_entities, discovery_info=None -): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up an InComfort/Intouch water_heater device.""" + if discovery_info is None: + return + client = hass.data[DOMAIN]["client"] heater = hass.data[DOMAIN]["heater"] - async_add_entities([IncomfortWaterHeater(client, heater)], update_before_add=True) + async_add_entities([IncomfortWaterHeater(client, heater)]) class IncomfortWaterHeater(WaterHeaterDevice): """Representation of an InComfort/Intouch water_heater device.""" - def __init__(self, client, heater): + def __init__(self, client, heater) -> None: """Initialize the water_heater device.""" + self._unique_id = f"{heater.serial_no}" + self._client = client self._heater = heater @property - def name(self): + def unique_id(self) -> Optional[str]: + """Return a unique ID.""" + return self._unique_id + + @property + def name(self) -> str: """Return the name of the water_heater device.""" - return HEATER_NAME + return "Boiler" @property - def icon(self): + def icon(self) -> str: """Return the icon of the water_heater device.""" - return "mdi:oil-temperature" + return "mdi:thermometer-lines" @property - def device_state_attributes(self): + def device_state_attributes(self) -> Dict[str, Any]: """Return the device state attributes.""" - state = { - k: self._heater.status[k] for k in self._heater.status if k in HEATER_ATTRS - } - return state + return {k: v for k, v in self._heater.status.items() if k in HEATER_ATTRS} @property - def current_temperature(self): + def current_temperature(self) -> float: """Return the current temperature.""" if self._heater.is_tapping: return self._heater.tap_temp @@ -73,34 +66,34 @@ class IncomfortWaterHeater(WaterHeaterDevice): return max(self._heater.heater_temp, self._heater.tap_temp) @property - def min_temp(self): + def min_temp(self) -> float: """Return max valid temperature that can be set.""" - return HEATER_MIN_TEMP + return 80.0 @property - def max_temp(self): + def max_temp(self) -> float: """Return max valid temperature that can be set.""" - return HEATER_MAX_TEMP + return 30.0 @property - def temperature_unit(self): + def temperature_unit(self) -> str: """Return the unit of measurement.""" return TEMP_CELSIUS @property - def supported_features(self): + def supported_features(self) -> int: """Return the list of supported features.""" - return HEATER_SUPPORT_FLAGS + return 0 @property - def current_operation(self): + def current_operation(self) -> str: """Return the current operation mode.""" if self._heater.is_failed: return f"Fault code: {self._heater.fault_code}" return self._heater.display_text - async def async_update(self): + async def async_update(self) -> None: """Get the latest state data from the gateway.""" try: await self._heater.update() @@ -108,4 +101,5 @@ class IncomfortWaterHeater(WaterHeaterDevice): except (ClientResponseError, asyncio.TimeoutError) as err: _LOGGER.warning("Update failed, message is: %s", err) - async_dispatcher_send(self.hass, DOMAIN) + else: + async_dispatcher_send(self.hass, DOMAIN) diff --git a/homeassistant/components/influxdb/manifest.json b/homeassistant/components/influxdb/manifest.json index 20652ddd046..df936eafa90 100644 --- a/homeassistant/components/influxdb/manifest.json +++ b/homeassistant/components/influxdb/manifest.json @@ -1,12 +1,12 @@ { "domain": "influxdb", "name": "Influxdb", - "documentation": "https://www.home-assistant.io/components/influxdb", + "documentation": "https://www.home-assistant.io/integrations/influxdb", "requirements": [ - "influxdb==5.2.0" + "influxdb==5.2.3" ], "dependencies": [], "codeowners": [ "@fabaff" ] -} +} \ No newline at end of file diff --git a/homeassistant/components/input_boolean/manifest.json b/homeassistant/components/input_boolean/manifest.json index e233b5635fc..09ae235e6b8 100644 --- a/homeassistant/components/input_boolean/manifest.json +++ b/homeassistant/components/input_boolean/manifest.json @@ -1,7 +1,7 @@ { "domain": "input_boolean", "name": "Input boolean", - "documentation": "https://www.home-assistant.io/components/input_boolean", + "documentation": "https://www.home-assistant.io/integrations/input_boolean", "requirements": [], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/input_datetime/manifest.json b/homeassistant/components/input_datetime/manifest.json index 287777e2ccf..9808c45aa74 100644 --- a/homeassistant/components/input_datetime/manifest.json +++ b/homeassistant/components/input_datetime/manifest.json @@ -1,7 +1,7 @@ { "domain": "input_datetime", "name": "Input datetime", - "documentation": "https://www.home-assistant.io/components/input_datetime", + "documentation": "https://www.home-assistant.io/integrations/input_datetime", "requirements": [], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/input_number/__init__.py b/homeassistant/components/input_number/__init__.py index 007ed6517ef..9b4d5a961ba 100644 --- a/homeassistant/components/input_number/__init__.py +++ b/homeassistant/components/input_number/__init__.py @@ -7,6 +7,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.config_validation import ENTITY_SERVICE_SCHEMA from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, + ATTR_MODE, CONF_ICON, CONF_NAME, CONF_MODE, @@ -32,7 +33,6 @@ ATTR_VALUE = "value" ATTR_MIN = "min" ATTR_MAX = "max" ATTR_STEP = "step" -ATTR_MODE = "mode" SERVICE_SET_VALUE = "set_value" SERVICE_INCREMENT = "increment" diff --git a/homeassistant/components/input_number/manifest.json b/homeassistant/components/input_number/manifest.json index 2015b8ea734..31e00d0fce8 100644 --- a/homeassistant/components/input_number/manifest.json +++ b/homeassistant/components/input_number/manifest.json @@ -1,7 +1,7 @@ { "domain": "input_number", "name": "Input number", - "documentation": "https://www.home-assistant.io/components/input_number", + "documentation": "https://www.home-assistant.io/integrations/input_number", "requirements": [], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/input_select/manifest.json b/homeassistant/components/input_select/manifest.json index a71fb53a5d1..d71674fd40c 100644 --- a/homeassistant/components/input_select/manifest.json +++ b/homeassistant/components/input_select/manifest.json @@ -1,7 +1,7 @@ { "domain": "input_select", "name": "Input select", - "documentation": "https://www.home-assistant.io/components/input_select", + "documentation": "https://www.home-assistant.io/integrations/input_select", "requirements": [], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/input_text/__init__.py b/homeassistant/components/input_text/__init__.py index 41d78e6e7c5..1b4670cf1e6 100644 --- a/homeassistant/components/input_text/__init__.py +++ b/homeassistant/components/input_text/__init__.py @@ -7,6 +7,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.config_validation import ENTITY_SERVICE_SCHEMA from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, + ATTR_MODE, CONF_ICON, CONF_NAME, CONF_MODE, @@ -30,7 +31,6 @@ ATTR_VALUE = "value" ATTR_MIN = "min" ATTR_MAX = "max" ATTR_PATTERN = "pattern" -ATTR_MODE = "mode" SERVICE_SET_VALUE = "set_value" diff --git a/homeassistant/components/input_text/manifest.json b/homeassistant/components/input_text/manifest.json index 6362e679319..eaddaf49b84 100644 --- a/homeassistant/components/input_text/manifest.json +++ b/homeassistant/components/input_text/manifest.json @@ -1,7 +1,7 @@ { "domain": "input_text", "name": "Input text", - "documentation": "https://www.home-assistant.io/components/input_text", + "documentation": "https://www.home-assistant.io/integrations/input_text", "requirements": [], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/insteon/manifest.json b/homeassistant/components/insteon/manifest.json index 5fcc2c5b507..c8821f3b176 100644 --- a/homeassistant/components/insteon/manifest.json +++ b/homeassistant/components/insteon/manifest.json @@ -1,7 +1,7 @@ { "domain": "insteon", "name": "Insteon", - "documentation": "https://www.home-assistant.io/components/insteon", + "documentation": "https://www.home-assistant.io/integrations/insteon", "requirements": [ "insteonplm==0.16.5" ], diff --git a/homeassistant/components/integration/manifest.json b/homeassistant/components/integration/manifest.json index 869ad2766f9..d910e7bdc78 100644 --- a/homeassistant/components/integration/manifest.json +++ b/homeassistant/components/integration/manifest.json @@ -1,7 +1,7 @@ { "domain": "integration", "name": "Integration", - "documentation": "https://www.home-assistant.io/components/integration", + "documentation": "https://www.home-assistant.io/integrations/integration", "requirements": [], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/intent_script/manifest.json b/homeassistant/components/intent_script/manifest.json index 891be6b2180..f2d9a338560 100644 --- a/homeassistant/components/intent_script/manifest.json +++ b/homeassistant/components/intent_script/manifest.json @@ -1,7 +1,7 @@ { "domain": "intent_script", "name": "Intent script", - "documentation": "https://www.home-assistant.io/components/intent_script", + "documentation": "https://www.home-assistant.io/integrations/intent_script", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/ios/.translations/no.json b/homeassistant/components/ios/.translations/no.json index a125b96a070..798ae93d33b 100644 --- a/homeassistant/components/ios/.translations/no.json +++ b/homeassistant/components/ios/.translations/no.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "Kun en enkelt konfigurasjon av Home Assistant iOS er n\u00f8dvendig." + "single_instance_allowed": "Kun en konfigurasjon av Home Assistant iOS er n\u00f8dvendig." }, "step": { "confirm": { diff --git a/homeassistant/components/ios/manifest.json b/homeassistant/components/ios/manifest.json index 28c9ea1e952..6e011a43ded 100644 --- a/homeassistant/components/ios/manifest.json +++ b/homeassistant/components/ios/manifest.json @@ -2,7 +2,7 @@ "domain": "ios", "name": "Ios", "config_flow": true, - "documentation": "https://www.home-assistant.io/components/ios", + "documentation": "https://www.home-assistant.io/integrations/ios", "requirements": [], "dependencies": [ "device_tracker", diff --git a/homeassistant/components/ios/notify.py b/homeassistant/components/ios/notify.py index 83e4c089b9a..ee74b369629 100644 --- a/homeassistant/components/ios/notify.py +++ b/homeassistant/components/ios/notify.py @@ -1,5 +1,4 @@ """Support for iOS push notifications.""" -from datetime import datetime, timezone import logging import requests @@ -25,7 +24,7 @@ def log_rate_limits(hass, target, resp, level=20): """Output rate limit log line at given level.""" rate_limits = resp["rateLimits"] resetsAt = dt_util.parse_datetime(rate_limits["resetsAt"]) - resetsAtTime = resetsAt - datetime.now(timezone.utc) + resetsAtTime = resetsAt - dt_util.utcnow() rate_limit_msg = ( "iOS push notification rate limits for %s: " "%d sent, %d allowed, %d errors, " diff --git a/homeassistant/components/iota/manifest.json b/homeassistant/components/iota/manifest.json index d83defbbec3..018ea4ac167 100644 --- a/homeassistant/components/iota/manifest.json +++ b/homeassistant/components/iota/manifest.json @@ -1,7 +1,7 @@ { "domain": "iota", "name": "Iota", - "documentation": "https://www.home-assistant.io/components/iota", + "documentation": "https://www.home-assistant.io/integrations/iota", "requirements": [ "pyota==2.0.5" ], diff --git a/homeassistant/components/iperf3/manifest.json b/homeassistant/components/iperf3/manifest.json index e35be24fc80..c3b1e27c77a 100644 --- a/homeassistant/components/iperf3/manifest.json +++ b/homeassistant/components/iperf3/manifest.json @@ -1,9 +1,9 @@ { "domain": "iperf3", "name": "Iperf3", - "documentation": "https://www.home-assistant.io/components/iperf3", + "documentation": "https://www.home-assistant.io/integrations/iperf3", "requirements": [ - "iperf3==0.1.10" + "iperf3==0.1.11" ], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/ipma/.translations/nn.json b/homeassistant/components/ipma/.translations/nn.json new file mode 100644 index 00000000000..0e024a0e1eb --- /dev/null +++ b/homeassistant/components/ipma/.translations/nn.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "name": "Namn" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ipma/.translations/ru.json b/homeassistant/components/ipma/.translations/ru.json index a260efa5bd9..a302572ed12 100644 --- a/homeassistant/components/ipma/.translations/ru.json +++ b/homeassistant/components/ipma/.translations/ru.json @@ -1,7 +1,7 @@ { "config": { "error": { - "name_exists": "\u0418\u043c\u044f \u0443\u0436\u0435 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u0435\u0442" + "name_exists": "\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." }, "step": { "user": { diff --git a/homeassistant/components/ipma/manifest.json b/homeassistant/components/ipma/manifest.json index 093ccbf6a5b..47759759a56 100644 --- a/homeassistant/components/ipma/manifest.json +++ b/homeassistant/components/ipma/manifest.json @@ -2,7 +2,7 @@ "domain": "ipma", "name": "Ipma", "config_flow": true, - "documentation": "https://www.home-assistant.io/components/ipma", + "documentation": "https://www.home-assistant.io/integrations/ipma", "requirements": [ "pyipma==1.2.1" ], diff --git a/homeassistant/components/iqvia/.translations/nn.json b/homeassistant/components/iqvia/.translations/nn.json new file mode 100644 index 00000000000..89922b66f03 --- /dev/null +++ b/homeassistant/components/iqvia/.translations/nn.json @@ -0,0 +1,10 @@ +{ + "config": { + "step": { + "user": { + "title": "IQVIA" + } + }, + "title": "IQVIA" + } +} \ No newline at end of file diff --git a/homeassistant/components/iqvia/.translations/pl.json b/homeassistant/components/iqvia/.translations/pl.json index 7a6e9a8a915..b528cdeb70f 100644 --- a/homeassistant/components/iqvia/.translations/pl.json +++ b/homeassistant/components/iqvia/.translations/pl.json @@ -9,7 +9,7 @@ "data": { "zip_code": "Kod pocztowy" }, - "description": "Wprowad\u017a sw\u00f3j ameryka\u0144ski lub kanadyjski kod pocztowy.", + "description": "Wprowad\u017a ameryka\u0144ski lub kanadyjski kod pocztowy.", "title": "IQVIA" } }, diff --git a/homeassistant/components/iqvia/.translations/ru.json b/homeassistant/components/iqvia/.translations/ru.json index 06a5b7e69dd..0c3afc88c94 100644 --- a/homeassistant/components/iqvia/.translations/ru.json +++ b/homeassistant/components/iqvia/.translations/ru.json @@ -1,7 +1,7 @@ { "config": { "error": { - "identifier_exists": "\u041f\u043e\u0447\u0442\u043e\u0432\u044b\u0439 \u0438\u043d\u0434\u0435\u043a\u0441 \u0443\u0436\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d", + "identifier_exists": "\u041f\u043e\u0447\u0442\u043e\u0432\u044b\u0439 \u0438\u043d\u0434\u0435\u043a\u0441 \u0443\u0436\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d.", "invalid_zip_code": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043f\u043e\u0447\u0442\u043e\u0432\u044b\u0439 \u0438\u043d\u0434\u0435\u043a\u0441" }, "step": { diff --git a/homeassistant/components/iqvia/manifest.json b/homeassistant/components/iqvia/manifest.json index 7392c931f48..caf422938b2 100644 --- a/homeassistant/components/iqvia/manifest.json +++ b/homeassistant/components/iqvia/manifest.json @@ -2,7 +2,7 @@ "domain": "iqvia", "name": "IQVIA", "config_flow": true, - "documentation": "https://www.home-assistant.io/components/iqvia", + "documentation": "https://www.home-assistant.io/integrations/iqvia", "requirements": [ "numpy==1.17.1", "pyiqvia==0.2.1" diff --git a/homeassistant/components/irish_rail_transport/manifest.json b/homeassistant/components/irish_rail_transport/manifest.json index 5961400e68e..da15d87ab02 100644 --- a/homeassistant/components/irish_rail_transport/manifest.json +++ b/homeassistant/components/irish_rail_transport/manifest.json @@ -1,7 +1,7 @@ { "domain": "irish_rail_transport", "name": "Irish rail transport", - "documentation": "https://www.home-assistant.io/components/irish_rail_transport", + "documentation": "https://www.home-assistant.io/integrations/irish_rail_transport", "requirements": [ "pyirishrail==0.0.2" ], diff --git a/homeassistant/components/islamic_prayer_times/manifest.json b/homeassistant/components/islamic_prayer_times/manifest.json index 4dc9e2cb7c3..035b61d0f2d 100644 --- a/homeassistant/components/islamic_prayer_times/manifest.json +++ b/homeassistant/components/islamic_prayer_times/manifest.json @@ -1,7 +1,7 @@ { "domain": "islamic_prayer_times", "name": "Islamic prayer times", - "documentation": "https://www.home-assistant.io/components/islamic_prayer_times", + "documentation": "https://www.home-assistant.io/integrations/islamic_prayer_times", "requirements": [ "prayer_times_calculator==0.0.3" ], diff --git a/homeassistant/components/iss/manifest.json b/homeassistant/components/iss/manifest.json index dc71e81ac08..72521e67af9 100644 --- a/homeassistant/components/iss/manifest.json +++ b/homeassistant/components/iss/manifest.json @@ -1,7 +1,7 @@ { "domain": "iss", "name": "Iss", - "documentation": "https://www.home-assistant.io/components/iss", + "documentation": "https://www.home-assistant.io/integrations/iss", "requirements": [ "pyiss==1.0.1" ], diff --git a/homeassistant/components/isy994/__init__.py b/homeassistant/components/isy994/__init__.py index 727ec91dc37..324dcb019b3 100644 --- a/homeassistant/components/isy994/__init__.py +++ b/homeassistant/components/isy994/__init__.py @@ -323,9 +323,9 @@ def _categorize_nodes( # determine if it should be a binary_sensor. if _is_sensor_a_binary_sensor(hass, node): continue - else: - hass.data[ISY994_NODES]["sensor"].append(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 diff --git a/homeassistant/components/isy994/manifest.json b/homeassistant/components/isy994/manifest.json index 0dd0f1eae80..759e1b78e8e 100644 --- a/homeassistant/components/isy994/manifest.json +++ b/homeassistant/components/isy994/manifest.json @@ -1,7 +1,7 @@ { "domain": "isy994", "name": "Isy994", - "documentation": "https://www.home-assistant.io/components/isy994", + "documentation": "https://www.home-assistant.io/integrations/isy994", "requirements": [ "PyISY==1.1.2" ], diff --git a/homeassistant/components/itach/manifest.json b/homeassistant/components/itach/manifest.json index c26b19c636e..86bb362f8dc 100644 --- a/homeassistant/components/itach/manifest.json +++ b/homeassistant/components/itach/manifest.json @@ -1,7 +1,7 @@ { "domain": "itach", "name": "Itach", - "documentation": "https://www.home-assistant.io/components/itach", + "documentation": "https://www.home-assistant.io/integrations/itach", "requirements": [ "pyitachip2ir==0.0.7" ], diff --git a/homeassistant/components/itunes/manifest.json b/homeassistant/components/itunes/manifest.json index 6f05125661e..ec47deabc23 100644 --- a/homeassistant/components/itunes/manifest.json +++ b/homeassistant/components/itunes/manifest.json @@ -1,7 +1,7 @@ { "domain": "itunes", "name": "Itunes", - "documentation": "https://www.home-assistant.io/components/itunes", + "documentation": "https://www.home-assistant.io/integrations/itunes", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/izone/.translations/bg.json b/homeassistant/components/izone/.translations/bg.json new file mode 100644 index 00000000000..26a55aa4ed8 --- /dev/null +++ b/homeassistant/components/izone/.translations/bg.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "\u041d\u0435 \u0441\u0430 \u043d\u0430\u043c\u0435\u0440\u0435\u043d\u0438 iZone \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0432 \u043c\u0440\u0435\u0436\u0430\u0442\u0430.", + "single_instance_allowed": "\u041d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u0430 \u0435 \u0441\u0430\u043c\u043e \u0435\u0434\u043d\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u043d\u0430 iZone." + }, + "step": { + "confirm": { + "description": "\u0418\u0441\u043a\u0430\u0442\u0435 \u043b\u0438 \u0434\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u0435 iZone?", + "title": "iZone" + } + }, + "title": "iZone" + } +} \ No newline at end of file diff --git a/homeassistant/components/izone/.translations/ca.json b/homeassistant/components/izone/.translations/ca.json new file mode 100644 index 00000000000..b80d9bee4e2 --- /dev/null +++ b/homeassistant/components/izone/.translations/ca.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "No s'han trobat dispositius iZone a la xarxa.", + "single_instance_allowed": "Nom\u00e9s cal una \u00fanica configuraci\u00f3 de iZone." + }, + "step": { + "confirm": { + "description": "Vols configurar iZone?", + "title": "iZone" + } + }, + "title": "iZone" + } +} \ No newline at end of file diff --git a/homeassistant/components/izone/.translations/da.json b/homeassistant/components/izone/.translations/da.json new file mode 100644 index 00000000000..9dc3d88322c --- /dev/null +++ b/homeassistant/components/izone/.translations/da.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "Der blev ikke fundet nogen iZone-enheder p\u00e5 netv\u00e6rket.", + "single_instance_allowed": "Det er kun n\u00f8dvendigt med en ops\u00e6tning af iZone" + }, + "step": { + "confirm": { + "description": "Vil du konfigurere iZone?", + "title": "iZone" + } + }, + "title": "iZone" + } +} \ No newline at end of file diff --git a/homeassistant/components/izone/.translations/en.json b/homeassistant/components/izone/.translations/en.json new file mode 100644 index 00000000000..5293ad2a1fe --- /dev/null +++ b/homeassistant/components/izone/.translations/en.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "No iZone devices found on the network.", + "single_instance_allowed": "Only a single configuration of iZone is necessary." + }, + "step": { + "confirm": { + "description": "Do you want to set up iZone?", + "title": "iZone" + } + }, + "title": "iZone" + } +} \ No newline at end of file diff --git a/homeassistant/components/izone/.translations/es.json b/homeassistant/components/izone/.translations/es.json new file mode 100644 index 00000000000..9f82b1a7b1f --- /dev/null +++ b/homeassistant/components/izone/.translations/es.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "No se han encontrado dispositivos iZone en la red.", + "single_instance_allowed": "Solo es necesaria una \u00fanica configuraci\u00f3n de iZone." + }, + "step": { + "confirm": { + "description": "\u00bfQuieres configurar iZone?", + "title": "iZone" + } + }, + "title": "iZone" + } +} \ No newline at end of file diff --git a/homeassistant/components/izone/.translations/fr.json b/homeassistant/components/izone/.translations/fr.json new file mode 100644 index 00000000000..c90416b0619 --- /dev/null +++ b/homeassistant/components/izone/.translations/fr.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "Aucun p\u00e9riph\u00e9rique iZone trouv\u00e9 sur le r\u00e9seau.", + "single_instance_allowed": "Une seule configuration d'iZone est n\u00e9cessaire." + }, + "step": { + "confirm": { + "description": "Voulez-vous configurer iZone?", + "title": "iZone" + } + }, + "title": "iZone" + } +} \ No newline at end of file diff --git a/homeassistant/components/izone/.translations/it.json b/homeassistant/components/izone/.translations/it.json new file mode 100644 index 00000000000..5498624a061 --- /dev/null +++ b/homeassistant/components/izone/.translations/it.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "Nessun dispositivo iZone trovato in rete.", + "single_instance_allowed": "\u00c8 necessaria una sola configurazione di iZone." + }, + "step": { + "confirm": { + "description": "Vuoi configurare iZone?", + "title": "iZone" + } + }, + "title": "iZone" + } +} \ No newline at end of file diff --git a/homeassistant/components/izone/.translations/ko.json b/homeassistant/components/izone/.translations/ko.json new file mode 100644 index 00000000000..69b8ce8a31e --- /dev/null +++ b/homeassistant/components/izone/.translations/ko.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "iZone \uae30\uae30\uac00 \ub124\ud2b8\uc6cc\ud06c\uc5d0\uc11c \ubc1c\uacac\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4.", + "single_instance_allowed": "\ud558\ub098\uc758 iZone \ub9cc \uad6c\uc131 \ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4." + }, + "step": { + "confirm": { + "description": "iZone \uc744 \uc124\uc815 \ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", + "title": "iZone" + } + }, + "title": "iZone" + } +} \ No newline at end of file diff --git a/homeassistant/components/izone/.translations/lb.json b/homeassistant/components/izone/.translations/lb.json new file mode 100644 index 00000000000..c6e075683ad --- /dev/null +++ b/homeassistant/components/izone/.translations/lb.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "Keng iZone Apparater am Netzwierk fonnt.", + "single_instance_allowed": "N\u00ebmmen eng eenzeg Konfiguratioun vun iZone ass n\u00e9ideg." + }, + "step": { + "confirm": { + "description": "Soll iZone konfigur\u00e9iert ginn?", + "title": "iZone" + } + }, + "title": "iZone" + } +} \ No newline at end of file diff --git a/homeassistant/components/izone/.translations/nn.json b/homeassistant/components/izone/.translations/nn.json new file mode 100644 index 00000000000..eaf7601be9c --- /dev/null +++ b/homeassistant/components/izone/.translations/nn.json @@ -0,0 +1,10 @@ +{ + "config": { + "step": { + "confirm": { + "title": "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 new file mode 100644 index 00000000000..9068b18c82d --- /dev/null +++ b/homeassistant/components/izone/.translations/no.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "Finner ingen iZone-enheter p\u00e5 nettverket.", + "single_instance_allowed": "Bare en konfigurasjon av iZone er n\u00f8dvendig." + }, + "step": { + "confirm": { + "description": "Vil du konfigurere iZone?", + "title": "iZone" + } + }, + "title": "iZone" + } +} \ No newline at end of file diff --git a/homeassistant/components/izone/.translations/pl.json b/homeassistant/components/izone/.translations/pl.json new file mode 100644 index 00000000000..4f90cf71cbc --- /dev/null +++ b/homeassistant/components/izone/.translations/pl.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "Nie znaleziono w sieci urz\u0105dze\u0144 iZone.", + "single_instance_allowed": "Wymagana jest tylko jedna konfiguracja iZone." + }, + "step": { + "confirm": { + "description": "Chcesz skonfigurowa\u0107 iZone?", + "title": "iZone" + } + }, + "title": "iZone" + } +} \ No newline at end of file diff --git a/homeassistant/components/izone/.translations/ru.json b/homeassistant/components/izone/.translations/ru.json new file mode 100644 index 00000000000..7e632c8dd62 --- /dev/null +++ b/homeassistant/components/izone/.translations/ru.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 iZone \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 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430." + }, + "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 iZone?", + "title": "iZone" + } + }, + "title": "iZone" + } +} \ No newline at end of file diff --git a/homeassistant/components/izone/.translations/sl.json b/homeassistant/components/izone/.translations/sl.json new file mode 100644 index 00000000000..cb2fe821297 --- /dev/null +++ b/homeassistant/components/izone/.translations/sl.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "V omre\u017eju ni najdenih naprav iZone.", + "single_instance_allowed": "Potrebna je samo ena konfiguracija iZone." + }, + "step": { + "confirm": { + "description": "Ali \u017eelite nastaviti iZone?", + "title": "iZone" + } + }, + "title": "iZone" + } +} \ No newline at end of file diff --git a/homeassistant/components/izone/.translations/zh-Hant.json b/homeassistant/components/izone/.translations/zh-Hant.json new file mode 100644 index 00000000000..7448100158e --- /dev/null +++ b/homeassistant/components/izone/.translations/zh-Hant.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "\u5728\u7db2\u8def\u4e0a\u627e\u4e0d\u5230 iZone \u8a2d\u5099\u3002", + "single_instance_allowed": "\u50c5\u9700\u8a2d\u5b9a\u4e00\u6b21 iZone \u5373\u53ef\u3002" + }, + "step": { + "confirm": { + "description": "\u662f\u5426\u8981\u8a2d\u5b9a iZone\uff1f", + "title": "iZone" + } + }, + "title": "iZone" + } +} \ No newline at end of file diff --git a/homeassistant/components/izone/__init__.py b/homeassistant/components/izone/__init__.py new file mode 100644 index 00000000000..6fecbc1f3a6 --- /dev/null +++ b/homeassistant/components/izone/__init__.py @@ -0,0 +1,67 @@ +""" +Platform for the iZone AC. + +For more details about this component, please refer to the documentation +https://home-assistant.io/integrations/izone/ +""" +import logging + +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_EXCLUDE +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.typing import ConfigType, HomeAssistantType + +from .const import IZONE, DATA_CONFIG +from .discovery import async_start_discovery_service, async_stop_discovery_service + +_LOGGER = logging.getLogger(__name__) + +CONFIG_SCHEMA = vol.Schema( + { + IZONE: vol.Schema( + { + vol.Optional(CONF_EXCLUDE, default=[]): vol.All( + cv.ensure_list, [cv.string] + ) + } + ) + }, + extra=vol.ALLOW_EXTRA, +) + + +async def async_setup(hass: HomeAssistantType, config: ConfigType): + """Register the iZone component config.""" + conf = config.get(IZONE) + if not conf: + return True + + hass.data[DATA_CONFIG] = conf + + # Explicitly added in the config file, create a config entry. + hass.async_create_task( + hass.config_entries.flow.async_init( + IZONE, context={"source": config_entries.SOURCE_IMPORT} + ) + ) + + return True + + +async def async_setup_entry(hass, entry): + """Set up from a config entry.""" + await async_start_discovery_service(hass) + + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, "climate") + ) + return True + + +async def async_unload_entry(hass, entry): + """Unload the config entry and stop discovery process.""" + await async_stop_discovery_service(hass) + await hass.config_entries.async_forward_entry_unload(entry, "climate") + return True diff --git a/homeassistant/components/izone/climate.py b/homeassistant/components/izone/climate.py new file mode 100644 index 00000000000..c932c66627b --- /dev/null +++ b/homeassistant/components/izone/climate.py @@ -0,0 +1,546 @@ +"""Support for the iZone HVAC.""" +import logging +from typing import Optional, List + +from pizone import Zone, Controller + +from homeassistant.core import callback +from homeassistant.components.climate import ClimateDevice +from homeassistant.components.climate.const import ( + HVAC_MODE_HEAT_COOL, + HVAC_MODE_COOL, + HVAC_MODE_DRY, + HVAC_MODE_FAN_ONLY, + HVAC_MODE_HEAT, + HVAC_MODE_OFF, + FAN_LOW, + FAN_MEDIUM, + FAN_HIGH, + FAN_AUTO, + PRESET_ECO, + PRESET_NONE, + SUPPORT_FAN_MODE, + SUPPORT_PRESET_MODE, + SUPPORT_TARGET_TEMPERATURE, +) +from homeassistant.const import ( + ATTR_TEMPERATURE, + PRECISION_HALVES, + TEMP_CELSIUS, + CONF_EXCLUDE, +) +from homeassistant.helpers.temperature import display_temp as show_temp +from homeassistant.helpers.typing import ConfigType, HomeAssistantType +from homeassistant.helpers.dispatcher import async_dispatcher_connect + +from .const import ( + DATA_DISCOVERY_SERVICE, + IZONE, + DISPATCH_CONTROLLER_DISCOVERED, + DISPATCH_CONTROLLER_DISCONNECTED, + DISPATCH_CONTROLLER_RECONNECTED, + DISPATCH_CONTROLLER_UPDATE, + DISPATCH_ZONE_UPDATE, + DATA_CONFIG, +) + +_LOGGER = logging.getLogger(__name__) + +_IZONE_FAN_TO_HA = { + Controller.Fan.LOW: FAN_LOW, + Controller.Fan.MED: FAN_MEDIUM, + Controller.Fan.HIGH: FAN_HIGH, + Controller.Fan.AUTO: FAN_AUTO, +} + + +async def async_setup_entry( + hass: HomeAssistantType, config: ConfigType, async_add_entities +): + """Initialize an IZone Controller.""" + disco = hass.data[DATA_DISCOVERY_SERVICE] + + @callback + def init_controller(ctrl: Controller): + """Register the controller device and the containing zones.""" + conf = hass.data.get(DATA_CONFIG) # type: ConfigType + + # Filter out any entities excluded in the config file + if conf and ctrl.device_uid in conf[CONF_EXCLUDE]: + _LOGGER.info("Controller UID=%s ignored as excluded", ctrl.device_uid) + return + _LOGGER.info("Controller UID=%s discovered", ctrl.device_uid) + + device = ControllerDevice(ctrl) + async_add_entities([device]) + async_add_entities(device.zones.values()) + + # create any components not yet created + for controller in disco.pi_disco.controllers.values(): + init_controller(controller) + + # connect to register any further components + async_dispatcher_connect(hass, DISPATCH_CONTROLLER_DISCOVERED, init_controller) + + return True + + +class ControllerDevice(ClimateDevice): + """Representation of iZone Controller.""" + + def __init__(self, controller: Controller) -> None: + """Initialise ControllerDevice.""" + self._controller = controller + + self._supported_features = SUPPORT_FAN_MODE + + if ( + controller.ras_mode == "master" and controller.zone_ctrl == 13 + ) or controller.ras_mode == "RAS": + self._supported_features |= SUPPORT_TARGET_TEMPERATURE + + self._state_to_pizone = { + HVAC_MODE_COOL: Controller.Mode.COOL, + HVAC_MODE_HEAT: Controller.Mode.HEAT, + HVAC_MODE_HEAT_COOL: Controller.Mode.AUTO, + HVAC_MODE_FAN_ONLY: Controller.Mode.VENT, + HVAC_MODE_DRY: Controller.Mode.DRY, + } + if controller.free_air_enabled: + self._supported_features |= SUPPORT_PRESET_MODE + + self._fan_to_pizone = {} + for fan in controller.fan_modes: + self._fan_to_pizone[_IZONE_FAN_TO_HA[fan]] = fan + self._available = True + + self._device_info = { + "identifiers": {(IZONE, self.unique_id)}, + "name": self.name, + "manufacturer": "IZone", + "model": self._controller.sys_type, + } + + # Create the zones + self.zones = {} + for zone in controller.zones: + self.zones[zone] = ZoneDevice(self, zone) + + async def async_added_to_hass(self): + """Call on adding to hass.""" + # Register for connect/disconnect/update events + @callback + def controller_disconnected(ctrl: Controller, ex: Exception) -> None: + """Disconnected from controller.""" + if ctrl is not self._controller: + return + self.set_available(False, ex) + + self.async_on_remove( + async_dispatcher_connect( + self.hass, DISPATCH_CONTROLLER_DISCONNECTED, controller_disconnected + ) + ) + + @callback + def controller_reconnected(ctrl: Controller) -> None: + """Reconnected to controller.""" + if ctrl is not self._controller: + return + self.set_available(True) + + self.async_on_remove( + async_dispatcher_connect( + self.hass, DISPATCH_CONTROLLER_RECONNECTED, controller_reconnected + ) + ) + + @callback + def controller_update(ctrl: Controller) -> None: + """Handle controller data updates.""" + if ctrl is not self._controller: + return + self.async_schedule_update_ha_state() + + self.async_on_remove( + async_dispatcher_connect( + self.hass, DISPATCH_CONTROLLER_UPDATE, controller_update + ) + ) + + @property + def available(self) -> bool: + """Return True if entity is available.""" + return self._available + + @callback + def set_available(self, available: bool, ex: Exception = None) -> None: + """ + Set availability for the controller. + + Also sets zone availability as they follow the same availability. + """ + if self.available == available: + return + + if available: + _LOGGER.info("Reconnected controller %s ", self._controller.device_uid) + else: + _LOGGER.info( + "Controller %s disconnected due to exception: %s", + self._controller.device_uid, + ex, + ) + + self._available = available + self.async_schedule_update_ha_state() + for zone in self.zones.values(): + zone.async_schedule_update_ha_state() + + @property + def device_info(self): + """Return the device info for the iZone system.""" + return self._device_info + + @property + def unique_id(self): + """Return the ID of the controller device.""" + return self._controller.device_uid + + @property + def name(self) -> str: + """Return the name of the entity.""" + return f"iZone Controller {self._controller.device_uid}" + + @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 + + @property + def supported_features(self) -> int: + """Return the list of supported features.""" + return self._supported_features + + @property + def temperature_unit(self) -> str: + """Return the unit of measurement which this thermostat uses.""" + return TEMP_CELSIUS + + @property + def precision(self) -> float: + """Return the precision of the system.""" + return PRECISION_HALVES + + @property + def device_state_attributes(self): + """Return the optional state attributes.""" + return { + "supply_temperature": show_temp( + self.hass, + self.supply_temperature, + self.temperature_unit, + self.precision, + ), + "temp_setpoint": show_temp( + self.hass, + self._controller.temp_setpoint, + self.temperature_unit, + self.precision, + ), + } + + @property + def hvac_mode(self) -> str: + """Return current operation ie. heat, cool, idle.""" + if not self._controller.is_on: + return HVAC_MODE_OFF + mode = self._controller.mode + for (key, value) in self._state_to_pizone.items(): + if value == mode: + return key + assert False, "Should be unreachable" + + @property + def hvac_modes(self) -> List[str]: + """Return the list of available operation modes.""" + if self._controller.free_air: + return [HVAC_MODE_OFF, HVAC_MODE_FAN_ONLY] + return [HVAC_MODE_OFF, *self._state_to_pizone] + + @property + def preset_mode(self): + """Eco mode is external air.""" + return PRESET_ECO if self._controller.free_air else PRESET_NONE + + @property + def preset_modes(self): + """Available preset modes, normal or eco.""" + if self._controller.free_air_enabled: + return [PRESET_NONE, PRESET_ECO] + return [PRESET_NONE] + + @property + def current_temperature(self) -> Optional[float]: + """Return the current temperature.""" + if self._controller.mode == Controller.Mode.FREE_AIR: + return self._controller.temp_supply + return self._controller.temp_return + + @property + def target_temperature(self) -> Optional[float]: + """Return the temperature we try to reach.""" + if not self._supported_features & SUPPORT_TARGET_TEMPERATURE: + return None + return self._controller.temp_setpoint + + @property + def supply_temperature(self) -> float: + """Return the current supply, or in duct, temperature.""" + return self._controller.temp_supply + + @property + def target_temperature_step(self) -> Optional[float]: + """Return the supported step of target temperature.""" + return 0.5 + + @property + def fan_mode(self) -> Optional[str]: + """Return the fan setting.""" + return _IZONE_FAN_TO_HA[self._controller.fan] + + @property + def fan_modes(self) -> Optional[List[str]]: + """Return the list of available fan modes.""" + return list(self._fan_to_pizone) + + @property + def min_temp(self) -> float: + """Return the minimum temperature.""" + return self._controller.temp_min + + @property + def max_temp(self) -> float: + """Return the maximum temperature.""" + return self._controller.temp_max + + async def wrap_and_catch(self, coro): + """Catch any connection errors and set unavailable.""" + try: + await coro + except ConnectionError as ex: + self.set_available(False, ex) + else: + self.set_available(True) + + async def async_set_temperature(self, **kwargs) -> None: + """Set new target temperature.""" + if not self.supported_features & SUPPORT_TARGET_TEMPERATURE: + self.async_schedule_update_ha_state(True) + return + temp = kwargs.get(ATTR_TEMPERATURE) + if temp is not None: + await self.wrap_and_catch(self._controller.set_temp_setpoint(temp)) + + async def async_set_fan_mode(self, fan_mode: str) -> None: + """Set new target fan mode.""" + fan = self._fan_to_pizone[fan_mode] + await self.wrap_and_catch(self._controller.set_fan(fan)) + + async def async_set_hvac_mode(self, hvac_mode: str) -> None: + """Set new target operation mode.""" + if hvac_mode == HVAC_MODE_OFF: + await self.wrap_and_catch(self._controller.set_on(False)) + return + if not self._controller.is_on: + await self.wrap_and_catch(self._controller.set_on(True)) + if self._controller.free_air: + return + mode = self._state_to_pizone[hvac_mode] + await self.wrap_and_catch(self._controller.set_mode(mode)) + + async def async_set_preset_mode(self, preset_mode: str) -> None: + """Set the preset mode.""" + await self.wrap_and_catch( + self._controller.set_free_air(preset_mode == PRESET_ECO) + ) + + async def async_turn_on(self) -> None: + """Turn the entity on.""" + await self.wrap_and_catch(self._controller.set_on(True)) + + +class ZoneDevice(ClimateDevice): + """Representation of iZone Zone.""" + + def __init__(self, controller: ControllerDevice, zone: Zone) -> None: + """Initialise ZoneDevice.""" + self._controller = controller + self._zone = zone + self._name = zone.name.title() + + self._supported_features = 0 + if zone.type != Zone.Type.AUTO: + self._state_to_pizone = { + HVAC_MODE_OFF: Zone.Mode.CLOSE, + HVAC_MODE_FAN_ONLY: Zone.Mode.OPEN, + } + else: + self._state_to_pizone = { + HVAC_MODE_OFF: Zone.Mode.CLOSE, + HVAC_MODE_FAN_ONLY: Zone.Mode.OPEN, + HVAC_MODE_HEAT_COOL: Zone.Mode.AUTO, + } + self._supported_features |= SUPPORT_TARGET_TEMPERATURE + + self._device_info = { + "identifiers": {(IZONE, controller.unique_id, zone.index)}, + "name": self.name, + "manufacturer": "IZone", + "via_device": (IZONE, controller.unique_id), + "model": zone.type.name.title(), + } + + async def async_added_to_hass(self): + """Call on adding to hass.""" + + @callback + def zone_update(ctrl: Controller, zone: Zone) -> None: + """Handle zone data updates.""" + if zone is not self._zone: + return + self._name = zone.name.title() + self.async_schedule_update_ha_state() + + self.async_on_remove( + async_dispatcher_connect(self.hass, DISPATCH_ZONE_UPDATE, zone_update) + ) + + @property + def available(self) -> bool: + """Return True if entity is available.""" + return self._controller.available + + @property + def assumed_state(self) -> bool: + """Return True if unable to access real state of the entity.""" + return self._controller.assumed_state + + @property + def device_info(self): + """Return the device info for the iZone system.""" + return self._device_info + + @property + def unique_id(self): + """Return the ID of the controller device.""" + return "{}_z{}".format(self._controller.unique_id, self._zone.index + 1) + + @property + def name(self) -> str: + """Return the name of the entity.""" + return self._name + + @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 + + @property + def supported_features(self): + """Return the list of supported features.""" + try: + if self._zone.mode == Zone.Mode.AUTO: + return self._supported_features + return self._supported_features & ~SUPPORT_TARGET_TEMPERATURE + except ConnectionError: + return None + + @property + def temperature_unit(self): + """Return the unit of measurement which this thermostat uses.""" + return TEMP_CELSIUS + + @property + def precision(self): + """Return the precision of the system.""" + return PRECISION_HALVES + + @property + def hvac_mode(self): + """Return current operation ie. heat, cool, idle.""" + mode = self._zone.mode + for (key, value) in self._state_to_pizone.items(): + if value == mode: + return key + return None + + @property + def hvac_modes(self): + """Return the list of available operation modes.""" + return list(self._state_to_pizone.keys()) + + @property + def current_temperature(self): + """Return the current temperature.""" + return self._zone.temp_current + + @property + def target_temperature(self): + """Return the temperature we try to reach.""" + if self._zone.type != Zone.Type.AUTO: + return None + return self._zone.temp_setpoint + + @property + def target_temperature_step(self): + """Return the supported step of target temperature.""" + return 0.5 + + @property + def min_temp(self): + """Return the minimum temperature.""" + return self._controller.min_temp + + @property + def max_temp(self): + """Return the maximum temperature.""" + return self._controller.max_temp + + async def async_set_temperature(self, **kwargs): + """Set new target temperature.""" + if self._zone.mode != Zone.Mode.AUTO: + return + temp = kwargs.get(ATTR_TEMPERATURE) + if temp is not None: + await self._controller.wrap_and_catch(self._zone.set_temp_setpoint(temp)) + + async def async_set_hvac_mode(self, hvac_mode: str) -> None: + """Set new target operation mode.""" + mode = self._state_to_pizone[hvac_mode] + await self._controller.wrap_and_catch(self._zone.set_mode(mode)) + self.async_schedule_update_ha_state() + + @property + def is_on(self): + """Return true if on.""" + return self._zone.mode != Zone.Mode.CLOSE + + async def async_turn_on(self): + """Turn device on (open zone).""" + if self._zone.type == Zone.Type.AUTO: + await self._controller.wrap_and_catch(self._zone.set_mode(Zone.Mode.AUTO)) + else: + await self._controller.wrap_and_catch(self._zone.set_mode(Zone.Mode.OPEN)) + self.async_schedule_update_ha_state() + + async def async_turn_off(self): + """Turn device off (close zone).""" + await self._controller.wrap_and_catch(self._zone.set_mode(Zone.Mode.CLOSE)) + self.async_schedule_update_ha_state() diff --git a/homeassistant/components/izone/config_flow.py b/homeassistant/components/izone/config_flow.py new file mode 100644 index 00000000000..eb57a36a2bb --- /dev/null +++ b/homeassistant/components/izone/config_flow.py @@ -0,0 +1,45 @@ +"""Config flow for izone.""" + +import logging +import asyncio + +from async_timeout import timeout + +from homeassistant import config_entries +from homeassistant.helpers import config_entry_flow +from homeassistant.helpers.dispatcher import async_dispatcher_connect + +from .const import IZONE, TIMEOUT_DISCOVERY, DISPATCH_CONTROLLER_DISCOVERED + + +_LOGGER = logging.getLogger(__name__) + + +async def _async_has_devices(hass): + from .discovery import async_start_discovery_service, async_stop_discovery_service + + controller_ready = asyncio.Event() + async_dispatcher_connect( + hass, DISPATCH_CONTROLLER_DISCOVERED, lambda x: controller_ready.set() + ) + + disco = await async_start_discovery_service(hass) + + try: + async with timeout(TIMEOUT_DISCOVERY): + await controller_ready.wait() + except asyncio.TimeoutError: + pass + + if not disco.pi_disco.controllers: + await async_stop_discovery_service(hass) + _LOGGER.debug("No controllers found") + return False + + _LOGGER.debug("Controllers %s", disco.pi_disco.controllers) + return True + + +config_entry_flow.register_discovery_flow( + IZONE, "iZone Aircon", _async_has_devices, config_entries.CONN_CLASS_LOCAL_POLL +) diff --git a/homeassistant/components/izone/const.py b/homeassistant/components/izone/const.py new file mode 100644 index 00000000000..4da7bc9e4af --- /dev/null +++ b/homeassistant/components/izone/const.py @@ -0,0 +1,14 @@ +"""Constants used by the izone component.""" + +IZONE = "izone" + +DATA_DISCOVERY_SERVICE = "izone_discovery" +DATA_CONFIG = "izone_config" + +DISPATCH_CONTROLLER_DISCOVERED = "izone_controller_discovered" +DISPATCH_CONTROLLER_DISCONNECTED = "izone_controller_disconnected" +DISPATCH_CONTROLLER_RECONNECTED = "izone_controller_disconnected" +DISPATCH_CONTROLLER_UPDATE = "izone_controller_update" +DISPATCH_ZONE_UPDATE = "izone_zone_update" + +TIMEOUT_DISCOVERY = 20 diff --git a/homeassistant/components/izone/discovery.py b/homeassistant/components/izone/discovery.py new file mode 100644 index 00000000000..3630c28605b --- /dev/null +++ b/homeassistant/components/izone/discovery.py @@ -0,0 +1,87 @@ +"""Internal discovery service for iZone AC.""" + +import logging +import pizone + +from homeassistant.const import EVENT_HOMEASSISTANT_STOP +from homeassistant.helpers import aiohttp_client +from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.helpers.dispatcher import async_dispatcher_send + +from .const import ( + DATA_DISCOVERY_SERVICE, + DISPATCH_CONTROLLER_DISCOVERED, + DISPATCH_CONTROLLER_DISCONNECTED, + DISPATCH_CONTROLLER_RECONNECTED, + DISPATCH_CONTROLLER_UPDATE, + DISPATCH_ZONE_UPDATE, +) + +_LOGGER = logging.getLogger(__name__) + + +class DiscoveryService(pizone.Listener): + """Discovery data and interfacing with pizone library.""" + + def __init__(self, hass): + """Initialise discovery service.""" + super().__init__() + self.hass = hass + self.pi_disco = None + + # Listener interface + def controller_discovered(self, ctrl: pizone.Controller) -> None: + """Handle new controller discoverery.""" + async_dispatcher_send(self.hass, DISPATCH_CONTROLLER_DISCOVERED, ctrl) + + def controller_disconnected(self, ctrl: pizone.Controller, ex: Exception) -> None: + """On disconnect from controller.""" + async_dispatcher_send(self.hass, DISPATCH_CONTROLLER_DISCONNECTED, ctrl, ex) + + def controller_reconnected(self, ctrl: pizone.Controller) -> None: + """On reconnect to controller.""" + async_dispatcher_send(self.hass, DISPATCH_CONTROLLER_RECONNECTED, ctrl) + + def controller_update(self, ctrl: pizone.Controller) -> None: + """System update message is recieved from the controller.""" + async_dispatcher_send(self.hass, DISPATCH_CONTROLLER_UPDATE, ctrl) + + def zone_update(self, ctrl: pizone.Controller, zone: pizone.Zone) -> None: + """Zone update message is recieved from the controller.""" + async_dispatcher_send(self.hass, DISPATCH_ZONE_UPDATE, ctrl, zone) + + +async def async_start_discovery_service(hass: HomeAssistantType): + """Set up the pizone internal discovery.""" + disco = hass.data.get(DATA_DISCOVERY_SERVICE) + if disco: + # Already started + return disco + + # discovery local services + disco = DiscoveryService(hass) + hass.data[DATA_DISCOVERY_SERVICE] = disco + + # Start the pizone discovery service, disco is the listener + session = aiohttp_client.async_get_clientsession(hass) + loop = hass.loop + + disco.pi_disco = pizone.discovery(disco, loop=loop, session=session) + await disco.pi_disco.start_discovery() + + async def shutdown_event(event): + await async_stop_discovery_service(hass) + + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, shutdown_event) + + return disco + + +async def async_stop_discovery_service(hass: HomeAssistantType): + """Stop the discovery service.""" + disco = hass.data.get(DATA_DISCOVERY_SERVICE) + if not disco: + return + + await disco.pi_disco.close() + del hass.data[DATA_DISCOVERY_SERVICE] diff --git a/homeassistant/components/izone/manifest.json b/homeassistant/components/izone/manifest.json new file mode 100644 index 00000000000..a3aa3ad3fa8 --- /dev/null +++ b/homeassistant/components/izone/manifest.json @@ -0,0 +1,9 @@ +{ + "domain": "izone", + "name": "izone", + "documentation": "https://www.home-assistant.io/integrations/izone", + "requirements": [ "python-izone==1.1.1" ], + "dependencies": [], + "codeowners": [ "@Swamp-Ig" ], + "config_flow": true +} diff --git a/homeassistant/components/izone/strings.json b/homeassistant/components/izone/strings.json new file mode 100644 index 00000000000..7cb14b03c6c --- /dev/null +++ b/homeassistant/components/izone/strings.json @@ -0,0 +1,15 @@ +{ + "config": { + "title": "iZone", + "step": { + "confirm": { + "title": "iZone", + "description": "Do you want to set up iZone?" + } + }, + "abort": { + "single_instance_allowed": "Only a single configuration of iZone is necessary.", + "no_devices_found": "No iZone devices found on the network." + } + } +} diff --git a/homeassistant/components/jewish_calendar/manifest.json b/homeassistant/components/jewish_calendar/manifest.json index fdc1d2943e6..7b6653ba832 100644 --- a/homeassistant/components/jewish_calendar/manifest.json +++ b/homeassistant/components/jewish_calendar/manifest.json @@ -1,7 +1,7 @@ { "domain": "jewish_calendar", "name": "Jewish calendar", - "documentation": "https://www.home-assistant.io/components/jewish_calendar", + "documentation": "https://www.home-assistant.io/integrations/jewish_calendar", "requirements": [ "hdate==0.9.0" ], diff --git a/homeassistant/components/joaoapps_join/manifest.json b/homeassistant/components/joaoapps_join/manifest.json index 220f2af2035..a2c2e4b11b6 100644 --- a/homeassistant/components/joaoapps_join/manifest.json +++ b/homeassistant/components/joaoapps_join/manifest.json @@ -1,7 +1,7 @@ { "domain": "joaoapps_join", "name": "Joaoapps join", - "documentation": "https://www.home-assistant.io/components/joaoapps_join", + "documentation": "https://www.home-assistant.io/integrations/joaoapps_join", "requirements": [ "python-join-api==0.0.4" ], diff --git a/homeassistant/components/juicenet/manifest.json b/homeassistant/components/juicenet/manifest.json index e65aab2b69d..1ef84b74502 100644 --- a/homeassistant/components/juicenet/manifest.json +++ b/homeassistant/components/juicenet/manifest.json @@ -1,7 +1,7 @@ { "domain": "juicenet", "name": "Juicenet", - "documentation": "https://www.home-assistant.io/components/juicenet", + "documentation": "https://www.home-assistant.io/integrations/juicenet", "requirements": [ "python-juicenet==0.0.5" ], diff --git a/homeassistant/components/kaiterra/__init__.py b/homeassistant/components/kaiterra/__init__.py new file mode 100644 index 00000000000..8c61ad54184 --- /dev/null +++ b/homeassistant/components/kaiterra/__init__.py @@ -0,0 +1,92 @@ +"""Support for Kaiterra devices.""" +import voluptuous as vol + +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.event import async_track_time_interval +from homeassistant.helpers.discovery import async_load_platform +from homeassistant.helpers import config_validation as cv + +from homeassistant.const import ( + CONF_API_KEY, + CONF_DEVICES, + CONF_DEVICE_ID, + CONF_SCAN_INTERVAL, + CONF_TYPE, + CONF_NAME, +) + +from .const import ( + AVAILABLE_AQI_STANDARDS, + AVAILABLE_UNITS, + AVAILABLE_DEVICE_TYPES, + CONF_AQI_STANDARD, + CONF_PREFERRED_UNITS, + DOMAIN, + DEFAULT_AQI_STANDARD, + DEFAULT_PREFERRED_UNIT, + DEFAULT_SCAN_INTERVAL, + KAITERRA_COMPONENTS, +) + +from .api_data import KaiterraApiData + +KAITERRA_DEVICE_SCHEMA = vol.Schema( + { + vol.Required(CONF_DEVICE_ID): cv.string, + vol.Required(CONF_TYPE): vol.In(AVAILABLE_DEVICE_TYPES), + vol.Optional(CONF_NAME): cv.string, + } +) + +KAITERRA_SCHEMA = vol.Schema( + { + vol.Required(CONF_API_KEY): cv.string, + vol.Required(CONF_DEVICES): vol.All(cv.ensure_list, [KAITERRA_DEVICE_SCHEMA]), + vol.Optional(CONF_AQI_STANDARD, default=DEFAULT_AQI_STANDARD): vol.In( + AVAILABLE_AQI_STANDARDS + ), + vol.Optional(CONF_PREFERRED_UNITS, default=DEFAULT_PREFERRED_UNIT): vol.All( + cv.ensure_list, [vol.In(AVAILABLE_UNITS)] + ), + vol.Optional(CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL): cv.time_period, + } +) + +CONFIG_SCHEMA = vol.Schema({DOMAIN: KAITERRA_SCHEMA}, extra=vol.ALLOW_EXTRA) + + +async def async_setup(hass, config): + """Set up the Kaiterra components.""" + + conf = config[DOMAIN] + scan_interval = conf[CONF_SCAN_INTERVAL] + devices = conf[CONF_DEVICES] + session = async_get_clientsession(hass) + api = hass.data[DOMAIN] = KaiterraApiData(hass, conf, session) + + await api.async_update() + + async def _update(now=None): + """Periodic update.""" + await api.async_update() + + async_track_time_interval(hass, _update, scan_interval) + + # Load platforms for each device + for device in devices: + device_name, device_id = ( + device.get(CONF_NAME) or device[CONF_TYPE], + device[CONF_DEVICE_ID], + ) + for component in KAITERRA_COMPONENTS: + hass.async_create_task( + async_load_platform( + hass, + component, + DOMAIN, + {CONF_NAME: device_name, CONF_DEVICE_ID: device_id}, + config, + ) + ) + + return True diff --git a/homeassistant/components/kaiterra/air_quality.py b/homeassistant/components/kaiterra/air_quality.py new file mode 100644 index 00000000000..70699de394c --- /dev/null +++ b/homeassistant/components/kaiterra/air_quality.py @@ -0,0 +1,120 @@ +"""Support for Kaiterra Air Quality Sensors.""" +from homeassistant.components.air_quality import AirQualityEntity + +from homeassistant.helpers.dispatcher import async_dispatcher_connect + +from homeassistant.const import CONF_DEVICE_ID, CONF_NAME + +from .const import ( + DOMAIN, + ATTR_VOC, + ATTR_AQI_LEVEL, + ATTR_AQI_POLLUTANT, + DISPATCHER_KAITERRA, +) + + +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Set up the air_quality kaiterra sensor.""" + if discovery_info is None: + return + + api = hass.data[DOMAIN] + name = discovery_info[CONF_NAME] + device_id = discovery_info[CONF_DEVICE_ID] + + async_add_entities([KaiterraAirQuality(api, name, device_id)]) + + +class KaiterraAirQuality(AirQualityEntity): + """Implementation of a Kaittera air quality sensor.""" + + def __init__(self, api, name, device_id): + """Initialize the sensor.""" + self._api = api + self._name = f"{name} Air Quality" + self._device_id = device_id + + def _data(self, key): + return self._device.get(key, {}).get("value") + + @property + def _device(self): + return self._api.data.get(self._device_id, {}) + + @property + def should_poll(self): + """Return that the sensor should not be polled.""" + return False + + @property + def available(self): + """Return the availability of the sensor.""" + return self._api.data.get(self._device_id) is not None + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @property + def air_quality_index(self): + """Return the Air Quality Index (AQI).""" + return self._data("aqi") + + @property + def air_quality_index_level(self): + """Return the Air Quality Index level.""" + return self._data("aqi_level") + + @property + def air_quality_index_pollutant(self): + """Return the Air Quality Index level.""" + return self._data("aqi_pollutant") + + @property + def particulate_matter_2_5(self): + """Return the particulate matter 2.5 level.""" + return self._data("rpm25c") + + @property + def particulate_matter_10(self): + """Return the particulate matter 10 level.""" + return self._data("rpm10c") + + @property + def carbon_dioxide(self): + """Return the CO2 (carbon dioxide) level.""" + return self._data("rco2") + + @property + def volatile_organic_compounds(self): + """Return the VOC (Volatile Organic Compounds) level.""" + return self._data("rtvoc") + + @property + def unique_id(self): + """Return the sensor's unique id.""" + return f"{self._device_id}_air_quality" + + @property + def device_state_attributes(self): + """Return the device state attributes.""" + data = {} + attributes = [ + (ATTR_VOC, self.volatile_organic_compounds), + (ATTR_AQI_LEVEL, self.air_quality_index_level), + (ATTR_AQI_POLLUTANT, self.air_quality_index_pollutant), + ] + + for attr, value in attributes: + if value is not None: + data[attr] = value + + return data + + async def async_added_to_hass(self): + """Register callback.""" + async_dispatcher_connect( + self.hass, DISPATCHER_KAITERRA, self.async_write_ha_state + ) diff --git a/homeassistant/components/kaiterra/api_data.py b/homeassistant/components/kaiterra/api_data.py new file mode 100644 index 00000000000..81e28438d56 --- /dev/null +++ b/homeassistant/components/kaiterra/api_data.py @@ -0,0 +1,109 @@ +"""Data for all Kaiterra devices.""" +from logging import getLogger + +import asyncio + +import async_timeout + +from aiohttp.client_exceptions import ClientResponseError + +from kaiterra_async_client import KaiterraAPIClient, AQIStandard, Units + +from homeassistant.helpers.dispatcher import async_dispatcher_send + +from homeassistant.const import CONF_API_KEY, CONF_DEVICES, CONF_DEVICE_ID, CONF_TYPE + +from .const import ( + AQI_SCALE, + AQI_LEVEL, + CONF_AQI_STANDARD, + CONF_PREFERRED_UNITS, + DISPATCHER_KAITERRA, +) + +_LOGGER = getLogger(__name__) + +POLLUTANTS = {"rpm25c": "PM2.5", "rpm10c": "PM10", "rtvoc": "TVOC", "rco2": "CO2"} + + +class KaiterraApiData: + """Get data from Kaiterra API.""" + + def __init__(self, hass, config, session): + """Initialize the API data object.""" + + api_key = config[CONF_API_KEY] + aqi_standard = config[CONF_AQI_STANDARD] + devices = config[CONF_DEVICES] + units = config[CONF_PREFERRED_UNITS] + + self._hass = hass + self._api = KaiterraAPIClient( + session, + api_key=api_key, + aqi_standard=AQIStandard.from_str(aqi_standard), + preferred_units=[Units.from_str(unit) for unit in units], + ) + self._devices_ids = [device[CONF_DEVICE_ID] for device in devices] + self._devices = [ + f"/{device[CONF_TYPE]}s/{device[CONF_DEVICE_ID]}" for device in devices + ] + self._scale = AQI_SCALE[aqi_standard] + self._level = AQI_LEVEL[aqi_standard] + self._update_listeners = [] + self.data = {} + + async def async_update(self) -> None: + """Get the data from Kaiterra API.""" + + try: + with async_timeout.timeout(10): + data = await self._api.get_latest_sensor_readings(self._devices) + except (ClientResponseError, asyncio.TimeoutError): + _LOGGER.debug("Couldn't fetch data") + self.data = {} + async_dispatcher_send(self._hass, DISPATCHER_KAITERRA) + + _LOGGER.debug("New data retrieved: %s", data) + + try: + self.data = {} + for i, device in enumerate(data): + if not device: + self.data[self._devices_ids[i]] = {} + continue + + aqi, main_pollutant = None, None + for sensor_name, sensor in device.items(): + points = sensor.get("points") + + if not points: + continue + + point = points[0] + sensor["value"] = point.get("value") + + if "aqi" not in point: + continue + + sensor["aqi"] = point["aqi"] + if not aqi or aqi < point["aqi"]: + aqi = point["aqi"] + main_pollutant = POLLUTANTS.get(sensor_name) + + level = None + for j in range(1, len(self._scale)): + if aqi <= self._scale[j]: + level = self._level[j - 1] + break + + device["aqi"] = {"value": aqi} + device["aqi_level"] = {"value": level} + device["aqi_pollutant"] = {"value": main_pollutant} + + self.data[self._devices_ids[i]] = device + + async_dispatcher_send(self._hass, DISPATCHER_KAITERRA) + except IndexError as err: + _LOGGER.error("Parsing error %s", err) + async_dispatcher_send(self._hass, DISPATCHER_KAITERRA) diff --git a/homeassistant/components/kaiterra/const.py b/homeassistant/components/kaiterra/const.py new file mode 100644 index 00000000000..7e23edb1259 --- /dev/null +++ b/homeassistant/components/kaiterra/const.py @@ -0,0 +1,57 @@ +"""Consts for Kaiterra integration.""" + +from datetime import timedelta + +DOMAIN = "kaiterra" + +DISPATCHER_KAITERRA = "kaiterra_update" + +AQI_SCALE = { + "cn": [0, 50, 100, 150, 200, 300, 400, 500], + "in": [0, 50, 100, 200, 300, 400, 500], + "us": [0, 50, 100, 150, 200, 300, 500], +} +AQI_LEVEL = { + "cn": [ + "Good", + "Satisfactory", + "Moderate", + "Unhealthy for sensitive groups", + "Unhealthy", + "Very unhealthy", + "Hazardous", + ], + "in": [ + "Good", + "Satisfactory", + "Moderately polluted", + "Poor", + "Very poor", + "Severe", + ], + "us": [ + "Good", + "Moderate", + "Unhealthy for sensitive groups", + "Unhealthy", + "Very unhealthy", + "Hazardous", + ], +} + +ATTR_VOC = "volatile_organic_compounds" +ATTR_AQI_LEVEL = "air_quality_index_level" +ATTR_AQI_POLLUTANT = "air_quality_index_pollutant" + +AVAILABLE_AQI_STANDARDS = ["us", "cn", "in"] +AVAILABLE_UNITS = ["x", "%", "C", "F", "mg/m³", "µg/m³", "ppm", "ppb"] +AVAILABLE_DEVICE_TYPES = ["laseregg", "sensedge"] + +CONF_AQI_STANDARD = "aqi_standard" +CONF_PREFERRED_UNITS = "preferred_units" + +DEFAULT_AQI_STANDARD = "us" +DEFAULT_PREFERRED_UNIT = [] +DEFAULT_SCAN_INTERVAL = timedelta(seconds=30) + +KAITERRA_COMPONENTS = ["sensor", "air_quality"] diff --git a/homeassistant/components/kaiterra/manifest.json b/homeassistant/components/kaiterra/manifest.json new file mode 100644 index 00000000000..eb3626a315b --- /dev/null +++ b/homeassistant/components/kaiterra/manifest.json @@ -0,0 +1,8 @@ +{ + "domain": "kaiterra", + "name": "Kaiterra", + "documentation": "https://www.home-assistant.io/integrations/kaiterra", + "requirements": ["kaiterra-async-client==0.0.2"], + "codeowners": ["@Michsior14"], + "dependencies": [] +} \ No newline at end of file diff --git a/homeassistant/components/kaiterra/sensor.py b/homeassistant/components/kaiterra/sensor.py new file mode 100644 index 00000000000..4ff6435b64d --- /dev/null +++ b/homeassistant/components/kaiterra/sensor.py @@ -0,0 +1,95 @@ +"""Support for Kaiterra Temperature ahn Humidity Sensors.""" +from homeassistant.helpers.entity import Entity + +from homeassistant.helpers.dispatcher import async_dispatcher_connect + +from homeassistant.const import CONF_DEVICE_ID, CONF_NAME, TEMP_CELSIUS, TEMP_FAHRENHEIT + +from .const import DOMAIN, DISPATCHER_KAITERRA + +SENSORS = [ + {"name": "Temperature", "prop": "rtemp", "device_class": "temperature"}, + {"name": "Humidity", "prop": "rhumid", "device_class": "humidity"}, +] + + +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Set up the kaiterra temperature and humidity sensor.""" + if discovery_info is None: + return + + api = hass.data[DOMAIN] + name = discovery_info[CONF_NAME] + device_id = discovery_info[CONF_DEVICE_ID] + + async_add_entities( + [KaiterraSensor(api, name, device_id, sensor) for sensor in SENSORS] + ) + + +class KaiterraSensor(Entity): + """Implementation of a Kaittera sensor.""" + + def __init__(self, api, name, device_id, sensor): + """Initialize the sensor.""" + self._api = api + self._name = f'{name} {sensor["name"]}' + self._device_id = device_id + self._kind = sensor["name"].lower() + self._property = sensor["prop"] + self._device_class = sensor["device_class"] + + @property + def _sensor(self): + """Return the sensor data.""" + return self._api.data.get(self._device_id, {}).get(self._property, {}) + + @property + def should_poll(self): + """Return that the sensor should not be polled.""" + return False + + @property + def available(self): + """Return the availability of the sensor.""" + return self._api.data.get(self._device_id) is not None + + @property + def device_class(self): + """Return the device class.""" + return self._device_class + + @property + def name(self): + """Return the name.""" + return self._name + + @property + def state(self): + """Return the state.""" + return self._sensor.get("value") + + @property + def unique_id(self): + """Return the sensor's unique id.""" + return f"{self._device_id}_{self._kind}" + + @property + def unit_of_measurement(self): + """Return the unit the value is expressed in.""" + if not self._sensor.get("units"): + return None + + value = self._sensor["units"].value + + if value == "F": + return TEMP_FAHRENHEIT + if value == "C": + return TEMP_CELSIUS + return value + + async def async_added_to_hass(self): + """Register callback.""" + async_dispatcher_connect( + self.hass, DISPATCHER_KAITERRA, self.async_write_ha_state + ) diff --git a/homeassistant/components/kankun/manifest.json b/homeassistant/components/kankun/manifest.json index 8e4e9747901..ef6bcbf92e2 100644 --- a/homeassistant/components/kankun/manifest.json +++ b/homeassistant/components/kankun/manifest.json @@ -1,7 +1,7 @@ { "domain": "kankun", "name": "Kankun", - "documentation": "https://www.home-assistant.io/components/kankun", + "documentation": "https://www.home-assistant.io/integrations/kankun", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/keba/manifest.json b/homeassistant/components/keba/manifest.json index 9e959f35c9f..422a79cd0be 100644 --- a/homeassistant/components/keba/manifest.json +++ b/homeassistant/components/keba/manifest.json @@ -1,7 +1,7 @@ { "domain": "keba", "name": "Keba Charging Station", - "documentation": "https://www.home-assistant.io/components/keba", + "documentation": "https://www.home-assistant.io/integrations/keba", "requirements": ["keba-kecontact==0.2.0"], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/keenetic_ndms2/manifest.json b/homeassistant/components/keenetic_ndms2/manifest.json index 42d8d89a021..41e45a9e578 100644 --- a/homeassistant/components/keenetic_ndms2/manifest.json +++ b/homeassistant/components/keenetic_ndms2/manifest.json @@ -1,9 +1,9 @@ { "domain": "keenetic_ndms2", "name": "Keenetic ndms2", - "documentation": "https://www.home-assistant.io/components/keenetic_ndms2", + "documentation": "https://www.home-assistant.io/integrations/keenetic_ndms2", "requirements": [ - "ndms2_client==0.0.8" + "ndms2_client==0.0.9" ], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/keyboard/manifest.json b/homeassistant/components/keyboard/manifest.json index 0e8ade339c2..a3cf7a51ed7 100644 --- a/homeassistant/components/keyboard/manifest.json +++ b/homeassistant/components/keyboard/manifest.json @@ -1,7 +1,7 @@ { "domain": "keyboard", "name": "Keyboard", - "documentation": "https://www.home-assistant.io/components/keyboard", + "documentation": "https://www.home-assistant.io/integrations/keyboard", "requirements": [ "pyuserinput==0.1.11" ], diff --git a/homeassistant/components/keyboard_remote/manifest.json b/homeassistant/components/keyboard_remote/manifest.json index d87d1abca48..6172de132bb 100644 --- a/homeassistant/components/keyboard_remote/manifest.json +++ b/homeassistant/components/keyboard_remote/manifest.json @@ -1,7 +1,7 @@ { "domain": "keyboard_remote", "name": "Keyboard remote", - "documentation": "https://www.home-assistant.io/components/keyboard_remote", + "documentation": "https://www.home-assistant.io/integrations/keyboard_remote", "requirements": [ "evdev==0.6.1" ], diff --git a/homeassistant/components/kira/manifest.json b/homeassistant/components/kira/manifest.json index b7edd1f6c5f..78542bb7b66 100644 --- a/homeassistant/components/kira/manifest.json +++ b/homeassistant/components/kira/manifest.json @@ -1,7 +1,7 @@ { "domain": "kira", "name": "Kira", - "documentation": "https://www.home-assistant.io/components/kira", + "documentation": "https://www.home-assistant.io/integrations/kira", "requirements": [ "pykira==0.1.1" ], diff --git a/homeassistant/components/kiwi/manifest.json b/homeassistant/components/kiwi/manifest.json index 9f1595ebd77..888c6533013 100644 --- a/homeassistant/components/kiwi/manifest.json +++ b/homeassistant/components/kiwi/manifest.json @@ -1,7 +1,7 @@ { "domain": "kiwi", "name": "Kiwi", - "documentation": "https://www.home-assistant.io/components/kiwi", + "documentation": "https://www.home-assistant.io/integrations/kiwi", "requirements": [ "kiwiki-client==0.1.1" ], diff --git a/homeassistant/components/knx/manifest.json b/homeassistant/components/knx/manifest.json index 3b2d1414034..f99ec2f22c0 100644 --- a/homeassistant/components/knx/manifest.json +++ b/homeassistant/components/knx/manifest.json @@ -1,9 +1,9 @@ { "domain": "knx", "name": "Knx", - "documentation": "https://www.home-assistant.io/components/knx", + "documentation": "https://www.home-assistant.io/integrations/knx", "requirements": [ - "xknx==0.11.1" + "xknx==0.11.2" ], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/kodi/manifest.json b/homeassistant/components/kodi/manifest.json index 8c684d495e9..ef138bb2ee2 100644 --- a/homeassistant/components/kodi/manifest.json +++ b/homeassistant/components/kodi/manifest.json @@ -1,7 +1,7 @@ { "domain": "kodi", "name": "Kodi", - "documentation": "https://www.home-assistant.io/components/kodi", + "documentation": "https://www.home-assistant.io/integrations/kodi", "requirements": [ "jsonrpc-async==0.6", "jsonrpc-websocket==0.6" diff --git a/homeassistant/components/konnected/manifest.json b/homeassistant/components/konnected/manifest.json index e4129af39bd..397373499ae 100644 --- a/homeassistant/components/konnected/manifest.json +++ b/homeassistant/components/konnected/manifest.json @@ -1,7 +1,7 @@ { "domain": "konnected", "name": "Konnected", - "documentation": "https://www.home-assistant.io/components/konnected", + "documentation": "https://www.home-assistant.io/integrations/konnected", "requirements": [ "konnected==0.1.5" ], diff --git a/homeassistant/components/kwb/manifest.json b/homeassistant/components/kwb/manifest.json index 783907c0220..f79b0f5b352 100644 --- a/homeassistant/components/kwb/manifest.json +++ b/homeassistant/components/kwb/manifest.json @@ -1,7 +1,7 @@ { "domain": "kwb", "name": "Kwb", - "documentation": "https://www.home-assistant.io/components/kwb", + "documentation": "https://www.home-assistant.io/integrations/kwb", "requirements": [ "pykwb==0.0.8" ], diff --git a/homeassistant/components/lacrosse/manifest.json b/homeassistant/components/lacrosse/manifest.json index 99dd4889213..c30a147342b 100644 --- a/homeassistant/components/lacrosse/manifest.json +++ b/homeassistant/components/lacrosse/manifest.json @@ -1,7 +1,7 @@ { "domain": "lacrosse", "name": "Lacrosse", - "documentation": "https://www.home-assistant.io/components/lacrosse", + "documentation": "https://www.home-assistant.io/integrations/lacrosse", "requirements": [ "pylacrosse==0.4.0" ], diff --git a/homeassistant/components/lametric/manifest.json b/homeassistant/components/lametric/manifest.json index bbf22918a75..72e12e78ba4 100644 --- a/homeassistant/components/lametric/manifest.json +++ b/homeassistant/components/lametric/manifest.json @@ -1,7 +1,7 @@ { "domain": "lametric", "name": "Lametric", - "documentation": "https://www.home-assistant.io/components/lametric", + "documentation": "https://www.home-assistant.io/integrations/lametric", "requirements": [ "lmnotify==0.0.4" ], diff --git a/homeassistant/components/lannouncer/manifest.json b/homeassistant/components/lannouncer/manifest.json index 951dd3ff85b..47bdd1ee0ae 100644 --- a/homeassistant/components/lannouncer/manifest.json +++ b/homeassistant/components/lannouncer/manifest.json @@ -1,7 +1,7 @@ { "domain": "lannouncer", "name": "Lannouncer", - "documentation": "https://www.home-assistant.io/components/lannouncer", + "documentation": "https://www.home-assistant.io/integrations/lannouncer", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/lastfm/manifest.json b/homeassistant/components/lastfm/manifest.json index 2617b3e206b..78ecfd4efb0 100644 --- a/homeassistant/components/lastfm/manifest.json +++ b/homeassistant/components/lastfm/manifest.json @@ -1,7 +1,7 @@ { "domain": "lastfm", "name": "Lastfm", - "documentation": "https://www.home-assistant.io/components/lastfm", + "documentation": "https://www.home-assistant.io/integrations/lastfm", "requirements": [ "pylast==3.1.0" ], diff --git a/homeassistant/components/launch_library/manifest.json b/homeassistant/components/launch_library/manifest.json index bbe9fa8ad05..5bf63f2b099 100644 --- a/homeassistant/components/launch_library/manifest.json +++ b/homeassistant/components/launch_library/manifest.json @@ -1,7 +1,7 @@ { "domain": "launch_library", "name": "Launch library", - "documentation": "https://www.home-assistant.io/components/launch_library", + "documentation": "https://www.home-assistant.io/integrations/launch_library", "requirements": [ "pylaunches==0.2.0" ], diff --git a/homeassistant/components/lcn/const.py b/homeassistant/components/lcn/const.py index ecef388c0d4..c49319abf42 100644 --- a/homeassistant/components/lcn/const.py +++ b/homeassistant/components/lcn/const.py @@ -1,4 +1,3 @@ -# coding: utf-8 """Constants for the LCN component.""" from itertools import product diff --git a/homeassistant/components/lcn/manifest.json b/homeassistant/components/lcn/manifest.json index 5a85b6673f2..dcafe908d5c 100644 --- a/homeassistant/components/lcn/manifest.json +++ b/homeassistant/components/lcn/manifest.json @@ -1,7 +1,7 @@ { "domain": "lcn", "name": "Lcn", - "documentation": "https://www.home-assistant.io/components/lcn", + "documentation": "https://www.home-assistant.io/integrations/lcn", "requirements": [ "pypck==0.6.3" ], diff --git a/homeassistant/components/lg_netcast/manifest.json b/homeassistant/components/lg_netcast/manifest.json index 1728aa50614..3f3d9d85c52 100644 --- a/homeassistant/components/lg_netcast/manifest.json +++ b/homeassistant/components/lg_netcast/manifest.json @@ -1,7 +1,7 @@ { "domain": "lg_netcast", "name": "Lg netcast", - "documentation": "https://www.home-assistant.io/components/lg_netcast", + "documentation": "https://www.home-assistant.io/integrations/lg_netcast", "requirements": [ "pylgnetcast-homeassistant==0.2.0.dev0" ], diff --git a/homeassistant/components/lg_soundbar/manifest.json b/homeassistant/components/lg_soundbar/manifest.json index b09c8809382..603f755fac1 100644 --- a/homeassistant/components/lg_soundbar/manifest.json +++ b/homeassistant/components/lg_soundbar/manifest.json @@ -1,7 +1,7 @@ { "domain": "lg_soundbar", "name": "Lg soundbar", - "documentation": "https://www.home-assistant.io/components/lg_soundbar", + "documentation": "https://www.home-assistant.io/integrations/lg_soundbar", "requirements": [ "temescal==0.1" ], diff --git a/homeassistant/components/life360/.translations/bg.json b/homeassistant/components/life360/.translations/bg.json index 4fae0249fd0..02354204f24 100644 --- a/homeassistant/components/life360/.translations/bg.json +++ b/homeassistant/components/life360/.translations/bg.json @@ -10,6 +10,7 @@ "error": { "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", "invalid_username": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u043e \u043f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435", + "unexpected": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430 \u043f\u0440\u0438 \u043a\u043e\u043c\u0443\u043d\u0438\u043a\u0430\u0446\u0438\u044f \u0441\u044a\u0441 \u0441\u044a\u0440\u0432\u044a\u0440\u0430 Life360", "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" }, "step": { diff --git a/homeassistant/components/life360/.translations/es-419.json b/homeassistant/components/life360/.translations/es-419.json index 3f9bfab3304..512d0285ac5 100644 --- a/homeassistant/components/life360/.translations/es-419.json +++ b/homeassistant/components/life360/.translations/es-419.json @@ -1,5 +1,23 @@ { "config": { + "abort": { + "user_already_configured": "La cuenta ya ha sido configurada" + }, + "error": { + "invalid_credentials": "Credenciales no v\u00e1lidas", + "invalid_username": "Nombre de usuario inv\u00e1lido", + "unexpected": "Error inesperado al comunicarse con el servidor Life360", + "user_already_configured": "La cuenta ya ha sido configurada" + }, + "step": { + "user": { + "data": { + "password": "Contrase\u00f1a", + "username": "Nombre de usuario" + }, + "title": "Informaci\u00f3n de la cuenta Life360" + } + }, "title": "Life360" } } \ No newline at end of file diff --git a/homeassistant/components/life360/.translations/lb.json b/homeassistant/components/life360/.translations/lb.json index bfed5937e24..3af9ab00728 100644 --- a/homeassistant/components/life360/.translations/lb.json +++ b/homeassistant/components/life360/.translations/lb.json @@ -10,6 +10,7 @@ "error": { "invalid_credentials": "Ong\u00eblteg Login Informatioune", "invalid_username": "Ong\u00ebltege Benotzernumm", + "unexpected": "Onerwaarte Feeler bei der Kommunikatioun mam Life360 Server", "user_already_configured": "Kont ass scho konfigur\u00e9iert" }, "step": { diff --git a/homeassistant/components/life360/.translations/nn.json b/homeassistant/components/life360/.translations/nn.json new file mode 100644 index 00000000000..98345b022f2 --- /dev/null +++ b/homeassistant/components/life360/.translations/nn.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "username": "Brukarnamn" + } + } + }, + "title": "Life360" + } +} \ No newline at end of file diff --git a/homeassistant/components/life360/.translations/pt-BR.json b/homeassistant/components/life360/.translations/pt-BR.json index ca4cee896b3..5181c37969a 100644 --- a/homeassistant/components/life360/.translations/pt-BR.json +++ b/homeassistant/components/life360/.translations/pt-BR.json @@ -10,6 +10,7 @@ "error": { "invalid_credentials": "Credenciais inv\u00e1lidas", "invalid_username": "Nome de usu\u00e1rio Inv\u00e1lido", + "unexpected": "Erro inesperado na comunica\u00e7\u00e3o com o servidor Life360", "user_already_configured": "A conta j\u00e1 foi configurada" }, "step": { diff --git a/homeassistant/components/life360/.translations/ru.json b/homeassistant/components/life360/.translations/ru.json index 1e962142373..d033da4bae7 100644 --- a/homeassistant/components/life360/.translations/ru.json +++ b/homeassistant/components/life360/.translations/ru.json @@ -2,7 +2,7 @@ "config": { "abort": { "invalid_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435", - "user_already_configured": "\u0423\u0447\u0435\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430" + "user_already_configured": "\u0423\u0447\u0435\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430." }, "create_entry": { "default": "\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 \u0440\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u043d\u043e\u0439 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438." @@ -11,7 +11,7 @@ "invalid_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435", "invalid_username": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043b\u043e\u0433\u0438\u043d", "unexpected": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430 \u0441\u0432\u044f\u0437\u0438 \u0441 \u0441\u0435\u0440\u0432\u0435\u0440\u043e\u043c Life360", - "user_already_configured": "\u0423\u0447\u0435\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430" + "user_already_configured": "\u0423\u0447\u0435\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430." }, "step": { "user": { diff --git a/homeassistant/components/life360/config_flow.py b/homeassistant/components/life360/config_flow.py index 6af999a575d..e0ea645b197 100644 --- a/homeassistant/components/life360/config_flow.py +++ b/homeassistant/components/life360/config_flow.py @@ -13,7 +13,7 @@ from .helpers import get_api _LOGGER = logging.getLogger(__name__) -DOCS_URL = "https://www.home-assistant.io/components/life360" +DOCS_URL = "https://www.home-assistant.io/integrations/life360" @config_entries.HANDLERS.register(DOMAIN) diff --git a/homeassistant/components/life360/manifest.json b/homeassistant/components/life360/manifest.json index 9eae371070a..a890ec39375 100644 --- a/homeassistant/components/life360/manifest.json +++ b/homeassistant/components/life360/manifest.json @@ -2,7 +2,7 @@ "domain": "life360", "name": "Life360", "config_flow": true, - "documentation": "https://www.home-assistant.io/components/life360", + "documentation": "https://www.home-assistant.io/integrations/life360", "dependencies": [], "codeowners": [ "@pnbruckner" diff --git a/homeassistant/components/lifx/.translations/nn.json b/homeassistant/components/lifx/.translations/nn.json new file mode 100644 index 00000000000..c78905b09c8 --- /dev/null +++ b/homeassistant/components/lifx/.translations/nn.json @@ -0,0 +1,10 @@ +{ + "config": { + "step": { + "confirm": { + "title": "LIFX" + } + }, + "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 63080a30ff1..ae32d43f1b6 100644 --- a/homeassistant/components/lifx/.translations/no.json +++ b/homeassistant/components/lifx/.translations/no.json @@ -2,7 +2,7 @@ "config": { "abort": { "no_devices_found": "Ingen LIFX-enheter funnet p\u00e5 nettverket.", - "single_instance_allowed": "Kun en enkelt konfigurasjon av LIFX er mulig." + "single_instance_allowed": "Kun en konfigurasjon av LIFX er mulig." }, "step": { "confirm": { diff --git a/homeassistant/components/lifx/.translations/zh-Hant.json b/homeassistant/components/lifx/.translations/zh-Hant.json index 4c66f0d0133..908394bcfd8 100644 --- a/homeassistant/components/lifx/.translations/zh-Hant.json +++ b/homeassistant/components/lifx/.translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "no_devices_found": "\u5728\u7db2\u8def\u4e0a\u627e\u4e0d\u5230 LIFX \u88dd\u7f6e\u3002", + "no_devices_found": "\u5728\u7db2\u8def\u4e0a\u627e\u4e0d\u5230 LIFX \u8a2d\u5099\u3002", "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44 LIFX\u3002" }, "step": { diff --git a/homeassistant/components/lifx/light.py b/homeassistant/components/lifx/light.py index ed26db3d49e..d183dcb0fa2 100644 --- a/homeassistant/components/lifx/light.py +++ b/homeassistant/components/lifx/light.py @@ -33,7 +33,7 @@ from homeassistant.components.light import ( Light, preprocess_turn_on_alternatives, ) -from homeassistant.const import ATTR_ENTITY_ID, EVENT_HOMEASSISTANT_STOP +from homeassistant.const import ATTR_ENTITY_ID, ATTR_MODE, EVENT_HOMEASSISTANT_STOP from homeassistant.core import callback import homeassistant.helpers.config_validation as cv import homeassistant.helpers.device_registry as dr @@ -77,7 +77,6 @@ SERVICE_EFFECT_COLORLOOP = "lifx_effect_colorloop" SERVICE_EFFECT_STOP = "lifx_effect_stop" ATTR_POWER_ON = "power_on" -ATTR_MODE = "mode" ATTR_PERIOD = "period" ATTR_CYCLES = "cycles" ATTR_SPREAD = "spread" diff --git a/homeassistant/components/lifx/manifest.json b/homeassistant/components/lifx/manifest.json index fd74d9831fc..a8c2755aefb 100644 --- a/homeassistant/components/lifx/manifest.json +++ b/homeassistant/components/lifx/manifest.json @@ -2,7 +2,7 @@ "domain": "lifx", "name": "Lifx", "config_flow": true, - "documentation": "https://www.home-assistant.io/components/lifx", + "documentation": "https://www.home-assistant.io/integrations/lifx", "requirements": [ "aiolifx==0.6.7", "aiolifx_effects==0.2.2" @@ -13,7 +13,5 @@ ] }, "dependencies": [], - "codeowners": [ - "@amelchio" - ] + "codeowners": [] } diff --git a/homeassistant/components/lifx_cloud/manifest.json b/homeassistant/components/lifx_cloud/manifest.json index c2834fbc788..f6aa8ccb7c5 100644 --- a/homeassistant/components/lifx_cloud/manifest.json +++ b/homeassistant/components/lifx_cloud/manifest.json @@ -1,10 +1,8 @@ { "domain": "lifx_cloud", "name": "Lifx cloud", - "documentation": "https://www.home-assistant.io/components/lifx_cloud", + "documentation": "https://www.home-assistant.io/integrations/lifx_cloud", "requirements": [], "dependencies": [], - "codeowners": [ - "@amelchio" - ] + "codeowners": [] } diff --git a/homeassistant/components/lifx_legacy/manifest.json b/homeassistant/components/lifx_legacy/manifest.json index 4ff59ac1770..de5f5ff04de 100644 --- a/homeassistant/components/lifx_legacy/manifest.json +++ b/homeassistant/components/lifx_legacy/manifest.json @@ -1,12 +1,10 @@ { "domain": "lifx_legacy", "name": "Lifx legacy", - "documentation": "https://www.home-assistant.io/components/lifx_legacy", + "documentation": "https://www.home-assistant.io/integrations/lifx_legacy", "requirements": [ "liffylights==0.9.4" ], "dependencies": [], - "codeowners": [ - "@amelchio" - ] + "codeowners": [] } diff --git a/homeassistant/components/light/.translations/bg.json b/homeassistant/components/light/.translations/bg.json index 533ba76b6a7..33b57d9e7cd 100644 --- a/homeassistant/components/light/.translations/bg.json +++ b/homeassistant/components/light/.translations/bg.json @@ -8,6 +8,10 @@ "condition_type": { "is_off": "{entity_name} \u0435 \u0438\u0437\u043a\u043b.", "is_on": "{entity_name} \u0435 \u0432\u043a\u043b." + }, + "trigger_type": { + "turned_off": "{entity_name} \u0435 \u0438\u0437\u043a\u043b\u044e\u0447\u0435\u043d", + "turned_on": "{entity_name} \u0435 \u0432\u043a\u043b\u044e\u0447\u0435\u043d" } } } \ No newline at end of file diff --git a/homeassistant/components/light/.translations/ca.json b/homeassistant/components/light/.translations/ca.json index c9b727088ab..b8b3bbb8125 100644 --- a/homeassistant/components/light/.translations/ca.json +++ b/homeassistant/components/light/.translations/ca.json @@ -1,17 +1,17 @@ { "device_automation": { "action_type": { - "toggle": "Commuta {name}", - "turn_off": "Apaga {name}", - "turn_on": "Enc\u00e9n {name}" + "toggle": "Commuta {entity_name}", + "turn_off": "Apaga {entity_name}", + "turn_on": "Enc\u00e9n {entity_name}" }, "condition_type": { - "is_off": "{name} est\u00e0 apagat", - "is_on": "{name} est\u00e0 enc\u00e8s" + "is_off": "{entity_name} est\u00e0 apagat", + "is_on": "{entity_name} est\u00e0 enc\u00e8s" }, "trigger_type": { - "turned_off": "{name} apagat", - "turned_on": "{name} enc\u00e8s" + "turned_off": "{entity_name} apagat", + "turned_on": "{entity_name} enc\u00e8s" } } } \ No newline at end of file diff --git a/homeassistant/components/light/.translations/da.json b/homeassistant/components/light/.translations/da.json index 4ea4a94014e..14a747f6eff 100644 --- a/homeassistant/components/light/.translations/da.json +++ b/homeassistant/components/light/.translations/da.json @@ -1,8 +1,8 @@ { "device_automation": { "trigger_type": { - "turned_off": "{name} slukket", - "turned_on": "{name} t\u00e6ndt" + "turned_off": "{entity_name} slukket", + "turned_on": "{entity_name} t\u00e6ndt" } } } \ No newline at end of file diff --git a/homeassistant/components/light/.translations/de.json b/homeassistant/components/light/.translations/de.json index 2fe1c6b42dc..be8966d9556 100644 --- a/homeassistant/components/light/.translations/de.json +++ b/homeassistant/components/light/.translations/de.json @@ -1,8 +1,17 @@ { "device_automation": { + "action_type": { + "toggle": "Schalte {entity_name} um.", + "turn_off": "Schalte {entity_name} aus.", + "turn_on": "Schalte {entity_name} ein." + }, + "condition_type": { + "is_off": "{entity_name} ausgeschaltet", + "is_on": "{entity_name} ist eingeschaltet" + }, "trigger_type": { - "turned_off": "{name} ausgeschaltet", - "turned_on": "{name} eingeschaltet" + "turned_off": "{entity_name} ausgeschaltet", + "turned_on": "{entity_name} eingeschaltet" } } } \ No newline at end of file diff --git a/homeassistant/components/light/.translations/es-419.json b/homeassistant/components/light/.translations/es-419.json new file mode 100644 index 00000000000..b63f0d44452 --- /dev/null +++ b/homeassistant/components/light/.translations/es-419.json @@ -0,0 +1,12 @@ +{ + "device_automation": { + "condition_type": { + "is_off": "{entity_name} est\u00e1 apagada", + "is_on": "{entity_name} est\u00e1 encendida" + }, + "trigger_type": { + "turned_off": "{entity_name} desactivada", + "turned_on": "{entity_name} activada" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/light/.translations/hu.json b/homeassistant/components/light/.translations/hu.json new file mode 100644 index 00000000000..7d7e158f3cb --- /dev/null +++ b/homeassistant/components/light/.translations/hu.json @@ -0,0 +1,17 @@ +{ + "device_automation": { + "action_type": { + "toggle": "{entity_name} fel/lekapcsol\u00e1sa", + "turn_off": "{entity_name} lekapcsol\u00e1sa", + "turn_on": "{entity_name} felkapcsol\u00e1sa" + }, + "condition_type": { + "is_off": "{entity_name} le van kapcsolva", + "is_on": "{entity_name} fel van kapcsolva" + }, + "trigger_type": { + "turned_off": "{entity_name} le lett kapcsolva", + "turned_on": "{entity_name} fel lett kapcsolva" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/light/.translations/nl.json b/homeassistant/components/light/.translations/nl.json index 63954ca83a9..e742a337cca 100644 --- a/homeassistant/components/light/.translations/nl.json +++ b/homeassistant/components/light/.translations/nl.json @@ -1,17 +1,17 @@ { "device_automation": { "action_type": { - "toggle": "Omschakelen {naam}", + "toggle": "Omschakelen {entity_name}", "turn_off": "{entity_name} uitschakelen", "turn_on": "{entity_name} inschakelen" }, "condition_type": { - "is_off": "{name} is uitgeschakeld", - "is_on": "{name} is ingeschakeld" + "is_off": "{entity_name} is uitgeschakeld", + "is_on": "{entity_name} is ingeschakeld" }, "trigger_type": { - "turned_off": "{name} is uitgeschakeld", - "turned_on": "{name} is ingeschakeld" + "turned_off": "{entity_name} is uitgeschakeld", + "turned_on": "{entity_name} is ingeschakeld" } } } \ No newline at end of file diff --git a/homeassistant/components/light/.translations/no.json b/homeassistant/components/light/.translations/no.json index 008123739d9..785e9ca2912 100644 --- a/homeassistant/components/light/.translations/no.json +++ b/homeassistant/components/light/.translations/no.json @@ -10,8 +10,8 @@ "is_on": "{entity_name} er p\u00e5" }, "trigger_type": { - "turned_off": "{name} sl\u00e5tt av", - "turned_on": "{name} sl\u00e5tt p\u00e5" + "turned_off": "{entity_name} sl\u00e5tt av", + "turned_on": "{entity_name} sl\u00e5tt p\u00e5" } } } \ No newline at end of file diff --git a/homeassistant/components/light/.translations/pl.json b/homeassistant/components/light/.translations/pl.json index 22a93909578..33a38fc930e 100644 --- a/homeassistant/components/light/.translations/pl.json +++ b/homeassistant/components/light/.translations/pl.json @@ -1,17 +1,17 @@ { "device_automation": { "action_type": { - "toggle": "Prze\u0142\u0105cz {entity_name}", - "turn_off": "Wy\u0142\u0105cz {entity_name}", - "turn_on": "W\u0142\u0105cz {entity_name}" + "toggle": "prze\u0142\u0105cz {entity_name}", + "turn_off": "wy\u0142\u0105cz {entity_name}", + "turn_on": "w\u0142\u0105cz {entity_name}" }, "condition_type": { - "is_off": "(entity_name} jest wy\u0142\u0105czony.", - "is_on": "(entity_name} jest w\u0142\u0105czony." + "is_off": "\u015bwiat\u0142o {entity_name} jest wy\u0142\u0105czone", + "is_on": "\u015bwiat\u0142o {entity_name} jest w\u0142\u0105czone" }, "trigger_type": { - "turned_off": "{nazwa} wy\u0142\u0105czone", - "turned_on": "{name} w\u0142\u0105czone" + "turned_off": "wy\u0142\u0105czenie {entity_name}", + "turned_on": "w\u0142\u0105czenie {entity_name}" } } } \ No newline at end of file diff --git a/homeassistant/components/light/.translations/pt-BR.json b/homeassistant/components/light/.translations/pt-BR.json new file mode 100644 index 00000000000..05414b1e03c --- /dev/null +++ b/homeassistant/components/light/.translations/pt-BR.json @@ -0,0 +1,13 @@ +{ + "device_automation": { + "action_type": { + "toggle": "Alternar {entity_name}", + "turn_off": "Desligar {entity_name}", + "turn_on": "Ligar {entity_name}" + }, + "condition_type": { + "is_off": "{entity_name} est\u00e1 desligado", + "is_on": "{entity_name} est\u00e1 ligado" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/light/.translations/ru.json b/homeassistant/components/light/.translations/ru.json index ba9339c1a94..a6a7994b7c3 100644 --- a/homeassistant/components/light/.translations/ru.json +++ b/homeassistant/components/light/.translations/ru.json @@ -10,8 +10,8 @@ "is_on": "{entity_name} \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u043e" }, "trigger_type": { - "turned_off": "{entity_name} \u0432\u044b\u043a\u043b\u044e\u0447\u0435\u043d\u043e", - "turned_on": "{entity_name} \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u043e" + "turned_off": "{entity_name} \u0432\u044b\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435", + "turned_on": "{entity_name} \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435" } } } \ No newline at end of file diff --git a/homeassistant/components/light/device_action.py b/homeassistant/components/light/device_action.py new file mode 100644 index 00000000000..9d8ef6bceaf --- /dev/null +++ b/homeassistant/components/light/device_action.py @@ -0,0 +1,29 @@ +"""Provides device actions for lights.""" +from typing import List +import voluptuous as vol + +from homeassistant.core import HomeAssistant, Context +from homeassistant.components.device_automation import toggle_entity +from homeassistant.const import CONF_DOMAIN +from homeassistant.helpers.typing import TemplateVarsType, ConfigType +from . import DOMAIN + + +ACTION_SCHEMA = toggle_entity.ACTION_SCHEMA.extend({vol.Required(CONF_DOMAIN): DOMAIN}) + + +async def async_call_action_from_config( + hass: HomeAssistant, + config: ConfigType, + variables: TemplateVarsType, + context: Context, +) -> None: + """Change state based on configuration.""" + await toggle_entity.async_call_action_from_config( + hass, config, variables, context, DOMAIN + ) + + +async def async_get_actions(hass: HomeAssistant, device_id: str) -> List[dict]: + """List device actions.""" + return await toggle_entity.async_get_actions(hass, device_id, DOMAIN) diff --git a/homeassistant/components/light/device_automation.py b/homeassistant/components/light/device_automation.py deleted file mode 100644 index 61292d47449..00000000000 --- a/homeassistant/components/light/device_automation.py +++ /dev/null @@ -1,56 +0,0 @@ -"""Provides device automations for lights.""" -import voluptuous as vol - -from homeassistant.components.device_automation import toggle_entity -from homeassistant.const import CONF_DOMAIN -from . import DOMAIN - - -# mypy: allow-untyped-defs, no-check-untyped-defs - -ACTION_SCHEMA = toggle_entity.ACTION_SCHEMA.extend({vol.Required(CONF_DOMAIN): DOMAIN}) - -CONDITION_SCHEMA = toggle_entity.CONDITION_SCHEMA.extend( - {vol.Required(CONF_DOMAIN): DOMAIN} -) - -TRIGGER_SCHEMA = toggle_entity.TRIGGER_SCHEMA.extend( - {vol.Required(CONF_DOMAIN): DOMAIN} -) - - -async def async_call_action_from_config(hass, config, variables, context): - """Change state based on configuration.""" - config = ACTION_SCHEMA(config) - await toggle_entity.async_call_action_from_config( - hass, config, variables, context, DOMAIN - ) - - -def async_condition_from_config(config, config_validation): - """Evaluate state based on configuration.""" - config = CONDITION_SCHEMA(config) - return toggle_entity.async_condition_from_config(config, config_validation) - - -async def async_trigger(hass, config, action, automation_info): - """Listen for state changes based on configuration.""" - config = TRIGGER_SCHEMA(config) - return await toggle_entity.async_attach_trigger( - hass, config, action, automation_info - ) - - -async def async_get_actions(hass, device_id): - """List device actions.""" - return await toggle_entity.async_get_actions(hass, device_id, DOMAIN) - - -async def async_get_conditions(hass, device_id): - """List device conditions.""" - return await toggle_entity.async_get_conditions(hass, device_id, DOMAIN) - - -async def async_get_triggers(hass, device_id): - """List device triggers.""" - return await toggle_entity.async_get_triggers(hass, device_id, DOMAIN) diff --git a/homeassistant/components/light/device_condition.py b/homeassistant/components/light/device_condition.py new file mode 100644 index 00000000000..4abf34e6661 --- /dev/null +++ b/homeassistant/components/light/device_condition.py @@ -0,0 +1,29 @@ +"""Provides device conditions for lights.""" +from typing import List +import voluptuous as vol + +from homeassistant.core import HomeAssistant +from homeassistant.components.device_automation import toggle_entity +from homeassistant.const import CONF_DOMAIN +from homeassistant.helpers.typing import ConfigType +from homeassistant.helpers.condition import ConditionCheckerType +from . import DOMAIN + + +CONDITION_SCHEMA = toggle_entity.CONDITION_SCHEMA.extend( + {vol.Required(CONF_DOMAIN): DOMAIN} +) + + +def async_condition_from_config( + config: ConfigType, config_validation: bool +) -> ConditionCheckerType: + """Evaluate state based on configuration.""" + if config_validation: + config = CONDITION_SCHEMA(config) + return toggle_entity.async_condition_from_config(config, config_validation) + + +async def async_get_conditions(hass: HomeAssistant, device_id: str) -> List[dict]: + """List device conditions.""" + return await toggle_entity.async_get_conditions(hass, device_id, DOMAIN) diff --git a/homeassistant/components/light/device_trigger.py b/homeassistant/components/light/device_trigger.py new file mode 100644 index 00000000000..5bd5d83e1c0 --- /dev/null +++ b/homeassistant/components/light/device_trigger.py @@ -0,0 +1,37 @@ +"""Provides device trigger for lights.""" +from typing import List +import voluptuous as vol + +from homeassistant.core import HomeAssistant, CALLBACK_TYPE +from homeassistant.components.automation import AutomationActionType +from homeassistant.components.device_automation import toggle_entity +from homeassistant.const import CONF_DOMAIN +from homeassistant.helpers.typing import ConfigType +from . import DOMAIN + + +TRIGGER_SCHEMA = toggle_entity.TRIGGER_SCHEMA.extend( + {vol.Required(CONF_DOMAIN): DOMAIN} +) + + +async def async_attach_trigger( + hass: HomeAssistant, + config: ConfigType, + action: AutomationActionType, + automation_info: dict, +) -> CALLBACK_TYPE: + """Listen for state changes based on configuration.""" + return await toggle_entity.async_attach_trigger( + hass, config, action, automation_info + ) + + +async def async_get_triggers(hass: HomeAssistant, device_id: str) -> List[dict]: + """List device triggers.""" + return await toggle_entity.async_get_triggers(hass, device_id, DOMAIN) + + +async def async_get_trigger_capabilities(hass: HomeAssistant, trigger: dict) -> dict: + """List trigger capabilities.""" + return await toggle_entity.async_get_trigger_capabilities(hass, trigger) diff --git a/homeassistant/components/light/manifest.json b/homeassistant/components/light/manifest.json index 62eb96967f5..88e7585f802 100644 --- a/homeassistant/components/light/manifest.json +++ b/homeassistant/components/light/manifest.json @@ -1,7 +1,7 @@ { "domain": "light", "name": "Light", - "documentation": "https://www.home-assistant.io/components/light", + "documentation": "https://www.home-assistant.io/integrations/light", "requirements": [], "dependencies": [ "group" diff --git a/homeassistant/components/lightwave/manifest.json b/homeassistant/components/lightwave/manifest.json index a26500f69a6..4b2456f0df5 100644 --- a/homeassistant/components/lightwave/manifest.json +++ b/homeassistant/components/lightwave/manifest.json @@ -1,7 +1,7 @@ { "domain": "lightwave", "name": "Lightwave", - "documentation": "https://www.home-assistant.io/components/lightwave", + "documentation": "https://www.home-assistant.io/integrations/lightwave", "requirements": [ "lightwave==0.15" ], diff --git a/homeassistant/components/limitlessled/manifest.json b/homeassistant/components/limitlessled/manifest.json index f8b42fabcbe..5eff655e806 100644 --- a/homeassistant/components/limitlessled/manifest.json +++ b/homeassistant/components/limitlessled/manifest.json @@ -1,7 +1,7 @@ { "domain": "limitlessled", "name": "Limitlessled", - "documentation": "https://www.home-assistant.io/components/limitlessled", + "documentation": "https://www.home-assistant.io/integrations/limitlessled", "requirements": [ "limitlessled==1.1.3" ], diff --git a/homeassistant/components/linksys_ap/__init__.py b/homeassistant/components/linksys_ap/__init__.py deleted file mode 100644 index 5898aa36e98..00000000000 --- a/homeassistant/components/linksys_ap/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""The linksys_ap component.""" diff --git a/homeassistant/components/linksys_ap/device_tracker.py b/homeassistant/components/linksys_ap/device_tracker.py deleted file mode 100644 index d40de718f90..00000000000 --- a/homeassistant/components/linksys_ap/device_tracker.py +++ /dev/null @@ -1,107 +0,0 @@ -"""Support for Linksys Access Points.""" -import base64 -import logging - -import requests -import voluptuous as vol - -import homeassistant.helpers.config_validation as cv -from homeassistant.components.device_tracker import ( - DOMAIN, - PLATFORM_SCHEMA, - DeviceScanner, -) -from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME, CONF_VERIFY_SSL - -INTERFACES = 2 -DEFAULT_TIMEOUT = 10 - -_LOGGER = logging.getLogger(__name__) - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_HOST): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Required(CONF_USERNAME): cv.string, - vol.Optional(CONF_VERIFY_SSL, default=True): cv.boolean, - } -) - - -def get_scanner(hass, config): - """Validate the configuration and return a Linksys AP scanner.""" - _LOGGER.warning( - "The linksys_ap integration is deprecated and will be removed " - "in Home Assistant 0.100.0. For more information see ADR-0004:" - "https://github.com/home-assistant/architecture/blob/master/adr/0004-webscraping.md" - ) - - try: - return LinksysAPDeviceScanner(config[DOMAIN]) - except ConnectionError: - return None - - -class LinksysAPDeviceScanner(DeviceScanner): - """This class queries a Linksys Access Point.""" - - def __init__(self, config): - """Initialize the scanner.""" - self.host = config[CONF_HOST] - self.username = config[CONF_USERNAME] - self.password = config[CONF_PASSWORD] - self.verify_ssl = config[CONF_VERIFY_SSL] - self.last_results = [] - - # Check if the access point is accessible - response = self._make_request() - if not response.status_code == 200: - raise ConnectionError("Cannot connect to Linksys Access Point") - - def scan_devices(self): - """Scan for new devices and return a list with found device IDs.""" - self._update_info() - - return self.last_results - - def get_device_name(self, device): - """ - Return the name (if known) of the device. - - Linksys does not provide an API to get a name for a device, - so we just return None - """ - return None - - def _update_info(self): - """Check for connected devices.""" - from bs4 import BeautifulSoup as BS - - _LOGGER.info("Checking Linksys AP") - - self.last_results = [] - for interface in range(INTERFACES): - request = self._make_request(interface) - self.last_results.extend( - [ - x.find_all("td")[1].text - for x in BS(request.content, "html.parser").find_all( - class_="section-row" - ) - ] - ) - - return True - - def _make_request(self, unit=0): - """Create a request to get the data.""" - # No, the '&&' is not a typo - this is expected by the web interface. - login = base64.b64encode(bytes(self.username, "utf8")).decode("ascii") - pwd = base64.b64encode(bytes(self.password, "utf8")).decode("ascii") - url = "https://{}/StatusClients.htm&&unit={}&vap=0".format(self.host, unit) - return requests.get( - url, - timeout=DEFAULT_TIMEOUT, - verify=self.verify_ssl, - cookies={"LoginName": login, "LoginPWD": pwd}, - ) diff --git a/homeassistant/components/linksys_ap/manifest.json b/homeassistant/components/linksys_ap/manifest.json deleted file mode 100644 index 31fafe17edd..00000000000 --- a/homeassistant/components/linksys_ap/manifest.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "domain": "linksys_ap", - "name": "Linksys ap", - "documentation": "https://www.home-assistant.io/components/linksys_ap", - "requirements": [ - "beautifulsoup4==4.8.0" - ], - "dependencies": [], - "codeowners": [] -} diff --git a/homeassistant/components/linksys_smart/manifest.json b/homeassistant/components/linksys_smart/manifest.json index 19bb079c29c..28ed3f52036 100644 --- a/homeassistant/components/linksys_smart/manifest.json +++ b/homeassistant/components/linksys_smart/manifest.json @@ -1,7 +1,7 @@ { "domain": "linksys_smart", "name": "Linksys smart", - "documentation": "https://www.home-assistant.io/components/linksys_smart", + "documentation": "https://www.home-assistant.io/integrations/linksys_smart", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/linky/.translations/es-419.json b/homeassistant/components/linky/.translations/es-419.json new file mode 100644 index 00000000000..130a856826e --- /dev/null +++ b/homeassistant/components/linky/.translations/es-419.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "username_exists": "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.)", + "unknown": "Error desconocido: por favor, vuelva a intentarlo m\u00e1s tarde (normalmente no entre las 11 p.m. y las 2 a.m.)", + "username_exists": "La cuenta ya ha sido configurada", + "wrong_login": "Error de inicio de sesi\u00f3n: por favor revise su direcci\u00f3n de correo electr\u00f3nico y contrase\u00f1a" + }, + "step": { + "user": { + "data": { + "password": "Contrase\u00f1a", + "username": "Correo electr\u00f3nico" + }, + "description": "Ingrese sus credenciales", + "title": "Linky" + } + }, + "title": "Linky" + } +} \ No newline at end of file diff --git a/homeassistant/components/linky/.translations/nn.json b/homeassistant/components/linky/.translations/nn.json new file mode 100644 index 00000000000..6e084d1a9d2 --- /dev/null +++ b/homeassistant/components/linky/.translations/nn.json @@ -0,0 +1,10 @@ +{ + "config": { + "step": { + "user": { + "title": "Linky" + } + }, + "title": "Linky" + } +} \ No newline at end of file diff --git a/homeassistant/components/linky/.translations/pt-BR.json b/homeassistant/components/linky/.translations/pt-BR.json new file mode 100644 index 00000000000..23f519353b4 --- /dev/null +++ b/homeassistant/components/linky/.translations/pt-BR.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "username_exists": "Conta j\u00e1 configurada", + "wrong_login": "Erro de Login: por favor, verifique seu e-mail e senha" + }, + "step": { + "user": { + "data": { + "password": "Senha", + "username": "E-mail" + }, + "description": "Insira suas credenciais", + "title": "Linky" + } + }, + "title": "Linky" + } +} \ No newline at end of file diff --git a/homeassistant/components/linky/.translations/ru.json b/homeassistant/components/linky/.translations/ru.json index 498b5b2f12f..b569cce9239 100644 --- a/homeassistant/components/linky/.translations/ru.json +++ b/homeassistant/components/linky/.translations/ru.json @@ -1,13 +1,13 @@ { "config": { "abort": { - "username_exists": "\u0423\u0447\u0435\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430" + "username_exists": "\u0423\u0447\u0435\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430." }, "error": { "access": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u0434\u043e\u0441\u0442\u0443\u043f \u043a Enedis.fr, \u043f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a \u0418\u043d\u0442\u0435\u0440\u043d\u0435\u0442\u0443", "enedis": "Enedis.fr \u043e\u0442\u043f\u0440\u0430\u0432\u0438\u043b \u043e\u0442\u0432\u0435\u0442 \u0441 \u043e\u0448\u0438\u0431\u043a\u043e\u0439: \u043f\u043e\u0432\u0442\u043e\u0440\u0438\u0442\u0435 \u043f\u043e\u043f\u044b\u0442\u043a\u0443 \u043f\u043e\u0437\u0436\u0435 (\u043d\u0435 \u0432 \u043f\u0440\u043e\u043c\u0435\u0436\u0443\u0442\u043a\u0435 \u0441 23:00 \u043f\u043e 2:00)", "unknown": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430: \u043f\u043e\u0432\u0442\u043e\u0440\u0438\u0442\u0435 \u043f\u043e\u043f\u044b\u0442\u043a\u0443 \u043f\u043e\u0437\u0436\u0435 (\u043d\u0435 \u0432 \u043f\u0440\u043e\u043c\u0435\u0436\u0443\u0442\u043a\u0435 \u0441 23:00 \u043f\u043e 2:00)", - "username_exists": "\u0423\u0447\u0435\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430", + "username_exists": "\u0423\u0447\u0435\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430.", "wrong_login": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0432\u0445\u043e\u0434\u0430: \u043f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u0430\u0434\u0440\u0435\u0441 \u044d\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0439 \u043f\u043e\u0447\u0442\u044b \u0438 \u043f\u0430\u0440\u043e\u043b\u044c" }, "step": { diff --git a/homeassistant/components/linky/.translations/zh-Hans.json b/homeassistant/components/linky/.translations/zh-Hans.json new file mode 100644 index 00000000000..b450a3cbdb0 --- /dev/null +++ b/homeassistant/components/linky/.translations/zh-Hans.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "username_exists": "\u8d26\u6237\u5df2\u914d\u7f6e\u5b8c\u6210" + }, + "step": { + "user": { + "data": { + "password": "\u5bc6\u7801", + "username": "\u7535\u5b50\u90ae\u7bb1" + }, + "description": "\u8f93\u5165\u60a8\u7684\u8eab\u4efd\u8ba4\u8bc1" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/linky/manifest.json b/homeassistant/components/linky/manifest.json index 10a5bbcf864..a2505427f45 100644 --- a/homeassistant/components/linky/manifest.json +++ b/homeassistant/components/linky/manifest.json @@ -2,7 +2,7 @@ "domain": "linky", "name": "Linky", "config_flow": true, - "documentation": "https://www.home-assistant.io/components/linky", + "documentation": "https://www.home-assistant.io/integrations/linky", "requirements": [ "pylinky==0.4.0" ], diff --git a/homeassistant/components/linky/sensor.py b/homeassistant/components/linky/sensor.py index 5ff04c5ee70..489e66c2b12 100644 --- a/homeassistant/components/linky/sensor.py +++ b/homeassistant/components/linky/sensor.py @@ -145,8 +145,8 @@ class LinkySensor(Entity): def device_info(self): """Return device information.""" return { - "identifiers": {(DOMAIN, self.unique_id)}, - "name": self.name, + "identifiers": {(DOMAIN, self._username)}, + "name": "Linky meter", "manufacturer": "Enedis", } diff --git a/homeassistant/components/linode/manifest.json b/homeassistant/components/linode/manifest.json index 7dc2e0d7518..064f7e1ccf0 100644 --- a/homeassistant/components/linode/manifest.json +++ b/homeassistant/components/linode/manifest.json @@ -1,7 +1,7 @@ { "domain": "linode", "name": "Linode", - "documentation": "https://www.home-assistant.io/components/linode", + "documentation": "https://www.home-assistant.io/integrations/linode", "requirements": [ "linode-api==4.1.9b1" ], diff --git a/homeassistant/components/linux_battery/manifest.json b/homeassistant/components/linux_battery/manifest.json index 4c32b88b2d5..3730f01622f 100644 --- a/homeassistant/components/linux_battery/manifest.json +++ b/homeassistant/components/linux_battery/manifest.json @@ -1,7 +1,7 @@ { "domain": "linux_battery", "name": "Linux battery", - "documentation": "https://www.home-assistant.io/components/linux_battery", + "documentation": "https://www.home-assistant.io/integrations/linux_battery", "requirements": [ "batinfo==0.4.2" ], diff --git a/homeassistant/components/lirc/manifest.json b/homeassistant/components/lirc/manifest.json index d11cf0b2f1e..b15799b54e3 100644 --- a/homeassistant/components/lirc/manifest.json +++ b/homeassistant/components/lirc/manifest.json @@ -1,7 +1,7 @@ { "domain": "lirc", "name": "Lirc", - "documentation": "https://www.home-assistant.io/components/lirc", + "documentation": "https://www.home-assistant.io/integrations/lirc", "requirements": [ "python-lirc==1.2.3" ], diff --git a/homeassistant/components/litejet/manifest.json b/homeassistant/components/litejet/manifest.json index 08bcac67903..988e2bd1ed4 100644 --- a/homeassistant/components/litejet/manifest.json +++ b/homeassistant/components/litejet/manifest.json @@ -1,7 +1,7 @@ { "domain": "litejet", "name": "Litejet", - "documentation": "https://www.home-assistant.io/components/litejet", + "documentation": "https://www.home-assistant.io/integrations/litejet", "requirements": [ "pylitejet==0.1" ], diff --git a/homeassistant/components/liveboxplaytv/manifest.json b/homeassistant/components/liveboxplaytv/manifest.json index 3393022a363..bcb2b53f081 100644 --- a/homeassistant/components/liveboxplaytv/manifest.json +++ b/homeassistant/components/liveboxplaytv/manifest.json @@ -1,7 +1,7 @@ { "domain": "liveboxplaytv", "name": "Liveboxplaytv", - "documentation": "https://www.home-assistant.io/components/liveboxplaytv", + "documentation": "https://www.home-assistant.io/integrations/liveboxplaytv", "requirements": [ "liveboxplaytv==2.0.2", "pyteleloisirs==3.5" diff --git a/homeassistant/components/llamalab_automate/manifest.json b/homeassistant/components/llamalab_automate/manifest.json index e66050fceb5..2f46e6e790c 100644 --- a/homeassistant/components/llamalab_automate/manifest.json +++ b/homeassistant/components/llamalab_automate/manifest.json @@ -1,7 +1,7 @@ { "domain": "llamalab_automate", "name": "Llamalab automate", - "documentation": "https://www.home-assistant.io/components/llamalab_automate", + "documentation": "https://www.home-assistant.io/integrations/llamalab_automate", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/local_file/manifest.json b/homeassistant/components/local_file/manifest.json index 14a503f33f5..ded748be8dc 100644 --- a/homeassistant/components/local_file/manifest.json +++ b/homeassistant/components/local_file/manifest.json @@ -1,7 +1,7 @@ { "domain": "local_file", "name": "Local file", - "documentation": "https://www.home-assistant.io/components/local_file", + "documentation": "https://www.home-assistant.io/integrations/local_file", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/locative/.translations/it.json b/homeassistant/components/locative/.translations/it.json index de62d2ac2f7..4fdef0a987e 100644 --- a/homeassistant/components/locative/.translations/it.json +++ b/homeassistant/components/locative/.translations/it.json @@ -5,7 +5,7 @@ "one_instance_allowed": "\u00c8 necessaria una sola istanza." }, "create_entry": { - "default": "Per inviare localit\u00e0 a Home Assistant, dovrai configurare la funzionalit\u00e0 webhook nell'app Locative.\n\n Compila le seguenti informazioni: \n\n - URL: ` {webhook_url} ` \n - Method: POST \n\n Vedi [la documentazione]({docs_url}) for ulteriori dettagli." + "default": "Per inviare localit\u00e0 a Home Assistant, dovrai configurare la funzionalit\u00e0 Webhook nell'app Locative.\n\n Compila le seguenti informazioni: \n\n - URL: ` {webhook_url} ` \n - Method: POST \n\n Vedi [la documentazione]({docs_url}) for ulteriori dettagli." }, "step": { "user": { diff --git a/homeassistant/components/locative/.translations/no.json b/homeassistant/components/locative/.translations/no.json index 00e3337dfe1..c5ad3043004 100644 --- a/homeassistant/components/locative/.translations/no.json +++ b/homeassistant/components/locative/.translations/no.json @@ -2,7 +2,7 @@ "config": { "abort": { "not_internet_accessible": "Home Assistant m\u00e5 v\u00e6re tilgjengelig fra internett for \u00e5 kunne motta meldinger fra Geofency.", - "one_instance_allowed": "Kun en enkelt forekomst er n\u00f8dvendig." + "one_instance_allowed": "Kun en forekomst er n\u00f8dvendig." }, "create_entry": { "default": "For \u00e5 kunne sende steder til Home Assistant, m\u00e5 du sette opp webhook-funksjonen i Locative. \n\n Fyll ut f\u00f8lgende informasjon: \n\n - URL: `{webhook_url}` \n - Metode: POST \n\n Se [dokumentasjonen]({docs_url}) for ytterligere detaljer." @@ -10,9 +10,9 @@ "step": { "user": { "description": "Er du sikker p\u00e5 at du vil sette opp Locative Webhook?", - "title": "Sett opp Lokative Webhook" + "title": "Sett opp Locative Webhook" } }, - "title": "Lokative Webhook" + "title": "Locative Webhook" } } \ No newline at end of file diff --git a/homeassistant/components/locative/.translations/zh-Hant.json b/homeassistant/components/locative/.translations/zh-Hant.json index 62bb6bb9d96..7dd598c8fc2 100644 --- a/homeassistant/components/locative/.translations/zh-Hant.json +++ b/homeassistant/components/locative/.translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "not_internet_accessible": "Home Assistant \u5be6\u4f8b\u5fc5\u9808\u80fd\u5920\u7531\u7db2\u969b\u7db2\u8def\u5b58\u53d6\uff0c\u65b9\u80fd\u63a5\u53d7 Locative \u8a0a\u606f\u3002", + "not_internet_accessible": "Home Assistant \u7269\u4ef6\u5fc5\u9808\u80fd\u5920\u7531\u7db2\u969b\u7db2\u8def\u5b58\u53d6\uff0c\u65b9\u80fd\u63a5\u53d7 Locative \u8a0a\u606f\u3002", "one_instance_allowed": "\u50c5\u9700\u8a2d\u5b9a\u4e00\u7d44\u7269\u4ef6\u5373\u53ef\u3002" }, "create_entry": { diff --git a/homeassistant/components/locative/config_flow.py b/homeassistant/components/locative/config_flow.py index 4b7d776300d..b4fb43d5e4e 100644 --- a/homeassistant/components/locative/config_flow.py +++ b/homeassistant/components/locative/config_flow.py @@ -6,5 +6,5 @@ from .const import DOMAIN config_entry_flow.register_webhook_flow( DOMAIN, "Locative Webhook", - {"docs_url": "https://www.home-assistant.io/components/locative/"}, + {"docs_url": "https://www.home-assistant.io/integrations/locative/"}, ) diff --git a/homeassistant/components/locative/manifest.json b/homeassistant/components/locative/manifest.json index be2eb07a23c..46a2d4de20e 100644 --- a/homeassistant/components/locative/manifest.json +++ b/homeassistant/components/locative/manifest.json @@ -2,7 +2,7 @@ "domain": "locative", "name": "Locative", "config_flow": true, - "documentation": "https://www.home-assistant.io/components/locative", + "documentation": "https://www.home-assistant.io/integrations/locative", "requirements": [], "dependencies": [ "webhook" diff --git a/homeassistant/components/lock/manifest.json b/homeassistant/components/lock/manifest.json index 29a7a5513d0..0a76628a5b5 100644 --- a/homeassistant/components/lock/manifest.json +++ b/homeassistant/components/lock/manifest.json @@ -1,7 +1,7 @@ { "domain": "lock", "name": "Lock", - "documentation": "https://www.home-assistant.io/components/lock", + "documentation": "https://www.home-assistant.io/integrations/lock", "requirements": [], "dependencies": [ "group" diff --git a/homeassistant/components/lockitron/manifest.json b/homeassistant/components/lockitron/manifest.json index b515d65a14f..18ab9036c5e 100644 --- a/homeassistant/components/lockitron/manifest.json +++ b/homeassistant/components/lockitron/manifest.json @@ -1,7 +1,7 @@ { "domain": "lockitron", "name": "Lockitron", - "documentation": "https://www.home-assistant.io/components/lockitron", + "documentation": "https://www.home-assistant.io/integrations/lockitron", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/logbook/manifest.json b/homeassistant/components/logbook/manifest.json index cedce8152a2..e8e3ad8ac2e 100644 --- a/homeassistant/components/logbook/manifest.json +++ b/homeassistant/components/logbook/manifest.json @@ -1,7 +1,7 @@ { "domain": "logbook", "name": "Logbook", - "documentation": "https://www.home-assistant.io/components/logbook", + "documentation": "https://www.home-assistant.io/integrations/logbook", "requirements": [], "dependencies": [ "frontend", diff --git a/homeassistant/components/logentries/manifest.json b/homeassistant/components/logentries/manifest.json index 60be8f275ee..c546030853f 100644 --- a/homeassistant/components/logentries/manifest.json +++ b/homeassistant/components/logentries/manifest.json @@ -1,7 +1,7 @@ { "domain": "logentries", "name": "Logentries", - "documentation": "https://www.home-assistant.io/components/logentries", + "documentation": "https://www.home-assistant.io/integrations/logentries", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/logger/manifest.json b/homeassistant/components/logger/manifest.json index c6b62387039..ac201fb00a1 100644 --- a/homeassistant/components/logger/manifest.json +++ b/homeassistant/components/logger/manifest.json @@ -1,7 +1,7 @@ { "domain": "logger", "name": "Logger", - "documentation": "https://www.home-assistant.io/components/logger", + "documentation": "https://www.home-assistant.io/integrations/logger", "requirements": [], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/logi_circle/.translations/no.json b/homeassistant/components/logi_circle/.translations/no.json index 03c128f636c..23b951bfa62 100644 --- a/homeassistant/components/logi_circle/.translations/no.json +++ b/homeassistant/components/logi_circle/.translations/no.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_setup": "Du kan bare konfigurere en enkelt Logi Circle konto.", + "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/)." @@ -12,7 +12,7 @@ "error": { "auth_error": "API-autorisasjonen mislyktes.", "auth_timeout": "Autorisasjon ble tidsavbrutt da du ba om token.", - "follow_link": "Vennligst f\u00f8lg lenken og godkjen f\u00f8r du trykker send." + "follow_link": "Vennligst f\u00f8lg lenken og godkjenn f\u00f8r du trykker send." }, "step": { "auth": { diff --git a/homeassistant/components/logi_circle/.translations/zh-Hant.json b/homeassistant/components/logi_circle/.translations/zh-Hant.json index b9f82b6e2e5..1eb3b71c942 100644 --- a/homeassistant/components/logi_circle/.translations/zh-Hant.json +++ b/homeassistant/components/logi_circle/.translations/zh-Hant.json @@ -7,7 +7,7 @@ "no_flows": "\u5fc5\u9808\u5148\u8a2d\u5b9a Logi Circle \u65b9\u80fd\u9032\u884c\u8a8d\u8b49\u3002[\u8acb\u53c3\u95b1\u6559\u5b78\u6307\u5f15]\uff08https://www.home-assistant.io/components/logi_circle/\uff09\u3002" }, "create_entry": { - "default": "\u5df2\u6210\u529f\u8a8d\u8b49 Logi Circle \u88dd\u7f6e\u3002" + "default": "\u5df2\u6210\u529f\u8a8d\u8b49 Logi Circle \u8a2d\u5099\u3002" }, "error": { "auth_error": "API \u8a8d\u8b49\u5931\u6557\u3002", diff --git a/homeassistant/components/logi_circle/__init__.py b/homeassistant/components/logi_circle/__init__.py index 12484a655d6..f7ed3a73fce 100644 --- a/homeassistant/components/logi_circle/__init__.py +++ b/homeassistant/components/logi_circle/__init__.py @@ -8,6 +8,7 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.components.camera import ATTR_FILENAME, CAMERA_SERVICE_SCHEMA from homeassistant.const import ( + ATTR_MODE, CONF_MONITORED_CONDITIONS, CONF_SENSORS, EVENT_HOMEASSISTANT_STOP, @@ -42,7 +43,6 @@ SERVICE_SET_CONFIG = "set_config" SERVICE_LIVESTREAM_SNAPSHOT = "livestream_snapshot" SERVICE_LIVESTREAM_RECORD = "livestream_record" -ATTR_MODE = "mode" ATTR_VALUE = "value" ATTR_DURATION = "duration" diff --git a/homeassistant/components/logi_circle/manifest.json b/homeassistant/components/logi_circle/manifest.json index b1767748395..22502956e06 100644 --- a/homeassistant/components/logi_circle/manifest.json +++ b/homeassistant/components/logi_circle/manifest.json @@ -2,7 +2,7 @@ "domain": "logi_circle", "name": "Logi Circle", "config_flow": true, - "documentation": "https://www.home-assistant.io/components/logi_circle", + "documentation": "https://www.home-assistant.io/integrations/logi_circle", "requirements": ["logi_circle==0.2.2"], "dependencies": ["ffmpeg"], "codeowners": ["@evanjd"] diff --git a/homeassistant/components/london_air/manifest.json b/homeassistant/components/london_air/manifest.json index 3f0c97edfe0..cca4a54bda8 100644 --- a/homeassistant/components/london_air/manifest.json +++ b/homeassistant/components/london_air/manifest.json @@ -1,7 +1,7 @@ { "domain": "london_air", "name": "London air", - "documentation": "https://www.home-assistant.io/components/london_air", + "documentation": "https://www.home-assistant.io/integrations/london_air", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/london_underground/manifest.json b/homeassistant/components/london_underground/manifest.json index 5262fa4837e..e66d5c1820d 100644 --- a/homeassistant/components/london_underground/manifest.json +++ b/homeassistant/components/london_underground/manifest.json @@ -1,7 +1,7 @@ { "domain": "london_underground", "name": "London underground", - "documentation": "https://www.home-assistant.io/components/london_underground", + "documentation": "https://www.home-assistant.io/integrations/london_underground", "requirements": [ "london-tube-status==0.2" ], diff --git a/homeassistant/components/loopenergy/manifest.json b/homeassistant/components/loopenergy/manifest.json index 20fe6fac2aa..41e3d0dd6b0 100644 --- a/homeassistant/components/loopenergy/manifest.json +++ b/homeassistant/components/loopenergy/manifest.json @@ -1,7 +1,7 @@ { "domain": "loopenergy", "name": "Loopenergy", - "documentation": "https://www.home-assistant.io/components/loopenergy", + "documentation": "https://www.home-assistant.io/integrations/loopenergy", "requirements": [ "pyloopenergy==0.1.3" ], diff --git a/homeassistant/components/loopenergy/sensor.py b/homeassistant/components/loopenergy/sensor.py index 56a87ce4345..994c3e2fd89 100644 --- a/homeassistant/components/loopenergy/sensor.py +++ b/homeassistant/components/loopenergy/sensor.py @@ -122,7 +122,7 @@ class LoopEnergyElec(LoopEnergyDevice): def __init__(self, controller): """Initialize the sensor.""" - super(LoopEnergyElec, self).__init__(controller) + super().__init__(controller) self._name = "Power Usage" self._controller.subscribe_elecricity(self._callback) @@ -136,7 +136,7 @@ class LoopEnergyGas(LoopEnergyDevice): def __init__(self, controller): """Initialize the sensor.""" - super(LoopEnergyGas, self).__init__(controller) + super().__init__(controller) self._name = "Gas Usage" self._controller.subscribe_gas(self._callback) diff --git a/homeassistant/components/lovelace/manifest.json b/homeassistant/components/lovelace/manifest.json index dd8da40efe4..72be440d54c 100644 --- a/homeassistant/components/lovelace/manifest.json +++ b/homeassistant/components/lovelace/manifest.json @@ -1,7 +1,7 @@ { "domain": "lovelace", "name": "Lovelace", - "documentation": "https://www.home-assistant.io/components/lovelace", + "documentation": "https://www.home-assistant.io/integrations/lovelace", "requirements": [], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/luci/manifest.json b/homeassistant/components/luci/manifest.json index 153f6b5aea6..646fc1a3cbf 100644 --- a/homeassistant/components/luci/manifest.json +++ b/homeassistant/components/luci/manifest.json @@ -1,10 +1,9 @@ { "domain": "luci", "name": "Luci", - "documentation": "https://www.home-assistant.io/components/luci", + "documentation": "https://www.home-assistant.io/integrations/luci", "requirements": [ - "openwrt-luci-rpc==1.1.0", - "packaging==19.1" + "openwrt-luci-rpc==1.1.1" ], "dependencies": [], "codeowners": ["@fbradyirl"] diff --git a/homeassistant/components/luftdaten/.translations/ru.json b/homeassistant/components/luftdaten/.translations/ru.json index d37aa3567d1..7ae83b550e3 100644 --- a/homeassistant/components/luftdaten/.translations/ru.json +++ b/homeassistant/components/luftdaten/.translations/ru.json @@ -3,7 +3,7 @@ "error": { "communication_error": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a API Luftdaten", "invalid_sensor": "\u0414\u0430\u0442\u0447\u0438\u043a \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u0435\u043d \u0438\u043b\u0438 \u043d\u0435\u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0442\u0435\u043b\u0435\u043d", - "sensor_exists": "\u0414\u0430\u0442\u0447\u0438\u043a \u0443\u0436\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d" + "sensor_exists": "\u0414\u0430\u0442\u0447\u0438\u043a \u0443\u0436\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d." }, "step": { "user": { diff --git a/homeassistant/components/luftdaten/manifest.json b/homeassistant/components/luftdaten/manifest.json index 26d6c21f3a9..112b58ba65d 100644 --- a/homeassistant/components/luftdaten/manifest.json +++ b/homeassistant/components/luftdaten/manifest.json @@ -2,7 +2,7 @@ "domain": "luftdaten", "name": "Luftdaten", "config_flow": true, - "documentation": "https://www.home-assistant.io/components/luftdaten", + "documentation": "https://www.home-assistant.io/integrations/luftdaten", "requirements": [ "luftdaten==0.6.3" ], diff --git a/homeassistant/components/lupusec/manifest.json b/homeassistant/components/lupusec/manifest.json index 344ec82d976..bb6b18243ec 100644 --- a/homeassistant/components/lupusec/manifest.json +++ b/homeassistant/components/lupusec/manifest.json @@ -1,7 +1,7 @@ { "domain": "lupusec", "name": "Lupusec", - "documentation": "https://www.home-assistant.io/components/lupusec", + "documentation": "https://www.home-assistant.io/integrations/lupusec", "requirements": [ "lupupy==0.0.17" ], diff --git a/homeassistant/components/lutron/manifest.json b/homeassistant/components/lutron/manifest.json index bece55ae09d..cace2770de0 100644 --- a/homeassistant/components/lutron/manifest.json +++ b/homeassistant/components/lutron/manifest.json @@ -1,9 +1,9 @@ { "domain": "lutron", "name": "Lutron", - "documentation": "https://www.home-assistant.io/components/lutron", + "documentation": "https://www.home-assistant.io/integrations/lutron", "requirements": [ - "pylutron==0.2.2" + "pylutron==0.2.5" ], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/lutron_caseta/manifest.json b/homeassistant/components/lutron_caseta/manifest.json index 4da58cdfc40..d1501a562db 100644 --- a/homeassistant/components/lutron_caseta/manifest.json +++ b/homeassistant/components/lutron_caseta/manifest.json @@ -1,7 +1,7 @@ { "domain": "lutron_caseta", "name": "Lutron caseta", - "documentation": "https://www.home-assistant.io/components/lutron_caseta", + "documentation": "https://www.home-assistant.io/integrations/lutron_caseta", "requirements": [ "pylutron-caseta==0.5.0" ], diff --git a/homeassistant/components/lw12wifi/manifest.json b/homeassistant/components/lw12wifi/manifest.json index 205072055bb..7f830cda25b 100644 --- a/homeassistant/components/lw12wifi/manifest.json +++ b/homeassistant/components/lw12wifi/manifest.json @@ -1,7 +1,7 @@ { "domain": "lw12wifi", "name": "Lw12wifi", - "documentation": "https://www.home-assistant.io/components/lw12wifi", + "documentation": "https://www.home-assistant.io/integrations/lw12wifi", "requirements": [ "lw12==0.9.2" ], diff --git a/homeassistant/components/lyft/manifest.json b/homeassistant/components/lyft/manifest.json index ff7da7190d9..e8b593b3145 100644 --- a/homeassistant/components/lyft/manifest.json +++ b/homeassistant/components/lyft/manifest.json @@ -1,7 +1,7 @@ { "domain": "lyft", "name": "Lyft", - "documentation": "https://www.home-assistant.io/components/lyft", + "documentation": "https://www.home-assistant.io/integrations/lyft", "requirements": [ "lyft_rides==0.2" ], diff --git a/homeassistant/components/magicseaweed/manifest.json b/homeassistant/components/magicseaweed/manifest.json index 6534d927f1b..41795c117a9 100644 --- a/homeassistant/components/magicseaweed/manifest.json +++ b/homeassistant/components/magicseaweed/manifest.json @@ -1,7 +1,7 @@ { "domain": "magicseaweed", "name": "Magicseaweed", - "documentation": "https://www.home-assistant.io/components/magicseaweed", + "documentation": "https://www.home-assistant.io/integrations/magicseaweed", "requirements": [ "magicseaweed==1.0.3" ], diff --git a/homeassistant/components/mailbox/manifest.json b/homeassistant/components/mailbox/manifest.json index 4ca1db564a4..2883c9caf34 100644 --- a/homeassistant/components/mailbox/manifest.json +++ b/homeassistant/components/mailbox/manifest.json @@ -1,7 +1,7 @@ { "domain": "mailbox", "name": "Mailbox", - "documentation": "https://www.home-assistant.io/components/mailbox", + "documentation": "https://www.home-assistant.io/integrations/mailbox", "requirements": [], "dependencies": [ "http" diff --git a/homeassistant/components/mailgun/.translations/nn.json b/homeassistant/components/mailgun/.translations/nn.json new file mode 100644 index 00000000000..2bab2e43001 --- /dev/null +++ b/homeassistant/components/mailgun/.translations/nn.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Mailgun" + } +} \ No newline at end of file diff --git a/homeassistant/components/mailgun/.translations/no.json b/homeassistant/components/mailgun/.translations/no.json index 91c616b69af..3d1cbd41a34 100644 --- a/homeassistant/components/mailgun/.translations/no.json +++ b/homeassistant/components/mailgun/.translations/no.json @@ -2,7 +2,7 @@ "config": { "abort": { "not_internet_accessible": "Din Home Assistant forekomst m\u00e5 v\u00e6re tilgjengelig fra internett for \u00e5 motta Mailgun-meldinger.", - "one_instance_allowed": "Kun \u00e9n enkelt forekomst er n\u00f8dvendig." + "one_instance_allowed": "Kun en forekomst er n\u00f8dvendig." }, "create_entry": { "default": "For \u00e5 sende hendelser til Home Assistant, m\u00e5 du sette opp [Webhooks with Mailgun]({mailgun_url}).\n\nFyll ut f\u00f8lgende informasjon:\n\n- URL: `{webhook_url}`\n- Metode: POST\n- Innholdstype: application/json\n\nSe [dokumentasjonen]({docs_url}) om hvordan du konfigurerer automatiseringer for \u00e5 h\u00e5ndtere innkommende data." diff --git a/homeassistant/components/mailgun/.translations/zh-Hant.json b/homeassistant/components/mailgun/.translations/zh-Hant.json index 4b9ab3a7abb..f16533312fa 100644 --- a/homeassistant/components/mailgun/.translations/zh-Hant.json +++ b/homeassistant/components/mailgun/.translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "not_internet_accessible": "Home Assistant \u5be6\u4f8b\u5fc5\u9808\u80fd\u5920\u7531\u7db2\u969b\u7db2\u8def\u5b58\u53d6\uff0c\u65b9\u80fd\u63a5\u53d7 Mailgun \u8a0a\u606f\u3002", + "not_internet_accessible": "Home Assistant \u7269\u4ef6\u5fc5\u9808\u80fd\u5920\u7531\u7db2\u969b\u7db2\u8def\u5b58\u53d6\uff0c\u65b9\u80fd\u63a5\u53d7 Mailgun \u8a0a\u606f\u3002", "one_instance_allowed": "\u50c5\u9700\u8a2d\u5b9a\u4e00\u7d44\u7269\u4ef6\u5373\u53ef\u3002" }, "create_entry": { diff --git a/homeassistant/components/mailgun/config_flow.py b/homeassistant/components/mailgun/config_flow.py index 650a5e8ee67..e1c6abbc1ca 100644 --- a/homeassistant/components/mailgun/config_flow.py +++ b/homeassistant/components/mailgun/config_flow.py @@ -8,6 +8,6 @@ config_entry_flow.register_webhook_flow( "Mailgun Webhook", { "mailgun_url": "https://documentation.mailgun.com/en/latest/user_manual.html#webhooks", # noqa: E501 pylint: disable=line-too-long - "docs_url": "https://www.home-assistant.io/components/mailgun/", + "docs_url": "https://www.home-assistant.io/integrations/mailgun/", }, ) diff --git a/homeassistant/components/mailgun/manifest.json b/homeassistant/components/mailgun/manifest.json index 9ed7a50a8e3..c0bd0823b8f 100644 --- a/homeassistant/components/mailgun/manifest.json +++ b/homeassistant/components/mailgun/manifest.json @@ -2,7 +2,7 @@ "domain": "mailgun", "name": "Mailgun", "config_flow": true, - "documentation": "https://www.home-assistant.io/components/mailgun", + "documentation": "https://www.home-assistant.io/integrations/mailgun", "requirements": [ "pymailgunner==1.4" ], diff --git a/homeassistant/components/manual/manifest.json b/homeassistant/components/manual/manifest.json index 6c788971629..12d5297c010 100644 --- a/homeassistant/components/manual/manifest.json +++ b/homeassistant/components/manual/manifest.json @@ -1,7 +1,7 @@ { "domain": "manual", "name": "Manual", - "documentation": "https://www.home-assistant.io/components/manual", + "documentation": "https://www.home-assistant.io/integrations/manual", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/manual_mqtt/manifest.json b/homeassistant/components/manual_mqtt/manifest.json index 81cd1338450..d9ec004e6a8 100644 --- a/homeassistant/components/manual_mqtt/manifest.json +++ b/homeassistant/components/manual_mqtt/manifest.json @@ -1,7 +1,7 @@ { "domain": "manual_mqtt", "name": "Manual mqtt", - "documentation": "https://www.home-assistant.io/components/manual_mqtt", + "documentation": "https://www.home-assistant.io/integrations/manual_mqtt", "requirements": [], "dependencies": [ "mqtt" diff --git a/homeassistant/components/map/manifest.json b/homeassistant/components/map/manifest.json index d26d7d9530f..e64d18c92e1 100644 --- a/homeassistant/components/map/manifest.json +++ b/homeassistant/components/map/manifest.json @@ -1,7 +1,7 @@ { "domain": "map", "name": "Map", - "documentation": "https://www.home-assistant.io/components/map", + "documentation": "https://www.home-assistant.io/integrations/map", "requirements": [], "dependencies": [ "frontend" diff --git a/homeassistant/components/marytts/manifest.json b/homeassistant/components/marytts/manifest.json index 5316935c442..0188f405e14 100644 --- a/homeassistant/components/marytts/manifest.json +++ b/homeassistant/components/marytts/manifest.json @@ -1,7 +1,7 @@ { "domain": "marytts", "name": "Marytts", - "documentation": "https://www.home-assistant.io/components/marytts", + "documentation": "https://www.home-assistant.io/integrations/marytts", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/mastodon/manifest.json b/homeassistant/components/mastodon/manifest.json index 4005e51e373..e041ba2f669 100644 --- a/homeassistant/components/mastodon/manifest.json +++ b/homeassistant/components/mastodon/manifest.json @@ -1,7 +1,7 @@ { "domain": "mastodon", "name": "Mastodon", - "documentation": "https://www.home-assistant.io/components/mastodon", + "documentation": "https://www.home-assistant.io/integrations/mastodon", "requirements": [ "Mastodon.py==1.4.6" ], diff --git a/homeassistant/components/matrix/manifest.json b/homeassistant/components/matrix/manifest.json index 9ea1a6f0c55..a467518c04e 100644 --- a/homeassistant/components/matrix/manifest.json +++ b/homeassistant/components/matrix/manifest.json @@ -1,7 +1,7 @@ { "domain": "matrix", "name": "Matrix", - "documentation": "https://www.home-assistant.io/components/matrix", + "documentation": "https://www.home-assistant.io/integrations/matrix", "requirements": [ "matrix-client==0.2.0" ], diff --git a/homeassistant/components/maxcube/manifest.json b/homeassistant/components/maxcube/manifest.json index a28096c5eb7..1f5b1eef935 100644 --- a/homeassistant/components/maxcube/manifest.json +++ b/homeassistant/components/maxcube/manifest.json @@ -1,7 +1,7 @@ { "domain": "maxcube", "name": "Maxcube", - "documentation": "https://www.home-assistant.io/components/maxcube", + "documentation": "https://www.home-assistant.io/integrations/maxcube", "requirements": [ "maxcube-api==0.1.0" ], diff --git a/homeassistant/components/mcp23017/manifest.json b/homeassistant/components/mcp23017/manifest.json index 41048683c92..2dbffd829f8 100644 --- a/homeassistant/components/mcp23017/manifest.json +++ b/homeassistant/components/mcp23017/manifest.json @@ -1,7 +1,7 @@ { "domain": "mcp23017", "name": "MCP23017 I/O Expander", - "documentation": "https://www.home-assistant.io/components/mcp23017", + "documentation": "https://www.home-assistant.io/integrations/mcp23017", "requirements": [ "RPi.GPIO==0.6.5", "adafruit-blinka==1.2.1", diff --git a/homeassistant/components/media_extractor/manifest.json b/homeassistant/components/media_extractor/manifest.json index 419d4b72864..886535555d5 100644 --- a/homeassistant/components/media_extractor/manifest.json +++ b/homeassistant/components/media_extractor/manifest.json @@ -1,9 +1,9 @@ { "domain": "media_extractor", "name": "Media extractor", - "documentation": "https://www.home-assistant.io/components/media_extractor", + "documentation": "https://www.home-assistant.io/integrations/media_extractor", "requirements": [ - "youtube_dl==2019.09.01" + "youtube_dl==2019.09.28" ], "dependencies": [ "media_player" diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py index 791dacb7024..98da19fd98e 100644 --- a/homeassistant/components/media_player/__init__.py +++ b/homeassistant/components/media_player/__init__.py @@ -7,6 +7,7 @@ import functools as ft import hashlib import logging from random import SystemRandom +from typing import Optional from urllib.parse import urlparse from aiohttp import web @@ -347,7 +348,7 @@ async def async_unload_entry(hass, entry): class MediaPlayerDevice(Entity): """ABC for media player devices.""" - _access_token = None + _access_token: Optional[str] = None # Implement these for your media player @property @@ -356,7 +357,7 @@ class MediaPlayerDevice(Entity): return None @property - def access_token(self): + def access_token(self) -> str: """Access token for this media player.""" if self._access_token is None: self._access_token = hashlib.sha256( diff --git a/homeassistant/components/media_player/manifest.json b/homeassistant/components/media_player/manifest.json index bf6f8fabafa..4df8ad8442a 100644 --- a/homeassistant/components/media_player/manifest.json +++ b/homeassistant/components/media_player/manifest.json @@ -1,7 +1,7 @@ { "domain": "media_player", "name": "Media player", - "documentation": "https://www.home-assistant.io/components/media_player", + "documentation": "https://www.home-assistant.io/integrations/media_player", "requirements": [], "dependencies": [ "http" diff --git a/homeassistant/components/media_player/reproduce_state.py b/homeassistant/components/media_player/reproduce_state.py index 4eba4657d95..dac08afe471 100644 --- a/homeassistant/components/media_player/reproduce_state.py +++ b/homeassistant/components/media_player/reproduce_state.py @@ -36,7 +36,7 @@ from .const import ( ) -# mypy: allow-incomplete-defs, allow-untyped-defs +# mypy: allow-untyped-defs async def _async_reproduce_states( @@ -44,7 +44,7 @@ async def _async_reproduce_states( ) -> None: """Reproduce component states.""" - async def call_service(service: str, keys: Iterable): + async def call_service(service: str, keys: Iterable) -> None: """Call service with set of attributes given.""" data = {} data["entity_id"] = state.entity_id diff --git a/homeassistant/components/mediaroom/manifest.json b/homeassistant/components/mediaroom/manifest.json index 134d85fa171..0e42cd7e535 100644 --- a/homeassistant/components/mediaroom/manifest.json +++ b/homeassistant/components/mediaroom/manifest.json @@ -1,7 +1,7 @@ { "domain": "mediaroom", "name": "Mediaroom", - "documentation": "https://www.home-assistant.io/components/mediaroom", + "documentation": "https://www.home-assistant.io/integrations/mediaroom", "requirements": [ "pymediaroom==0.6.4" ], diff --git a/homeassistant/components/melissa/manifest.json b/homeassistant/components/melissa/manifest.json index f9fa1cab502..64760338f35 100644 --- a/homeassistant/components/melissa/manifest.json +++ b/homeassistant/components/melissa/manifest.json @@ -1,7 +1,7 @@ { "domain": "melissa", "name": "Melissa", - "documentation": "https://www.home-assistant.io/components/melissa", + "documentation": "https://www.home-assistant.io/integrations/melissa", "requirements": [ "py-melissa-climate==2.0.0" ], diff --git a/homeassistant/components/meraki/manifest.json b/homeassistant/components/meraki/manifest.json index d03679ed41e..2add8663555 100644 --- a/homeassistant/components/meraki/manifest.json +++ b/homeassistant/components/meraki/manifest.json @@ -1,7 +1,7 @@ { "domain": "meraki", "name": "Meraki", - "documentation": "https://www.home-assistant.io/components/meraki", + "documentation": "https://www.home-assistant.io/integrations/meraki", "requirements": [], "dependencies": ["http"], "codeowners": [] diff --git a/homeassistant/components/message_bird/manifest.json b/homeassistant/components/message_bird/manifest.json index a6c49b3c396..79428f951f1 100644 --- a/homeassistant/components/message_bird/manifest.json +++ b/homeassistant/components/message_bird/manifest.json @@ -1,7 +1,7 @@ { "domain": "message_bird", "name": "Message bird", - "documentation": "https://www.home-assistant.io/components/message_bird", + "documentation": "https://www.home-assistant.io/integrations/message_bird", "requirements": [ "messagebird==1.2.0" ], diff --git a/homeassistant/components/met/.translations/de.json b/homeassistant/components/met/.translations/de.json index b70d3f12a83..2fd772c8619 100644 --- a/homeassistant/components/met/.translations/de.json +++ b/homeassistant/components/met/.translations/de.json @@ -1,7 +1,7 @@ { "config": { "error": { - "name_exists": "Name existiert bereits" + "name_exists": "Ort existiert bereits" }, "step": { "user": { diff --git a/homeassistant/components/met/.translations/hu.json b/homeassistant/components/met/.translations/hu.json index 3b34d8f6354..dcbc40b4c71 100644 --- a/homeassistant/components/met/.translations/hu.json +++ b/homeassistant/components/met/.translations/hu.json @@ -1,9 +1,20 @@ { "config": { + "error": { + "name_exists": "A hely m\u00e1r l\u00e9tezik" + }, "step": { "user": { + "data": { + "elevation": "Magass\u00e1g", + "latitude": "Sz\u00e9less\u00e9g", + "longitude": "Hossz\u00fas\u00e1g", + "name": "N\u00e9v" + }, + "description": "Meteorol\u00f3giai int\u00e9zet", "title": "Elhelyezked\u00e9s" } - } + }, + "title": "Met.no" } } \ No newline at end of file diff --git a/homeassistant/components/met/.translations/nn.json b/homeassistant/components/met/.translations/nn.json new file mode 100644 index 00000000000..0e024a0e1eb --- /dev/null +++ b/homeassistant/components/met/.translations/nn.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "name": "Namn" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/met/.translations/no.json b/homeassistant/components/met/.translations/no.json index 6ebaa08457f..9a3ef350ab1 100644 --- a/homeassistant/components/met/.translations/no.json +++ b/homeassistant/components/met/.translations/no.json @@ -1,7 +1,7 @@ { "config": { "error": { - "name_exists": "Navnet eksisterer allerede" + "name_exists": "Lokasjonen finnes allerede" }, "step": { "user": { diff --git a/homeassistant/components/met/.translations/pl.json b/homeassistant/components/met/.translations/pl.json index d44142213bf..f647dcf7b45 100644 --- a/homeassistant/components/met/.translations/pl.json +++ b/homeassistant/components/met/.translations/pl.json @@ -1,7 +1,7 @@ { "config": { "error": { - "name_exists": "Nazwa ju\u017c istnieje" + "name_exists": "Lokalizacja ju\u017c istnieje" }, "step": { "user": { diff --git a/homeassistant/components/met/.translations/ru.json b/homeassistant/components/met/.translations/ru.json index d92d28d9484..559382cf209 100644 --- a/homeassistant/components/met/.translations/ru.json +++ b/homeassistant/components/met/.translations/ru.json @@ -1,7 +1,7 @@ { "config": { "error": { - "name_exists": "\u041c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435 \u0443\u0436\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u043e" + "name_exists": "\u041c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435 \u0443\u0436\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u043e." }, "step": { "user": { diff --git a/homeassistant/components/met/.translations/zh-Hans.json b/homeassistant/components/met/.translations/zh-Hans.json index 9565bb66618..9027347174d 100644 --- a/homeassistant/components/met/.translations/zh-Hans.json +++ b/homeassistant/components/met/.translations/zh-Hans.json @@ -1,10 +1,15 @@ { "config": { + "error": { + "name_exists": "\u4f4d\u7f6e\u5df2\u5b58\u5728" + }, "step": { "user": { "data": { + "elevation": "\u6d77\u62d4", "latitude": "\u7eac\u5ea6", - "longitude": "\u7ecf\u5ea6" + "longitude": "\u7ecf\u5ea6", + "name": "\u540d\u79f0" }, "title": "\u4f4d\u7f6e" } diff --git a/homeassistant/components/met/manifest.json b/homeassistant/components/met/manifest.json index 426d0faf860..2652e33b76c 100644 --- a/homeassistant/components/met/manifest.json +++ b/homeassistant/components/met/manifest.json @@ -2,7 +2,7 @@ "domain": "met", "name": "Met", "config_flow": true, - "documentation": "https://www.home-assistant.io/components/met", + "documentation": "https://www.home-assistant.io/integrations/met", "requirements": [ "pyMetno==0.4.6" ], diff --git a/homeassistant/components/meteo_france/manifest.json b/homeassistant/components/meteo_france/manifest.json index b485458be40..ba043bf2a71 100644 --- a/homeassistant/components/meteo_france/manifest.json +++ b/homeassistant/components/meteo_france/manifest.json @@ -1,7 +1,7 @@ { "domain": "meteo_france", "name": "Meteo france", - "documentation": "https://www.home-assistant.io/components/meteo_france", + "documentation": "https://www.home-assistant.io/integrations/meteo_france", "requirements": [ "meteofrance==0.3.7", "vigilancemeteo==3.0.0" diff --git a/homeassistant/components/meteoalarm/manifest.json b/homeassistant/components/meteoalarm/manifest.json index 21483756213..ee14ce7d26d 100644 --- a/homeassistant/components/meteoalarm/manifest.json +++ b/homeassistant/components/meteoalarm/manifest.json @@ -1,9 +1,9 @@ { "domain": "meteoalarm", "name": "meteoalarm", - "documentation": "https://www.home-assistant.io/components/meteoalarm", + "documentation": "https://www.home-assistant.io/integrations/meteoalarm", "requirements": [ - "meteoalertapi==0.1.5" + "meteoalertapi==0.1.6" ], "dependencies": [], "codeowners": ["@rolfberkenbosch"] diff --git a/homeassistant/components/metoffice/manifest.json b/homeassistant/components/metoffice/manifest.json index f5d358854f6..f5624e33edb 100644 --- a/homeassistant/components/metoffice/manifest.json +++ b/homeassistant/components/metoffice/manifest.json @@ -1,7 +1,7 @@ { "domain": "metoffice", "name": "Metoffice", - "documentation": "https://www.home-assistant.io/components/metoffice", + "documentation": "https://www.home-assistant.io/integrations/metoffice", "requirements": [ "datapoint==0.4.3" ], diff --git a/homeassistant/components/mfi/manifest.json b/homeassistant/components/mfi/manifest.json index 1e84b39a366..c08b4e4c88a 100644 --- a/homeassistant/components/mfi/manifest.json +++ b/homeassistant/components/mfi/manifest.json @@ -1,7 +1,7 @@ { "domain": "mfi", "name": "Mfi", - "documentation": "https://www.home-assistant.io/components/mfi", + "documentation": "https://www.home-assistant.io/integrations/mfi", "requirements": [ "mficlient==0.3.0" ], diff --git a/homeassistant/components/mhz19/manifest.json b/homeassistant/components/mhz19/manifest.json index 8545db90e27..5bffcf8e92c 100644 --- a/homeassistant/components/mhz19/manifest.json +++ b/homeassistant/components/mhz19/manifest.json @@ -1,7 +1,7 @@ { "domain": "mhz19", "name": "Mhz19", - "documentation": "https://www.home-assistant.io/components/mhz19", + "documentation": "https://www.home-assistant.io/integrations/mhz19", "requirements": [ "pmsensor==0.4" ], diff --git a/homeassistant/components/microsoft/manifest.json b/homeassistant/components/microsoft/manifest.json index 827d961a093..16ae94c212e 100644 --- a/homeassistant/components/microsoft/manifest.json +++ b/homeassistant/components/microsoft/manifest.json @@ -1,7 +1,7 @@ { "domain": "microsoft", "name": "Microsoft", - "documentation": "https://www.home-assistant.io/components/microsoft", + "documentation": "https://www.home-assistant.io/integrations/microsoft", "requirements": [ "pycsspeechtts==1.0.2" ], diff --git a/homeassistant/components/microsoft_face/manifest.json b/homeassistant/components/microsoft_face/manifest.json index 7f6c4fbd935..1d51dca42cd 100644 --- a/homeassistant/components/microsoft_face/manifest.json +++ b/homeassistant/components/microsoft_face/manifest.json @@ -1,7 +1,7 @@ { "domain": "microsoft_face", "name": "Microsoft face", - "documentation": "https://www.home-assistant.io/components/microsoft_face", + "documentation": "https://www.home-assistant.io/integrations/microsoft_face", "requirements": [], "dependencies": [ "camera" diff --git a/homeassistant/components/microsoft_face_detect/manifest.json b/homeassistant/components/microsoft_face_detect/manifest.json index b272a299cf5..12d73623e75 100644 --- a/homeassistant/components/microsoft_face_detect/manifest.json +++ b/homeassistant/components/microsoft_face_detect/manifest.json @@ -1,7 +1,7 @@ { "domain": "microsoft_face_detect", "name": "Microsoft face detect", - "documentation": "https://www.home-assistant.io/components/microsoft_face_detect", + "documentation": "https://www.home-assistant.io/integrations/microsoft_face_detect", "requirements": [], "dependencies": ["microsoft_face"], "codeowners": [] diff --git a/homeassistant/components/microsoft_face_identify/manifest.json b/homeassistant/components/microsoft_face_identify/manifest.json index 10e4bde103c..a52aca1ac0a 100644 --- a/homeassistant/components/microsoft_face_identify/manifest.json +++ b/homeassistant/components/microsoft_face_identify/manifest.json @@ -1,7 +1,7 @@ { "domain": "microsoft_face_identify", "name": "Microsoft face identify", - "documentation": "https://www.home-assistant.io/components/microsoft_face_identify", + "documentation": "https://www.home-assistant.io/integrations/microsoft_face_identify", "requirements": [], "dependencies": ["microsoft_face"], "codeowners": [] diff --git a/homeassistant/components/miflora/manifest.json b/homeassistant/components/miflora/manifest.json index c7ef2b89611..54fa59135b3 100644 --- a/homeassistant/components/miflora/manifest.json +++ b/homeassistant/components/miflora/manifest.json @@ -1,7 +1,7 @@ { "domain": "miflora", "name": "Miflora", - "documentation": "https://www.home-assistant.io/components/miflora", + "documentation": "https://www.home-assistant.io/integrations/miflora", "requirements": [ "bluepy==1.1.4", "miflora==0.4.0" diff --git a/homeassistant/components/mikrotik/manifest.json b/homeassistant/components/mikrotik/manifest.json index 92869856545..9a05f5a9f87 100644 --- a/homeassistant/components/mikrotik/manifest.json +++ b/homeassistant/components/mikrotik/manifest.json @@ -1,7 +1,7 @@ { "domain": "mikrotik", "name": "Mikrotik", - "documentation": "https://www.home-assistant.io/components/mikrotik", + "documentation": "https://www.home-assistant.io/integrations/mikrotik", "requirements": [ "librouteros==2.3.0" ], diff --git a/homeassistant/components/mill/manifest.json b/homeassistant/components/mill/manifest.json index 05efb845c12..85e70c78ede 100644 --- a/homeassistant/components/mill/manifest.json +++ b/homeassistant/components/mill/manifest.json @@ -1,7 +1,7 @@ { "domain": "mill", "name": "Mill", - "documentation": "https://www.home-assistant.io/components/mill", + "documentation": "https://www.home-assistant.io/integrations/mill", "requirements": [ "millheater==0.3.4" ], diff --git a/homeassistant/components/min_max/manifest.json b/homeassistant/components/min_max/manifest.json index ea6befe498b..ed899a0438f 100644 --- a/homeassistant/components/min_max/manifest.json +++ b/homeassistant/components/min_max/manifest.json @@ -1,7 +1,7 @@ { "domain": "min_max", "name": "Min max", - "documentation": "https://www.home-assistant.io/components/min_max", + "documentation": "https://www.home-assistant.io/integrations/min_max", "requirements": [], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/minio/manifest.json b/homeassistant/components/minio/manifest.json index 2b2f84836ea..bc373633503 100644 --- a/homeassistant/components/minio/manifest.json +++ b/homeassistant/components/minio/manifest.json @@ -1,7 +1,7 @@ { "domain": "minio", "name": "Minio", - "documentation": "https://www.home-assistant.io/components/minio", + "documentation": "https://www.home-assistant.io/integrations/minio", "requirements": [ "minio==4.0.9" ], diff --git a/homeassistant/components/mitemp_bt/manifest.json b/homeassistant/components/mitemp_bt/manifest.json index 2324a861b38..612e7c19f8b 100644 --- a/homeassistant/components/mitemp_bt/manifest.json +++ b/homeassistant/components/mitemp_bt/manifest.json @@ -1,7 +1,7 @@ { "domain": "mitemp_bt", "name": "Mitemp bt", - "documentation": "https://www.home-assistant.io/components/mitemp_bt", + "documentation": "https://www.home-assistant.io/integrations/mitemp_bt", "requirements": [ "mitemp_bt==0.0.1" ], diff --git a/homeassistant/components/mjpeg/manifest.json b/homeassistant/components/mjpeg/manifest.json index 2ecd66910be..93f01ac2e3a 100644 --- a/homeassistant/components/mjpeg/manifest.json +++ b/homeassistant/components/mjpeg/manifest.json @@ -1,7 +1,7 @@ { "domain": "mjpeg", "name": "Mjpeg", - "documentation": "https://www.home-assistant.io/components/mjpeg", + "documentation": "https://www.home-assistant.io/integrations/mjpeg", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/mobile_app/manifest.json b/homeassistant/components/mobile_app/manifest.json index 85c6231daa8..8c95ca4ad41 100644 --- a/homeassistant/components/mobile_app/manifest.json +++ b/homeassistant/components/mobile_app/manifest.json @@ -2,7 +2,7 @@ "domain": "mobile_app", "name": "Home Assistant Mobile App Support", "config_flow": true, - "documentation": "https://www.home-assistant.io/components/mobile_app", + "documentation": "https://www.home-assistant.io/integrations/mobile_app", "requirements": [ "PyNaCl==1.3.0" ], diff --git a/homeassistant/components/mobile_app/notify.py b/homeassistant/components/mobile_app/notify.py index d16ed23266a..1e6a0517026 100644 --- a/homeassistant/components/mobile_app/notify.py +++ b/homeassistant/components/mobile_app/notify.py @@ -1,6 +1,5 @@ """Support for mobile_app push notifications.""" import asyncio -from datetime import datetime, timezone import logging import async_timeout @@ -60,7 +59,7 @@ def log_rate_limits(hass, device_name, resp, level=logging.INFO): rate_limits = resp[ATTR_PUSH_RATE_LIMITS] resetsAt = rate_limits[ATTR_PUSH_RATE_LIMITS_RESETS_AT] - resetsAtTime = dt_util.parse_datetime(resetsAt) - datetime.now(timezone.utc) + resetsAtTime = dt_util.parse_datetime(resetsAt) - dt_util.utcnow() rate_limit_msg = ( "mobile_app push notification rate limits for %s: " "%d sent, %d allowed, %d errors, " diff --git a/homeassistant/components/mochad/__init__.py b/homeassistant/components/mochad/__init__.py index 07028de0d42..77426e8ae2c 100644 --- a/homeassistant/components/mochad/__init__.py +++ b/homeassistant/components/mochad/__init__.py @@ -64,7 +64,7 @@ class MochadCtrl: def __init__(self, host, port): """Initialize a PyMochad controller.""" - super(MochadCtrl, self).__init__() + super().__init__() self._host = host self._port = port diff --git a/homeassistant/components/mochad/manifest.json b/homeassistant/components/mochad/manifest.json index 0e5c4dd1ff3..8994223fe31 100644 --- a/homeassistant/components/mochad/manifest.json +++ b/homeassistant/components/mochad/manifest.json @@ -1,7 +1,7 @@ { "domain": "mochad", "name": "Mochad", - "documentation": "https://www.home-assistant.io/components/mochad", + "documentation": "https://www.home-assistant.io/integrations/mochad", "requirements": [ "pymochad==0.2.0" ], diff --git a/homeassistant/components/modbus/manifest.json b/homeassistant/components/modbus/manifest.json index e27f594b0af..8d271d5a95f 100644 --- a/homeassistant/components/modbus/manifest.json +++ b/homeassistant/components/modbus/manifest.json @@ -1,7 +1,7 @@ { "domain": "modbus", "name": "Modbus", - "documentation": "https://www.home-assistant.io/components/modbus", + "documentation": "https://www.home-assistant.io/integrations/modbus", "requirements": [ "pymodbus==1.5.2" ], diff --git a/homeassistant/components/modem_callerid/manifest.json b/homeassistant/components/modem_callerid/manifest.json index e3d6d19b803..80174b1a83a 100644 --- a/homeassistant/components/modem_callerid/manifest.json +++ b/homeassistant/components/modem_callerid/manifest.json @@ -1,7 +1,7 @@ { "domain": "modem_callerid", "name": "Modem callerid", - "documentation": "https://www.home-assistant.io/components/modem_callerid", + "documentation": "https://www.home-assistant.io/integrations/modem_callerid", "requirements": [ "basicmodem==0.7" ], diff --git a/homeassistant/components/mold_indicator/manifest.json b/homeassistant/components/mold_indicator/manifest.json index de4680927a4..1205b53ccaf 100644 --- a/homeassistant/components/mold_indicator/manifest.json +++ b/homeassistant/components/mold_indicator/manifest.json @@ -1,7 +1,7 @@ { "domain": "mold_indicator", "name": "Mold indicator", - "documentation": "https://www.home-assistant.io/components/mold_indicator", + "documentation": "https://www.home-assistant.io/integrations/mold_indicator", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/monoprice/manifest.json b/homeassistant/components/monoprice/manifest.json index aa07911a697..47db73ce0de 100644 --- a/homeassistant/components/monoprice/manifest.json +++ b/homeassistant/components/monoprice/manifest.json @@ -1,7 +1,7 @@ { "domain": "monoprice", "name": "Monoprice", - "documentation": "https://www.home-assistant.io/components/monoprice", + "documentation": "https://www.home-assistant.io/integrations/monoprice", "requirements": [ "pymonoprice==0.3" ], diff --git a/homeassistant/components/moon/.translations/sensor.no.json b/homeassistant/components/moon/.translations/sensor.no.json index a440fdde4f2..3998494606f 100644 --- a/homeassistant/components/moon/.translations/sensor.no.json +++ b/homeassistant/components/moon/.translations/sensor.no.json @@ -2,7 +2,7 @@ "state": { "first_quarter": "F\u00f8rste kvartal", "full_moon": "Fullm\u00e5ne", - "last_quarter": "Siste kvarter", + "last_quarter": "Siste kvartal", "new_moon": "Nym\u00e5ne", "waning_crescent": "Minkende m\u00e5nesigd", "waning_gibbous": "Minkende m\u00e5ne", diff --git a/homeassistant/components/moon/manifest.json b/homeassistant/components/moon/manifest.json index 50a93fce20a..56b5a1b8181 100644 --- a/homeassistant/components/moon/manifest.json +++ b/homeassistant/components/moon/manifest.json @@ -1,7 +1,7 @@ { "domain": "moon", "name": "Moon", - "documentation": "https://www.home-assistant.io/components/moon", + "documentation": "https://www.home-assistant.io/integrations/moon", "requirements": [], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/mopar/manifest.json b/homeassistant/components/mopar/manifest.json index 5acd5bbdcdb..ddb51c9e682 100644 --- a/homeassistant/components/mopar/manifest.json +++ b/homeassistant/components/mopar/manifest.json @@ -1,7 +1,7 @@ { "domain": "mopar", "name": "Mopar", - "documentation": "https://www.home-assistant.io/components/mopar", + "documentation": "https://www.home-assistant.io/integrations/mopar", "requirements": [ "motorparts==1.1.0" ], diff --git a/homeassistant/components/mpchc/manifest.json b/homeassistant/components/mpchc/manifest.json index e874ca28891..7d419243472 100644 --- a/homeassistant/components/mpchc/manifest.json +++ b/homeassistant/components/mpchc/manifest.json @@ -1,7 +1,7 @@ { "domain": "mpchc", "name": "Mpchc", - "documentation": "https://www.home-assistant.io/components/mpchc", + "documentation": "https://www.home-assistant.io/integrations/mpchc", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/mpd/manifest.json b/homeassistant/components/mpd/manifest.json index beee3137ef5..b7440f655db 100644 --- a/homeassistant/components/mpd/manifest.json +++ b/homeassistant/components/mpd/manifest.json @@ -1,7 +1,7 @@ { "domain": "mpd", "name": "Mpd", - "documentation": "https://www.home-assistant.io/components/mpd", + "documentation": "https://www.home-assistant.io/integrations/mpd", "requirements": [ "python-mpd2==1.0.0" ], diff --git a/homeassistant/components/mqtt/.translations/no.json b/homeassistant/components/mqtt/.translations/no.json index b3f1e4740b9..99a760dce4a 100644 --- a/homeassistant/components/mqtt/.translations/no.json +++ b/homeassistant/components/mqtt/.translations/no.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "Kun en enkelt konfigurasjon av MQTT er tillatt." + "single_instance_allowed": "Kun en konfigurasjon av MQTT er tillatt." }, "error": { "cannot_connect": "Kan ikke koble til megleren." diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index 8d83cd0cc2b..9b25a6ef6e4 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -39,7 +39,7 @@ from homeassistant.helpers import config_validation as cv, template from homeassistant.helpers.entity import Entity from homeassistant.helpers.typing import ConfigType, HomeAssistantType, ServiceDataType from homeassistant.loader import bind_hass -from homeassistant.util.async_ import run_callback_threadsafe, run_coroutine_threadsafe +from homeassistant.util.async_ import run_callback_threadsafe from homeassistant.util.logging import catch_log_exception # Loading the config flow file will register the flow @@ -463,7 +463,7 @@ def subscribe( encoding: str = "utf-8", ) -> Callable[[], None]: """Subscribe to an MQTT topic.""" - async_remove = run_coroutine_threadsafe( + async_remove = asyncio.run_coroutine_threadsafe( async_subscribe(hass, topic, msg_callback, qos, encoding), hass.loop ).result() diff --git a/homeassistant/components/mqtt/binary_sensor.py b/homeassistant/components/mqtt/binary_sensor.py index 4617fcf054a..bcf398464bc 100644 --- a/homeassistant/components/mqtt/binary_sensor.py +++ b/homeassistant/components/mqtt/binary_sensor.py @@ -1,4 +1,5 @@ """Support for MQTT binary sensors.""" +from datetime import timedelta import logging import voluptuous as vol @@ -21,7 +22,9 @@ from homeassistant.core import callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_connect import homeassistant.helpers.event as evt +from homeassistant.helpers.event import async_track_point_in_utc_time from homeassistant.helpers.typing import ConfigType, HomeAssistantType +from homeassistant.util import dt as dt_util from . import ( ATTR_DISCOVERY_HASH, @@ -43,12 +46,14 @@ CONF_OFF_DELAY = "off_delay" DEFAULT_PAYLOAD_OFF = "OFF" DEFAULT_PAYLOAD_ON = "ON" DEFAULT_FORCE_UPDATE = False +CONF_EXPIRE_AFTER = "expire_after" PLATFORM_SCHEMA = ( mqtt.MQTT_RO_PLATFORM_SCHEMA.extend( { vol.Optional(CONF_DEVICE): mqtt.MQTT_ENTITY_DEVICE_INFO_SCHEMA, vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA, + vol.Optional(CONF_EXPIRE_AFTER): cv.positive_int, vol.Optional(CONF_FORCE_UPDATE, default=DEFAULT_FORCE_UPDATE): cv.boolean, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_OFF_DELAY): vol.All(vol.Coerce(int), vol.Range(min=0)), @@ -112,8 +117,9 @@ class MqttBinarySensor( self._unique_id = config.get(CONF_UNIQUE_ID) self._state = None self._sub_state = None + self._expiration_trigger = None self._delay_listener = None - + self._expired = None device_config = config.get(CONF_DEVICE) MqttAttributes.__init__(self, config) @@ -153,6 +159,26 @@ class MqttBinarySensor( def state_message_received(msg): """Handle a new received MQTT state message.""" payload = msg.payload + # auto-expire enabled? + expire_after = self._config.get(CONF_EXPIRE_AFTER) + + if expire_after is not None and expire_after > 0: + + # When expire_after is set, and we receive a message, assume device is not expired since it has to be to receive the message + self._expired = False + + # Reset old trigger + if self._expiration_trigger: + self._expiration_trigger() + self._expiration_trigger = None + + # Set new trigger + expiration_at = dt_util.utcnow() + timedelta(seconds=expire_after) + + self._expiration_trigger = async_track_point_in_utc_time( + self.hass, self.value_is_expired, expiration_at + ) + value_template = self._config.get(CONF_VALUE_TEMPLATE) if value_template is not None: payload = value_template.async_render_with_possible_json_value( @@ -202,6 +228,15 @@ class MqttBinarySensor( await MqttAttributes.async_will_remove_from_hass(self) await MqttAvailability.async_will_remove_from_hass(self) + @callback + def value_is_expired(self, *_): + """Triggered when value is expired.""" + + self._expiration_trigger = None + self._expired = True + + self.async_write_ha_state() + @property def should_poll(self): """Return the polling state.""" @@ -231,3 +266,12 @@ class MqttBinarySensor( def unique_id(self): """Return a unique ID.""" return self._unique_id + + @property + def available(self) -> bool: + """Return true if the device is available and value has not expired.""" + expire_after = self._config.get(CONF_EXPIRE_AFTER) + # pylint: disable=no-member + return MqttAvailability.available.fget(self) and ( + expire_after is None or not self._expired + ) diff --git a/homeassistant/components/mqtt/manifest.json b/homeassistant/components/mqtt/manifest.json index d63d1707fac..c1b6659e1c0 100644 --- a/homeassistant/components/mqtt/manifest.json +++ b/homeassistant/components/mqtt/manifest.json @@ -2,9 +2,9 @@ "domain": "mqtt", "name": "MQTT", "config_flow": true, - "documentation": "https://www.home-assistant.io/components/mqtt", + "documentation": "https://www.home-assistant.io/integrations/mqtt", "requirements": [ - "hbmqtt==0.9.4", + "hbmqtt==0.9.5", "paho-mqtt==1.4.0" ], "dependencies": [ diff --git a/homeassistant/components/mqtt_eventstream/manifest.json b/homeassistant/components/mqtt_eventstream/manifest.json index e795c8aaf18..0d36cd6616d 100644 --- a/homeassistant/components/mqtt_eventstream/manifest.json +++ b/homeassistant/components/mqtt_eventstream/manifest.json @@ -1,7 +1,7 @@ { "domain": "mqtt_eventstream", "name": "Mqtt eventstream", - "documentation": "https://www.home-assistant.io/components/mqtt_eventstream", + "documentation": "https://www.home-assistant.io/integrations/mqtt_eventstream", "requirements": [], "dependencies": [ "mqtt" diff --git a/homeassistant/components/mqtt_json/manifest.json b/homeassistant/components/mqtt_json/manifest.json index a1986b2bf2e..b5448839794 100644 --- a/homeassistant/components/mqtt_json/manifest.json +++ b/homeassistant/components/mqtt_json/manifest.json @@ -1,7 +1,7 @@ { "domain": "mqtt_json", "name": "Mqtt json", - "documentation": "https://www.home-assistant.io/components/mqtt_json", + "documentation": "https://www.home-assistant.io/integrations/mqtt_json", "requirements": [], "dependencies": [ "mqtt" diff --git a/homeassistant/components/mqtt_room/manifest.json b/homeassistant/components/mqtt_room/manifest.json index 8fc90b0bcb1..93450b00163 100644 --- a/homeassistant/components/mqtt_room/manifest.json +++ b/homeassistant/components/mqtt_room/manifest.json @@ -1,7 +1,7 @@ { "domain": "mqtt_room", "name": "Mqtt room", - "documentation": "https://www.home-assistant.io/components/mqtt_room", + "documentation": "https://www.home-assistant.io/integrations/mqtt_room", "requirements": [], "dependencies": [ "mqtt" diff --git a/homeassistant/components/mqtt_statestream/manifest.json b/homeassistant/components/mqtt_statestream/manifest.json index 5fa99363729..840a53591a8 100644 --- a/homeassistant/components/mqtt_statestream/manifest.json +++ b/homeassistant/components/mqtt_statestream/manifest.json @@ -1,7 +1,7 @@ { "domain": "mqtt_statestream", "name": "Mqtt statestream", - "documentation": "https://www.home-assistant.io/components/mqtt_statestream", + "documentation": "https://www.home-assistant.io/integrations/mqtt_statestream", "requirements": [], "dependencies": [ "mqtt" diff --git a/homeassistant/components/mvglive/manifest.json b/homeassistant/components/mvglive/manifest.json index 5626e244484..e47b7c07d8b 100644 --- a/homeassistant/components/mvglive/manifest.json +++ b/homeassistant/components/mvglive/manifest.json @@ -1,7 +1,7 @@ { "domain": "mvglive", "name": "Mvglive", - "documentation": "https://www.home-assistant.io/components/mvglive", + "documentation": "https://www.home-assistant.io/integrations/mvglive", "requirements": [ "PyMVGLive==1.1.4" ], diff --git a/homeassistant/components/mvglive/sensor.py b/homeassistant/components/mvglive/sensor.py index 6da26784d9c..3c753d832e0 100644 --- a/homeassistant/components/mvglive/sensor.py +++ b/homeassistant/components/mvglive/sensor.py @@ -189,17 +189,19 @@ class MVGLiveData: and _departure["destination"] not in self._destinations ): continue - elif ( + + if ( "" not in self._directions[:1] and _departure["direction"] not in self._directions ): continue - elif ( - "" not in self._lines[:1] and _departure["linename"] not in self._lines - ): + + if "" not in self._lines[:1] and _departure["linename"] not in self._lines: continue - elif _departure["time"] < self._timeoffset: + + if _departure["time"] < self._timeoffset: continue + # now select the relevant data _nextdep = {ATTR_ATTRIBUTION: ATTRIBUTION} for k in ["destination", "linename", "time", "direction", "product"]: diff --git a/homeassistant/components/mychevy/manifest.json b/homeassistant/components/mychevy/manifest.json index 1ff997372ed..20933c9b2fc 100644 --- a/homeassistant/components/mychevy/manifest.json +++ b/homeassistant/components/mychevy/manifest.json @@ -1,7 +1,7 @@ { "domain": "mychevy", "name": "Mychevy", - "documentation": "https://www.home-assistant.io/components/mychevy", + "documentation": "https://www.home-assistant.io/integrations/mychevy", "requirements": [ "mychevy==1.2.0" ], diff --git a/homeassistant/components/mycroft/manifest.json b/homeassistant/components/mycroft/manifest.json index 77e5a524aac..5d5ee195381 100644 --- a/homeassistant/components/mycroft/manifest.json +++ b/homeassistant/components/mycroft/manifest.json @@ -1,7 +1,7 @@ { "domain": "mycroft", "name": "Mycroft", - "documentation": "https://www.home-assistant.io/components/mycroft", + "documentation": "https://www.home-assistant.io/integrations/mycroft", "requirements": [ "mycroftapi==2.0" ], diff --git a/homeassistant/components/myq/manifest.json b/homeassistant/components/myq/manifest.json index b870ff66309..213679b320a 100644 --- a/homeassistant/components/myq/manifest.json +++ b/homeassistant/components/myq/manifest.json @@ -1,7 +1,7 @@ { "domain": "myq", "name": "Myq", - "documentation": "https://www.home-assistant.io/components/myq", + "documentation": "https://www.home-assistant.io/integrations/myq", "requirements": [ "pymyq==1.2.1" ], diff --git a/homeassistant/components/mysensors/climate.py b/homeassistant/components/mysensors/climate.py index f534f96b780..dc053e60de1 100644 --- a/homeassistant/components/mysensors/climate.py +++ b/homeassistant/components/mysensors/climate.py @@ -156,7 +156,9 @@ class MySensorsHVAC(mysensors.device.MySensorsEntity, ClimateDevice): (set_req.V_HVAC_SETPOINT_COOL, high), ] for value_type, value in updates: - self.gateway.set_child_value(self.node_id, self.child_id, value_type, value) + self.gateway.set_child_value( + self.node_id, self.child_id, value_type, value, ack=1 + ) if self.gateway.optimistic: # Optimistically assume that device has changed state self._values[value_type] = value @@ -166,7 +168,7 @@ class MySensorsHVAC(mysensors.device.MySensorsEntity, ClimateDevice): """Set new target temperature.""" set_req = self.gateway.const.SetReq self.gateway.set_child_value( - self.node_id, self.child_id, set_req.V_HVAC_SPEED, fan_mode + self.node_id, self.child_id, set_req.V_HVAC_SPEED, fan_mode, ack=1 ) if self.gateway.optimistic: # Optimistically assume that device has changed state @@ -176,7 +178,11 @@ class MySensorsHVAC(mysensors.device.MySensorsEntity, ClimateDevice): async def async_set_hvac_mode(self, hvac_mode): """Set new target temperature.""" self.gateway.set_child_value( - self.node_id, self.child_id, self.value_type, DICT_HA_TO_MYS[hvac_mode] + self.node_id, + self.child_id, + self.value_type, + DICT_HA_TO_MYS[hvac_mode], + ack=1, ) if self.gateway.optimistic: # Optimistically assume that device has changed state diff --git a/homeassistant/components/mysensors/const.py b/homeassistant/components/mysensors/const.py index d12ecd9d3a6..45f603a2cb4 100644 --- a/homeassistant/components/mysensors/const.py +++ b/homeassistant/components/mysensors/const.py @@ -26,72 +26,72 @@ TYPE = "type" UPDATE_DELAY = 0.1 BINARY_SENSOR_TYPES = { - "S_DOOR": "V_TRIPPED", - "S_MOTION": "V_TRIPPED", - "S_SMOKE": "V_TRIPPED", - "S_SPRINKLER": "V_TRIPPED", - "S_WATER_LEAK": "V_TRIPPED", - "S_SOUND": "V_TRIPPED", - "S_VIBRATION": "V_TRIPPED", - "S_MOISTURE": "V_TRIPPED", + "S_DOOR": {"V_TRIPPED"}, + "S_MOTION": {"V_TRIPPED"}, + "S_SMOKE": {"V_TRIPPED"}, + "S_SPRINKLER": {"V_TRIPPED"}, + "S_WATER_LEAK": {"V_TRIPPED"}, + "S_SOUND": {"V_TRIPPED"}, + "S_VIBRATION": {"V_TRIPPED"}, + "S_MOISTURE": {"V_TRIPPED"}, } -CLIMATE_TYPES = {"S_HVAC": "V_HVAC_FLOW_STATE"} +CLIMATE_TYPES = {"S_HVAC": {"V_HVAC_FLOW_STATE"}} -COVER_TYPES = {"S_COVER": ["V_DIMMER", "V_PERCENTAGE", "V_LIGHT", "V_STATUS"]} +COVER_TYPES = {"S_COVER": {"V_DIMMER", "V_PERCENTAGE", "V_LIGHT", "V_STATUS"}} -DEVICE_TRACKER_TYPES = {"S_GPS": "V_POSITION"} +DEVICE_TRACKER_TYPES = {"S_GPS": {"V_POSITION"}} LIGHT_TYPES = { - "S_DIMMER": ["V_DIMMER", "V_PERCENTAGE"], - "S_RGB_LIGHT": "V_RGB", - "S_RGBW_LIGHT": "V_RGBW", + "S_DIMMER": {"V_DIMMER", "V_PERCENTAGE"}, + "S_RGB_LIGHT": {"V_RGB"}, + "S_RGBW_LIGHT": {"V_RGBW"}, } -NOTIFY_TYPES = {"S_INFO": "V_TEXT"} +NOTIFY_TYPES = {"S_INFO": {"V_TEXT"}} SENSOR_TYPES = { - "S_SOUND": "V_LEVEL", - "S_VIBRATION": "V_LEVEL", - "S_MOISTURE": "V_LEVEL", - "S_INFO": "V_TEXT", - "S_GPS": "V_POSITION", - "S_TEMP": "V_TEMP", - "S_HUM": "V_HUM", - "S_BARO": ["V_PRESSURE", "V_FORECAST"], - "S_WIND": ["V_WIND", "V_GUST", "V_DIRECTION"], - "S_RAIN": ["V_RAIN", "V_RAINRATE"], - "S_UV": "V_UV", - "S_WEIGHT": ["V_WEIGHT", "V_IMPEDANCE"], - "S_POWER": ["V_WATT", "V_KWH", "V_VAR", "V_VA", "V_POWER_FACTOR"], - "S_DISTANCE": "V_DISTANCE", - "S_LIGHT_LEVEL": ["V_LIGHT_LEVEL", "V_LEVEL"], - "S_IR": "V_IR_RECEIVE", - "S_WATER": ["V_FLOW", "V_VOLUME"], - "S_CUSTOM": ["V_VAR1", "V_VAR2", "V_VAR3", "V_VAR4", "V_VAR5", "V_CUSTOM"], - "S_SCENE_CONTROLLER": ["V_SCENE_ON", "V_SCENE_OFF"], - "S_COLOR_SENSOR": "V_RGB", - "S_MULTIMETER": ["V_VOLTAGE", "V_CURRENT", "V_IMPEDANCE"], - "S_GAS": ["V_FLOW", "V_VOLUME"], - "S_WATER_QUALITY": ["V_TEMP", "V_PH", "V_ORP", "V_EC"], - "S_AIR_QUALITY": ["V_DUST_LEVEL", "V_LEVEL"], - "S_DUST": ["V_DUST_LEVEL", "V_LEVEL"], + "S_SOUND": {"V_LEVEL"}, + "S_VIBRATION": {"V_LEVEL"}, + "S_MOISTURE": {"V_LEVEL"}, + "S_INFO": {"V_TEXT"}, + "S_GPS": {"V_POSITION"}, + "S_TEMP": {"V_TEMP"}, + "S_HUM": {"V_HUM"}, + "S_BARO": {"V_PRESSURE", "V_FORECAST"}, + "S_WIND": {"V_WIND", "V_GUST", "V_DIRECTION"}, + "S_RAIN": {"V_RAIN", "V_RAINRATE"}, + "S_UV": {"V_UV"}, + "S_WEIGHT": {"V_WEIGHT", "V_IMPEDANCE"}, + "S_POWER": {"V_WATT", "V_KWH", "V_VAR", "V_VA", "V_POWER_FACTOR"}, + "S_DISTANCE": {"V_DISTANCE"}, + "S_LIGHT_LEVEL": {"V_LIGHT_LEVEL", "V_LEVEL"}, + "S_IR": {"V_IR_RECEIVE"}, + "S_WATER": {"V_FLOW", "V_VOLUME"}, + "S_CUSTOM": {"V_VAR1", "V_VAR2", "V_VAR3", "V_VAR4", "V_VAR5", "V_CUSTOM"}, + "S_SCENE_CONTROLLER": {"V_SCENE_ON", "V_SCENE_OFF"}, + "S_COLOR_SENSOR": {"V_RGB"}, + "S_MULTIMETER": {"V_VOLTAGE", "V_CURRENT", "V_IMPEDANCE"}, + "S_GAS": {"V_FLOW", "V_VOLUME"}, + "S_WATER_QUALITY": {"V_TEMP", "V_PH", "V_ORP", "V_EC"}, + "S_AIR_QUALITY": {"V_DUST_LEVEL", "V_LEVEL"}, + "S_DUST": {"V_DUST_LEVEL", "V_LEVEL"}, } SWITCH_TYPES = { - "S_LIGHT": "V_LIGHT", - "S_BINARY": "V_STATUS", - "S_DOOR": "V_ARMED", - "S_MOTION": "V_ARMED", - "S_SMOKE": "V_ARMED", - "S_SPRINKLER": "V_STATUS", - "S_WATER_LEAK": "V_ARMED", - "S_SOUND": "V_ARMED", - "S_VIBRATION": "V_ARMED", - "S_MOISTURE": "V_ARMED", - "S_IR": "V_IR_SEND", - "S_LOCK": "V_LOCK_STATUS", - "S_WATER_QUALITY": "V_STATUS", + "S_LIGHT": {"V_LIGHT"}, + "S_BINARY": {"V_STATUS"}, + "S_DOOR": {"V_ARMED"}, + "S_MOTION": {"V_ARMED"}, + "S_SMOKE": {"V_ARMED"}, + "S_SPRINKLER": {"V_STATUS"}, + "S_WATER_LEAK": {"V_ARMED"}, + "S_SOUND": {"V_ARMED"}, + "S_VIBRATION": {"V_ARMED"}, + "S_MOISTURE": {"V_ARMED"}, + "S_IR": {"V_IR_SEND"}, + "S_LOCK": {"V_LOCK_STATUS"}, + "S_WATER_QUALITY": {"V_STATUS"}, } diff --git a/homeassistant/components/mysensors/cover.py b/homeassistant/components/mysensors/cover.py index bb764e375f3..6c02e430ba4 100644 --- a/homeassistant/components/mysensors/cover.py +++ b/homeassistant/components/mysensors/cover.py @@ -43,7 +43,9 @@ class MySensorsCover(mysensors.device.MySensorsEntity, CoverDevice): async def async_open_cover(self, **kwargs): """Move the cover up.""" set_req = self.gateway.const.SetReq - self.gateway.set_child_value(self.node_id, self.child_id, set_req.V_UP, 1) + self.gateway.set_child_value( + self.node_id, self.child_id, set_req.V_UP, 1, ack=1 + ) if self.gateway.optimistic: # Optimistically assume that cover has changed state. if set_req.V_DIMMER in self._values: @@ -55,7 +57,9 @@ class MySensorsCover(mysensors.device.MySensorsEntity, CoverDevice): async def async_close_cover(self, **kwargs): """Move the cover down.""" set_req = self.gateway.const.SetReq - self.gateway.set_child_value(self.node_id, self.child_id, set_req.V_DOWN, 1) + self.gateway.set_child_value( + self.node_id, self.child_id, set_req.V_DOWN, 1, ack=1 + ) if self.gateway.optimistic: # Optimistically assume that cover has changed state. if set_req.V_DIMMER in self._values: @@ -69,7 +73,7 @@ class MySensorsCover(mysensors.device.MySensorsEntity, CoverDevice): position = kwargs.get(ATTR_POSITION) set_req = self.gateway.const.SetReq self.gateway.set_child_value( - self.node_id, self.child_id, set_req.V_DIMMER, position + self.node_id, self.child_id, set_req.V_DIMMER, position, ack=1 ) if self.gateway.optimistic: # Optimistically assume that cover has changed state. @@ -79,4 +83,6 @@ class MySensorsCover(mysensors.device.MySensorsEntity, CoverDevice): async def async_stop_cover(self, **kwargs): """Stop the device.""" set_req = self.gateway.const.SetReq - self.gateway.set_child_value(self.node_id, self.child_id, set_req.V_STOP, 1) + self.gateway.set_child_value( + self.node_id, self.child_id, set_req.V_STOP, 1, ack=1 + ) diff --git a/homeassistant/components/mysensors/helpers.py b/homeassistant/components/mysensors/helpers.py index fda89158293..f0e9b06b762 100644 --- a/homeassistant/components/mysensors/helpers.py +++ b/homeassistant/components/mysensors/helpers.py @@ -121,20 +121,23 @@ def validate_child(gateway, node_id, child, value_type=None): child_type_name = next( (member.name for member in pres if member.value == child.type), None ) - value_types = [value_type] if value_type else [*child.values] - value_type_names = [ + value_types = {value_type} if value_type else {*child.values} + value_type_names = { member.name for member in set_req if member.value in value_types - ] + } platforms = TYPE_TO_PLATFORMS.get(child_type_name, []) if not platforms: _LOGGER.warning("Child type %s is not supported", child.type) return validated for platform in platforms: - v_names = FLAT_PLATFORM_TYPES[platform, child_type_name] - if not isinstance(v_names, list): - v_names = [v_names] - v_names = [v_name for v_name in v_names if v_name in value_type_names] + platform_v_names = FLAT_PLATFORM_TYPES[platform, child_type_name] + v_names = platform_v_names & value_type_names + if not v_names: + child_value_names = { + member.name for member in set_req if member.value in child.values + } + v_names = platform_v_names & child_value_names for v_name in v_names: child_schema_gen = SCHEMAS.get((platform, v_name), default_schema) diff --git a/homeassistant/components/mysensors/light.py b/homeassistant/components/mysensors/light.py index 3936aefab0c..8f0d0906311 100644 --- a/homeassistant/components/mysensors/light.py +++ b/homeassistant/components/mysensors/light.py @@ -75,7 +75,9 @@ class MySensorsLight(mysensors.device.MySensorsEntity, Light): if self._state: return - self.gateway.set_child_value(self.node_id, self.child_id, set_req.V_LIGHT, 1) + self.gateway.set_child_value( + self.node_id, self.child_id, set_req.V_LIGHT, 1, ack=1 + ) if self.gateway.optimistic: # optimistically assume that light has changed state @@ -96,7 +98,7 @@ class MySensorsLight(mysensors.device.MySensorsEntity, Light): brightness = kwargs[ATTR_BRIGHTNESS] percent = round(100 * brightness / 255) self.gateway.set_child_value( - self.node_id, self.child_id, set_req.V_DIMMER, percent + self.node_id, self.child_id, set_req.V_DIMMER, percent, ack=1 ) if self.gateway.optimistic: @@ -129,7 +131,7 @@ class MySensorsLight(mysensors.device.MySensorsEntity, Light): if len(rgb) > 3: white = rgb.pop() self.gateway.set_child_value( - self.node_id, self.child_id, self.value_type, hex_color + self.node_id, self.child_id, self.value_type, hex_color, ack=1 ) if self.gateway.optimistic: @@ -141,7 +143,7 @@ class MySensorsLight(mysensors.device.MySensorsEntity, Light): async def async_turn_off(self, **kwargs): """Turn the device off.""" value_type = self.gateway.const.SetReq.V_LIGHT - self.gateway.set_child_value(self.node_id, self.child_id, value_type, 0) + self.gateway.set_child_value(self.node_id, self.child_id, value_type, 0, ack=1) if self.gateway.optimistic: # optimistically assume that light has changed state self._state = False diff --git a/homeassistant/components/mysensors/manifest.json b/homeassistant/components/mysensors/manifest.json index f18f5d4f8dd..8701424ea60 100644 --- a/homeassistant/components/mysensors/manifest.json +++ b/homeassistant/components/mysensors/manifest.json @@ -1,7 +1,7 @@ { "domain": "mysensors", "name": "Mysensors", - "documentation": "https://www.home-assistant.io/components/mysensors", + "documentation": "https://www.home-assistant.io/integrations/mysensors", "requirements": [ "pymysensors==0.18.0" ], @@ -9,5 +9,7 @@ "after_dependencies": [ "mqtt" ], - "codeowners": [] + "codeowners": [ + "@MartinHjelmare" + ] } diff --git a/homeassistant/components/mysensors/switch.py b/homeassistant/components/mysensors/switch.py index df429460541..c624aaafa34 100644 --- a/homeassistant/components/mysensors/switch.py +++ b/homeassistant/components/mysensors/switch.py @@ -92,7 +92,9 @@ class MySensorsSwitch(mysensors.device.MySensorsEntity, SwitchDevice): async def async_turn_on(self, **kwargs): """Turn the switch on.""" - self.gateway.set_child_value(self.node_id, self.child_id, self.value_type, 1) + self.gateway.set_child_value( + self.node_id, self.child_id, self.value_type, 1, ack=1 + ) if self.gateway.optimistic: # Optimistically assume that switch has changed state self._values[self.value_type] = STATE_ON @@ -100,7 +102,9 @@ class MySensorsSwitch(mysensors.device.MySensorsEntity, SwitchDevice): async def async_turn_off(self, **kwargs): """Turn the switch off.""" - self.gateway.set_child_value(self.node_id, self.child_id, self.value_type, 0) + self.gateway.set_child_value( + self.node_id, self.child_id, self.value_type, 0, ack=1 + ) if self.gateway.optimistic: # Optimistically assume that switch has changed state self._values[self.value_type] = STATE_OFF @@ -129,7 +133,9 @@ class MySensorsIRSwitch(MySensorsSwitch): self.gateway.set_child_value( self.node_id, self.child_id, self.value_type, self._ir_code ) - self.gateway.set_child_value(self.node_id, self.child_id, set_req.V_LIGHT, 1) + self.gateway.set_child_value( + self.node_id, self.child_id, set_req.V_LIGHT, 1, ack=1 + ) if self.gateway.optimistic: # Optimistically assume that switch has changed state self._values[self.value_type] = self._ir_code @@ -141,7 +147,9 @@ class MySensorsIRSwitch(MySensorsSwitch): async def async_turn_off(self, **kwargs): """Turn the IR switch off.""" set_req = self.gateway.const.SetReq - self.gateway.set_child_value(self.node_id, self.child_id, set_req.V_LIGHT, 0) + self.gateway.set_child_value( + self.node_id, self.child_id, set_req.V_LIGHT, 0, ack=1 + ) if self.gateway.optimistic: # Optimistically assume that switch has changed state self._values[set_req.V_LIGHT] = STATE_OFF diff --git a/homeassistant/components/mystrom/manifest.json b/homeassistant/components/mystrom/manifest.json index 0e17f33f72e..fe09461bc82 100644 --- a/homeassistant/components/mystrom/manifest.json +++ b/homeassistant/components/mystrom/manifest.json @@ -1,7 +1,7 @@ { "domain": "mystrom", "name": "Mystrom", - "documentation": "https://www.home-assistant.io/components/mystrom", + "documentation": "https://www.home-assistant.io/integrations/mystrom", "requirements": [ "python-mystrom==0.5.0" ], diff --git a/homeassistant/components/mythicbeastsdns/manifest.json b/homeassistant/components/mythicbeastsdns/manifest.json index 4e37544a99a..b912a80f75d 100644 --- a/homeassistant/components/mythicbeastsdns/manifest.json +++ b/homeassistant/components/mythicbeastsdns/manifest.json @@ -1,7 +1,7 @@ { "domain": "mythicbeastsdns", "name": "Mythicbeastsdns", - "documentation": "https://www.home-assistant.io/components/mythicbeastsdns", + "documentation": "https://www.home-assistant.io/integrations/mythicbeastsdns", "requirements": [ "mbddns==0.1.2" ], diff --git a/homeassistant/components/n26/manifest.json b/homeassistant/components/n26/manifest.json index b49932887d5..7f010ec6d39 100644 --- a/homeassistant/components/n26/manifest.json +++ b/homeassistant/components/n26/manifest.json @@ -1,7 +1,7 @@ { "domain": "n26", "name": "N26", - "documentation": "https://www.home-assistant.io/components/n26", + "documentation": "https://www.home-assistant.io/integrations/n26", "requirements": [ "n26==0.2.7" ], diff --git a/homeassistant/components/nad/manifest.json b/homeassistant/components/nad/manifest.json index c624acd73da..7e01f818e35 100644 --- a/homeassistant/components/nad/manifest.json +++ b/homeassistant/components/nad/manifest.json @@ -1,7 +1,7 @@ { "domain": "nad", "name": "Nad", - "documentation": "https://www.home-assistant.io/components/nad", + "documentation": "https://www.home-assistant.io/integrations/nad", "requirements": [ "nad_receiver==0.0.11" ], diff --git a/homeassistant/components/namecheapdns/manifest.json b/homeassistant/components/namecheapdns/manifest.json index e75e2caa37a..aa1e2eb7422 100644 --- a/homeassistant/components/namecheapdns/manifest.json +++ b/homeassistant/components/namecheapdns/manifest.json @@ -1,7 +1,7 @@ { "domain": "namecheapdns", "name": "Namecheapdns", - "documentation": "https://www.home-assistant.io/components/namecheapdns", + "documentation": "https://www.home-assistant.io/integrations/namecheapdns", "requirements": [ "defusedxml==0.6.0" ], diff --git a/homeassistant/components/nanoleaf/manifest.json b/homeassistant/components/nanoleaf/manifest.json index a59a6352af2..3318ad3438f 100644 --- a/homeassistant/components/nanoleaf/manifest.json +++ b/homeassistant/components/nanoleaf/manifest.json @@ -1,7 +1,7 @@ { "domain": "nanoleaf", "name": "Nanoleaf", - "documentation": "https://www.home-assistant.io/components/nanoleaf", + "documentation": "https://www.home-assistant.io/integrations/nanoleaf", "requirements": [ "pynanoleaf==0.0.5" ], diff --git a/homeassistant/components/neato/.translations/ca.json b/homeassistant/components/neato/.translations/ca.json new file mode 100644 index 00000000000..d30f9e5ad4b --- /dev/null +++ b/homeassistant/components/neato/.translations/ca.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Ja configurat", + "invalid_credentials": "Credencials inv\u00e0lides" + }, + "create_entry": { + "default": "Consulta la [documentaci\u00f3 de Neato]({docs_url})." + }, + "error": { + "invalid_credentials": "Credencials inv\u00e0lides", + "unexpected_error": "Error inesperat" + }, + "step": { + "user": { + "data": { + "password": "Contrasenya", + "username": "Nom d'usuari", + "vendor": "Venedor" + }, + "description": "Consulta la [documentaci\u00f3 de Neato]({docs_url}).", + "title": "Informaci\u00f3 del compte Neato" + } + }, + "title": "Neato" + } +} \ No newline at end of file diff --git a/homeassistant/components/neato/.translations/da.json b/homeassistant/components/neato/.translations/da.json new file mode 100644 index 00000000000..7f0d122f38b --- /dev/null +++ b/homeassistant/components/neato/.translations/da.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Allerede konfigureret", + "invalid_credentials": "Ugyldige legitimationsoplysninger" + }, + "error": { + "invalid_credentials": "Ugyldige legitimationsoplysninger", + "unexpected_error": "Uventet fejl" + }, + "step": { + "user": { + "data": { + "password": "Adgangskode", + "username": "Brugernavn" + }, + "description": "Se [Neato-dokumentation] ({docs_url}).", + "title": "Neato kontooplysninger" + } + }, + "title": "Neato" + } +} \ No newline at end of file diff --git a/homeassistant/components/neato/.translations/de.json b/homeassistant/components/neato/.translations/de.json new file mode 100644 index 00000000000..2a75d11a9ec --- /dev/null +++ b/homeassistant/components/neato/.translations/de.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Bereits konfiguriert", + "invalid_credentials": "Ung\u00fcltige Anmeldeinformationen" + }, + "create_entry": { + "default": "Siehe [Neato-Dokumentation]({docs_url})." + }, + "error": { + "invalid_credentials": "Ung\u00fcltige Anmeldeinformationen", + "unexpected_error": "Unerwarteter Fehler" + }, + "step": { + "user": { + "data": { + "password": "Passwort", + "username": "Benutzername", + "vendor": "Hersteller" + }, + "description": "Siehe [Neato-Dokumentation]({docs_url}).", + "title": "Neato-Kontoinformationen" + } + }, + "title": "Neato" + } +} \ No newline at end of file diff --git a/homeassistant/components/neato/.translations/en.json b/homeassistant/components/neato/.translations/en.json new file mode 100644 index 00000000000..73628c8646e --- /dev/null +++ b/homeassistant/components/neato/.translations/en.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Already configured", + "invalid_credentials": "Invalid credentials" + }, + "create_entry": { + "default": "See [Neato documentation]({docs_url})." + }, + "error": { + "invalid_credentials": "Invalid credentials", + "unexpected_error": "Unexpected error" + }, + "step": { + "user": { + "data": { + "password": "Password", + "username": "Username", + "vendor": "Vendor" + }, + "description": "See [Neato documentation]({docs_url}).", + "title": "Neato Account Info" + } + }, + "title": "Neato" + } +} \ No newline at end of file diff --git a/homeassistant/components/neato/.translations/es.json b/homeassistant/components/neato/.translations/es.json new file mode 100644 index 00000000000..99e7574e4b2 --- /dev/null +++ b/homeassistant/components/neato/.translations/es.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Ya est\u00e1 configurado", + "invalid_credentials": "Credenciales no v\u00e1lidas" + }, + "create_entry": { + "default": "Ver [documentaci\u00f3n Neato]({docs_url})." + }, + "error": { + "invalid_credentials": "Credenciales no v\u00e1lidas", + "unexpected_error": "Error inesperado" + }, + "step": { + "user": { + "data": { + "password": "Contrase\u00f1a", + "username": "Usuario", + "vendor": "Vendedor" + }, + "description": "Ver [documentaci\u00f3n Neato]({docs_url}).", + "title": "Informaci\u00f3n de la cuenta de Neato" + } + }, + "title": "Neato" + } +} \ No newline at end of file diff --git a/homeassistant/components/neato/.translations/fr.json b/homeassistant/components/neato/.translations/fr.json new file mode 100644 index 00000000000..941ed18660e --- /dev/null +++ b/homeassistant/components/neato/.translations/fr.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "D\u00e9j\u00e0 configur\u00e9", + "invalid_credentials": "Informations d'identification invalides" + }, + "create_entry": { + "default": "Voir [Documentation Neato]({docs_url})." + }, + "error": { + "invalid_credentials": "Informations d'identification invalides", + "unexpected_error": "Erreur inattendue" + }, + "step": { + "user": { + "data": { + "password": "Mot de passe", + "username": "Nom d'utilisateur", + "vendor": "Vendeur" + }, + "description": "Voir [Documentation Neato] ( {docs_url} ).", + "title": "Informations compte Neato" + } + }, + "title": "Neato" + } +} \ No newline at end of file diff --git a/homeassistant/components/neato/.translations/it.json b/homeassistant/components/neato/.translations/it.json new file mode 100644 index 00000000000..d5615815dc7 --- /dev/null +++ b/homeassistant/components/neato/.translations/it.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Gi\u00e0 configurato", + "invalid_credentials": "Credenziali non valide" + }, + "create_entry": { + "default": "Vedere la [Documentazione di Neato]({docs_url})." + }, + "error": { + "invalid_credentials": "Credenziali non valide", + "unexpected_error": "Errore inaspettato" + }, + "step": { + "user": { + "data": { + "password": "Password", + "username": "Nome utente", + "vendor": "Fornitore" + }, + "description": "Vedere la [Documentazione di Neato]({docs_url}).", + "title": "Informazioni sull'account Neato" + } + }, + "title": "Neato" + } +} \ No newline at end of file diff --git a/homeassistant/components/neato/.translations/lb.json b/homeassistant/components/neato/.translations/lb.json new file mode 100644 index 00000000000..3043ec6ec37 --- /dev/null +++ b/homeassistant/components/neato/.translations/lb.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Scho konfigur\u00e9iert", + "invalid_credentials": "Ong\u00eblteg Login Informatioune" + }, + "create_entry": { + "default": "Kuckt [Neato Dokumentatioun]({docs_url})." + }, + "error": { + "invalid_credentials": "Ong\u00eblteg Login Informatioune", + "unexpected_error": "Onerwaarte Feeler" + }, + "step": { + "user": { + "data": { + "password": "Passwuert", + "username": "Benotzernumm", + "vendor": "Hiersteller" + }, + "description": "Kuckt [Neato Dokumentatioun]({docs_url}).", + "title": "Neato Kont Informatiounen" + } + }, + "title": "Neato" + } +} \ No newline at end of file diff --git a/homeassistant/components/neato/.translations/nn.json b/homeassistant/components/neato/.translations/nn.json new file mode 100644 index 00000000000..e04e73aef24 --- /dev/null +++ b/homeassistant/components/neato/.translations/nn.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "username": "Brukarnamn" + } + } + }, + "title": "Neato" + } +} \ No newline at end of file diff --git a/homeassistant/components/neato/.translations/no.json b/homeassistant/components/neato/.translations/no.json new file mode 100644 index 00000000000..084c4b50e45 --- /dev/null +++ b/homeassistant/components/neato/.translations/no.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Allerede konfigurert", + "invalid_credentials": "Ugyldig brukerinformasjon" + }, + "create_entry": { + "default": "Se [Neato dokumentasjon]({docs_url})." + }, + "error": { + "invalid_credentials": "Ugyldig brukerinformasjon", + "unexpected_error": "Uventet feil" + }, + "step": { + "user": { + "data": { + "password": "Passord", + "username": "Brukernavn", + "vendor": "Leverand\u00f8r" + }, + "description": "Se [Neato dokumentasjon]({docs_url}).", + "title": "Neato kontoinformasjon" + } + }, + "title": "Neato" + } +} \ No newline at end of file diff --git a/homeassistant/components/neato/.translations/pl.json b/homeassistant/components/neato/.translations/pl.json new file mode 100644 index 00000000000..caea115b7d5 --- /dev/null +++ b/homeassistant/components/neato/.translations/pl.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Konto jest ju\u017c skonfigurowane", + "invalid_credentials": "Nieprawid\u0142owe dane uwierzytelniaj\u0105ce" + }, + "create_entry": { + "default": "Zapoznaj si\u0119 z [dokumentacj\u0105 Neato]({docs_url})." + }, + "error": { + "invalid_credentials": "Nieprawid\u0142owe dane uwierzytelniaj\u0105ce", + "unexpected_error": "Niespodziewany b\u0142\u0105d" + }, + "step": { + "user": { + "data": { + "password": "Has\u0142o", + "username": "Nazwa u\u017cytkownika", + "vendor": "Dostawca" + }, + "description": "Zapoznaj si\u0119 z [dokumentacj\u0105 Neato]({docs_url}).", + "title": "Informacje o koncie Neato" + } + }, + "title": "Neato" + } +} \ No newline at end of file diff --git a/homeassistant/components/neato/.translations/ru.json b/homeassistant/components/neato/.translations/ru.json new file mode 100644 index 00000000000..1a206258e24 --- /dev/null +++ b/homeassistant/components/neato/.translations/ru.json @@ -0,0 +1,27 @@ +{ + "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.", + "invalid_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435" + }, + "create_entry": { + "default": "\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 \u0440\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u043d\u043e\u0439 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438." + }, + "error": { + "invalid_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435", + "unexpected_error": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + }, + "step": { + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u041b\u043e\u0433\u0438\u043d", + "vendor": "\u041f\u0440\u043e\u0438\u0437\u0432\u043e\u0434\u0438\u0442\u0435\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]({docs_url}) \u0434\u043b\u044f \u0440\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u043d\u043e\u0439 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438.", + "title": "Neato" + } + }, + "title": "Neato" + } +} \ No newline at end of file diff --git a/homeassistant/components/neato/.translations/sl.json b/homeassistant/components/neato/.translations/sl.json new file mode 100644 index 00000000000..7acbb718d17 --- /dev/null +++ b/homeassistant/components/neato/.translations/sl.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "\u017de konfigurirano", + "invalid_credentials": "Neveljavne poverilnice" + }, + "create_entry": { + "default": "Glejte [neato dokumentacija] ({docs_url})." + }, + "error": { + "invalid_credentials": "Neveljavne poverilnice", + "unexpected_error": "Nepri\u010dakovana napaka" + }, + "step": { + "user": { + "data": { + "password": "Geslo", + "username": "Uporabni\u0161ko ime", + "vendor": "Prodajalec" + }, + "description": "Glejte [neato dokumentacija] ({docs_url}).", + "title": "Podatki o ra\u010dunu Neato" + } + }, + "title": "Neato" + } +} \ No newline at end of file diff --git a/homeassistant/components/neato/.translations/zh-Hant.json b/homeassistant/components/neato/.translations/zh-Hant.json new file mode 100644 index 00000000000..61f49cd5da0 --- /dev/null +++ b/homeassistant/components/neato/.translations/zh-Hant.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "\u5df2\u8a2d\u5b9a\u5b8c\u6210", + "invalid_credentials": "\u6191\u8b49\u7121\u6548" + }, + "create_entry": { + "default": "\u8acb\u53c3\u95b1 [Neato \u6587\u4ef6]({docs_url})\u3002" + }, + "error": { + "invalid_credentials": "\u6191\u8b49\u7121\u6548", + "unexpected_error": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "step": { + "user": { + "data": { + "password": "\u5bc6\u78bc", + "username": "\u4f7f\u7528\u8005\u540d\u7a31", + "vendor": "\u5ee0\u5546" + }, + "description": "\u8acb\u53c3\u95b1 [Neato \u6587\u4ef6]({docs_url})\u3002", + "title": "Neato \u5e33\u865f\u8cc7\u8a0a" + } + }, + "title": "Neato" + } +} \ No newline at end of file diff --git a/homeassistant/components/neato/manifest.json b/homeassistant/components/neato/manifest.json index 71553fabc8e..8b0c5acc723 100644 --- a/homeassistant/components/neato/manifest.json +++ b/homeassistant/components/neato/manifest.json @@ -1,7 +1,7 @@ { "domain": "neato", "name": "Neato", - "documentation": "https://www.home-assistant.io/components/neato", + "documentation": "https://www.home-assistant.io/integrations/neato", "requirements": [ "pybotvac==0.0.15" ], diff --git a/homeassistant/components/neato/vacuum.py b/homeassistant/components/neato/vacuum.py index 93fe285dcfd..f284b2eda1e 100644 --- a/homeassistant/components/neato/vacuum.py +++ b/homeassistant/components/neato/vacuum.py @@ -27,7 +27,7 @@ from homeassistant.components.vacuum import ( SUPPORT_STOP, StateVacuumDevice, ) -from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.const import ATTR_ENTITY_ID, ATTR_MODE import homeassistant.helpers.config_validation as cv from homeassistant.helpers.service import extract_entity_ids @@ -66,7 +66,6 @@ ATTR_CLEAN_BATTERY_END = "battery_level_at_clean_end" ATTR_CLEAN_SUSP_COUNT = "clean_suspension_count" ATTR_CLEAN_SUSP_TIME = "clean_suspension_time" -ATTR_MODE = "mode" ATTR_NAVIGATION = "navigation" ATTR_CATEGORY = "category" ATTR_ZONE = "zone" diff --git a/homeassistant/components/nederlandse_spoorwegen/manifest.json b/homeassistant/components/nederlandse_spoorwegen/manifest.json index baa6551cc7c..c1360ecbe30 100644 --- a/homeassistant/components/nederlandse_spoorwegen/manifest.json +++ b/homeassistant/components/nederlandse_spoorwegen/manifest.json @@ -1,7 +1,7 @@ { "domain": "nederlandse_spoorwegen", "name": "Nederlandse spoorwegen", - "documentation": "https://www.home-assistant.io/components/nederlandse_spoorwegen", + "documentation": "https://www.home-assistant.io/integrations/nederlandse_spoorwegen", "requirements": [ "nsapi==2.7.4" ], diff --git a/homeassistant/components/nello/manifest.json b/homeassistant/components/nello/manifest.json index 0caafd7e27a..e747bf7739f 100644 --- a/homeassistant/components/nello/manifest.json +++ b/homeassistant/components/nello/manifest.json @@ -1,7 +1,7 @@ { "domain": "nello", "name": "Nello", - "documentation": "https://www.home-assistant.io/components/nello", + "documentation": "https://www.home-assistant.io/integrations/nello", "requirements": [ "pynello==2.0.2" ], diff --git a/homeassistant/components/ness_alarm/manifest.json b/homeassistant/components/ness_alarm/manifest.json index 93b19470ac4..a1cae2df1d7 100644 --- a/homeassistant/components/ness_alarm/manifest.json +++ b/homeassistant/components/ness_alarm/manifest.json @@ -1,7 +1,7 @@ { "domain": "ness_alarm", "name": "Ness alarm", - "documentation": "https://www.home-assistant.io/components/ness_alarm", + "documentation": "https://www.home-assistant.io/integrations/ness_alarm", "requirements": [ "nessclient==0.9.15" ], diff --git a/homeassistant/components/nest/.translations/es-419.json b/homeassistant/components/nest/.translations/es-419.json index 78239148a4e..60a3eb65ca9 100644 --- a/homeassistant/components/nest/.translations/es-419.json +++ b/homeassistant/components/nest/.translations/es-419.json @@ -6,7 +6,8 @@ "no_flows": "Debe configurar Nest antes de poder autenticarse con \u00e9l. [Lea las instrucciones] (https://www.home-assistant.io/components/nest/)." }, "error": { - "invalid_code": "Codigo invalido" + "invalid_code": "Codigo invalido", + "unknown": "Error desconocido al validar el c\u00f3digo" }, "step": { "init": { diff --git a/homeassistant/components/nest/.translations/no.json b/homeassistant/components/nest/.translations/no.json index 03cf1a82b81..743c47e00c8 100644 --- a/homeassistant/components/nest/.translations/no.json +++ b/homeassistant/components/nest/.translations/no.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_setup": "Du kan bare konfigurere en enkelt Nest konto.", + "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/)." diff --git a/homeassistant/components/nest/binary_sensor.py b/homeassistant/components/nest/binary_sensor.py index 0f3ae7da710..05170a54ed1 100644 --- a/homeassistant/components/nest/binary_sensor.py +++ b/homeassistant/components/nest/binary_sensor.py @@ -141,7 +141,7 @@ class NestActivityZoneSensor(NestBinarySensor): def __init__(self, structure, device, zone): """Initialize the sensor.""" - super(NestActivityZoneSensor, self).__init__(structure, device, "") + super().__init__(structure, device, "") self.zone = zone self._name = f"{self._name} {self.zone.name} activity" diff --git a/homeassistant/components/nest/camera.py b/homeassistant/components/nest/camera.py index 37fd18efb6d..efc0bfbc822 100644 --- a/homeassistant/components/nest/camera.py +++ b/homeassistant/components/nest/camera.py @@ -34,7 +34,7 @@ class NestCamera(Camera): def __init__(self, structure, device): """Initialize a Nest Camera.""" - super(NestCamera, self).__init__() + super().__init__() self.structure = structure self.device = device self._location = None diff --git a/homeassistant/components/nest/climate.py b/homeassistant/components/nest/climate.py index dc4b0bd33ae..eec7108cdea 100644 --- a/homeassistant/components/nest/climate.py +++ b/homeassistant/components/nest/climate.py @@ -192,7 +192,10 @@ class NestThermostat(ClimateDevice): def hvac_mode(self): """Return current operation ie. heat, cool, idle.""" if self._mode == NEST_MODE_ECO: - # We assume the first operation in operation list is the main one + if self.device.previous_mode in MODE_NEST_TO_HASS: + return MODE_NEST_TO_HASS[self.device.previous_mode] + + # previous_mode not supported so return the first compatible mode return self._operation_list[0] return MODE_NEST_TO_HASS[self._mode] @@ -270,7 +273,7 @@ class NestThermostat(ClimateDevice): if self._mode == NEST_MODE_ECO: return PRESET_ECO - return None + return PRESET_NONE @property def preset_modes(self): @@ -294,7 +297,7 @@ class NestThermostat(ClimateDevice): if need_eco: self.device.mode = NEST_MODE_ECO else: - self.device.mode = MODE_HASS_TO_NEST[self._operation_list[0]] + self.device.mode = self.device.previous_mode @property def fan_mode(self): diff --git a/homeassistant/components/nest/manifest.json b/homeassistant/components/nest/manifest.json index 8a6e8ec611a..b7d5f132045 100644 --- a/homeassistant/components/nest/manifest.json +++ b/homeassistant/components/nest/manifest.json @@ -2,7 +2,7 @@ "domain": "nest", "name": "Nest", "config_flow": true, - "documentation": "https://www.home-assistant.io/components/nest", + "documentation": "https://www.home-assistant.io/integrations/nest", "requirements": [ "python-nest==4.1.0" ], diff --git a/homeassistant/components/netatmo/camera.py b/homeassistant/components/netatmo/camera.py index d18ff9fc46c..60428961cb9 100644 --- a/homeassistant/components/netatmo/camera.py +++ b/homeassistant/components/netatmo/camera.py @@ -69,7 +69,7 @@ class NetatmoCamera(Camera): def __init__(self, data, camera_name, home, camera_type, verify_ssl, quality): """Set up for access to the Netatmo camera images.""" - super(NetatmoCamera, self).__init__() + super().__init__() self._data = data self._camera_name = camera_name self._verify_ssl = verify_ssl diff --git a/homeassistant/components/netatmo/manifest.json b/homeassistant/components/netatmo/manifest.json index 82f32c34407..83091368aff 100644 --- a/homeassistant/components/netatmo/manifest.json +++ b/homeassistant/components/netatmo/manifest.json @@ -1,7 +1,7 @@ { "domain": "netatmo", "name": "Netatmo", - "documentation": "https://www.home-assistant.io/components/netatmo", + "documentation": "https://www.home-assistant.io/integrations/netatmo", "requirements": [ "pyatmo==2.2.1" ], diff --git a/homeassistant/components/netdata/manifest.json b/homeassistant/components/netdata/manifest.json index 9c3b8ad33d2..da4d15c28aa 100644 --- a/homeassistant/components/netdata/manifest.json +++ b/homeassistant/components/netdata/manifest.json @@ -1,7 +1,7 @@ { "domain": "netdata", "name": "Netdata", - "documentation": "https://www.home-assistant.io/components/netdata", + "documentation": "https://www.home-assistant.io/integrations/netdata", "requirements": [ "netdata==0.1.2" ], diff --git a/homeassistant/components/netgear/manifest.json b/homeassistant/components/netgear/manifest.json index 3ee3b189939..af709c755c8 100644 --- a/homeassistant/components/netgear/manifest.json +++ b/homeassistant/components/netgear/manifest.json @@ -1,7 +1,7 @@ { "domain": "netgear", "name": "Netgear", - "documentation": "https://www.home-assistant.io/components/netgear", + "documentation": "https://www.home-assistant.io/integrations/netgear", "requirements": [ "pynetgear==0.6.1" ], diff --git a/homeassistant/components/netgear_lte/manifest.json b/homeassistant/components/netgear_lte/manifest.json index 609ea72cc69..7e085d06307 100644 --- a/homeassistant/components/netgear_lte/manifest.json +++ b/homeassistant/components/netgear_lte/manifest.json @@ -1,12 +1,10 @@ { "domain": "netgear_lte", "name": "Netgear lte", - "documentation": "https://www.home-assistant.io/components/netgear_lte", + "documentation": "https://www.home-assistant.io/integrations/netgear_lte", "requirements": [ "eternalegypt==0.0.10" ], "dependencies": [], - "codeowners": [ - "@amelchio" - ] + "codeowners": [] } diff --git a/homeassistant/components/netio/manifest.json b/homeassistant/components/netio/manifest.json index e3675db04d7..3755746aee1 100644 --- a/homeassistant/components/netio/manifest.json +++ b/homeassistant/components/netio/manifest.json @@ -1,7 +1,7 @@ { "domain": "netio", "name": "Netio", - "documentation": "https://www.home-assistant.io/components/netio", + "documentation": "https://www.home-assistant.io/integrations/netio", "requirements": [ "pynetio==0.1.9.1" ], diff --git a/homeassistant/components/neurio_energy/manifest.json b/homeassistant/components/neurio_energy/manifest.json index 04420d5c4f2..735baf58e5a 100644 --- a/homeassistant/components/neurio_energy/manifest.json +++ b/homeassistant/components/neurio_energy/manifest.json @@ -1,7 +1,7 @@ { "domain": "neurio_energy", "name": "Neurio energy", - "documentation": "https://www.home-assistant.io/components/neurio_energy", + "documentation": "https://www.home-assistant.io/integrations/neurio_energy", "requirements": [ "neurio==0.3.1" ], diff --git a/homeassistant/components/nextbus/manifest.json b/homeassistant/components/nextbus/manifest.json index 63bdbf8a928..525947a8c74 100644 --- a/homeassistant/components/nextbus/manifest.json +++ b/homeassistant/components/nextbus/manifest.json @@ -1,8 +1,8 @@ { "domain": "nextbus", "name": "NextBus", - "documentation": "https://www.home-assistant.io/components/nextbus", + "documentation": "https://www.home-assistant.io/integrations/nextbus", "dependencies": [], "codeowners": ["@vividboarder"], - "requirements": ["py_nextbus==0.1.2"] + "requirements": ["py_nextbusnext==0.1.4"] } diff --git a/homeassistant/components/nfandroidtv/manifest.json b/homeassistant/components/nfandroidtv/manifest.json index 8f3d88b58ee..45eedf8a154 100644 --- a/homeassistant/components/nfandroidtv/manifest.json +++ b/homeassistant/components/nfandroidtv/manifest.json @@ -1,7 +1,7 @@ { "domain": "nfandroidtv", "name": "Nfandroidtv", - "documentation": "https://www.home-assistant.io/components/nfandroidtv", + "documentation": "https://www.home-assistant.io/integrations/nfandroidtv", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/niko_home_control/manifest.json b/homeassistant/components/niko_home_control/manifest.json index 8cb58a7b74c..0e168601c4c 100644 --- a/homeassistant/components/niko_home_control/manifest.json +++ b/homeassistant/components/niko_home_control/manifest.json @@ -1,7 +1,7 @@ { "domain": "niko_home_control", "name": "Niko home control", - "documentation": "https://www.home-assistant.io/components/niko_home_control", + "documentation": "https://www.home-assistant.io/integrations/niko_home_control", "requirements": [ "niko-home-control==0.2.1" ], diff --git a/homeassistant/components/nilu/manifest.json b/homeassistant/components/nilu/manifest.json index ee7645653e6..fe7a92bc270 100644 --- a/homeassistant/components/nilu/manifest.json +++ b/homeassistant/components/nilu/manifest.json @@ -1,7 +1,7 @@ { "domain": "nilu", "name": "Nilu", - "documentation": "https://www.home-assistant.io/components/nilu", + "documentation": "https://www.home-assistant.io/integrations/nilu", "requirements": [ "niluclient==0.1.2" ], diff --git a/homeassistant/components/nissan_leaf/manifest.json b/homeassistant/components/nissan_leaf/manifest.json index 70aaa112414..a3181292147 100644 --- a/homeassistant/components/nissan_leaf/manifest.json +++ b/homeassistant/components/nissan_leaf/manifest.json @@ -1,7 +1,7 @@ { "domain": "nissan_leaf", "name": "Nissan leaf", - "documentation": "https://www.home-assistant.io/components/nissan_leaf", + "documentation": "https://www.home-assistant.io/integrations/nissan_leaf", "requirements": [ "pycarwings2==2.9" ], diff --git a/homeassistant/components/nmap_tracker/manifest.json b/homeassistant/components/nmap_tracker/manifest.json index 0380acba1ac..b207bd07a42 100644 --- a/homeassistant/components/nmap_tracker/manifest.json +++ b/homeassistant/components/nmap_tracker/manifest.json @@ -1,7 +1,7 @@ { "domain": "nmap_tracker", "name": "Nmap tracker", - "documentation": "https://www.home-assistant.io/components/nmap_tracker", + "documentation": "https://www.home-assistant.io/integrations/nmap_tracker", "requirements": [ "python-nmap==0.6.1", "getmac==0.8.1" diff --git a/homeassistant/components/nmbs/manifest.json b/homeassistant/components/nmbs/manifest.json index 1a2fa055688..5fe5f743fd3 100644 --- a/homeassistant/components/nmbs/manifest.json +++ b/homeassistant/components/nmbs/manifest.json @@ -1,7 +1,7 @@ { "domain": "nmbs", "name": "Nmbs", - "documentation": "https://www.home-assistant.io/components/nmbs", + "documentation": "https://www.home-assistant.io/integrations/nmbs", "requirements": [ "pyrail==0.0.3" ], diff --git a/homeassistant/components/no_ip/manifest.json b/homeassistant/components/no_ip/manifest.json index 12581599532..438b61136eb 100644 --- a/homeassistant/components/no_ip/manifest.json +++ b/homeassistant/components/no_ip/manifest.json @@ -1,7 +1,7 @@ { "domain": "no_ip", "name": "No ip", - "documentation": "https://www.home-assistant.io/components/no_ip", + "documentation": "https://www.home-assistant.io/integrations/no_ip", "requirements": [], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/noaa_tides/manifest.json b/homeassistant/components/noaa_tides/manifest.json index 9ffc0215fd1..069fc238f72 100644 --- a/homeassistant/components/noaa_tides/manifest.json +++ b/homeassistant/components/noaa_tides/manifest.json @@ -1,7 +1,7 @@ { "domain": "noaa_tides", "name": "Noaa tides", - "documentation": "https://www.home-assistant.io/components/noaa_tides", + "documentation": "https://www.home-assistant.io/integrations/noaa_tides", "requirements": [ "py_noaa==0.3.0" ], diff --git a/homeassistant/components/norway_air/manifest.json b/homeassistant/components/norway_air/manifest.json index 08c9932c36f..e7ee2d2dcd5 100644 --- a/homeassistant/components/norway_air/manifest.json +++ b/homeassistant/components/norway_air/manifest.json @@ -1,7 +1,7 @@ { "domain": "norway_air", "name": "Norway air", - "documentation": "https://www.home-assistant.io/components/norway_air", + "documentation": "https://www.home-assistant.io/integrations/norway_air", "requirements": [ "pyMetno==0.4.6" ], diff --git a/homeassistant/components/notify/manifest.json b/homeassistant/components/notify/manifest.json index bad39a1cb97..6586496abbe 100644 --- a/homeassistant/components/notify/manifest.json +++ b/homeassistant/components/notify/manifest.json @@ -1,7 +1,7 @@ { "domain": "notify", "name": "Notify", - "documentation": "https://www.home-assistant.io/components/notify", + "documentation": "https://www.home-assistant.io/integrations/notify", "requirements": [], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/notion/.translations/es-419.json b/homeassistant/components/notion/.translations/es-419.json index ad2f19b0668..1f4968f24e1 100644 --- a/homeassistant/components/notion/.translations/es-419.json +++ b/homeassistant/components/notion/.translations/es-419.json @@ -1,6 +1,7 @@ { "config": { "error": { + "identifier_exists": "Nombre de usuario ya registrado", "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/ru.json b/homeassistant/components/notion/.translations/ru.json index f43fbeb58b7..7345cf46295 100644 --- a/homeassistant/components/notion/.translations/ru.json +++ b/homeassistant/components/notion/.translations/ru.json @@ -1,9 +1,9 @@ { "config": { "error": { - "identifier_exists": "\u0423\u0447\u0435\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u0430", + "identifier_exists": "\u0423\u0447\u0435\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u0430.", "invalid_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043b\u043e\u0433\u0438\u043d \u0438\u043b\u0438 \u043f\u0430\u0440\u043e\u043b\u044c", - "no_devices": "\u0412 \u0443\u0447\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b" + "no_devices": "\u041d\u0435\u0442 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432, \u0441\u0432\u044f\u0437\u0430\u043d\u043d\u044b\u0445 \u0441 \u0443\u0447\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u044c\u044e" }, "step": { "user": { diff --git a/homeassistant/components/notion/.translations/zh-Hant.json b/homeassistant/components/notion/.translations/zh-Hant.json index af89dd3d39b..f672f519f40 100644 --- a/homeassistant/components/notion/.translations/zh-Hant.json +++ b/homeassistant/components/notion/.translations/zh-Hant.json @@ -3,7 +3,7 @@ "error": { "identifier_exists": "\u4f7f\u7528\u8005\u540d\u7a31\u5df2\u8a3b\u518a", "invalid_credentials": "\u4f7f\u7528\u8005\u540d\u7a31\u6216\u5bc6\u78bc\u7121\u6548", - "no_devices": "\u5e33\u865f\u4e2d\u627e\u4e0d\u5230\u4efb\u4f55\u88dd\u7f6e" + "no_devices": "\u5e33\u865f\u4e2d\u627e\u4e0d\u5230\u4efb\u4f55\u8a2d\u5099" }, "step": { "user": { diff --git a/homeassistant/components/notion/manifest.json b/homeassistant/components/notion/manifest.json index 827d406a1b5..2a400754b46 100644 --- a/homeassistant/components/notion/manifest.json +++ b/homeassistant/components/notion/manifest.json @@ -2,7 +2,7 @@ "domain": "notion", "name": "Notion", "config_flow": true, - "documentation": "https://www.home-assistant.io/components/notion", + "documentation": "https://www.home-assistant.io/integrations/notion", "requirements": [ "aionotion==1.1.0" ], diff --git a/homeassistant/components/nsw_fuel_station/manifest.json b/homeassistant/components/nsw_fuel_station/manifest.json index 6be24fb5a2c..744497c22c4 100644 --- a/homeassistant/components/nsw_fuel_station/manifest.json +++ b/homeassistant/components/nsw_fuel_station/manifest.json @@ -1,7 +1,7 @@ { "domain": "nsw_fuel_station", "name": "Nsw fuel station", - "documentation": "https://www.home-assistant.io/components/nsw_fuel_station", + "documentation": "https://www.home-assistant.io/integrations/nsw_fuel_station", "requirements": [ "nsw-fuel-api-client==1.0.10" ], diff --git a/homeassistant/components/nsw_rural_fire_service_feed/manifest.json b/homeassistant/components/nsw_rural_fire_service_feed/manifest.json index 4542eb45c82..3d16f0a57e3 100644 --- a/homeassistant/components/nsw_rural_fire_service_feed/manifest.json +++ b/homeassistant/components/nsw_rural_fire_service_feed/manifest.json @@ -1,7 +1,7 @@ { "domain": "nsw_rural_fire_service_feed", "name": "Nsw rural fire service feed", - "documentation": "https://www.home-assistant.io/components/nsw_rural_fire_service_feed", + "documentation": "https://www.home-assistant.io/integrations/nsw_rural_fire_service_feed", "requirements": [ "geojson_client==0.4" ], diff --git a/homeassistant/components/nuheat/manifest.json b/homeassistant/components/nuheat/manifest.json index c9e69c44ec2..3d055f8f975 100644 --- a/homeassistant/components/nuheat/manifest.json +++ b/homeassistant/components/nuheat/manifest.json @@ -1,7 +1,7 @@ { "domain": "nuheat", "name": "Nuheat", - "documentation": "https://www.home-assistant.io/components/nuheat", + "documentation": "https://www.home-assistant.io/integrations/nuheat", "requirements": [ "nuheat==0.3.0" ], diff --git a/homeassistant/components/nuimo_controller/__init__.py b/homeassistant/components/nuimo_controller/__init__.py index 42d35b60b25..8fa3897b735 100644 --- a/homeassistant/components/nuimo_controller/__init__.py +++ b/homeassistant/components/nuimo_controller/__init__.py @@ -76,7 +76,7 @@ class NuimoThread(threading.Thread): def __init__(self, hass, mac, name): """Initialize thread object.""" - super(NuimoThread, self).__init__() + super().__init__() self._hass = hass self._mac = mac self._name = name diff --git a/homeassistant/components/nuimo_controller/manifest.json b/homeassistant/components/nuimo_controller/manifest.json index 9f18d2849f8..a88faaa6f30 100644 --- a/homeassistant/components/nuimo_controller/manifest.json +++ b/homeassistant/components/nuimo_controller/manifest.json @@ -1,7 +1,7 @@ { "domain": "nuimo_controller", "name": "Nuimo controller", - "documentation": "https://www.home-assistant.io/components/nuimo_controller", + "documentation": "https://www.home-assistant.io/integrations/nuimo_controller", "requirements": [ "--only-binary=all nuimo==0.1.0" ], diff --git a/homeassistant/components/nuki/manifest.json b/homeassistant/components/nuki/manifest.json index e7f078a1a05..77043f37134 100644 --- a/homeassistant/components/nuki/manifest.json +++ b/homeassistant/components/nuki/manifest.json @@ -1,7 +1,7 @@ { "domain": "nuki", "name": "Nuki", - "documentation": "https://www.home-assistant.io/components/nuki", + "documentation": "https://www.home-assistant.io/integrations/nuki", "requirements": ["pynuki==1.3.3"], "dependencies": [], "codeowners": ["@pvizeli"] diff --git a/homeassistant/components/nut/manifest.json b/homeassistant/components/nut/manifest.json index 920e56fba7c..21231c27873 100644 --- a/homeassistant/components/nut/manifest.json +++ b/homeassistant/components/nut/manifest.json @@ -1,7 +1,7 @@ { "domain": "nut", "name": "Nut", - "documentation": "https://www.home-assistant.io/components/nut", + "documentation": "https://www.home-assistant.io/integrations/nut", "requirements": [ "pynut2==2.1.2" ], diff --git a/homeassistant/components/nws/manifest.json b/homeassistant/components/nws/manifest.json index b0e5fdb2088..b112a9ea4ea 100644 --- a/homeassistant/components/nws/manifest.json +++ b/homeassistant/components/nws/manifest.json @@ -1,8 +1,8 @@ { "domain": "nws", "name": "National Weather Service", - "documentation": "https://www.home-assistant.io/components/nws", + "documentation": "https://www.home-assistant.io/integrations/nws", "dependencies": [], "codeowners": ["@MatthewFlamm"], - "requirements": ["pynws==0.7.4"] + "requirements": ["pynws==0.8.1"] } diff --git a/homeassistant/components/nx584/binary_sensor.py b/homeassistant/components/nx584/binary_sensor.py index 8b26a958a6f..6f1c66d8d87 100644 --- a/homeassistant/components/nx584/binary_sensor.py +++ b/homeassistant/components/nx584/binary_sensor.py @@ -107,7 +107,7 @@ class NX584Watcher(threading.Thread): def __init__(self, client, zone_sensors): """Initialize NX584 watcher thread.""" - super(NX584Watcher, self).__init__() + super().__init__() self.daemon = True self._client = client self._zone_sensors = zone_sensors diff --git a/homeassistant/components/nx584/manifest.json b/homeassistant/components/nx584/manifest.json index 67b5b0e2eeb..64f72986d07 100644 --- a/homeassistant/components/nx584/manifest.json +++ b/homeassistant/components/nx584/manifest.json @@ -1,7 +1,7 @@ { "domain": "nx584", "name": "Nx584", - "documentation": "https://www.home-assistant.io/components/nx584", + "documentation": "https://www.home-assistant.io/integrations/nx584", "requirements": [ "pynx584==0.4" ], diff --git a/homeassistant/components/nzbget/__init__.py b/homeassistant/components/nzbget/__init__.py index 2480daf2ead..ae3ce7c5944 100644 --- a/homeassistant/components/nzbget/__init__.py +++ b/homeassistant/components/nzbget/__init__.py @@ -1 +1,165 @@ """The nzbget component.""" +from datetime import timedelta +import logging + +import pynzbgetapi +import voluptuous as vol + +from homeassistant.const import ( + CONF_HOST, + CONF_NAME, + CONF_PASSWORD, + CONF_PORT, + CONF_SCAN_INTERVAL, + CONF_SSL, + CONF_USERNAME, +) +from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.dispatcher import dispatcher_send +from homeassistant.helpers.event import track_time_interval + +_LOGGER = logging.getLogger(__name__) + +ATTR_SPEED = "speed" + +DOMAIN = "nzbget" +DATA_NZBGET = "data_nzbget" +DATA_UPDATED = "nzbget_data_updated" + +DEFAULT_NAME = "NZBGet" +DEFAULT_PORT = 6789 +DEFAULT_SPEED_LIMIT = 1000 # 1 Megabyte/Sec + +DEFAULT_SCAN_INTERVAL = timedelta(seconds=5) + +SERVICE_PAUSE = "pause" +SERVICE_RESUME = "resume" +SERVICE_SET_SPEED = "set_speed" + +SPEED_LIMIT_SCHEMA = vol.Schema( + {vol.Optional(ATTR_SPEED, default=DEFAULT_SPEED_LIMIT): cv.positive_int} +) + +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: vol.Schema( + { + vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_PASSWORD): cv.string, + vol.Optional(CONF_USERNAME): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional( + CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL + ): cv.time_period, + vol.Optional(CONF_SSL, default=False): cv.boolean, + } + ) + }, + extra=vol.ALLOW_EXTRA, +) + + +def setup(hass, config): + """Set up the NZBGet sensors.""" + + host = config[DOMAIN][CONF_HOST] + port = config[DOMAIN][CONF_PORT] + ssl = "s" if config[DOMAIN][CONF_SSL] else "" + name = config[DOMAIN][CONF_NAME] + username = config[DOMAIN].get(CONF_USERNAME) + password = config[DOMAIN].get(CONF_PASSWORD) + scan_interval = config[DOMAIN][CONF_SCAN_INTERVAL] + + try: + nzbget_api = pynzbgetapi.NZBGetAPI(host, username, password, ssl, ssl, port) + nzbget_api.version() + except pynzbgetapi.NZBGetAPIException as conn_err: + _LOGGER.error("Error setting up NZBGet API: %s", conn_err) + return False + + _LOGGER.debug("Successfully validated NZBGet API connection") + + nzbget_data = hass.data[DATA_NZBGET] = NZBGetData(hass, nzbget_api) + nzbget_data.update() + + def service_handler(service): + """Handle service calls.""" + if service.service == SERVICE_PAUSE: + nzbget_data.pause_download() + elif service.service == SERVICE_RESUME: + nzbget_data.resume_download() + elif service.service == SERVICE_SET_SPEED: + limit = service.data[ATTR_SPEED] + nzbget_data.rate(limit) + + hass.services.register( + DOMAIN, SERVICE_PAUSE, service_handler, schema=vol.Schema({}) + ) + + hass.services.register( + DOMAIN, SERVICE_RESUME, service_handler, schema=vol.Schema({}) + ) + + hass.services.register( + DOMAIN, SERVICE_SET_SPEED, service_handler, schema=SPEED_LIMIT_SCHEMA + ) + + def refresh(event_time): + """Get the latest data from NZBGet.""" + nzbget_data.update() + + track_time_interval(hass, refresh, scan_interval) + + sensorconfig = {"client_name": name} + + hass.helpers.discovery.load_platform("sensor", DOMAIN, sensorconfig, config) + + return True + + +class NZBGetData: + """Get the latest data and update the states.""" + + def __init__(self, hass, api): + """Initialize the NZBGet RPC API.""" + self.hass = hass + self.status = None + self.available = True + self._api = api + + def update(self): + """Get the latest data from NZBGet instance.""" + + try: + self.status = self._api.status() + self.available = True + dispatcher_send(self.hass, DATA_UPDATED) + except pynzbgetapi.NZBGetAPIException as err: + self.available = False + _LOGGER.error("Unable to refresh NZBGet data: %s", err) + + def pause_download(self): + """Pause download queue.""" + + try: + self._api.pausedownload() + except pynzbgetapi.NZBGetAPIException as err: + _LOGGER.error("Unable to pause queue: %s", err) + + def resume_download(self): + """Resume download queue.""" + + try: + self._api.resumedownload() + except pynzbgetapi.NZBGetAPIException as err: + _LOGGER.error("Unable to resume download queue: %s", err) + + def rate(self, limit): + """Set download speed.""" + + try: + if not self._api.rate(limit): + _LOGGER.error("Limit was out of range") + except pynzbgetapi.NZBGetAPIException as err: + _LOGGER.error("Unable to set download speed: %s", err) diff --git a/homeassistant/components/nzbget/manifest.json b/homeassistant/components/nzbget/manifest.json index 69293ede516..1dc16fdba40 100644 --- a/homeassistant/components/nzbget/manifest.json +++ b/homeassistant/components/nzbget/manifest.json @@ -1,8 +1,8 @@ { "domain": "nzbget", "name": "Nzbget", - "documentation": "https://www.home-assistant.io/components/nzbget", - "requirements": [], + "documentation": "https://www.home-assistant.io/integrations/nzbget", + "requirements": ["pynzbgetapi==0.2.0"], "dependencies": [], - "codeowners": [] + "codeowners": ["@chriscla"] } diff --git a/homeassistant/components/nzbget/sensor.py b/homeassistant/components/nzbget/sensor.py index 73643a5383c..ce1fda0839e 100644 --- a/homeassistant/components/nzbget/sensor.py +++ b/homeassistant/components/nzbget/sensor.py @@ -1,32 +1,15 @@ -"""Support for monitoring NZBGet NZB client.""" -from datetime import timedelta +"""Monitor the NZBGet API.""" import logging -from aiohttp.hdrs import CONTENT_TYPE -import requests -import voluptuous as vol - -from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import ( - CONF_SSL, - CONF_HOST, - CONF_NAME, - CONF_PORT, - CONF_PASSWORD, - CONF_USERNAME, - CONTENT_TYPE_JSON, - CONF_MONITORED_VARIABLES, -) -import homeassistant.helpers.config_validation as cv +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import Entity -from homeassistant.util import Throttle + +from . import DATA_NZBGET, DATA_UPDATED _LOGGER = logging.getLogger(__name__) DEFAULT_NAME = "NZBGet" -DEFAULT_PORT = 6789 - -MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=5) SENSOR_TYPES = { "article_cache": ["ArticleCacheMB", "Article Cache", "MB"], @@ -40,66 +23,39 @@ SENSOR_TYPES = { "uptime": ["UpTimeSec", "Uptime", "min"], } -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_HOST): cv.string, - vol.Optional(CONF_MONITORED_VARIABLES, default=["download_rate"]): vol.All( - cv.ensure_list, [vol.In(SENSOR_TYPES)] - ), - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_PASSWORD): cv.string, - vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, - vol.Optional(CONF_SSL, default=False): cv.boolean, - vol.Optional(CONF_USERNAME): cv.string, - } -) - def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the NZBGet sensors.""" - host = config.get(CONF_HOST) - port = config.get(CONF_PORT) - ssl = "s" if config.get(CONF_SSL) else "" - name = config.get(CONF_NAME) - username = config.get(CONF_USERNAME) - password = config.get(CONF_PASSWORD) - monitored_types = config.get(CONF_MONITORED_VARIABLES) + """Create NZBGet sensors.""" - url = f"http{ssl}://{host}:{port}/jsonrpc" + if discovery_info is None: + return - try: - nzbgetapi = NZBGetAPI(api_url=url, username=username, password=password) - nzbgetapi.update() - except ( - requests.exceptions.ConnectionError, - requests.exceptions.HTTPError, - ) as conn_err: - _LOGGER.error("Error setting up NZBGet API: %s", conn_err) - return False + nzbget_data = hass.data[DATA_NZBGET] + name = discovery_info["client_name"] devices = [] - for ng_type in monitored_types: + for sensor_type, sensor_config in SENSOR_TYPES.items(): new_sensor = NZBGetSensor( - api=nzbgetapi, sensor_type=SENSOR_TYPES.get(ng_type), client_name=name + nzbget_data, sensor_type, name, sensor_config[0], sensor_config[1] ) devices.append(new_sensor) - add_entities(devices) + add_entities(devices, True) class NZBGetSensor(Entity): """Representation of a NZBGet sensor.""" - def __init__(self, api, sensor_type, client_name): + def __init__( + self, nzbget_data, sensor_type, client_name, sensor_name, unit_of_measurement + ): """Initialize a new NZBGet sensor.""" - self._name = "{} {}".format(client_name, sensor_type[1]) - self.type = sensor_type[0] + self._name = f"{client_name} {sensor_type}" + self.type = sensor_name self.client_name = client_name - self.api = api + self.nzbget_data = nzbget_data self._state = None - self._unit_of_measurement = sensor_type[2] - self.update() - _LOGGER.debug("Created NZBGet sensor: %s", self.type) + self._unit_of_measurement = unit_of_measurement @property def name(self): @@ -116,21 +72,31 @@ class NZBGetSensor(Entity): """Return the unit of measurement of this entity, if any.""" return self._unit_of_measurement + @property + def available(self): + """Return whether the sensor is available.""" + return self.nzbget_data.available + + async def async_added_to_hass(self): + """Handle entity which will be added.""" + async_dispatcher_connect( + self.hass, DATA_UPDATED, self._schedule_immediate_update + ) + + @callback + def _schedule_immediate_update(self): + self.async_schedule_update_ha_state(True) + def update(self): """Update state of sensor.""" - try: - self.api.update() - except requests.exceptions.ConnectionError: - # Error calling the API, already logged in api.update() - return - if self.api.status is None: + if self.nzbget_data.status is None: _LOGGER.debug( "Update of %s requested, but no status is available", self._name ) return - value = self.api.status.get(self.type) + value = self.nzbget_data.status.get(self.type) if value is None: _LOGGER.warning("Unable to locate value for %s", self.type) return @@ -143,48 +109,3 @@ class NZBGetSensor(Entity): self._state = round(value / 60, 2) else: self._state = value - - -class NZBGetAPI: - """Simple JSON-RPC wrapper for NZBGet's API.""" - - def __init__(self, api_url, username=None, password=None): - """Initialize NZBGet API and set headers needed later.""" - self.api_url = api_url - self.status = None - self.headers = {CONTENT_TYPE: CONTENT_TYPE_JSON} - - if username is not None and password is not None: - self.auth = (username, password) - else: - self.auth = None - self.update() - - def post(self, method, params=None): - """Send a POST request and return the response as a dict.""" - payload = {"method": method} - - if params: - payload["params"] = params - try: - response = requests.post( - self.api_url, - json=payload, - auth=self.auth, - headers=self.headers, - timeout=5, - ) - response.raise_for_status() - return response.json() - except requests.exceptions.ConnectionError as conn_exc: - _LOGGER.error( - "Failed to update NZBGet status from %s. Error: %s", - self.api_url, - conn_exc, - ) - raise - - @Throttle(MIN_TIME_BETWEEN_UPDATES) - def update(self): - """Update cached response.""" - self.status = self.post("status")["result"] diff --git a/homeassistant/components/nzbget/services.yaml b/homeassistant/components/nzbget/services.yaml new file mode 100644 index 00000000000..84e4af15b5d --- /dev/null +++ b/homeassistant/components/nzbget/services.yaml @@ -0,0 +1,14 @@ +# Describes the format for available nzbget services + +pause: + description: Pause download queue. + +resume: + description: Resume download queue. + +set_speed: + description: Set download speed limit + fields: + speed: + description: Speed limit in KB/s. 0 is unlimited. + example: 1000 \ No newline at end of file diff --git a/homeassistant/components/oasa_telematics/manifest.json b/homeassistant/components/oasa_telematics/manifest.json index 15bf40e63c8..e6cc998352d 100644 --- a/homeassistant/components/oasa_telematics/manifest.json +++ b/homeassistant/components/oasa_telematics/manifest.json @@ -1,7 +1,7 @@ { "domain": "oasa_telematics", "name": "OASA Telematics", - "documentation": "https://www.home-assistant.io/components/oasa_telematics/", + "documentation": "https://www.home-assistant.io/integrations/oasa_telematics/", "requirements": [ "oasatelematics==0.3" ], diff --git a/homeassistant/components/obihai/manifest.json b/homeassistant/components/obihai/manifest.json index e7706b0435c..09087663b47 100644 --- a/homeassistant/components/obihai/manifest.json +++ b/homeassistant/components/obihai/manifest.json @@ -1,9 +1,9 @@ { "domain": "obihai", "name": "Obihai", - "documentation": "https://www.home-assistant.io/components/obihai", + "documentation": "https://www.home-assistant.io/integrations/obihai", "requirements": [ - "pyobihai==1.1.0" + "pyobihai==1.2.0" ], "dependencies": [], "codeowners": ["@dshokouhi"] diff --git a/homeassistant/components/obihai/sensor.py b/homeassistant/components/obihai/sensor.py index 4eb3881e95b..89bfee7d4ee 100644 --- a/homeassistant/components/obihai/sensor.py +++ b/homeassistant/components/obihai/sensor.py @@ -44,17 +44,29 @@ def setup_platform(hass, config, add_entities, discovery_info=None): sensors = [] - pyobihai = PyObihai() + pyobihai = PyObihai(host, username, password) - services = pyobihai.get_state(host, username, password) + login = pyobihai.check_account() + if not login: + _LOGGER.error("Invalid credentials") + return - line_services = pyobihai.get_line_state(host, username, password) + serial = pyobihai.get_device_serial() + + services = pyobihai.get_state() + + line_services = pyobihai.get_line_state() + + call_direction = pyobihai.get_call_direction() for key in services: - sensors.append(ObihaiServiceSensors(pyobihai, host, username, password, key)) + sensors.append(ObihaiServiceSensors(pyobihai, serial, key)) for key in line_services: - sensors.append(ObihaiServiceSensors(pyobihai, host, username, password, key)) + sensors.append(ObihaiServiceSensors(pyobihai, serial, key)) + + for key in call_direction: + sensors.append(ObihaiServiceSensors(pyobihai, serial, key)) add_entities(sensors) @@ -62,15 +74,13 @@ def setup_platform(hass, config, add_entities, discovery_info=None): class ObihaiServiceSensors(Entity): """Get the status of each Obihai Lines.""" - def __init__(self, pyobihai, host, username, password, service_name): + def __init__(self, pyobihai, serial, service_name): """Initialize monitor sensor.""" - self._host = host - self._username = username - self._password = password self._service_name = service_name self._state = None self._name = f"{OBIHAI} {self._service_name}" self._pyobihai = pyobihai + self._unique_id = f"{serial}-{self._service_name}" @property def name(self): @@ -82,6 +92,18 @@ class ObihaiServiceSensors(Entity): """Return the state of the sensor.""" return self._state + @property + def available(self): + """Return if sensor is available.""" + if self._state is not None: + return True + return False + + @property + def unique_id(self): + """Return the unique ID.""" + return self._unique_id + @property def device_class(self): """Return the device class for uptime sensor.""" @@ -89,16 +111,38 @@ class ObihaiServiceSensors(Entity): return DEVICE_CLASS_TIMESTAMP return None + @property + def icon(self): + """Return an icon.""" + if self._service_name == "Call Direction": + if self._state == "No Active Calls": + return "mdi:phone-off" + if self._state == "Inbound Call": + return "mdi:phone-incoming" + return "mdi:phone-outgoing" + if "Caller Info" in self._service_name: + return "mdi:phone-log" + if "Port" in self._service_name: + if self._state == "Ringing": + return "mdi:phone-ring" + if self._state == "Off Hook": + return "mdi:phone-in-talk" + return "mdi:phone-hangup" + return "mdi:phone" + def update(self): """Update the sensor.""" - services = self._pyobihai.get_state(self._host, self._username, self._password) + services = self._pyobihai.get_state() if self._service_name in services: self._state = services.get(self._service_name) - services = self._pyobihai.get_line_state( - self._host, self._username, self._password - ) + services = self._pyobihai.get_line_state() if self._service_name in services: self._state = services.get(self._service_name) + + call_direction = self._pyobihai.get_call_direction() + + if self._service_name in call_direction: + self._state = call_direction.get(self._service_name) diff --git a/homeassistant/components/octoprint/manifest.json b/homeassistant/components/octoprint/manifest.json index c34e1458e4b..77f6a22e69f 100644 --- a/homeassistant/components/octoprint/manifest.json +++ b/homeassistant/components/octoprint/manifest.json @@ -1,7 +1,7 @@ { "domain": "octoprint", "name": "Octoprint", - "documentation": "https://www.home-assistant.io/components/octoprint", + "documentation": "https://www.home-assistant.io/integrations/octoprint", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/oem/manifest.json b/homeassistant/components/oem/manifest.json index d23b07b2756..1634676a60b 100644 --- a/homeassistant/components/oem/manifest.json +++ b/homeassistant/components/oem/manifest.json @@ -1,7 +1,7 @@ { "domain": "oem", "name": "Oem", - "documentation": "https://www.home-assistant.io/components/oem", + "documentation": "https://www.home-assistant.io/integrations/oem", "requirements": [ "oemthermostat==1.1" ], diff --git a/homeassistant/components/ohmconnect/manifest.json b/homeassistant/components/ohmconnect/manifest.json index 33c93bc8ac1..c958d23e725 100644 --- a/homeassistant/components/ohmconnect/manifest.json +++ b/homeassistant/components/ohmconnect/manifest.json @@ -1,7 +1,7 @@ { "domain": "ohmconnect", "name": "Ohmconnect", - "documentation": "https://www.home-assistant.io/components/ohmconnect", + "documentation": "https://www.home-assistant.io/integrations/ohmconnect", "requirements": [ "defusedxml==0.6.0" ], diff --git a/homeassistant/components/ombi/__init__.py b/homeassistant/components/ombi/__init__.py new file mode 100644 index 00000000000..860c7d4dcb4 --- /dev/null +++ b/homeassistant/components/ombi/__init__.py @@ -0,0 +1,149 @@ +"""Support for Ombi.""" +import logging + +import pyombi +import voluptuous as vol + +from homeassistant.const import ( + CONF_API_KEY, + CONF_HOST, + CONF_PORT, + CONF_SSL, + CONF_USERNAME, +) +import homeassistant.helpers.config_validation as cv + +from .const import ( + ATTR_NAME, + ATTR_SEASON, + CONF_URLBASE, + DEFAULT_PORT, + DEFAULT_SEASON, + DEFAULT_SSL, + DEFAULT_URLBASE, + DOMAIN, + SERVICE_MOVIE_REQUEST, + SERVICE_MUSIC_REQUEST, + SERVICE_TV_REQUEST, +) + +_LOGGER = logging.getLogger(__name__) + + +def urlbase(value) -> str: + """Validate and transform urlbase.""" + if value is None: + raise vol.Invalid("string value is None") + value = str(value).strip("/") + if not value: + return value + return value + "/" + + +SUBMIT_MOVIE_REQUEST_SERVICE_SCHEMA = vol.Schema({vol.Required(ATTR_NAME): cv.string}) + +SUBMIT_MUSIC_REQUEST_SERVICE_SCHEMA = vol.Schema({vol.Required(ATTR_NAME): cv.string}) + +SUBMIT_TV_REQUEST_SERVICE_SCHEMA = vol.Schema( + { + vol.Required(ATTR_NAME): cv.string, + vol.Optional(ATTR_SEASON, default=DEFAULT_SEASON): vol.In( + ["first", "latest", "all"] + ), + } +) + +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: vol.Schema( + { + vol.Required(CONF_API_KEY): cv.string, + vol.Required(CONF_HOST): cv.string, + vol.Required(CONF_USERNAME): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Optional(CONF_URLBASE, default=DEFAULT_URLBASE): urlbase, + vol.Optional(CONF_SSL, default=DEFAULT_SSL): cv.boolean, + } + ) + }, + extra=vol.ALLOW_EXTRA, +) + + +def setup(hass, config): + """Set up the Ombi component platform.""" + + ombi = pyombi.Ombi( + ssl=config[DOMAIN][CONF_SSL], + host=config[DOMAIN][CONF_HOST], + port=config[DOMAIN][CONF_PORT], + api_key=config[DOMAIN][CONF_API_KEY], + username=config[DOMAIN][CONF_USERNAME], + urlbase=config[DOMAIN][CONF_URLBASE], + ) + + try: + ombi.test_connection() + except pyombi.OmbiError as err: + _LOGGER.warning("Unable to setup Ombi: %s", err) + return False + + hass.data[DOMAIN] = {"instance": ombi} + + def submit_movie_request(call): + """Submit request for movie.""" + name = call.data[ATTR_NAME] + movies = ombi.search_movie(name) + if movies: + movie = movies[0] + ombi.request_movie(movie["theMovieDbId"]) + else: + raise Warning("No movie found.") + + def submit_tv_request(call): + """Submit request for TV show.""" + name = call.data[ATTR_NAME] + tv_shows = ombi.search_tv(name) + + if tv_shows: + season = call.data[ATTR_SEASON] + show = tv_shows[0]["id"] + if season == "first": + ombi.request_tv(show, request_first=True) + elif season == "latest": + ombi.request_tv(show, request_latest=True) + elif season == "all": + ombi.request_tv(show, request_all=True) + else: + raise Warning("No TV show found.") + + def submit_music_request(call): + """Submit request for music album.""" + name = call.data[ATTR_NAME] + music = ombi.search_music_album(name) + if music: + ombi.request_music(music[0]["foreignAlbumId"]) + else: + raise Warning("No music album found.") + + hass.services.register( + DOMAIN, + SERVICE_MOVIE_REQUEST, + submit_movie_request, + schema=SUBMIT_MOVIE_REQUEST_SERVICE_SCHEMA, + ) + hass.services.register( + DOMAIN, + SERVICE_MUSIC_REQUEST, + submit_music_request, + schema=SUBMIT_MUSIC_REQUEST_SERVICE_SCHEMA, + ) + hass.services.register( + DOMAIN, + SERVICE_TV_REQUEST, + submit_tv_request, + schema=SUBMIT_TV_REQUEST_SERVICE_SCHEMA, + ) + hass.helpers.discovery.load_platform("sensor", DOMAIN, {}, config) + + return True diff --git a/homeassistant/components/ombi/const.py b/homeassistant/components/ombi/const.py new file mode 100644 index 00000000000..42b58e7f50d --- /dev/null +++ b/homeassistant/components/ombi/const.py @@ -0,0 +1,24 @@ +"""Support for Ombi.""" +ATTR_NAME = "name" +ATTR_SEASON = "season" + +CONF_URLBASE = "urlbase" + +DEFAULT_NAME = DOMAIN = "ombi" +DEFAULT_PORT = 5000 +DEFAULT_SEASON = "latest" +DEFAULT_SSL = False +DEFAULT_URLBASE = "" + +SERVICE_MOVIE_REQUEST = "submit_movie_request" +SERVICE_MUSIC_REQUEST = "submit_music_request" +SERVICE_TV_REQUEST = "submit_tv_request" + +SENSOR_TYPES = { + "movies": {"type": "Movie requests", "icon": "mdi:movie"}, + "tv": {"type": "TV show requests", "icon": "mdi:television-classic"}, + "music": {"type": "Music album requests", "icon": "mdi:album"}, + "pending": {"type": "Pending requests", "icon": "mdi:clock-alert-outline"}, + "approved": {"type": "Approved requests", "icon": "mdi:check"}, + "available": {"type": "Available requests", "icon": "mdi:download"}, +} diff --git a/homeassistant/components/ombi/manifest.json b/homeassistant/components/ombi/manifest.json new file mode 100644 index 00000000000..fb6daf00f66 --- /dev/null +++ b/homeassistant/components/ombi/manifest.json @@ -0,0 +1,8 @@ +{ + "domain": "ombi", + "name": "Ombi", + "documentation": "https://www.home-assistant.io/integrations/ombi/", + "dependencies": [], + "codeowners": ["@larssont"], + "requirements": ["pyombi==0.1.5"] +} diff --git a/homeassistant/components/ombi/sensor.py b/homeassistant/components/ombi/sensor.py new file mode 100644 index 00000000000..2a2f50532b4 --- /dev/null +++ b/homeassistant/components/ombi/sensor.py @@ -0,0 +1,77 @@ +"""Support for Ombi.""" +from datetime import timedelta +import logging + +from pyombi import OmbiError + +from homeassistant.helpers.entity import Entity + +from .const import DOMAIN, SENSOR_TYPES + +_LOGGER = logging.getLogger(__name__) + +SCAN_INTERVAL = timedelta(seconds=60) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the Ombi sensor platform.""" + if discovery_info is None: + return + + sensors = [] + + ombi = hass.data[DOMAIN]["instance"] + + for sensor in SENSOR_TYPES: + sensor_label = sensor + sensor_type = SENSOR_TYPES[sensor]["type"] + sensor_icon = SENSOR_TYPES[sensor]["icon"] + sensors.append(OmbiSensor(sensor_label, sensor_type, ombi, sensor_icon)) + + add_entities(sensors, True) + + +class OmbiSensor(Entity): + """Representation of an Ombi sensor.""" + + def __init__(self, label, sensor_type, ombi, icon): + """Initialize the sensor.""" + self._state = None + self._label = label + self._type = sensor_type + self._ombi = ombi + self._icon = icon + + @property + def name(self): + """Return the name of the sensor.""" + return f"Ombi {self._type}" + + @property + def icon(self): + """Return the icon to use in the frontend.""" + return self._icon + + @property + def state(self): + """Return the state of the sensor.""" + return self._state + + def update(self): + """Update the sensor.""" + try: + if self._label == "movies": + self._state = self._ombi.movie_requests + elif self._label == "tv": + self._state = self._ombi.tv_requests + elif self._label == "music": + self._state = self._ombi.music_requests + elif self._label == "pending": + self._state = self._ombi.total_requests["pending"] + elif self._label == "approved": + self._state = self._ombi.total_requests["approved"] + elif self._label == "available": + self._state = self._ombi.total_requests["available"] + except OmbiError as err: + _LOGGER.warning("Unable to update Ombi sensor: %s", err) + self._state = None diff --git a/homeassistant/components/ombi/services.yaml b/homeassistant/components/ombi/services.yaml new file mode 100644 index 00000000000..5f4f2defe32 --- /dev/null +++ b/homeassistant/components/ombi/services.yaml @@ -0,0 +1,27 @@ +# Ombi services.yaml entries + +submit_movie_request: + description: Searches for a movie and requests the first result. + fields: + name: + description: Search parameter + example: "beverly hills cop" + + +submit_tv_request: + description: Searches for a TV show and requests the first result. + fields: + name: + description: Search parameter + example: "breaking bad" + season: + description: Which season(s) to request (first, latest or all) + example: "latest" + + +submit_music_request: + description: Searches for a music album and requests the first result. + fields: + name: + description: Search parameter + example: "nevermind" \ No newline at end of file diff --git a/homeassistant/components/onboarding/manifest.json b/homeassistant/components/onboarding/manifest.json index ffb01bd5602..2febfc481e0 100644 --- a/homeassistant/components/onboarding/manifest.json +++ b/homeassistant/components/onboarding/manifest.json @@ -1,7 +1,7 @@ { "domain": "onboarding", "name": "Onboarding", - "documentation": "https://www.home-assistant.io/components/onboarding", + "documentation": "https://www.home-assistant.io/integrations/onboarding", "requirements": [], "dependencies": [ "auth", diff --git a/homeassistant/components/onewire/manifest.json b/homeassistant/components/onewire/manifest.json index 00075d4485f..2d8c6c71071 100644 --- a/homeassistant/components/onewire/manifest.json +++ b/homeassistant/components/onewire/manifest.json @@ -1,7 +1,7 @@ { "domain": "onewire", "name": "Onewire", - "documentation": "https://www.home-assistant.io/components/onewire", + "documentation": "https://www.home-assistant.io/integrations/onewire", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/onkyo/manifest.json b/homeassistant/components/onkyo/manifest.json index 7fd27dd7edf..5bb116dece8 100644 --- a/homeassistant/components/onkyo/manifest.json +++ b/homeassistant/components/onkyo/manifest.json @@ -1,7 +1,7 @@ { "domain": "onkyo", "name": "Onkyo", - "documentation": "https://www.home-assistant.io/components/onkyo", + "documentation": "https://www.home-assistant.io/integrations/onkyo", "requirements": [ "onkyo-eiscp==1.2.4" ], diff --git a/homeassistant/components/onkyo/media_player.py b/homeassistant/components/onkyo/media_player.py index 023fb32e6e4..af92f6c5f05 100644 --- a/homeassistant/components/onkyo/media_player.py +++ b/homeassistant/components/onkyo/media_player.py @@ -264,8 +264,7 @@ class OnkyoDevice(MediaPlayerDevice): if source in self._source_mapping: self._current_source = self._source_mapping[source] break - else: - self._current_source = "_".join([i for i in current_source_tuples[1]]) + self._current_source = "_".join([i for i in current_source_tuples[1]]) if preset_raw and self._current_source.lower() == "radio": self._attributes[ATTR_PRESET] = preset_raw[1] elif ATTR_PRESET in self._attributes: @@ -374,7 +373,7 @@ class OnkyoDeviceZone(OnkyoDevice): """Initialize the Zone with the zone identifier.""" self._zone = zone self._supports_volume = True - super(OnkyoDeviceZone, self).__init__(receiver, sources, name) + super().__init__(receiver, sources, name) def update(self): """Get the latest state from the device.""" @@ -414,8 +413,7 @@ class OnkyoDeviceZone(OnkyoDevice): if source in self._source_mapping: self._current_source = self._source_mapping[source] break - else: - self._current_source = "_".join([i for i in current_source_tuples[1]]) + self._current_source = "_".join([i for i in current_source_tuples[1]]) self._muted = bool(mute_raw[1] == "on") if preset_raw and self._current_source.lower() == "radio": self._attributes[ATTR_PRESET] = preset_raw[1] diff --git a/homeassistant/components/onvif/camera.py b/homeassistant/components/onvif/camera.py index 0635a2d1f11..29af1049fae 100644 --- a/homeassistant/components/onvif/camera.py +++ b/homeassistant/components/onvif/camera.py @@ -23,7 +23,7 @@ from homeassistant.components.camera.const import DOMAIN from homeassistant.components.ffmpeg import DATA_FFMPEG, CONF_EXTRA_ARGUMENTS import homeassistant.helpers.config_validation as cv from homeassistant.helpers.aiohttp_client import async_aiohttp_proxy_stream -from homeassistant.helpers.service import extract_entity_ids +from homeassistant.helpers.service import async_extract_entity_ids import homeassistant.util.dt as dt_util _LOGGER = logging.getLogger(__name__) @@ -88,7 +88,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= tilt = service.data.get(ATTR_TILT, None) zoom = service.data.get(ATTR_ZOOM, None) all_cameras = hass.data[ONVIF_DATA][ENTITIES] - entity_ids = extract_entity_ids(hass, service) + entity_ids = await async_extract_entity_ids(hass, service) target_cameras = [] if not entity_ids: target_cameras = all_cameras @@ -156,7 +156,7 @@ class ONVIFHassCamera(Camera): Initializes the camera by obtaining the input uri and connecting to the camera. Also retrieves the ONVIF profiles. """ - from aiohttp.client_exceptions import ClientConnectorError + from aiohttp.client_exceptions import ClientConnectionError from homeassistant.exceptions import PlatformNotReady from zeep.exceptions import Fault @@ -167,7 +167,7 @@ class ONVIFHassCamera(Camera): await self.async_check_date_and_time() await self.async_obtain_input_uri() self.setup_ptz() - except ClientConnectorError as err: + except ClientConnectionError as err: _LOGGER.warning( "Couldn't connect to camera '%s', but will " "retry later. Error: %s", self._name, @@ -282,7 +282,7 @@ class ONVIFHassCamera(Camera): """Set up PTZ if available.""" _LOGGER.debug("Setting up the ONVIF PTZ service") if self._camera.get_service("ptz", create=False) is None: - _LOGGER.warning("PTZ is not available on this camera") + _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") diff --git a/homeassistant/components/onvif/manifest.json b/homeassistant/components/onvif/manifest.json index d86ec38ccb7..f6c23712188 100644 --- a/homeassistant/components/onvif/manifest.json +++ b/homeassistant/components/onvif/manifest.json @@ -1,7 +1,7 @@ { "domain": "onvif", "name": "Onvif", - "documentation": "https://www.home-assistant.io/components/onvif", + "documentation": "https://www.home-assistant.io/integrations/onvif", "requirements": [ "onvif-zeep-async==0.2.0" ], diff --git a/homeassistant/components/openalpr_cloud/manifest.json b/homeassistant/components/openalpr_cloud/manifest.json index f0421295836..56128f0e886 100644 --- a/homeassistant/components/openalpr_cloud/manifest.json +++ b/homeassistant/components/openalpr_cloud/manifest.json @@ -1,7 +1,7 @@ { "domain": "openalpr_cloud", "name": "Openalpr cloud", - "documentation": "https://www.home-assistant.io/components/openalpr_cloud", + "documentation": "https://www.home-assistant.io/integrations/openalpr_cloud", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/openalpr_local/manifest.json b/homeassistant/components/openalpr_local/manifest.json index 3c92e840f43..65fa6b1bec6 100644 --- a/homeassistant/components/openalpr_local/manifest.json +++ b/homeassistant/components/openalpr_local/manifest.json @@ -1,7 +1,7 @@ { "domain": "openalpr_local", "name": "Openalpr local", - "documentation": "https://www.home-assistant.io/components/openalpr_local", + "documentation": "https://www.home-assistant.io/integrations/openalpr_local", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/opencv/manifest.json b/homeassistant/components/opencv/manifest.json index e8ebeb102e6..40421674a4b 100644 --- a/homeassistant/components/opencv/manifest.json +++ b/homeassistant/components/opencv/manifest.json @@ -1,7 +1,7 @@ { "domain": "opencv", "name": "Opencv", - "documentation": "https://www.home-assistant.io/components/opencv", + "documentation": "https://www.home-assistant.io/integrations/opencv", "requirements": [ "numpy==1.17.1", "opencv-python-headless==4.1.1.26" diff --git a/homeassistant/components/openevse/manifest.json b/homeassistant/components/openevse/manifest.json index f37c769d20e..a56f0caddab 100644 --- a/homeassistant/components/openevse/manifest.json +++ b/homeassistant/components/openevse/manifest.json @@ -1,7 +1,7 @@ { "domain": "openevse", "name": "Openevse", - "documentation": "https://www.home-assistant.io/components/openevse", + "documentation": "https://www.home-assistant.io/integrations/openevse", "requirements": [ "openevsewifi==0.4" ], diff --git a/homeassistant/components/openexchangerates/manifest.json b/homeassistant/components/openexchangerates/manifest.json index ffb86d4a5e2..fae9e616e5e 100644 --- a/homeassistant/components/openexchangerates/manifest.json +++ b/homeassistant/components/openexchangerates/manifest.json @@ -1,7 +1,7 @@ { "domain": "openexchangerates", "name": "Openexchangerates", - "documentation": "https://www.home-assistant.io/components/openexchangerates", + "documentation": "https://www.home-assistant.io/integrations/openexchangerates", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/opengarage/manifest.json b/homeassistant/components/opengarage/manifest.json index 95f944b7087..4dcb53e98ce 100644 --- a/homeassistant/components/opengarage/manifest.json +++ b/homeassistant/components/opengarage/manifest.json @@ -1,7 +1,7 @@ { "domain": "opengarage", "name": "Opengarage", - "documentation": "https://www.home-assistant.io/components/opengarage", + "documentation": "https://www.home-assistant.io/integrations/opengarage", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/openhardwaremonitor/manifest.json b/homeassistant/components/openhardwaremonitor/manifest.json index d9281f08eda..4a17f933515 100644 --- a/homeassistant/components/openhardwaremonitor/manifest.json +++ b/homeassistant/components/openhardwaremonitor/manifest.json @@ -1,7 +1,7 @@ { "domain": "openhardwaremonitor", "name": "Openhardwaremonitor", - "documentation": "https://www.home-assistant.io/components/openhardwaremonitor", + "documentation": "https://www.home-assistant.io/integrations/openhardwaremonitor", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/openhome/manifest.json b/homeassistant/components/openhome/manifest.json index 276346ae79b..2ec58b86125 100644 --- a/homeassistant/components/openhome/manifest.json +++ b/homeassistant/components/openhome/manifest.json @@ -1,7 +1,7 @@ { "domain": "openhome", "name": "Openhome", - "documentation": "https://www.home-assistant.io/components/openhome", + "documentation": "https://www.home-assistant.io/integrations/openhome", "requirements": [ "openhomedevice==0.4.2" ], diff --git a/homeassistant/components/opensensemap/manifest.json b/homeassistant/components/opensensemap/manifest.json index ab03f1cf7c6..632ab82918e 100644 --- a/homeassistant/components/opensensemap/manifest.json +++ b/homeassistant/components/opensensemap/manifest.json @@ -1,7 +1,7 @@ { "domain": "opensensemap", "name": "Opensensemap", - "documentation": "https://www.home-assistant.io/components/opensensemap", + "documentation": "https://www.home-assistant.io/integrations/opensensemap", "requirements": [ "opensensemap-api==0.1.5" ], diff --git a/homeassistant/components/opensky/manifest.json b/homeassistant/components/opensky/manifest.json index dd58cdd4168..a98e828a52e 100644 --- a/homeassistant/components/opensky/manifest.json +++ b/homeassistant/components/opensky/manifest.json @@ -1,7 +1,7 @@ { "domain": "opensky", "name": "Opensky", - "documentation": "https://www.home-assistant.io/components/opensky", + "documentation": "https://www.home-assistant.io/integrations/opensky", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/opentherm_gw/.translations/ca.json b/homeassistant/components/opentherm_gw/.translations/ca.json new file mode 100644 index 00000000000..0224d663a83 --- /dev/null +++ b/homeassistant/components/opentherm_gw/.translations/ca.json @@ -0,0 +1,23 @@ +{ + "config": { + "error": { + "already_configured": "Passarel\u00b7la ja configurada", + "id_exists": "L'identificador de passarel\u00b7la ja existeix", + "serial_error": "S'ha produ\u00eft un error en connectar-se al dispositiu", + "timeout": "S'ha acabat el temps d'espera en l'intent de connexi\u00f3" + }, + "step": { + "init": { + "data": { + "device": "Ruta o URL", + "floor_temperature": "Temperatura del pis", + "id": "ID", + "name": "Nom", + "precision": "Precisi\u00f3 de la temperatura" + }, + "title": "Passarel\u00b7la d'OpenTherm" + } + }, + "title": "Passarel\u00b7la d'OpenTherm" + } +} \ No newline at end of file diff --git a/homeassistant/components/opentherm_gw/.translations/da.json b/homeassistant/components/opentherm_gw/.translations/da.json new file mode 100644 index 00000000000..b8abb48af4e --- /dev/null +++ b/homeassistant/components/opentherm_gw/.translations/da.json @@ -0,0 +1,20 @@ +{ + "config": { + "error": { + "already_configured": "Gateway allerede konfigureret", + "id_exists": "Gateway-id findes allerede", + "serial_error": "Fejl ved tilslutning til enheden" + }, + "step": { + "init": { + "data": { + "device": "Sti eller URL", + "id": "ID", + "name": "Navn" + }, + "title": "OpenTherm Gateway" + } + }, + "title": "OpenTherm Gateway" + } +} \ No newline at end of file diff --git a/homeassistant/components/opentherm_gw/.translations/de.json b/homeassistant/components/opentherm_gw/.translations/de.json new file mode 100644 index 00000000000..274dd46488b --- /dev/null +++ b/homeassistant/components/opentherm_gw/.translations/de.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "already_configured": "Gateway bereits konfiguriert", + "id_exists": "Gateway-ID ist bereits vorhanden", + "serial_error": "Fehler beim Verbinden mit dem Ger\u00e4t", + "timeout": "Zeit\u00fcberschreitung beim Verbindungsversuch" + }, + "step": { + "init": { + "data": { + "device": "Pfad oder URL", + "id": "ID", + "name": "Name" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/opentherm_gw/.translations/en.json b/homeassistant/components/opentherm_gw/.translations/en.json new file mode 100644 index 00000000000..65d7d9e92bb --- /dev/null +++ b/homeassistant/components/opentherm_gw/.translations/en.json @@ -0,0 +1,23 @@ +{ + "config": { + "error": { + "already_configured": "Gateway already configured", + "id_exists": "Gateway id already exists", + "serial_error": "Error connecting to device", + "timeout": "Connection attempt timed out" + }, + "step": { + "init": { + "data": { + "device": "Path or URL", + "floor_temperature": "Floor climate temperature", + "id": "ID", + "name": "Name", + "precision": "Climate temperature precision" + }, + "title": "OpenTherm Gateway" + } + }, + "title": "OpenTherm Gateway" + } +} \ No newline at end of file diff --git a/homeassistant/components/opentherm_gw/.translations/es.json b/homeassistant/components/opentherm_gw/.translations/es.json new file mode 100644 index 00000000000..8ad9d89b07a --- /dev/null +++ b/homeassistant/components/opentherm_gw/.translations/es.json @@ -0,0 +1,23 @@ +{ + "config": { + "error": { + "already_configured": "Gateway ya configurado", + "id_exists": "El ID del Gateway ya existe", + "serial_error": "Error de conexi\u00f3n al dispositivo", + "timeout": "Intento de conexi\u00f3n agotado" + }, + "step": { + "init": { + "data": { + "device": "Ruta o URL", + "floor_temperature": "Temperatura del suelo", + "id": "ID", + "name": "Nombre", + "precision": "Precisi\u00f3n de la temperatura clim\u00e1tica" + }, + "title": "Gateway OpenTherm" + } + }, + "title": "Gateway OpenTherm" + } +} \ No newline at end of file diff --git a/homeassistant/components/opentherm_gw/.translations/fr.json b/homeassistant/components/opentherm_gw/.translations/fr.json new file mode 100644 index 00000000000..82b9a7aee88 --- /dev/null +++ b/homeassistant/components/opentherm_gw/.translations/fr.json @@ -0,0 +1,22 @@ +{ + "config": { + "error": { + "already_configured": "Passerelle d\u00e9j\u00e0 configur\u00e9e", + "id_exists": "L'identifiant de la passerelle existe d\u00e9j\u00e0", + "serial_error": "Erreur de connexion \u00e0 l'appareil", + "timeout": "La tentative de connexion a expir\u00e9" + }, + "step": { + "init": { + "data": { + "device": "Chemin ou URL", + "id": "ID", + "name": "Nom", + "precision": "Pr\u00e9cision de la temp\u00e9rature climatique" + }, + "title": "Passerelle OpenTherm" + } + }, + "title": "Passerelle OpenTherm" + } +} \ No newline at end of file diff --git a/homeassistant/components/opentherm_gw/.translations/it.json b/homeassistant/components/opentherm_gw/.translations/it.json new file mode 100644 index 00000000000..9c62686e190 --- /dev/null +++ b/homeassistant/components/opentherm_gw/.translations/it.json @@ -0,0 +1,23 @@ +{ + "config": { + "error": { + "already_configured": "Gateway gi\u00e0 configurato", + "id_exists": "ID del gateway esiste gi\u00e0", + "serial_error": "Errore durante la connessione al dispositivo", + "timeout": "Tentativo di connessione scaduto" + }, + "step": { + "init": { + "data": { + "device": "Percorso o URL", + "floor_temperature": "Temperatura climatica del pavimento", + "id": "ID", + "name": "Nome", + "precision": "Precisione della temperatura climatica" + }, + "title": "OpenTherm Gateway" + } + }, + "title": "Gateway OpenTherm" + } +} \ No newline at end of file diff --git a/homeassistant/components/opentherm_gw/.translations/lb.json b/homeassistant/components/opentherm_gw/.translations/lb.json new file mode 100644 index 00000000000..ec1f719a6cc --- /dev/null +++ b/homeassistant/components/opentherm_gw/.translations/lb.json @@ -0,0 +1,23 @@ +{ + "config": { + "error": { + "already_configured": "Gateway ass scho konfigur\u00e9iert", + "id_exists": "Gateway ID g\u00ebtt et schonn", + "serial_error": "Feeler beim verbannen", + "timeout": "Z\u00e4it Iwwerschreidung beim Verbindungs Versuch" + }, + "step": { + "init": { + "data": { + "device": "Pfad oder URL", + "floor_temperature": "Buedem Klima Temperatur", + "id": "ID", + "name": "Numm", + "precision": "Klima Temperatur Prezisioun" + }, + "title": "OpenTherm Gateway" + } + }, + "title": "OpenTherm Gateway" + } +} \ No newline at end of file diff --git a/homeassistant/components/opentherm_gw/.translations/nl.json b/homeassistant/components/opentherm_gw/.translations/nl.json new file mode 100644 index 00000000000..4fec1baba7b --- /dev/null +++ b/homeassistant/components/opentherm_gw/.translations/nl.json @@ -0,0 +1,14 @@ +{ + "config": { + "step": { + "init": { + "data": { + "device": "Pad of URL", + "id": "ID" + }, + "title": "OpenTherm Gateway" + } + }, + "title": "OpenTherm Gateway" + } +} \ No newline at end of file diff --git a/homeassistant/components/opentherm_gw/.translations/nn.json b/homeassistant/components/opentherm_gw/.translations/nn.json new file mode 100644 index 00000000000..3d018a2292d --- /dev/null +++ b/homeassistant/components/opentherm_gw/.translations/nn.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "init": { + "data": { + "id": "ID", + "name": "Namn" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/opentherm_gw/.translations/no.json b/homeassistant/components/opentherm_gw/.translations/no.json new file mode 100644 index 00000000000..6104aa7de72 --- /dev/null +++ b/homeassistant/components/opentherm_gw/.translations/no.json @@ -0,0 +1,23 @@ +{ + "config": { + "error": { + "already_configured": "Gateway er allerede konfigurert", + "id_exists": "Gateway-ID finnes allerede", + "serial_error": "Feil ved tilkobling til enhet", + "timeout": "Tilkoblingsfors\u00f8k ble tidsavbrutt" + }, + "step": { + "init": { + "data": { + "device": "Bane eller URL-adresse", + "floor_temperature": "Gulv klimatemperatur", + "id": "ID", + "name": "Navn", + "precision": "Klima temperaturpresisjon" + }, + "title": "OpenTherm Gateway" + } + }, + "title": "OpenTherm Gateway" + } +} \ No newline at end of file diff --git a/homeassistant/components/opentherm_gw/.translations/pl.json b/homeassistant/components/opentherm_gw/.translations/pl.json new file mode 100644 index 00000000000..7e4a0eed013 --- /dev/null +++ b/homeassistant/components/opentherm_gw/.translations/pl.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "init": { + "data": { + "device": "\u015acie\u017cka lub adres URL", + "name": "Nazwa" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/opentherm_gw/.translations/ru.json b/homeassistant/components/opentherm_gw/.translations/ru.json new file mode 100644 index 00000000000..718322ec171 --- /dev/null +++ b/homeassistant/components/opentherm_gw/.translations/ru.json @@ -0,0 +1,23 @@ +{ + "config": { + "error": { + "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", + "id_exists": "\u0418\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440 \u0448\u043b\u044e\u0437\u0430 \u0443\u0436\u0435 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u0435\u0442.", + "serial_error": "\u041e\u0448\u0438\u0431\u043a\u0430 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443.", + "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": { + "init": { + "data": { + "device": "\u041f\u0443\u0442\u044c \u0438\u043b\u0438 URL-\u0430\u0434\u0440\u0435\u0441", + "floor_temperature": "\u0422\u0435\u043c\u043f\u0435\u0440\u0430\u0442\u0443\u0440\u0430 \u043f\u043e\u043b\u0430", + "id": "ID", + "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435", + "precision": "\u0422\u043e\u0447\u043d\u043e\u0441\u0442\u044c \u0442\u0435\u043c\u043f\u0435\u0440\u0430\u0442\u0443\u0440\u044b" + }, + "title": "OpenTherm" + } + }, + "title": "OpenTherm" + } +} \ No newline at end of file diff --git a/homeassistant/components/opentherm_gw/.translations/sl.json b/homeassistant/components/opentherm_gw/.translations/sl.json new file mode 100644 index 00000000000..5de551d5d0c --- /dev/null +++ b/homeassistant/components/opentherm_gw/.translations/sl.json @@ -0,0 +1,23 @@ +{ + "config": { + "error": { + "already_configured": "Prehod je \u017ee konfiguriran", + "id_exists": "ID prehoda \u017ee obstaja", + "serial_error": "Napaka pri povezovanju z napravo", + "timeout": "Poskus povezave je potekel" + }, + "step": { + "init": { + "data": { + "device": "Pot ali URL", + "floor_temperature": "Temperatura nadstropja", + "id": "ID", + "name": "Ime", + "precision": "Natan\u010dnost temperature " + }, + "title": "OpenTherm Prehod" + } + }, + "title": "OpenTherm Prehod" + } +} \ No newline at end of file diff --git a/homeassistant/components/opentherm_gw/.translations/zh-Hant.json b/homeassistant/components/opentherm_gw/.translations/zh-Hant.json new file mode 100644 index 00000000000..648f156e864 --- /dev/null +++ b/homeassistant/components/opentherm_gw/.translations/zh-Hant.json @@ -0,0 +1,23 @@ +{ + "config": { + "error": { + "already_configured": "\u9598\u9053\u5668\u5df2\u8a2d\u5b9a\u5b8c\u6210", + "id_exists": "\u9598\u9053\u5668 ID \u5df2\u5b58\u5728", + "serial_error": "\u9023\u7dda\u81f3\u88dd\u7f6e\u932f\u8aa4", + "timeout": "\u9023\u7dda\u5617\u8a66\u903e\u6642" + }, + "step": { + "init": { + "data": { + "device": "\u8def\u5f91\u6216 URL", + "floor_temperature": "\u6a13\u5c64\u6eab\u5ea6", + "id": "ID", + "name": "\u540d\u7a31", + "precision": "\u6eab\u63a7\u7cbe\u6e96\u5ea6" + }, + "title": "OpenTherm \u9598\u9053\u5668" + } + }, + "title": "OpenTherm \u9598\u9053\u5668" + } +} \ No newline at end of file diff --git a/homeassistant/components/opentherm_gw/__init__.py b/homeassistant/components/opentherm_gw/__init__.py index 0c145963653..a32c375ac65 100644 --- a/homeassistant/components/opentherm_gw/__init__.py +++ b/homeassistant/components/opentherm_gw/__init__.py @@ -12,6 +12,7 @@ from homeassistant.components.sensor import DOMAIN as COMP_SENSOR from homeassistant.const import ( ATTR_DATE, ATTR_ID, + ATTR_MODE, ATTR_TEMPERATURE, ATTR_TIME, CONF_DEVICE, @@ -28,7 +29,6 @@ import homeassistant.helpers.config_validation as cv from .const import ( ATTR_GW_ID, - ATTR_MODE, ATTR_LEVEL, ATTR_DHW_OVRD, CONF_CLIMATE, diff --git a/homeassistant/components/opentherm_gw/climate.py b/homeassistant/components/opentherm_gw/climate.py index d37422d4177..fab028560bb 100644 --- a/homeassistant/components/opentherm_gw/climate.py +++ b/homeassistant/components/opentherm_gw/climate.py @@ -5,11 +5,14 @@ from pyotgw import vars as gw_vars from homeassistant.components.climate import ClimateDevice from homeassistant.components.climate.const import ( + CURRENT_HVAC_COOL, + CURRENT_HVAC_HEAT, + CURRENT_HVAC_IDLE, HVAC_MODE_COOL, HVAC_MODE_HEAT, - HVAC_MODE_OFF, SUPPORT_TARGET_TEMPERATURE, PRESET_AWAY, + PRESET_NONE, SUPPORT_PRESET_MODE, ) from homeassistant.const import ( @@ -47,8 +50,9 @@ class OpenThermClimate(ClimateDevice): self.friendly_name = gw_dev.name self.floor_temp = gw_dev.climate_config[CONF_FLOOR_TEMP] self.temp_precision = gw_dev.climate_config.get(CONF_PRECISION) - self._current_operation = HVAC_MODE_OFF + self._current_operation = None self._current_temperature = None + self._hvac_mode = HVAC_MODE_HEAT self._new_target_temperature = None self._target_temperature = None self._away_mode_a = None @@ -70,11 +74,13 @@ class OpenThermClimate(ClimateDevice): flame_on = status.get(gw_vars.DATA_SLAVE_FLAME_ON) cooling_active = status.get(gw_vars.DATA_SLAVE_COOLING_ACTIVE) if ch_active and flame_on: - self._current_operation = HVAC_MODE_HEAT + self._current_operation = CURRENT_HVAC_HEAT + self._hvac_mode = HVAC_MODE_HEAT elif cooling_active: - self._current_operation = HVAC_MODE_COOL + self._current_operation = CURRENT_HVAC_COOL + self._hvac_mode = HVAC_MODE_COOL else: - self._current_operation = HVAC_MODE_OFF + self._current_operation = CURRENT_HVAC_IDLE self._current_temperature = status.get(gw_vars.DATA_ROOM_TEMP) temp_upd = status.get(gw_vars.DATA_ROOM_SETPOINT) @@ -138,16 +144,25 @@ class OpenThermClimate(ClimateDevice): """Return the unit of measurement used by the platform.""" return TEMP_CELSIUS + @property + def hvac_action(self): + """Return current HVAC operation.""" + return self._current_operation + @property def hvac_mode(self): """Return current HVAC mode.""" - return self._current_operation + return self._hvac_mode @property def hvac_modes(self): """Return available HVAC modes.""" return [] + def set_hvac_mode(self, hvac_mode): + """Set the HVAC mode.""" + _LOGGER.warning("Changing HVAC mode is not supported") + @property def current_temperature(self): """Return the current temperature.""" @@ -176,6 +191,7 @@ class OpenThermClimate(ClimateDevice): """Return current preset mode.""" if self._away_state_a or self._away_state_b: return PRESET_AWAY + return PRESET_NONE @property def preset_modes(self): diff --git a/homeassistant/components/opentherm_gw/const.py b/homeassistant/components/opentherm_gw/const.py index 77b0bf9b313..60042b92867 100644 --- a/homeassistant/components/opentherm_gw/const.py +++ b/homeassistant/components/opentherm_gw/const.py @@ -4,7 +4,6 @@ import pyotgw.vars as gw_vars from homeassistant.const import DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS ATTR_GW_ID = "gateway_id" -ATTR_MODE = "mode" ATTR_LEVEL = "level" ATTR_DHW_OVRD = "dhw_override" diff --git a/homeassistant/components/opentherm_gw/manifest.json b/homeassistant/components/opentherm_gw/manifest.json index c6097a01cc4..9c7f165c6df 100644 --- a/homeassistant/components/opentherm_gw/manifest.json +++ b/homeassistant/components/opentherm_gw/manifest.json @@ -1,7 +1,7 @@ { "domain": "opentherm_gw", "name": "Opentherm Gateway", - "documentation": "https://www.home-assistant.io/components/opentherm_gw", + "documentation": "https://www.home-assistant.io/integrations/opentherm_gw", "requirements": [ "pyotgw==0.4b4" ], diff --git a/homeassistant/components/opentherm_gw/services.yaml b/homeassistant/components/opentherm_gw/services.yaml index 4e9e727ef5a..29ad2ecdbe7 100644 --- a/homeassistant/components/opentherm_gw/services.yaml +++ b/homeassistant/components/opentherm_gw/services.yaml @@ -64,7 +64,7 @@ set_gpio_mode: mode: description: > Mode to set on the GPIO pin. Values 0 through 6 are accepted for both GPIOs, 7 is only accepted for GPIO "B". - See https://www.home-assistant.io/components/opentherm_gw/#gpio-modes for an explanation of the values. + See https://www.home-assistant.io/integrations/opentherm_gw/#gpio-modes for an explanation of the values. example: '5' set_led_mode: @@ -79,7 +79,7 @@ set_led_mode: mode: description: > The function to assign to the LED. One of "R", "X", "T", "B", "O", "F", "H", "W", "C", "E", "M" or "P". - See https://www.home-assistant.io/components/opentherm_gw/#led-modes for an explanation of the values. + See https://www.home-assistant.io/integrations/opentherm_gw/#led-modes for an explanation of the values. example: 'F' set_max_modulation: diff --git a/homeassistant/components/openuv/.translations/ru.json b/homeassistant/components/openuv/.translations/ru.json index 9683c5d7c36..58d57b28056 100644 --- a/homeassistant/components/openuv/.translations/ru.json +++ b/homeassistant/components/openuv/.translations/ru.json @@ -1,7 +1,7 @@ { "config": { "error": { - "identifier_exists": "\u041a\u043e\u043e\u0440\u0434\u0438\u043d\u0430\u0442\u044b \u0443\u0436\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u044b", + "identifier_exists": "\u041a\u043e\u043e\u0440\u0434\u0438\u043d\u0430\u0442\u044b \u0443\u0436\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u044b.", "invalid_api_key": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043a\u043b\u044e\u0447 API" }, "step": { diff --git a/homeassistant/components/openuv/binary_sensor.py b/homeassistant/components/openuv/binary_sensor.py index 59f6e4d1c67..621950965f6 100644 --- a/homeassistant/components/openuv/binary_sensor.py +++ b/homeassistant/components/openuv/binary_sensor.py @@ -102,6 +102,11 @@ class OpenUvBinarySensor(OpenUvEntity, BinarySensorDevice): if not data: return + for key in ("from_time", "to_time", "from_uv", "to_uv"): + if not data.get(key): + _LOGGER.info("Skipping update due to missing data: %s", key) + return + if self._sensor_type == TYPE_PROTECTION_WINDOW: self._state = ( parse_datetime(data["from_time"]) diff --git a/homeassistant/components/openuv/manifest.json b/homeassistant/components/openuv/manifest.json index 0cfb02e81d6..69342df235f 100644 --- a/homeassistant/components/openuv/manifest.json +++ b/homeassistant/components/openuv/manifest.json @@ -2,7 +2,7 @@ "domain": "openuv", "name": "Openuv", "config_flow": true, - "documentation": "https://www.home-assistant.io/components/openuv", + "documentation": "https://www.home-assistant.io/integrations/openuv", "requirements": [ "pyopenuv==1.0.9" ], diff --git a/homeassistant/components/openweathermap/manifest.json b/homeassistant/components/openweathermap/manifest.json index d24b23f64bb..c28d27c1b8b 100644 --- a/homeassistant/components/openweathermap/manifest.json +++ b/homeassistant/components/openweathermap/manifest.json @@ -1,7 +1,7 @@ { "domain": "openweathermap", "name": "Openweathermap", - "documentation": "https://www.home-assistant.io/components/openweathermap", + "documentation": "https://www.home-assistant.io/integrations/openweathermap", "requirements": [ "pyowm==2.10.0" ], diff --git a/homeassistant/components/opple/manifest.json b/homeassistant/components/opple/manifest.json index c10be48f3fa..cee6447abc1 100644 --- a/homeassistant/components/opple/manifest.json +++ b/homeassistant/components/opple/manifest.json @@ -1,7 +1,7 @@ { "domain": "opple", "name": "Opple", - "documentation": "https://www.home-assistant.io/components/opple", + "documentation": "https://www.home-assistant.io/integrations/opple", "requirements": [ "pyoppleio==1.0.5" ], diff --git a/homeassistant/components/orangepi_gpio/manifest.json b/homeassistant/components/orangepi_gpio/manifest.json index 65fd0f7de50..51bca8fbbbe 100644 --- a/homeassistant/components/orangepi_gpio/manifest.json +++ b/homeassistant/components/orangepi_gpio/manifest.json @@ -1,7 +1,7 @@ { "domain": "orangepi_gpio", "name": "Orangepi GPIO", - "documentation": "https://www.home-assistant.io/components/orangepi_gpio", + "documentation": "https://www.home-assistant.io/integrations/orangepi_gpio", "requirements": [ "OPi.GPIO==0.3.6" ], diff --git a/homeassistant/components/orvibo/manifest.json b/homeassistant/components/orvibo/manifest.json index 73f4eaed7da..9dee62697c3 100644 --- a/homeassistant/components/orvibo/manifest.json +++ b/homeassistant/components/orvibo/manifest.json @@ -1,7 +1,7 @@ { "domain": "orvibo", "name": "Orvibo", - "documentation": "https://www.home-assistant.io/components/orvibo", + "documentation": "https://www.home-assistant.io/integrations/orvibo", "requirements": [ "orvibo==1.1.1" ], diff --git a/homeassistant/components/osramlightify/manifest.json b/homeassistant/components/osramlightify/manifest.json index 0b158b96742..8c6c9f30b10 100644 --- a/homeassistant/components/osramlightify/manifest.json +++ b/homeassistant/components/osramlightify/manifest.json @@ -1,7 +1,7 @@ { "domain": "osramlightify", "name": "Osramlightify", - "documentation": "https://www.home-assistant.io/components/osramlightify", + "documentation": "https://www.home-assistant.io/integrations/osramlightify", "requirements": [ "lightify==1.0.7.2" ], diff --git a/homeassistant/components/otp/manifest.json b/homeassistant/components/otp/manifest.json index cea246af328..25ece71c11b 100644 --- a/homeassistant/components/otp/manifest.json +++ b/homeassistant/components/otp/manifest.json @@ -1,9 +1,9 @@ { "domain": "otp", "name": "Otp", - "documentation": "https://www.home-assistant.io/components/otp", + "documentation": "https://www.home-assistant.io/integrations/otp", "requirements": [ - "pyotp==2.2.7" + "pyotp==2.3.0" ], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/owlet/manifest.json b/homeassistant/components/owlet/manifest.json index edc51dcc533..2ba00671b48 100644 --- a/homeassistant/components/owlet/manifest.json +++ b/homeassistant/components/owlet/manifest.json @@ -1,9 +1,9 @@ { "domain": "owlet", "name": "Owlet", - "documentation": "https://www.home-assistant.io/components/owlet", + "documentation": "https://www.home-assistant.io/integrations/owlet", "requirements": [ - "pyowlet==1.0.2" + "pyowlet==1.0.3" ], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/owntracks/.translations/nn.json b/homeassistant/components/owntracks/.translations/nn.json new file mode 100644 index 00000000000..cdfd651beec --- /dev/null +++ b/homeassistant/components/owntracks/.translations/nn.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "OwnTracks" + } +} \ No newline at end of file diff --git a/homeassistant/components/owntracks/.translations/no.json b/homeassistant/components/owntracks/.translations/no.json index 9f86cd12cc4..5838dcad30b 100644 --- a/homeassistant/components/owntracks/.translations/no.json +++ b/homeassistant/components/owntracks/.translations/no.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "one_instance_allowed": "Kun \u00e9n enkelt forekomst er n\u00f8dvendig." + "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." diff --git a/homeassistant/components/owntracks/.translations/zh-Hant.json b/homeassistant/components/owntracks/.translations/zh-Hant.json index d8c195cb277..923f452450b 100644 --- a/homeassistant/components/owntracks/.translations/zh-Hant.json +++ b/homeassistant/components/owntracks/.translations/zh-Hant.json @@ -4,7 +4,7 @@ "one_instance_allowed": "\u50c5\u9700\u8a2d\u5b9a\u4e00\u7d44\u7269\u4ef6\u5373\u53ef\u3002" }, "create_entry": { - "default": "\n\n\u65bc Android \u88dd\u7f6e\uff0c\u6253\u958b [OwnTracks app]({android_url})\u3001\u9ede\u9078\u8a2d\u5b9a\uff08preferences\uff09 -> \u9023\u7dda\uff08connection\uff09\u3002\u8b8a\u66f4\u4ee5\u4e0b\u8a2d\u5b9a\uff1a\n - \u6a21\u5f0f\uff08Mode\uff09\uff1aPrivate HTTP\n - \u4e3b\u6a5f\u7aef\uff08Host\uff09\uff1a{webhook_url}\n - Identification\uff1a\n - Username\uff1a ``\n - Device ID\uff1a``\n\n\u65bc iOS \u88dd\u7f6e\uff0c\u6253\u958b [OwnTracks app]({ios_url})\u3001\u9ede\u9078\u5de6\u4e0a\u65b9\u7684 (i) \u5716\u793a -> \u8a2d\u5b9a\uff08settings\uff09\u3002\u8b8a\u66f4\u4ee5\u4e0b\u8a2d\u5b9a\uff1a\n - \u6a21\u5f0f\uff08Mode\uff09\uff1aHTTP\n - URL: {webhook_url}\n - \u958b\u555f authentication\n - UserID: ``\n\n{secret}\n\n\u8acb\u53c3\u95b1 [\u6587\u4ef6]({docs_url})\u4ee5\u4e86\u89e3\u66f4\u8a73\u7d30\u8cc7\u6599\u3002" + "default": "\n\n\u65bc Android \u8a2d\u5099\uff0c\u6253\u958b [OwnTracks app]({android_url})\u3001\u9ede\u9078\u8a2d\u5b9a\uff08preferences\uff09 -> \u9023\u7dda\uff08connection\uff09\u3002\u8b8a\u66f4\u4ee5\u4e0b\u8a2d\u5b9a\uff1a\n - \u6a21\u5f0f\uff08Mode\uff09\uff1aPrivate HTTP\n - \u4e3b\u6a5f\u7aef\uff08Host\uff09\uff1a{webhook_url}\n - Identification\uff1a\n - Username\uff1a ``\n - Device ID\uff1a``\n\n\u65bc iOS \u8a2d\u5099\uff0c\u6253\u958b [OwnTracks app]({ios_url})\u3001\u9ede\u9078\u5de6\u4e0a\u65b9\u7684 (i) \u5716\u793a -> \u8a2d\u5b9a\uff08settings\uff09\u3002\u8b8a\u66f4\u4ee5\u4e0b\u8a2d\u5b9a\uff1a\n - \u6a21\u5f0f\uff08Mode\uff09\uff1aHTTP\n - URL: {webhook_url}\n - \u958b\u555f authentication\n - UserID: ``\n\n{secret}\n\n\u8acb\u53c3\u95b1 [\u6587\u4ef6]({docs_url})\u4ee5\u4e86\u89e3\u66f4\u8a73\u7d30\u8cc7\u6599\u3002" }, "step": { "user": { diff --git a/homeassistant/components/owntracks/config_flow.py b/homeassistant/components/owntracks/config_flow.py index 93a75d44e9b..67553ef608f 100644 --- a/homeassistant/components/owntracks/config_flow.py +++ b/homeassistant/components/owntracks/config_flow.py @@ -58,7 +58,7 @@ class OwnTracksFlow(config_entries.ConfigFlow): "android_url": "https://play.google.com/store/apps/details?" "id=org.owntracks.android", "ios_url": "https://itunes.apple.com/us/app/owntracks/id692424691?mt=8", - "docs_url": "https://www.home-assistant.io/components/owntracks/", + "docs_url": "https://www.home-assistant.io/integrations/owntracks/", }, ) diff --git a/homeassistant/components/owntracks/manifest.json b/homeassistant/components/owntracks/manifest.json index bc4fe97bc7f..529d7990a86 100644 --- a/homeassistant/components/owntracks/manifest.json +++ b/homeassistant/components/owntracks/manifest.json @@ -2,7 +2,7 @@ "domain": "owntracks", "name": "Owntracks", "config_flow": true, - "documentation": "https://www.home-assistant.io/components/owntracks", + "documentation": "https://www.home-assistant.io/integrations/owntracks", "requirements": [ "PyNaCl==1.3.0" ], diff --git a/homeassistant/components/panasonic_bluray/manifest.json b/homeassistant/components/panasonic_bluray/manifest.json index fe2387744ab..7f386464dc9 100644 --- a/homeassistant/components/panasonic_bluray/manifest.json +++ b/homeassistant/components/panasonic_bluray/manifest.json @@ -1,7 +1,7 @@ { "domain": "panasonic_bluray", "name": "Panasonic bluray", - "documentation": "https://www.home-assistant.io/components/panasonic_bluray", + "documentation": "https://www.home-assistant.io/integrations/panasonic_bluray", "requirements": [ "panacotta==0.1" ], diff --git a/homeassistant/components/panasonic_viera/manifest.json b/homeassistant/components/panasonic_viera/manifest.json index 432e729ef20..e7e06479eec 100644 --- a/homeassistant/components/panasonic_viera/manifest.json +++ b/homeassistant/components/panasonic_viera/manifest.json @@ -1,7 +1,7 @@ { "domain": "panasonic_viera", "name": "Panasonic viera", - "documentation": "https://www.home-assistant.io/components/panasonic_viera", + "documentation": "https://www.home-assistant.io/integrations/panasonic_viera", "requirements": [ "panasonic_viera==0.3.2", "wakeonlan==1.1.6" diff --git a/homeassistant/components/pandora/manifest.json b/homeassistant/components/pandora/manifest.json index 68e8337a33d..a15267b7d85 100644 --- a/homeassistant/components/pandora/manifest.json +++ b/homeassistant/components/pandora/manifest.json @@ -1,7 +1,7 @@ { "domain": "pandora", "name": "Pandora", - "documentation": "https://www.home-assistant.io/components/pandora", + "documentation": "https://www.home-assistant.io/integrations/pandora", "requirements": [ "pexpect==4.6.0" ], diff --git a/homeassistant/components/panel_custom/manifest.json b/homeassistant/components/panel_custom/manifest.json index 06c9338742c..5d0cc555705 100644 --- a/homeassistant/components/panel_custom/manifest.json +++ b/homeassistant/components/panel_custom/manifest.json @@ -1,7 +1,7 @@ { "domain": "panel_custom", "name": "Panel custom", - "documentation": "https://www.home-assistant.io/components/panel_custom", + "documentation": "https://www.home-assistant.io/integrations/panel_custom", "requirements": [], "dependencies": [ "frontend" diff --git a/homeassistant/components/panel_iframe/manifest.json b/homeassistant/components/panel_iframe/manifest.json index e66f94bdcc2..6d7c66f6097 100644 --- a/homeassistant/components/panel_iframe/manifest.json +++ b/homeassistant/components/panel_iframe/manifest.json @@ -1,7 +1,7 @@ { "domain": "panel_iframe", "name": "Panel iframe", - "documentation": "https://www.home-assistant.io/components/panel_iframe", + "documentation": "https://www.home-assistant.io/integrations/panel_iframe", "requirements": [], "dependencies": [ "frontend" diff --git a/homeassistant/components/pencom/manifest.json b/homeassistant/components/pencom/manifest.json index 186e071d25b..d31b9a811cf 100644 --- a/homeassistant/components/pencom/manifest.json +++ b/homeassistant/components/pencom/manifest.json @@ -1,7 +1,7 @@ { "domain": "pencom", "name": "Pencom", - "documentation": "https://www.home-assistant.io/components/pencom", + "documentation": "https://www.home-assistant.io/integrations/pencom", "requirements": [ "pencompy==0.0.3" ], diff --git a/homeassistant/components/persistent_notification/__init__.py b/homeassistant/components/persistent_notification/__init__.py index 6c49784eded..6b9c7c44ddf 100644 --- a/homeassistant/components/persistent_notification/__init__.py +++ b/homeassistant/components/persistent_notification/__init__.py @@ -1,7 +1,7 @@ """Support for displaying persistent notifications.""" from collections import OrderedDict import logging -from typing import Awaitable +from typing import Any, Mapping, MutableMapping, Optional import voluptuous as vol @@ -14,6 +14,9 @@ from homeassistant.loader import bind_hass from homeassistant.util import slugify import homeassistant.util.dt as dt_util + +# mypy: allow-untyped-calls, allow-untyped-defs + ATTR_CREATED_AT = "created_at" ATTR_MESSAGE = "message" ATTR_NOTIFICATION_ID = "notification_id" @@ -70,7 +73,10 @@ def dismiss(hass, notification_id): @callback @bind_hass def async_create( - hass: HomeAssistant, message: str, title: str = None, notification_id: str = None + hass: HomeAssistant, + message: str, + title: Optional[str] = None, + notification_id: Optional[str] = None, ) -> None: """Generate a notification.""" data = { @@ -95,9 +101,9 @@ def async_dismiss(hass: HomeAssistant, notification_id: str) -> None: hass.async_create_task(hass.services.async_call(DOMAIN, SERVICE_DISMISS, data)) -async def async_setup(hass: HomeAssistant, config: dict) -> Awaitable[bool]: +async def async_setup(hass: HomeAssistant, config: dict) -> bool: """Set up the persistent notification component.""" - persistent_notifications = OrderedDict() + persistent_notifications: MutableMapping[str, MutableMapping] = OrderedDict() hass.data[DOMAIN] = {"notifications": persistent_notifications} @callback @@ -201,8 +207,10 @@ async def async_setup(hass: HomeAssistant, config: dict) -> Awaitable[bool]: @callback def websocket_get_notifications( - hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg -): + hass: HomeAssistant, + connection: websocket_api.ActiveConnection, + msg: Mapping[str, Any], +) -> None: """Return a list of persistent_notifications.""" connection.send_message( websocket_api.result_message( diff --git a/homeassistant/components/persistent_notification/manifest.json b/homeassistant/components/persistent_notification/manifest.json index 8bc343e1f08..9ee9692c655 100644 --- a/homeassistant/components/persistent_notification/manifest.json +++ b/homeassistant/components/persistent_notification/manifest.json @@ -1,7 +1,7 @@ { "domain": "persistent_notification", "name": "Persistent notification", - "documentation": "https://www.home-assistant.io/components/persistent_notification", + "documentation": "https://www.home-assistant.io/integrations/persistent_notification", "requirements": [], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/person/manifest.json b/homeassistant/components/person/manifest.json index d2cba929259..cf50b8029c2 100644 --- a/homeassistant/components/person/manifest.json +++ b/homeassistant/components/person/manifest.json @@ -1,7 +1,7 @@ { "domain": "person", "name": "Person", - "documentation": "https://www.home-assistant.io/components/person", + "documentation": "https://www.home-assistant.io/integrations/person", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/philips_js/manifest.json b/homeassistant/components/philips_js/manifest.json index 0b1579a139d..4845aa16a37 100644 --- a/homeassistant/components/philips_js/manifest.json +++ b/homeassistant/components/philips_js/manifest.json @@ -1,7 +1,7 @@ { "domain": "philips_js", "name": "Philips js", - "documentation": "https://www.home-assistant.io/components/philips_js", + "documentation": "https://www.home-assistant.io/integrations/philips_js", "requirements": [ "ha-philipsjs==0.0.8" ], diff --git a/homeassistant/components/pi_hole/__init__.py b/homeassistant/components/pi_hole/__init__.py index ffc9827eed4..95351083b5a 100644 --- a/homeassistant/components/pi_hole/__init__.py +++ b/homeassistant/components/pi_hole/__init__.py @@ -5,7 +5,13 @@ import voluptuous as vol from hole import Hole from hole.exceptions import HoleError -from homeassistant.const import CONF_HOST, CONF_NAME, CONF_SSL, CONF_VERIFY_SSL +from homeassistant.const import ( + CONF_HOST, + CONF_NAME, + CONF_API_KEY, + CONF_SSL, + CONF_VERIFY_SSL, +) from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -21,6 +27,9 @@ from .const import ( DEFAULT_SSL, DEFAULT_VERIFY_SSL, MIN_TIME_BETWEEN_UPDATES, + SERVICE_DISABLE, + SERVICE_DISABLE_ATTR_DURATION, + SERVICE_ENABLE, ) LOGGER = logging.getLogger(__name__) @@ -31,6 +40,7 @@ CONFIG_SCHEMA = vol.Schema( { vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_API_KEY): cv.string, vol.Optional(CONF_SSL, default=DEFAULT_SSL): cv.boolean, vol.Optional(CONF_LOCATION, default=DEFAULT_LOCATION): cv.string, vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): cv.boolean, @@ -40,6 +50,14 @@ CONFIG_SCHEMA = vol.Schema( extra=vol.ALLOW_EXTRA, ) +SERVICE_DISABLE_SCHEMA = vol.Schema( + { + vol.Required(SERVICE_DISABLE_ATTR_DURATION): vol.All( + cv.time_period_str, cv.positive_timedelta + ) + } +) + async def async_setup(hass, config): """Set up the pi_hole integration.""" @@ -50,18 +68,14 @@ async def async_setup(hass, config): use_tls = conf[CONF_SSL] verify_tls = conf[CONF_VERIFY_SSL] location = conf[CONF_LOCATION] + api_key = conf.get(CONF_API_KEY) LOGGER.debug("Setting up %s integration with host %s", DOMAIN, host) - session = async_get_clientsession(hass, True) + session = async_get_clientsession(hass, verify_tls) pi_hole = PiHoleData( Hole( - host, - hass.loop, - session, - location=location, - tls=use_tls, - verify_tls=verify_tls, + host, hass.loop, session, location=location, tls=use_tls, api_token=api_key ), name, ) @@ -70,6 +84,28 @@ async def async_setup(hass, config): hass.data[DOMAIN] = pi_hole + async def handle_disable(call): + if api_key is None: + raise vol.Invalid("Pi-hole api_key must be provided in configuration") + + duration = call.data[SERVICE_DISABLE_ATTR_DURATION].total_seconds() + + LOGGER.debug("Disabling %s %s for %d seconds", DOMAIN, host, duration) + await pi_hole.api.disable(duration) + + async def handle_enable(call): + if api_key is None: + raise vol.Invalid("Pi-hole api_key must be provided in configuration") + + LOGGER.debug("Enabling %s %s", DOMAIN, host) + await pi_hole.api.enable() + + hass.services.async_register( + DOMAIN, SERVICE_DISABLE, handle_disable, schema=SERVICE_DISABLE_SCHEMA + ) + + hass.services.async_register(DOMAIN, SERVICE_ENABLE, handle_enable) + hass.async_create_task(async_load_platform(hass, SENSOR_DOMAIN, DOMAIN, {}, config)) return True diff --git a/homeassistant/components/pi_hole/const.py b/homeassistant/components/pi_hole/const.py index ba83bf1d805..54220547950 100644 --- a/homeassistant/components/pi_hole/const.py +++ b/homeassistant/components/pi_hole/const.py @@ -12,6 +12,10 @@ DEFAULT_NAME = "Pi-Hole" DEFAULT_SSL = False DEFAULT_VERIFY_SSL = True +SERVICE_DISABLE = "disable" +SERVICE_ENABLE = "enable" +SERVICE_DISABLE_ATTR_DURATION = "duration" + ATTR_BLOCKED_DOMAINS = "domains_blocked" MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=5) diff --git a/homeassistant/components/pi_hole/manifest.json b/homeassistant/components/pi_hole/manifest.json index 2d19ab25fe7..089e1e60a11 100644 --- a/homeassistant/components/pi_hole/manifest.json +++ b/homeassistant/components/pi_hole/manifest.json @@ -1,12 +1,13 @@ { "domain": "pi_hole", "name": "Pi hole", - "documentation": "https://www.home-assistant.io/components/pi_hole", + "documentation": "https://www.home-assistant.io/integrations/pi_hole", "requirements": [ "hole==0.5.0" ], "dependencies": [], "codeowners": [ - "@fabaff" + "@fabaff", + "@johnluetke" ] } diff --git a/homeassistant/components/pi_hole/services.yaml b/homeassistant/components/pi_hole/services.yaml new file mode 100644 index 00000000000..b16ed21a5d3 --- /dev/null +++ b/homeassistant/components/pi_hole/services.yaml @@ -0,0 +1,8 @@ +disable: + description: Disable Pi-hole for an amount of time + fields: + duration: + description: Time that the Pi-hole should be disabled for + example: "00:00:15" +enable: + description: Enable Pi-hole \ No newline at end of file diff --git a/homeassistant/components/picotts/manifest.json b/homeassistant/components/picotts/manifest.json index bfe7f449ca0..5150ddd0404 100644 --- a/homeassistant/components/picotts/manifest.json +++ b/homeassistant/components/picotts/manifest.json @@ -1,7 +1,7 @@ { "domain": "picotts", "name": "Picotts", - "documentation": "https://www.home-assistant.io/components/picotts", + "documentation": "https://www.home-assistant.io/integrations/picotts", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/piglow/manifest.json b/homeassistant/components/piglow/manifest.json index 67b1033c51e..012ce4f014e 100644 --- a/homeassistant/components/piglow/manifest.json +++ b/homeassistant/components/piglow/manifest.json @@ -1,7 +1,7 @@ { "domain": "piglow", "name": "Piglow", - "documentation": "https://www.home-assistant.io/components/piglow", + "documentation": "https://www.home-assistant.io/integrations/piglow", "requirements": [ "piglow==1.2.4" ], diff --git a/homeassistant/components/pilight/manifest.json b/homeassistant/components/pilight/manifest.json index dfe4952e1a1..7613f9e2a34 100644 --- a/homeassistant/components/pilight/manifest.json +++ b/homeassistant/components/pilight/manifest.json @@ -1,7 +1,7 @@ { "domain": "pilight", "name": "Pilight", - "documentation": "https://www.home-assistant.io/components/pilight", + "documentation": "https://www.home-assistant.io/integrations/pilight", "requirements": [ "pilight==0.1.1" ], diff --git a/homeassistant/components/ping/manifest.json b/homeassistant/components/ping/manifest.json index d98adef87a7..19e84365fac 100644 --- a/homeassistant/components/ping/manifest.json +++ b/homeassistant/components/ping/manifest.json @@ -1,7 +1,7 @@ { "domain": "ping", "name": "Ping", - "documentation": "https://www.home-assistant.io/components/ping", + "documentation": "https://www.home-assistant.io/integrations/ping", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/pioneer/manifest.json b/homeassistant/components/pioneer/manifest.json index b06874149ed..3aa046f234d 100644 --- a/homeassistant/components/pioneer/manifest.json +++ b/homeassistant/components/pioneer/manifest.json @@ -1,7 +1,7 @@ { "domain": "pioneer", "name": "Pioneer", - "documentation": "https://www.home-assistant.io/components/pioneer", + "documentation": "https://www.home-assistant.io/integrations/pioneer", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/pjlink/manifest.json b/homeassistant/components/pjlink/manifest.json index 6901847bd8d..3c2b67e3425 100644 --- a/homeassistant/components/pjlink/manifest.json +++ b/homeassistant/components/pjlink/manifest.json @@ -1,7 +1,7 @@ { "domain": "pjlink", "name": "Pjlink", - "documentation": "https://www.home-assistant.io/components/pjlink", + "documentation": "https://www.home-assistant.io/integrations/pjlink", "requirements": [ "pypjlink2==1.2.0" ], diff --git a/homeassistant/components/plaato/.translations/nn.json b/homeassistant/components/plaato/.translations/nn.json new file mode 100644 index 00000000000..750e14b1dae --- /dev/null +++ b/homeassistant/components/plaato/.translations/nn.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Plaato Airlock" + } +} \ No newline at end of file diff --git a/homeassistant/components/plaato/.translations/no.json b/homeassistant/components/plaato/.translations/no.json index 4b47f52eef9..0de31a35eb2 100644 --- a/homeassistant/components/plaato/.translations/no.json +++ b/homeassistant/components/plaato/.translations/no.json @@ -2,7 +2,7 @@ "config": { "abort": { "not_internet_accessible": "Home Assistant m\u00e5 v\u00e6re tilgjengelig fra internett for \u00e5 kunne motta meldinger fra Plaato Airlock.", - "one_instance_allowed": "Kun en enkelt forekomst er n\u00f8dvendig." + "one_instance_allowed": "Kun en forekomst er n\u00f8dvendig." }, "create_entry": { "default": "For \u00e5 sende hendelser til Home Assistant, m\u00e5 du sette opp webhook-funksjonen i Plaato Airlock. \n\n Fyll ut f\u00f8lgende informasjon: \n\n - URL: `{webhook_url}` \n - Metode: POST \n\n Se [dokumentasjonen]({docs_url}) for ytterligere detaljer." diff --git a/homeassistant/components/plaato/.translations/zh-Hans.json b/homeassistant/components/plaato/.translations/zh-Hans.json new file mode 100644 index 00000000000..8d5c25babfa --- /dev/null +++ b/homeassistant/components/plaato/.translations/zh-Hans.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "\u60a8\u7684 Home Assistant \u5b9e\u4f8b\u9700\u8981\u53ef\u4ece\u4e92\u8054\u7f51\u8bbf\u95ee\u4ee5\u63a5\u6536Plaato Airlock\u6d88\u606f\u3002" + }, + "step": { + "user": { + "description": "\u4f60\u786e\u5b9a\u8981\u8bbe\u7f6ePlaato Airlock\u5417\uff1f", + "title": "\u8bbe\u7f6ePlaato Webhook" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/plaato/.translations/zh-Hant.json b/homeassistant/components/plaato/.translations/zh-Hant.json index 20cdb405f4e..1bf211476d8 100644 --- a/homeassistant/components/plaato/.translations/zh-Hant.json +++ b/homeassistant/components/plaato/.translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "not_internet_accessible": "Home Assistant \u5be6\u4f8b\u5fc5\u9808\u80fd\u5920\u7531\u7db2\u969b\u7db2\u8def\u5b58\u53d6\uff0c\u65b9\u80fd\u63a5\u53d7 Plaato Airlock \u8a0a\u606f\u3002", + "not_internet_accessible": "Home Assistant \u7269\u4ef6\u5fc5\u9808\u80fd\u5920\u7531\u7db2\u969b\u7db2\u8def\u5b58\u53d6\uff0c\u65b9\u80fd\u63a5\u53d7 Plaato Airlock \u8a0a\u606f\u3002", "one_instance_allowed": "\u50c5\u9700\u8a2d\u5b9a\u4e00\u7d44\u7269\u4ef6\u5373\u53ef\u3002" }, "create_entry": { diff --git a/homeassistant/components/plaato/config_flow.py b/homeassistant/components/plaato/config_flow.py index 2790d9a93ac..59cb270c616 100644 --- a/homeassistant/components/plaato/config_flow.py +++ b/homeassistant/components/plaato/config_flow.py @@ -3,5 +3,7 @@ from homeassistant.helpers import config_entry_flow from .const import DOMAIN config_entry_flow.register_webhook_flow( - DOMAIN, "Webhook", {"docs_url": "https://www.home-assistant.io/components/plaato/"} + DOMAIN, + "Webhook", + {"docs_url": "https://www.home-assistant.io/integrations/plaato/"}, ) diff --git a/homeassistant/components/plaato/manifest.json b/homeassistant/components/plaato/manifest.json index cd6111ba9da..658173d3db2 100644 --- a/homeassistant/components/plaato/manifest.json +++ b/homeassistant/components/plaato/manifest.json @@ -2,7 +2,7 @@ "domain": "plaato", "name": "Plaato Airlock", "config_flow": true, - "documentation": "https://www.home-assistant.io/components/plaato", + "documentation": "https://www.home-assistant.io/integrations/plaato", "dependencies": ["webhook"], "codeowners": ["@JohNan"], "requirements": [] diff --git a/homeassistant/components/plant/manifest.json b/homeassistant/components/plant/manifest.json index cbde894173b..721a57e7822 100644 --- a/homeassistant/components/plant/manifest.json +++ b/homeassistant/components/plant/manifest.json @@ -1,7 +1,7 @@ { "domain": "plant", "name": "Plant", - "documentation": "https://www.home-assistant.io/components/plant", + "documentation": "https://www.home-assistant.io/integrations/plant", "requirements": [], "dependencies": [ "group", diff --git a/homeassistant/components/plex/.translations/bg.json b/homeassistant/components/plex/.translations/bg.json new file mode 100644 index 00000000000..fb77e5da8cb --- /dev/null +++ b/homeassistant/components/plex/.translations/bg.json @@ -0,0 +1,45 @@ +{ + "config": { + "abort": { + "all_configured": "\u0412\u0441\u0438\u0447\u043a\u0438 \u0441\u0432\u044a\u0440\u0437\u0430\u043d\u0438 \u0441\u044a\u0440\u0432\u044a\u0440\u0438 \u0432\u0435\u0447\u0435 \u0441\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0438", + "already_configured": "\u0422\u043e\u0437\u0438 Plex \u0441\u044a\u0440\u0432\u044a\u0440 \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d", + "already_in_progress": "Plex \u0441\u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430", + "invalid_import": "\u0418\u043c\u043f\u043e\u0440\u0442\u0438\u0440\u0430\u043d\u0430\u0442\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u0435 \u043d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u0430", + "unknown": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u043f\u043e\u0440\u0430\u0434\u0438 \u043d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + }, + "error": { + "faulty_credentials": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u0430 \u043e\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u044f", + "no_servers": "\u041d\u044f\u043c\u0430 \u0441\u044a\u0440\u0432\u044a\u0440\u0438, \u0441\u0432\u044a\u0440\u0437\u0430\u043d\u0438 \u0441 \u0442\u043e\u0437\u0438 \u0430\u043a\u0430\u0443\u043d\u0442", + "no_token": "\u0412\u044a\u0432\u0435\u0434\u0435\u0442\u0435 \u043e\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u043e\u043d\u0435\u043d \u043a\u043e\u0434 \u0438\u043b\u0438 \u0438\u0437\u0431\u0435\u0440\u0435\u0442\u0435 \u0440\u044a\u0447\u043d\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430", + "not_found": "Plex \u0441\u044a\u0440\u0432\u044a\u0440\u044a\u0442 \u043d\u0435 \u0435 \u043d\u0430\u043c\u0435\u0440\u0435\u043d" + }, + "step": { + "manual_setup": { + "data": { + "host": "\u0410\u0434\u0440\u0435\u0441", + "port": "\u041f\u043e\u0440\u0442", + "ssl": "\u0418\u0437\u043f\u043e\u043b\u0437\u0432\u0430\u043d\u0435 \u043d\u0430 SSL", + "token": "\u041a\u043e\u0434 (\u0430\u043a\u043e \u0441\u0435 \u0438\u0437\u0438\u0441\u043a\u0432\u0430)", + "verify_ssl": "\u041f\u0440\u043e\u0432\u0435\u0440\u043a\u0430 \u043d\u0430 SSL \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442" + }, + "title": "Plex \u0441\u044a\u0440\u0432\u044a\u0440" + }, + "select_server": { + "data": { + "server": "\u0421\u044a\u0440\u0432\u044a\u0440" + }, + "description": "\u041d\u0430\u043b\u0438\u0447\u043d\u0438 \u0441\u0430 \u043d\u044f\u043a\u043e\u043b\u043a\u043e \u0441\u044a\u0440\u0432\u044a\u0440\u0430, \u0438\u0437\u0431\u0435\u0440\u0435\u0442\u0435 \u0435\u0434\u0438\u043d:", + "title": "\u0418\u0437\u0431\u0435\u0440\u0435\u0442\u0435 Plex \u0441\u044a\u0440\u0432\u044a\u0440" + }, + "user": { + "data": { + "manual_setup": "\u0420\u044a\u0447\u043d\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430", + "token": "Plex \u043a\u043e\u0434" + }, + "description": "\u0412\u044a\u0432\u0435\u0434\u0435\u0442\u0435 \u043a\u043e\u0434 \u0437\u0430 Plex \u0437\u0430 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u043d\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0438\u043b\u0438 \u0440\u044a\u0447\u043d\u043e \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 \u0441\u044a\u0440\u0432\u044a\u0440.", + "title": "\u0421\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435 \u043d\u0430 Plex \u0441\u044a\u0440\u0432\u044a\u0440" + } + }, + "title": "Plex" + } +} \ No newline at end of file diff --git a/homeassistant/components/plex/.translations/ca.json b/homeassistant/components/plex/.translations/ca.json new file mode 100644 index 00000000000..a3ba5185371 --- /dev/null +++ b/homeassistant/components/plex/.translations/ca.json @@ -0,0 +1,61 @@ +{ + "config": { + "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", + "invalid_import": "La configuraci\u00f3 importada \u00e9s inv\u00e0lida", + "token_request_timeout": "S'ha acabat el temps d'espera durant l'obtenci\u00f3 del testimoni.", + "unknown": "Ha fallat per motiu desconegut" + }, + "error": { + "faulty_credentials": "Ha fallat l'autoritzaci\u00f3", + "no_servers": "No hi ha servidors enlla\u00e7ats amb el compte", + "no_token": "Proporciona un testimoni d'autenticaci\u00f3 o selecciona configuraci\u00f3 manual", + "not_found": "No s'ha trobat el servidor Plex" + }, + "step": { + "manual_setup": { + "data": { + "host": "Amfitri\u00f3", + "port": "Port", + "ssl": "Utilitza SSL", + "token": "Testimoni d'autenticaci\u00f3 (si \u00e9s necessari)", + "verify_ssl": "Verifica el certificat SSL" + }, + "title": "Servidor Plex" + }, + "select_server": { + "data": { + "server": "Servidor" + }, + "description": "Hi ha diversos servidors disponibles, selecciona'n un:", + "title": "Selecciona servidor Plex" + }, + "start_website_auth": { + "description": "Continua l'autoritzaci\u00f3 a plex.tv.", + "title": "Connexi\u00f3 amb el servidor Plex" + }, + "user": { + "data": { + "manual_setup": "Configuraci\u00f3 manual", + "token": "Testimoni d'autenticaci\u00f3 Plex" + }, + "description": "Introdueix un testimoni d'autenticaci\u00f3 Plex per configurar-ho autom\u00e0ticament.", + "title": "Connexi\u00f3 amb el servidor Plex" + } + }, + "title": "Plex" + }, + "options": { + "step": { + "plex_mp_settings": { + "data": { + "show_all_controls": "Mostra tots els controls", + "use_episode_art": "Utilitza imatges de l'episodi" + }, + "description": "Opcions per als reproductors multim\u00e8dia Plex" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/plex/.translations/da.json b/homeassistant/components/plex/.translations/da.json new file mode 100644 index 00000000000..1da4b4b4b49 --- /dev/null +++ b/homeassistant/components/plex/.translations/da.json @@ -0,0 +1,57 @@ +{ + "config": { + "abort": { + "all_configured": "Alle linkede servere er allerede konfigureret", + "already_configured": "Denne Plex-server er allerede konfigureret", + "already_in_progress": "Plex konfigureres", + "invalid_import": "Importeret konfiguration er ugyldig", + "token_request_timeout": "Timeout ved hentning af token", + "unknown": "Mislykkedes af ukendt \u00e5rsag" + }, + "error": { + "faulty_credentials": "Godkendelse mislykkedes", + "no_servers": "Ingen servere knyttet til konto", + "no_token": "Angiv et token eller v\u00e6lg manuel ops\u00e6tning", + "not_found": "Plex-server ikke fundet" + }, + "step": { + "manual_setup": { + "data": { + "host": "V\u00e6rt", + "port": "Port", + "ssl": "Brug SSL", + "token": "Token (hvis n\u00f8dvendigt)", + "verify_ssl": "Bekr\u00e6ft SSL-certifikat" + }, + "title": "Plex-server" + }, + "select_server": { + "data": { + "server": "Server" + }, + "description": "Flere servere til r\u00e5dighed, v\u00e6lg en:", + "title": "V\u00e6lg Plex-server" + }, + "user": { + "data": { + "manual_setup": "Manuel ops\u00e6tning", + "token": "Plex token" + }, + "description": "Indtast et Plex-token til automatisk ops\u00e6tning eller konfigurerer en server manuelt.", + "title": "Tilslut Plex-server" + } + }, + "title": "Plex" + }, + "options": { + "step": { + "plex_mp_settings": { + "data": { + "show_all_controls": "Vis alle kontrolelementer", + "use_episode_art": "Brug episode kunst" + }, + "description": "Indstillinger for Plex-medieafspillere" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/plex/.translations/de.json b/homeassistant/components/plex/.translations/de.json new file mode 100644 index 00000000000..95083102273 --- /dev/null +++ b/homeassistant/components/plex/.translations/de.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "discovery_no_file": "Es wurde keine alte Konfigurationsdatei gefunden" + }, + "step": { + "manual_setup": { + "title": "Plex Server" + }, + "start_website_auth": { + "description": "Weiter zur Autorisierung unter plex.tv.", + "title": "Plex Server verbinden" + }, + "user": { + "description": "Fahren Sie mit der Autorisierung unter plex.tv fort oder konfigurieren Sie einen Server manuell." + } + } + }, + "options": { + "step": { + "plex_mp_settings": { + "description": "Optionen f\u00fcr Plex-Media-Player" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/plex/.translations/en.json b/homeassistant/components/plex/.translations/en.json new file mode 100644 index 00000000000..bf927b7f1be --- /dev/null +++ b/homeassistant/components/plex/.translations/en.json @@ -0,0 +1,62 @@ +{ + "config": { + "abort": { + "all_configured": "All linked servers already configured", + "already_configured": "This Plex server is already configured", + "already_in_progress": "Plex is being configured", + "discovery_no_file": "No legacy config file found", + "invalid_import": "Imported configuration is invalid", + "token_request_timeout": "Timed out obtaining token", + "unknown": "Failed for unknown reason" + }, + "error": { + "faulty_credentials": "Authorization failed", + "no_servers": "No servers linked to account", + "no_token": "Provide a token or select manual setup", + "not_found": "Plex server not found" + }, + "step": { + "manual_setup": { + "data": { + "host": "Host", + "port": "Port", + "ssl": "Use SSL", + "token": "Token (if required)", + "verify_ssl": "Verify SSL certificate" + }, + "title": "Plex server" + }, + "select_server": { + "data": { + "server": "Server" + }, + "description": "Multiple servers available, select one:", + "title": "Select Plex server" + }, + "start_website_auth": { + "description": "Continue to authorize at plex.tv.", + "title": "Connect Plex server" + }, + "user": { + "data": { + "manual_setup": "Manual setup", + "token": "Plex token" + }, + "description": "Continue to authorize at plex.tv or manually configure a server.", + "title": "Connect Plex server" + } + }, + "title": "Plex" + }, + "options": { + "step": { + "plex_mp_settings": { + "data": { + "show_all_controls": "Show all controls", + "use_episode_art": "Use episode art" + }, + "description": "Options for Plex Media Players" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/plex/.translations/es-419.json b/homeassistant/components/plex/.translations/es-419.json new file mode 100644 index 00000000000..2fc98a70ead --- /dev/null +++ b/homeassistant/components/plex/.translations/es-419.json @@ -0,0 +1,55 @@ +{ + "config": { + "abort": { + "all_configured": "Todos los servidores vinculados ya fueron configurados", + "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", + "token_request_timeout": "Se agot\u00f3 el tiempo de espera para obtener el token", + "unknown": "Fall\u00f3 por razones desconocidas" + }, + "error": { + "faulty_credentials": "Autorizaci\u00f3n fallida", + "no_servers": "No hay servidores vinculados a la cuenta", + "no_token": "Proporcione un token o seleccione la configuraci\u00f3n manual", + "not_found": "Servidor Plex no encontrado" + }, + "step": { + "manual_setup": { + "data": { + "host": "Host", + "port": "Puerto", + "ssl": "Usar SSL", + "token": "Token (si es necesario)", + "verify_ssl": "Verificar el certificado SSL" + }, + "title": "Servidor Plex" + }, + "select_server": { + "data": { + "server": "Servidor" + }, + "description": "M\u00faltiples servidores disponibles, seleccione uno:", + "title": "Seleccionar servidor Plex" + }, + "user": { + "data": { + "manual_setup": "Configuraci\u00f3n manual", + "token": "Token Plex" + }, + "title": "Conectar servidor Plex" + } + }, + "title": "Plex" + }, + "options": { + "step": { + "plex_mp_settings": { + "data": { + "show_all_controls": "Mostrar todos los controles" + }, + "description": "Opciones para reproductores multimedia Plex" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/plex/.translations/es.json b/homeassistant/components/plex/.translations/es.json new file mode 100644 index 00000000000..261ca951490 --- /dev/null +++ b/homeassistant/components/plex/.translations/es.json @@ -0,0 +1,62 @@ +{ + "config": { + "abort": { + "all_configured": "Todos los servidores vinculados ya configurados", + "already_configured": "Este servidor Plex ya est\u00e1 configurado", + "already_in_progress": "Plex se est\u00e1 configurando", + "discovery_no_file": "No se ha encontrado ning\u00fan archivo de configuraci\u00f3n antiguo", + "invalid_import": "La configuraci\u00f3n importada no es v\u00e1lida", + "token_request_timeout": "Tiempo de espera agotado para la obtenci\u00f3n del token", + "unknown": "Fall\u00f3 por razones desconocidas" + }, + "error": { + "faulty_credentials": "Error en la autorizaci\u00f3n", + "no_servers": "No hay servidores vinculados a la cuenta", + "no_token": "Proporcione un token o seleccione la configuraci\u00f3n manual", + "not_found": "No se ha encontrado el servidor Plex" + }, + "step": { + "manual_setup": { + "data": { + "host": "Host", + "port": "Puerto", + "ssl": "Usar SSL", + "token": "Token (es necesario)", + "verify_ssl": "Verificar certificado SSL" + }, + "title": "Servidor Plex" + }, + "select_server": { + "data": { + "server": "Servidor" + }, + "description": "Varios servidores disponibles, seleccione uno:", + "title": "Seleccione el servidor Plex" + }, + "start_website_auth": { + "description": "Contin\u00fae en plex.tv para autorizar", + "title": "Conectar servidor Plex" + }, + "user": { + "data": { + "manual_setup": "Configuraci\u00f3n manual", + "token": "Token Plex" + }, + "description": "Introduzca un token Plex para la configuraci\u00f3n autom\u00e1tica o configure manualmente un servidor.", + "title": "Conectar servidor Plex" + } + }, + "title": "Plex" + }, + "options": { + "step": { + "plex_mp_settings": { + "data": { + "show_all_controls": "Mostrar todos los controles", + "use_episode_art": "Usar el arte de episodios" + }, + "description": "Opciones para reproductores multimedia Plex" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/plex/.translations/fr.json b/homeassistant/components/plex/.translations/fr.json new file mode 100644 index 00000000000..c9e61dcf2e9 --- /dev/null +++ b/homeassistant/components/plex/.translations/fr.json @@ -0,0 +1,57 @@ +{ + "config": { + "abort": { + "all_configured": "Tous les serveurs li\u00e9s sont d\u00e9j\u00e0 configur\u00e9s", + "already_configured": "Ce serveur Plex est d\u00e9j\u00e0 configur\u00e9", + "already_in_progress": "Plex en cours de configuration", + "invalid_import": "La configuration import\u00e9e est invalide", + "token_request_timeout": "D\u00e9lai d'obtention du jeton", + "unknown": "\u00c9chec pour une raison inconnue" + }, + "error": { + "faulty_credentials": "L'autorisation \u00e0 \u00e9chou\u00e9e", + "no_servers": "Aucun serveur li\u00e9 au compte", + "no_token": "Fournir un jeton ou s\u00e9lectionner l'installation manuelle", + "not_found": "Serveur Plex introuvable" + }, + "step": { + "manual_setup": { + "data": { + "host": "H\u00f4te", + "port": "Port", + "ssl": "Utiliser SSL", + "token": "Jeton (si n\u00e9cessaire)", + "verify_ssl": "V\u00e9rifier le certificat SSL" + }, + "title": "Serveur Plex" + }, + "select_server": { + "data": { + "server": "Serveur" + }, + "description": "Plusieurs serveurs disponibles, s\u00e9lectionnez-en un:", + "title": "S\u00e9lectionnez le serveur Plex" + }, + "user": { + "data": { + "manual_setup": "Installation manuelle", + "token": "Jeton plex" + }, + "description": "Entrez un jeton Plex pour la configuration automatique.", + "title": "Connecter un serveur Plex" + } + }, + "title": "Plex" + }, + "options": { + "step": { + "plex_mp_settings": { + "data": { + "show_all_controls": "Afficher tous les contr\u00f4les", + "use_episode_art": "Utiliser l'art de l'\u00e9pisode" + }, + "description": "Options pour lecteurs multim\u00e9dia Plex" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/plex/.translations/it.json b/homeassistant/components/plex/.translations/it.json new file mode 100644 index 00000000000..8f61f968dba --- /dev/null +++ b/homeassistant/components/plex/.translations/it.json @@ -0,0 +1,62 @@ +{ + "config": { + "abort": { + "all_configured": "Tutti i server collegati sono gi\u00e0 configurati", + "already_configured": "Questo server Plex \u00e8 gi\u00e0 configurato", + "already_in_progress": "Plex \u00e8 in fase di configurazione", + "discovery_no_file": "Nessun file di configurazione legacy trovato", + "invalid_import": "La configurazione importata non \u00e8 valida", + "token_request_timeout": "Timeout per l'ottenimento del token", + "unknown": "Non riuscito per motivo sconosciuto" + }, + "error": { + "faulty_credentials": "Autorizzazione non riuscita", + "no_servers": "Nessun server collegato all'account", + "no_token": "Fornire un token o selezionare la configurazione manuale", + "not_found": "Server Plex non trovato" + }, + "step": { + "manual_setup": { + "data": { + "host": "Host", + "port": "Porta", + "ssl": "Usa SSL", + "token": "Token (se richiesto)", + "verify_ssl": "Verificare il certificato SSL" + }, + "title": "Server Plex" + }, + "select_server": { + "data": { + "server": "Server" + }, + "description": "Sono disponibili pi\u00f9 server, selezionarne uno:", + "title": "Selezionare il server Plex" + }, + "start_website_auth": { + "description": "Continuare ad autorizzare su plex.tv.", + "title": "Collegare il server Plex" + }, + "user": { + "data": { + "manual_setup": "Configurazione manuale", + "token": "Token Plex" + }, + "description": "Continuare ad autorizzare plex.tv o configurare manualmente un server.", + "title": "Collegare il server Plex" + } + }, + "title": "Plex" + }, + "options": { + "step": { + "plex_mp_settings": { + "data": { + "show_all_controls": "Mostra tutti i controlli", + "use_episode_art": "Usa la grafica dell'episodio" + }, + "description": "Opzioni per i lettori multimediali Plex" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/plex/.translations/ko.json b/homeassistant/components/plex/.translations/ko.json new file mode 100644 index 00000000000..171c656566d --- /dev/null +++ b/homeassistant/components/plex/.translations/ko.json @@ -0,0 +1,33 @@ +{ + "config": { + "abort": { + "all_configured": "\uc774\ubbf8 \uad6c\uc131\ub41c \ubaa8\ub4e0 \uc5f0\uacb0\ub41c \uc11c\ubc84", + "already_configured": "\uc774 Plex \uc11c\ubc84\ub294 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "already_in_progress": "Plex \ub97c \uad6c\uc131 \uc911\uc785\ub2c8\ub2e4", + "invalid_import": "\uac00\uc838\uc628 \uad6c\uc131 \ub0b4\uc6a9\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "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" + }, + "step": { + "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" + }, + "user": { + "data": { + "token": "Plex \ud1a0\ud070" + }, + "description": "\uc790\ub3d9 \uc124\uc815\uc744 \uc704\ud574 Plex \ud1a0\ud070\uc744 \uc785\ub825\ud558\uac70\ub098 \uc11c\ubc84\ub97c \uc218\ub3d9\uc73c\ub85c \uad6c\uc131\ud574\uc8fc\uc138\uc694.", + "title": "Plex \uc11c\ubc84 \uc5f0\uacb0" + } + }, + "title": "Plex" + } +} \ No newline at end of file diff --git a/homeassistant/components/plex/.translations/lb.json b/homeassistant/components/plex/.translations/lb.json new file mode 100644 index 00000000000..7b0f7232976 --- /dev/null +++ b/homeassistant/components/plex/.translations/lb.json @@ -0,0 +1,62 @@ +{ + "config": { + "abort": { + "all_configured": "All verbonne Server sinn scho konfigur\u00e9iert", + "already_configured": "D\u00ebse Plex Server ass scho konfigur\u00e9iert", + "already_in_progress": "Plex g\u00ebtt konfigur\u00e9iert", + "discovery_no_file": "Kee Konfiguratioun Fichier am ale Format fonnt.", + "invalid_import": "D\u00e9i importiert Konfiguratioun ass ong\u00eblteg", + "token_request_timeout": "Z\u00e4it Iwwerschreidung beim kr\u00e9ien vum Jeton", + "unknown": "Onbekannte Feeler opgetrueden" + }, + "error": { + "faulty_credentials": "Feeler beider Autorisatioun", + "no_servers": "Kee Server as mam Kont verbonnen", + "no_token": "Gitt en Token un oder wielt manuelle Setup", + "not_found": "Kee Plex Server fonnt" + }, + "step": { + "manual_setup": { + "data": { + "host": "Apparat", + "port": "Port", + "ssl": "SSL benotzen", + "token": "Jeton (falls n\u00e9ideg)", + "verify_ssl": "SSL Zertifikat iwwerpr\u00e9iwen" + }, + "title": "Plex Server" + }, + "select_server": { + "data": { + "server": "Server" + }, + "description": "M\u00e9i Server disponibel, wielt een aus:", + "title": "Plex Server auswielen" + }, + "start_website_auth": { + "description": "Weiderfueren op plex.tv fir d'Autorisatioun.", + "title": "Plex Server verbannen" + }, + "user": { + "data": { + "manual_setup": "Manuell Konfiguratioun", + "token": "Jeton fir de Plex" + }, + "description": "Gitt een Jeton fir de Plex un fir eng automatesch Konfiguratioun", + "title": "Plex Server verbannen" + } + }, + "title": "Plex" + }, + "options": { + "step": { + "plex_mp_settings": { + "data": { + "show_all_controls": "Weis all Kontrollen", + "use_episode_art": "Benotz Biller vun der Episode" + }, + "description": "Optioune fir Plex Medie Spiller" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/plex/.translations/nn.json b/homeassistant/components/plex/.translations/nn.json new file mode 100644 index 00000000000..a16deb2fca2 --- /dev/null +++ b/homeassistant/components/plex/.translations/nn.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Plex" + } +} \ No newline at end of file diff --git a/homeassistant/components/plex/.translations/no.json b/homeassistant/components/plex/.translations/no.json new file mode 100644 index 00000000000..18c4e865a84 --- /dev/null +++ b/homeassistant/components/plex/.translations/no.json @@ -0,0 +1,62 @@ +{ + "config": { + "abort": { + "all_configured": "Alle knyttet servere som allerede er konfigurert", + "already_configured": "Denne Plex-serveren er allerede konfigurert", + "already_in_progress": "Plex blir konfigurert", + "discovery_no_file": "Ingen eldre konfigurasjonsfil ble funnet", + "invalid_import": "Den importerte konfigurasjonen er ugyldig", + "token_request_timeout": "Tidsavbrudd ved innhenting av token", + "unknown": "Mislyktes av ukjent \u00e5rsak" + }, + "error": { + "faulty_credentials": "Autorisasjonen mislyktes", + "no_servers": "Ingen servere koblet til kontoen", + "no_token": "Angi et token eller velg manuelt oppsett", + "not_found": "Plex-server ikke funnet" + }, + "step": { + "manual_setup": { + "data": { + "host": "Vert", + "port": "Port", + "ssl": "Bruk SSL", + "token": "Token (hvis n\u00f8dvendig)", + "verify_ssl": "Verifisere SSL-sertifikat" + }, + "title": "Plex server" + }, + "select_server": { + "data": { + "server": "Server" + }, + "description": "Flere servere tilgjengelig, velg en:", + "title": "Velg Plex-server" + }, + "start_website_auth": { + "description": "Fortsett \u00e5 autorisere p\u00e5 plex.tv.", + "title": "Koble til Plex server" + }, + "user": { + "data": { + "manual_setup": "Manuelt oppsett", + "token": "Plex token" + }, + "description": "Fortsett \u00e5 autorisere p\u00e5 plex.tv eller manuelt konfigurere en server.", + "title": "Koble til Plex-server" + } + }, + "title": "Plex" + }, + "options": { + "step": { + "plex_mp_settings": { + "data": { + "show_all_controls": "Vis alle kontroller", + "use_episode_art": "Bruk episode bilde" + }, + "description": "Alternativer for Plex Media Players" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/plex/.translations/pl.json b/homeassistant/components/plex/.translations/pl.json new file mode 100644 index 00000000000..9b75a0061e8 --- /dev/null +++ b/homeassistant/components/plex/.translations/pl.json @@ -0,0 +1,57 @@ +{ + "config": { + "abort": { + "all_configured": "Wszystkie znalezione serwery s\u0105 ju\u017c skonfigurowane.", + "already_configured": "Serwer Plex jest ju\u017c skonfigurowany", + "already_in_progress": "Plex jest konfigurowany", + "invalid_import": "Zaimportowana konfiguracja jest nieprawid\u0142owa", + "token_request_timeout": "Przekroczono limit czasu na uzyskanie tokena", + "unknown": "Nieznany b\u0142\u0105d" + }, + "error": { + "faulty_credentials": "Autoryzacja nie powiod\u0142a si\u0119", + "no_servers": "Brak serwer\u00f3w po\u0142\u0105czonych z kontem", + "no_token": "Wprowad\u017a token lub wybierz konfiguracj\u0119 r\u0119czn\u0105", + "not_found": "Nie znaleziono serwera Plex" + }, + "step": { + "manual_setup": { + "data": { + "host": "Host", + "port": "Port", + "ssl": "U\u017cyj SSL", + "token": "Token (je\u015bli wymagany)", + "verify_ssl": "Weryfikacja certyfikatu SSL" + }, + "title": "Serwer Plex" + }, + "select_server": { + "data": { + "server": "Serwer" + }, + "description": "Dost\u0119pnych jest wiele serwer\u00f3w, wybierz jeden:", + "title": "Wybierz serwer Plex" + }, + "user": { + "data": { + "manual_setup": "Konfiguracja r\u0119czna", + "token": "Token Plex" + }, + "description": "Wprowad\u017a token Plex do automatycznej konfiguracji.", + "title": "Po\u0142\u0105cz z serwerem Plex" + } + }, + "title": "Plex" + }, + "options": { + "step": { + "plex_mp_settings": { + "data": { + "show_all_controls": "Poka\u017c wszystkie elementy steruj\u0105ce", + "use_episode_art": "U\u017cyj grafiki episodu" + }, + "description": "Opcje dla odtwarzaczy multimedialnych Plex" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/plex/.translations/pt-BR.json b/homeassistant/components/plex/.translations/pt-BR.json new file mode 100644 index 00000000000..9a759e309c2 --- /dev/null +++ b/homeassistant/components/plex/.translations/pt-BR.json @@ -0,0 +1,13 @@ +{ + "options": { + "step": { + "plex_mp_settings": { + "data": { + "show_all_controls": "Mostrar todos os controles", + "use_episode_art": "Usar arte epis\u00f3dio" + }, + "description": "Op\u00e7\u00f5es para Plex Media Players" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/plex/.translations/ro.json b/homeassistant/components/plex/.translations/ro.json new file mode 100644 index 00000000000..537bd5e3fac --- /dev/null +++ b/homeassistant/components/plex/.translations/ro.json @@ -0,0 +1,24 @@ +{ + "config": { + "error": { + "no_token": "Furniza\u021bi un token sau selecta\u021bi configurarea manual\u0103" + }, + "step": { + "manual_setup": { + "data": { + "host": "Gazd\u0103", + "port": "Port", + "ssl": "Folosi\u021bi SSL", + "token": "Token-ul (dac\u0103 este necesar)", + "verify_ssl": "Verifica\u021bi certificatul SSL" + }, + "title": "Server Plex" + }, + "user": { + "data": { + "manual_setup": "Configurare manual\u0103" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/plex/.translations/ru.json b/homeassistant/components/plex/.translations/ru.json new file mode 100644 index 00000000000..fe773f72be9 --- /dev/null +++ b/homeassistant/components/plex/.translations/ru.json @@ -0,0 +1,62 @@ +{ + "config": { + "abort": { + "all_configured": "\u0412\u0441\u0435 \u0441\u0432\u044f\u0437\u0430\u043d\u043d\u044b\u0435 \u0441\u0435\u0440\u0432\u0435\u0440\u044b \u0443\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u044b.", + "already_configured": "\u042d\u0442\u043e\u0442 \u0441\u0435\u0440\u0432\u0435\u0440 Plex \u0443\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d.", + "already_in_progress": "\u0412\u044b\u043f\u043e\u043b\u043d\u044f\u0435\u0442\u0441\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430.", + "discovery_no_file": "\u0421\u0442\u0430\u0440\u044b\u0439 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u043e\u043d\u043d\u044b\u0439 \u0444\u0430\u0439\u043b \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d.", + "invalid_import": "\u0418\u043c\u043f\u043e\u0440\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u0430\u044f \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u043d\u0435\u0432\u0435\u0440\u043d\u0430", + "token_request_timeout": "\u0418\u0441\u0442\u0435\u043a\u043b\u043e \u0432\u0440\u0435\u043c\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0442\u043e\u043a\u0435\u043d\u0430", + "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", + "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\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u044c\u044e", + "no_token": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0442\u043e\u043a\u0435\u043d \u0438\u043b\u0438 \u0432\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0440\u0443\u0447\u043d\u0443\u044e \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443", + "not_found": "\u0421\u0435\u0440\u0432\u0435\u0440 Plex \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d" + }, + "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 (\u0435\u0441\u043b\u0438 \u0442\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f)", + "verify_ssl": "\u041f\u0440\u043e\u0432\u0435\u0440\u044f\u0442\u044c \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442 SSL" + }, + "title": "\u0421\u0435\u0440\u0432\u0435\u0440 Plex" + }, + "select_server": { + "data": { + "server": "\u0421\u0435\u0440\u0432\u0435\u0440" + }, + "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u043e\u0434\u0438\u043d \u0438\u0437 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u044b\u0445 \u0441\u0435\u0440\u0432\u0435\u0440\u043e\u0432:", + "title": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0441\u0435\u0440\u0432\u0435\u0440 Plex" + }, + "start_website_auth": { + "description": "\u041f\u0440\u043e\u0434\u043e\u043b\u0436\u0438\u0442\u044c \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u044e \u043d\u0430 plex.tv.", + "title": "Plex" + }, + "user": { + "data": { + "manual_setup": "\u0420\u0443\u0447\u043d\u0430\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430", + "token": "\u0422\u043e\u043a\u0435\u043d" + }, + "description": "\u041f\u0440\u043e\u0434\u043e\u043b\u0436\u0430\u0439\u0442\u0435 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u044e \u043d\u0430 plex.tv \u0438\u043b\u0438 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 \u0441\u0435\u0440\u0432\u0435\u0440 \u0432\u0440\u0443\u0447\u043d\u0443\u044e.", + "title": "Plex" + } + }, + "title": "Plex" + }, + "options": { + "step": { + "plex_mp_settings": { + "data": { + "show_all_controls": "\u041f\u043e\u043a\u0430\u0437\u044b\u0432\u0430\u0442\u044c \u0432\u0441\u0435 \u044d\u043b\u0435\u043c\u0435\u043d\u0442\u044b \u0443\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u044f", + "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" + }, + "description": "\u0414\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/plex/.translations/sl.json b/homeassistant/components/plex/.translations/sl.json new file mode 100644 index 00000000000..9be270a017c --- /dev/null +++ b/homeassistant/components/plex/.translations/sl.json @@ -0,0 +1,62 @@ +{ + "config": { + "abort": { + "all_configured": "Vsi povezani stre\u017eniki so \u017ee konfigurirani", + "already_configured": "Ta stre\u017enik Plex je \u017ee konfiguriran", + "already_in_progress": "Plex se konfigurira", + "discovery_no_file": "Podatkovne konfiguracijske datoteke ni bilo mogo\u010de najti", + "invalid_import": "Uvo\u017eena konfiguracija ni veljavna", + "token_request_timeout": "Potekla \u010dasovna omejitev za pridobitev \u017eetona", + "unknown": "Ni uspelo iz neznanega razloga" + }, + "error": { + "faulty_credentials": "Avtorizacija ni uspela", + "no_servers": "Ni stre\u017enikov povezanih z ra\u010dunom", + "no_token": "Vnesite \u017eeton ali izberite ro\u010dno nastavitev", + "not_found": "Plex stre\u017enika ni mogo\u010de najti" + }, + "step": { + "manual_setup": { + "data": { + "host": "Gostitelj", + "port": "Vrata", + "ssl": "Uporaba SSL", + "token": "\u017deton (po potrebi)", + "verify_ssl": "Preverite SSL potrdilo" + }, + "title": "Plex stre\u017enik" + }, + "select_server": { + "data": { + "server": "Stre\u017enik" + }, + "description": "Na voljo je ve\u010d stre\u017enikov, izberite enega:", + "title": "Izberite stre\u017enik Plex" + }, + "start_website_auth": { + "description": "Nadaljujte z avtorizacijo na plex.tv.", + "title": "Pove\u017eite stre\u017enik Plex" + }, + "user": { + "data": { + "manual_setup": "Ro\u010dna nastavitev", + "token": "Plex \u017eeton" + }, + "description": "Vnesite \u017eeton Plex za samodejno nastavitev ali ro\u010dno konfigurirajte stre\u017enik.", + "title": "Pove\u017eite stre\u017enik Plex" + } + }, + "title": "Plex" + }, + "options": { + "step": { + "plex_mp_settings": { + "data": { + "show_all_controls": "Poka\u017ei vse kontrole", + "use_episode_art": "Uporabi naslovno sliko epizode" + }, + "description": "Mo\u017enosti za predvajalnike Plex" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/plex/.translations/sv.json b/homeassistant/components/plex/.translations/sv.json new file mode 100644 index 00000000000..702cec128c0 --- /dev/null +++ b/homeassistant/components/plex/.translations/sv.json @@ -0,0 +1,11 @@ +{ + "options": { + "step": { + "plex_mp_settings": { + "data": { + "show_all_controls": "Visa alla kontroller" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/plex/.translations/zh-Hant.json b/homeassistant/components/plex/.translations/zh-Hant.json new file mode 100644 index 00000000000..2d4ce1ea6aa --- /dev/null +++ b/homeassistant/components/plex/.translations/zh-Hant.json @@ -0,0 +1,62 @@ +{ + "config": { + "abort": { + "all_configured": "\u6240\u6709\u7d81\u5b9a\u4f3a\u670d\u5668\u90fd\u5df2\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "Plex \u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_in_progress": "Plex \u5df2\u7d93\u8a2d\u5b9a", + "discovery_no_file": "\u627e\u4e0d\u5230\u820a\u7248\u8a2d\u5b9a\u6a94\u6848", + "invalid_import": "\u532f\u5165\u4e4b\u8a2d\u5b9a\u7121\u6548", + "token_request_timeout": "\u53d6\u5f97\u5bc6\u9470\u903e\u6642", + "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", + "no_token": "\u63d0\u4f9b\u5bc6\u9470\u6216\u9078\u64c7\u624b\u52d5\u8a2d\u5b9a", + "not_found": "\u627e\u4e0d\u5230 Plex \u4f3a\u670d\u5668" + }, + "step": { + "manual_setup": { + "data": { + "host": "\u4e3b\u6a5f\u7aef", + "port": "\u901a\u8a0a\u57e0", + "ssl": "\u4f7f\u7528 SSL", + "token": "\u5bc6\u9470\uff08\u5982\u679c\u9700\u8981\uff09", + "verify_ssl": "\u78ba\u8a8d SSL \u8a8d\u8b49" + }, + "title": "Plex \u4f3a\u670d\u5668" + }, + "select_server": { + "data": { + "server": "\u4f3a\u670d\u5668" + }, + "description": "\u627e\u5230\u591a\u500b\u4f3a\u670d\u5668\uff0c\u8acb\u9078\u64c7\u4e00\u7d44\uff1a", + "title": "\u9078\u64c7 Plex \u4f3a\u670d\u5668" + }, + "start_website_auth": { + "description": "\u7e7c\u7e8c\u65bc Plex.tv \u9032\u884c\u8a8d\u8b49\u3002", + "title": "\u9023\u7dda\u81f3 Plex \u4f3a\u670d\u5668" + }, + "user": { + "data": { + "manual_setup": "\u624b\u52d5\u8a2d\u5b9a", + "token": "Plex \u5bc6\u9470" + }, + "description": "\u7e7c\u7e8c\u65bc Plex.tv \u9032\u884c\u8a8d\u8b49\u6216\u624b\u52d5\u8a2d\u5b9a\u4f3a\u670d\u5668\u3002", + "title": "\u9023\u7dda\u81f3 Plex \u4f3a\u670d\u5668" + } + }, + "title": "Plex" + }, + "options": { + "step": { + "plex_mp_settings": { + "data": { + "show_all_controls": "\u986f\u793a\u6240\u6709\u63a7\u5236", + "use_episode_art": "\u4f7f\u7528\u5f71\u96c6\u5287\u7167" + }, + "description": "Plex \u64ad\u653e\u5668\u9078\u9805" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/plex/__init__.py b/homeassistant/components/plex/__init__.py index 69e77c8854f..ed94b6913bc 100644 --- a/homeassistant/components/plex/__init__.py +++ b/homeassistant/components/plex/__init__.py @@ -1,11 +1,12 @@ """Support to embed Plex.""" +import asyncio import logging import plexapi.exceptions import requests.exceptions import voluptuous as vol -from homeassistant.components.discovery import SERVICE_PLEX +from homeassistant import config_entries from homeassistant.components.media_player import DOMAIN as MP_DOMAIN from homeassistant.const import ( CONF_HOST, @@ -16,20 +17,20 @@ from homeassistant.const import ( CONF_VERIFY_SSL, ) from homeassistant.helpers import config_validation as cv -from homeassistant.helpers import discovery -from homeassistant.util.json import load_json, save_json from .const import ( - CONF_SERVER, CONF_USE_EPISODE_ART, CONF_SHOW_ALL_CONTROLS, + CONF_SERVER, + CONF_SERVER_IDENTIFIER, DEFAULT_PORT, DEFAULT_SSL, DEFAULT_VERIFY_SSL, DOMAIN as PLEX_DOMAIN, PLATFORMS, - PLEX_CONFIG_FILE, PLEX_MEDIA_PLAYER_OPTIONS, + PLEX_SERVER_CONFIG, + REFRESH_LISTENERS, SERVERS, ) from .server import PlexServer @@ -58,151 +59,107 @@ SERVER_CONFIG_SCHEMA = vol.Schema( CONFIG_SCHEMA = vol.Schema({PLEX_DOMAIN: SERVER_CONFIG_SCHEMA}, extra=vol.ALLOW_EXTRA) -CONFIGURING = "configuring" _LOGGER = logging.getLogger(__package__) def setup(hass, config): """Set up the Plex component.""" - - def server_discovered(service, info): - """Pass back discovered Plex server details.""" - if hass.data[PLEX_DOMAIN][SERVERS]: - _LOGGER.debug("Plex server already configured, ignoring discovery.") - return - _LOGGER.debug("Discovered Plex server: %s:%s", info["host"], info["port"]) - setup_plex(discovery_info=info) - - def setup_plex(config=None, discovery_info=None, configurator_info=None): - """Return assembled server_config dict.""" - json_file = hass.config.path(PLEX_CONFIG_FILE) - file_config = load_json(json_file) - host_and_port = None - - if config: - server_config = config - if CONF_HOST in server_config: - host_and_port = ( - f"{server_config.pop(CONF_HOST)}:{server_config.pop(CONF_PORT)}" - ) - if MP_DOMAIN in server_config: - hass.data[PLEX_MEDIA_PLAYER_OPTIONS] = server_config.pop(MP_DOMAIN) - elif file_config: - _LOGGER.debug("Loading config from %s", json_file) - host_and_port, server_config = file_config.popitem() - server_config[CONF_VERIFY_SSL] = server_config.pop("verify") - elif discovery_info: - server_config = {} - host_and_port = f"{discovery_info[CONF_HOST]}:{discovery_info[CONF_PORT]}" - elif configurator_info: - server_config = configurator_info - host_and_port = server_config["host_and_port"] - else: - discovery.listen(hass, SERVICE_PLEX, server_discovered) - return True - - if host_and_port: - use_ssl = server_config.get(CONF_SSL, DEFAULT_SSL) - http_prefix = "https" if use_ssl else "http" - server_config[CONF_URL] = f"{http_prefix}://{host_and_port}" - - plex_server = PlexServer(server_config) - try: - plex_server.connect() - except requests.exceptions.ConnectionError as error: - _LOGGER.error( - "Plex server could not be reached, please verify host and port: [%s]", - error, - ) - return False - except ( - plexapi.exceptions.BadRequest, - plexapi.exceptions.Unauthorized, - plexapi.exceptions.NotFound, - ) as error: - _LOGGER.error( - "Connection to Plex server failed, please verify token and SSL settings: [%s]", - error, - ) - request_configuration(host_and_port) - return False - else: - hass.data[PLEX_DOMAIN][SERVERS][ - plex_server.machine_identifier - ] = plex_server - - if host_and_port in hass.data[PLEX_DOMAIN][CONFIGURING]: - request_id = hass.data[PLEX_DOMAIN][CONFIGURING].pop(host_and_port) - configurator = hass.components.configurator - configurator.request_done(request_id) - _LOGGER.debug("Discovery configuration done") - if configurator_info: - # Write plex.conf if created via discovery/configurator - save_json( - hass.config.path(PLEX_CONFIG_FILE), - { - host_and_port: { - CONF_TOKEN: server_config[CONF_TOKEN], - CONF_SSL: use_ssl, - "verify": server_config[CONF_VERIFY_SSL], - } - }, - ) - - if not hass.data.get(PLEX_MEDIA_PLAYER_OPTIONS): - hass.data[PLEX_MEDIA_PLAYER_OPTIONS] = MEDIA_PLAYER_SCHEMA({}) - - for platform in PLATFORMS: - hass.helpers.discovery.load_platform( - platform, PLEX_DOMAIN, {}, original_config - ) - - return True - - def request_configuration(host_and_port): - """Request configuration steps from the user.""" - configurator = hass.components.configurator - if host_and_port in hass.data[PLEX_DOMAIN][CONFIGURING]: - configurator.notify_errors( - hass.data[PLEX_DOMAIN][CONFIGURING][host_and_port], - "Failed to register, please try again.", - ) - return - - def plex_configuration_callback(data): - """Handle configuration changes.""" - config = { - "host_and_port": host_and_port, - CONF_TOKEN: data.get("token"), - CONF_SSL: cv.boolean(data.get("ssl")), - CONF_VERIFY_SSL: cv.boolean(data.get("verify_ssl")), - } - setup_plex(configurator_info=config) - - hass.data[PLEX_DOMAIN][CONFIGURING][ - host_and_port - ] = configurator.request_config( - "Plex Media Server", - plex_configuration_callback, - description="Enter the X-Plex-Token", - entity_picture="/static/images/logo_plex_mediaserver.png", - submit_caption="Confirm", - fields=[ - {"id": "token", "name": "X-Plex-Token", "type": ""}, - {"id": "ssl", "name": "Use SSL", "type": ""}, - {"id": "verify_ssl", "name": "Verify SSL", "type": ""}, - ], - ) - - # End of inner functions. - - original_config = config - - hass.data.setdefault(PLEX_DOMAIN, {SERVERS: {}, CONFIGURING: {}}) - - if hass.data[PLEX_DOMAIN][SERVERS]: - _LOGGER.debug("Plex server already configured") - return False + hass.data.setdefault(PLEX_DOMAIN, {SERVERS: {}, REFRESH_LISTENERS: {}}) plex_config = config.get(PLEX_DOMAIN, {}) - return setup_plex(config=plex_config) + if plex_config: + _setup_plex(hass, plex_config) + + return True + + +def _setup_plex(hass, config): + """Pass configuration to a config flow.""" + server_config = dict(config) + if MP_DOMAIN in server_config: + hass.data.setdefault(PLEX_MEDIA_PLAYER_OPTIONS, server_config.pop(MP_DOMAIN)) + if CONF_HOST in server_config: + prefix = "https" if server_config.pop(CONF_SSL) else "http" + server_config[ + CONF_URL + ] = f"{prefix}://{server_config.pop(CONF_HOST)}:{server_config.pop(CONF_PORT)}" + hass.async_create_task( + hass.config_entries.flow.async_init( + PLEX_DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data=server_config, + ) + ) + + +async def async_setup_entry(hass, entry): + """Set up Plex from a config entry.""" + server_config = entry.data[PLEX_SERVER_CONFIG] + + if MP_DOMAIN not in entry.options: + options = dict(entry.options) + options.setdefault( + MP_DOMAIN, + hass.data.get(PLEX_MEDIA_PLAYER_OPTIONS) or MEDIA_PLAYER_SCHEMA({}), + ) + hass.config_entries.async_update_entry(entry, options=options) + + plex_server = PlexServer(server_config, entry.options) + try: + await hass.async_add_executor_job(plex_server.connect) + except requests.exceptions.ConnectionError as error: + _LOGGER.error( + "Plex server (%s) could not be reached: [%s]", + server_config[CONF_URL], + error, + ) + return False + except ( + plexapi.exceptions.BadRequest, + plexapi.exceptions.Unauthorized, + plexapi.exceptions.NotFound, + ) as error: + _LOGGER.error( + "Login to %s failed, verify token and SSL settings: [%s]", + entry.data[CONF_SERVER], + error, + ) + return False + + _LOGGER.debug( + "Connected to: %s (%s)", plex_server.friendly_name, plex_server.url_in_use + ) + hass.data[PLEX_DOMAIN][SERVERS][plex_server.machine_identifier] = plex_server + + for platform in PLATFORMS: + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, platform) + ) + + entry.add_update_listener(async_options_updated) + + return True + + +async def async_unload_entry(hass, entry): + """Unload a config entry.""" + server_id = entry.data[CONF_SERVER_IDENTIFIER] + + cancel = hass.data[PLEX_DOMAIN][REFRESH_LISTENERS].pop(server_id) + await hass.async_add_executor_job(cancel) + + tasks = [ + hass.config_entries.async_forward_entry_unload(entry, platform) + for platform in PLATFORMS + ] + await asyncio.gather(*tasks) + + hass.data[PLEX_DOMAIN][SERVERS].pop(server_id) + + return True + + +async def async_options_updated(hass, entry): + """Triggered by config entry options updates.""" + server_id = entry.data[CONF_SERVER_IDENTIFIER] + hass.data[PLEX_DOMAIN][SERVERS][server_id].options = entry.options diff --git a/homeassistant/components/plex/config_flow.py b/homeassistant/components/plex/config_flow.py new file mode 100644 index 00000000000..38727ccff06 --- /dev/null +++ b/homeassistant/components/plex/config_flow.py @@ -0,0 +1,286 @@ +"""Config flow for Plex.""" +import copy +import logging + +from aiohttp import web_response +import plexapi.exceptions +from plexauth import PlexAuth +import requests.exceptions +import voluptuous as vol + +from homeassistant.components.http.view import HomeAssistantView +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant import config_entries +from homeassistant.components.media_player import DOMAIN as MP_DOMAIN +from homeassistant.const import CONF_URL, CONF_TOKEN, CONF_SSL, CONF_VERIFY_SSL +from homeassistant.core import callback +from homeassistant.util.json import load_json + +from .const import ( # pylint: disable=unused-import + AUTH_CALLBACK_NAME, + AUTH_CALLBACK_PATH, + CONF_SERVER, + CONF_SERVER_IDENTIFIER, + CONF_USE_EPISODE_ART, + CONF_SHOW_ALL_CONTROLS, + DEFAULT_VERIFY_SSL, + DOMAIN, + PLEX_CONFIG_FILE, + PLEX_SERVER_CONFIG, + X_PLEX_DEVICE_NAME, + X_PLEX_VERSION, + X_PLEX_PRODUCT, + X_PLEX_PLATFORM, +) +from .errors import NoServersFound, ServerNotSpecified +from .server import PlexServer + +_LOGGER = logging.getLogger(__package__) + + +@callback +def configured_servers(hass): + """Return a set of the configured Plex servers.""" + return set( + entry.data[CONF_SERVER_IDENTIFIER] + for entry in hass.config_entries.async_entries(DOMAIN) + ) + + +class PlexFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a Plex 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 PlexOptionsFlowHandler(config_entry) + + def __init__(self): + """Initialize the Plex flow.""" + self.current_login = {} + self.available_servers = None + self.plexauth = None + self.token = None + + async def async_step_user(self, user_input=None): + """Handle a flow initialized by the user.""" + return self.async_show_form(step_id="start_website_auth") + + 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_server_validate(self, server_config): + """Validate a provided configuration.""" + errors = {} + self.current_login = server_config + + plex_server = PlexServer(server_config) + try: + await self.hass.async_add_executor_job(plex_server.connect) + + except NoServersFound: + errors["base"] = "no_servers" + except (plexapi.exceptions.BadRequest, plexapi.exceptions.Unauthorized): + _LOGGER.error("Invalid credentials provided, config not created") + errors["base"] = "faulty_credentials" + except (plexapi.exceptions.NotFound, requests.exceptions.ConnectionError): + _LOGGER.error( + "Plex server could not be reached: %s", server_config[CONF_URL] + ) + errors["base"] = "not_found" + + except ServerNotSpecified as available_servers: + self.available_servers = available_servers.args[0] + return await self.async_step_select_server() + + except Exception as error: # pylint: disable=broad-except + _LOGGER.error("Unknown error connecting to Plex server: %s", error) + return self.async_abort(reason="unknown") + + if errors: + return self.async_show_form(step_id="start_website_auth", errors=errors) + + server_id = plex_server.machine_identifier + + for entry in self._async_current_entries(): + if entry.data[CONF_SERVER_IDENTIFIER] == server_id: + return self.async_abort(reason="already_configured") + + url = plex_server.url_in_use + token = server_config.get(CONF_TOKEN) + + entry_config = {CONF_URL: url} + if token: + entry_config[CONF_TOKEN] = token + if url.startswith("https"): + entry_config[CONF_VERIFY_SSL] = server_config.get( + CONF_VERIFY_SSL, DEFAULT_VERIFY_SSL + ) + + _LOGGER.debug("Valid config created for %s", plex_server.friendly_name) + + return self.async_create_entry( + title=plex_server.friendly_name, + data={ + CONF_SERVER: plex_server.friendly_name, + CONF_SERVER_IDENTIFIER: server_id, + PLEX_SERVER_CONFIG: entry_config, + }, + ) + + async def async_step_select_server(self, user_input=None): + """Use selected Plex server.""" + config = dict(self.current_login) + if user_input is not None: + config[CONF_SERVER] = user_input[CONF_SERVER] + return await self.async_step_server_validate(config) + + configured = configured_servers(self.hass) + available_servers = [ + name + for (name, server_id) in self.available_servers + if server_id not in configured + ] + + if not available_servers: + return self.async_abort(reason="all_configured") + if len(available_servers) == 1: + config[CONF_SERVER] = available_servers[0] + return await self.async_step_server_validate(config) + + return self.async_show_form( + step_id="select_server", + data_schema=vol.Schema( + {vol.Required(CONF_SERVER): vol.In(available_servers)} + ), + errors={}, + ) + + async def async_step_discovery(self, discovery_info): + """Set default host and port from discovery.""" + if self._async_current_entries() or self._async_in_progress(): + # Skip discovery if a config already exists or is in progress. + return self.async_abort(reason="already_configured") + + json_file = self.hass.config.path(PLEX_CONFIG_FILE) + file_config = await self.hass.async_add_executor_job(load_json, json_file) + + if file_config: + host_and_port, host_config = file_config.popitem() + prefix = "https" if host_config[CONF_SSL] else "http" + + server_config = { + CONF_URL: f"{prefix}://{host_and_port}", + CONF_TOKEN: host_config[CONF_TOKEN], + CONF_VERIFY_SSL: host_config["verify"], + } + _LOGGER.info("Imported legacy config, file can be removed: %s", json_file) + return await self.async_step_server_validate(server_config) + + return self.async_abort(reason="discovery_no_file") + + async def async_step_import(self, import_config): + """Import from Plex configuration.""" + _LOGGER.debug("Imported Plex configuration") + return await self.async_step_server_validate(import_config) + + async def async_step_plex_website_auth(self): + """Begin external auth flow on Plex website.""" + self.hass.http.register_view(PlexAuthorizationCallbackView) + payload = { + "X-Plex-Device-Name": X_PLEX_DEVICE_NAME, + "X-Plex-Version": X_PLEX_VERSION, + "X-Plex-Product": X_PLEX_PRODUCT, + "X-Plex-Device": self.hass.config.location_name, + "X-Plex-Platform": X_PLEX_PLATFORM, + "X-Plex-Model": "Plex OAuth", + } + 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}" + auth_url = self.plexauth.auth_url(forward_url) + return self.async_external_step(step_id="obtain_token", url=auth_url) + + async def async_step_obtain_token(self, user_input=None): + """Obtain token after external auth completed.""" + token = await self.plexauth.token(10) + + if not token: + return self.async_external_step_done(next_step_id="timed_out") + + self.token = token + return self.async_external_step_done(next_step_id="use_external_token") + + async def async_step_timed_out(self, user_input=None): + """Abort flow when time expires.""" + return self.async_abort(reason="token_request_timeout") + + async def async_step_use_external_token(self, user_input=None): + """Continue server validation with external token.""" + server_config = {CONF_TOKEN: self.token} + return await self.async_step_server_validate(server_config) + + +class PlexOptionsFlowHandler(config_entries.OptionsFlow): + """Handle Plex options.""" + + def __init__(self, config_entry): + """Initialize Plex options flow.""" + self.options = copy.deepcopy(config_entry.options) + + async def async_step_init(self, user_input=None): + """Manage the Plex options.""" + return await self.async_step_plex_mp_settings() + + async def async_step_plex_mp_settings(self, user_input=None): + """Manage the Plex media_player options.""" + if user_input is not None: + self.options[MP_DOMAIN][CONF_USE_EPISODE_ART] = user_input[ + CONF_USE_EPISODE_ART + ] + self.options[MP_DOMAIN][CONF_SHOW_ALL_CONTROLS] = user_input[ + CONF_SHOW_ALL_CONTROLS + ] + return self.async_create_entry(title="", data=self.options) + + return self.async_show_form( + step_id="plex_mp_settings", + data_schema=vol.Schema( + { + vol.Required( + CONF_USE_EPISODE_ART, + default=self.options[MP_DOMAIN][CONF_USE_EPISODE_ART], + ): bool, + vol.Required( + CONF_SHOW_ALL_CONTROLS, + default=self.options[MP_DOMAIN][CONF_SHOW_ALL_CONTROLS], + ): bool, + } + ), + ) + + +class PlexAuthorizationCallbackView(HomeAssistantView): + """Handle callback from external auth.""" + + url = AUTH_CALLBACK_PATH + name = AUTH_CALLBACK_NAME + requires_auth = False + + async def get(self, request): + """Receive authorization confirmation.""" + hass = request.app["hass"] + await hass.config_entries.flow.async_configure( + flow_id=request.query["flow_id"], user_input=None + ) + + return web_response.Response( + headers={"content-type": "text/html"}, + text="Success! This window can be closed", + ) diff --git a/homeassistant/components/plex/const.py b/homeassistant/components/plex/const.py index 6f19623c809..0b436c4e208 100644 --- a/homeassistant/components/plex/const.py +++ b/homeassistant/components/plex/const.py @@ -1,4 +1,6 @@ """Constants for the Plex component.""" +from homeassistant.const import __version__ + DOMAIN = "plex" NAME_FORMAT = "Plex {}" @@ -7,6 +9,7 @@ DEFAULT_SSL = False DEFAULT_VERIFY_SSL = True PLATFORMS = ["media_player", "sensor"] +REFRESH_LISTENERS = "refresh_listeners" SERVERS = "servers" PLEX_CONFIG_FILE = "plex.conf" @@ -14,5 +17,14 @@ PLEX_MEDIA_PLAYER_OPTIONS = "plex_mp_options" PLEX_SERVER_CONFIG = "server_config" CONF_SERVER = "server" +CONF_SERVER_IDENTIFIER = "server_id" CONF_USE_EPISODE_ART = "use_episode_art" CONF_SHOW_ALL_CONTROLS = "show_all_controls" + +AUTH_CALLBACK_PATH = "/auth/plex/callback" +AUTH_CALLBACK_NAME = "auth:plex:callback" + +X_PLEX_DEVICE_NAME = "Home Assistant" +X_PLEX_PLATFORM = "Home Assistant" +X_PLEX_PRODUCT = "Home Assistant" +X_PLEX_VERSION = __version__ diff --git a/homeassistant/components/plex/errors.py b/homeassistant/components/plex/errors.py new file mode 100644 index 00000000000..11c15404f45 --- /dev/null +++ b/homeassistant/components/plex/errors.py @@ -0,0 +1,14 @@ +"""Errors for the Plex component.""" +from homeassistant.exceptions import HomeAssistantError + + +class PlexException(HomeAssistantError): + """Base class for Plex exceptions.""" + + +class NoServersFound(PlexException): + """No servers found on Plex account.""" + + +class ServerNotSpecified(PlexException): + """Multiple servers linked to account without choice provided.""" diff --git a/homeassistant/components/plex/manifest.json b/homeassistant/components/plex/manifest.json index 4269400dc24..d4f2ae0517a 100644 --- a/homeassistant/components/plex/manifest.json +++ b/homeassistant/components/plex/manifest.json @@ -1,11 +1,15 @@ { "domain": "plex", "name": "Plex", - "documentation": "https://www.home-assistant.io/components/plex", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/plex", "requirements": [ - "plexapi==3.0.6" + "plexapi==3.0.6", + "plexauth==0.0.4" + ], + "dependencies": [ + "http" ], - "dependencies": ["configurator"], "codeowners": [ "@jjlawren" ] diff --git a/homeassistant/components/plex/media_player.py b/homeassistant/components/plex/media_player.py index cfc63948bee..a49e4c9c057 100644 --- a/homeassistant/components/plex/media_player.py +++ b/homeassistant/components/plex/media_player.py @@ -2,10 +2,9 @@ from datetime import timedelta import json import logging +from xml.etree.ElementTree import ParseError import plexapi.exceptions -import plexapi.playlist -import plexapi.playqueue import requests.exceptions from homeassistant.components.media_player import MediaPlayerDevice @@ -16,6 +15,7 @@ from homeassistant.components.media_player.const import ( SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY, + SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK, SUPPORT_STOP, SUPPORT_TURN_OFF, @@ -33,31 +33,43 @@ from homeassistant.helpers.event import track_time_interval from homeassistant.util import dt as dt_util from .const import ( - CONF_USE_EPISODE_ART, - CONF_SHOW_ALL_CONTROLS, + CONF_SERVER_IDENTIFIER, DOMAIN as PLEX_DOMAIN, NAME_FORMAT, - PLEX_MEDIA_PLAYER_OPTIONS, + REFRESH_LISTENERS, SERVERS, ) -SERVER_SETUP = "server_setup" - -_CONFIGURING = {} _LOGGER = logging.getLogger(__name__) -def setup_platform(hass, config, add_entities_callback, discovery_info=None): - """Set up the Plex platform.""" - if discovery_info is None: - return +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Set up the Plex media_player platform. - plexserver = list(hass.data[PLEX_DOMAIN][SERVERS].values())[0] - config = hass.data[PLEX_MEDIA_PLAYER_OPTIONS] + Deprecated. + """ + pass + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up Plex media_player from a config entry.""" + + def add_entities(entities, update_before_add=False): + """Sync version of async add entities.""" + hass.add_job(async_add_entities, entities, update_before_add) + + hass.async_add_executor_job(_setup_platform, hass, config_entry, add_entities) + + +def _setup_platform(hass, config_entry, add_entities_callback): + """Set up the Plex media_player platform.""" + server_id = config_entry.data[CONF_SERVER_IDENTIFIER] + plexserver = hass.data[PLEX_DOMAIN][SERVERS][server_id] plex_clients = {} plex_sessions = {} - track_time_interval(hass, lambda now: update_devices(), timedelta(seconds=10)) + hass.data[PLEX_DOMAIN][REFRESH_LISTENERS][server_id] = track_time_interval( + hass, lambda now: update_devices(), timedelta(seconds=10) + ) def update_devices(): """Update the devices objects.""" @@ -85,7 +97,7 @@ def setup_platform(hass, config, add_entities_callback, discovery_info=None): if device.machineIdentifier not in plex_clients: new_client = PlexClient( - config, device, None, plex_sessions, update_devices + plexserver, device, None, plex_sessions, update_devices ) plex_clients[device.machineIdentifier] = new_client _LOGGER.debug("New device: %s", device.machineIdentifier) @@ -124,7 +136,7 @@ def setup_platform(hass, config, add_entities_callback, discovery_info=None): and machine_identifier is not None ): new_client = PlexClient( - config, player, session, plex_sessions, update_devices + plexserver, player, session, plex_sessions, update_devices ) plex_clients[machine_identifier] = new_client _LOGGER.debug("New session: %s", machine_identifier) @@ -153,7 +165,7 @@ def setup_platform(hass, config, add_entities_callback, discovery_info=None): class PlexClient(MediaPlayerDevice): """Representation of a Plex device.""" - def __init__(self, config, device, session, plex_sessions, update_devices): + def __init__(self, plex_server, device, session, plex_sessions, update_devices): """Initialize the Plex device.""" self._app_name = "" self._device = None @@ -174,7 +186,7 @@ class PlexClient(MediaPlayerDevice): self._state = STATE_IDLE self._volume_level = 1 # since we can't retrieve remotely self._volume_muted = False # since we can't retrieve remotely - self.config = config + self.plex_server = plex_server self.plex_sessions = plex_sessions self.update_devices = update_devices # General @@ -300,8 +312,9 @@ class PlexClient(MediaPlayerDevice): def _set_media_image(self): thumb_url = self._session.thumbUrl - if self.media_content_type is MEDIA_TYPE_TVSHOW and not self.config.get( - CONF_USE_EPISODE_ART + if ( + self.media_content_type is MEDIA_TYPE_TVSHOW + and not self.plex_server.use_episode_art ): thumb_url = self._session.url(self._session.grandparentThumb) @@ -530,11 +543,8 @@ class PlexClient(MediaPlayerDevice): @property def supported_features(self): """Flag media player features that are supported.""" - if not self._is_player_active: - return 0 - # force show all controls - if self.config.get(CONF_SHOW_ALL_CONTROLS): + if self.plex_server.show_all_controls: return ( SUPPORT_PAUSE | SUPPORT_PREVIOUS_TRACK @@ -542,13 +552,11 @@ class PlexClient(MediaPlayerDevice): | SUPPORT_STOP | SUPPORT_VOLUME_SET | SUPPORT_PLAY + | SUPPORT_PLAY_MEDIA | SUPPORT_TURN_OFF | SUPPORT_VOLUME_MUTE ) - # only show controls when we know what device is connecting - if not self._make: - return 0 # no mute support if self.make.lower() == "shield android tv": _LOGGER.debug( @@ -562,8 +570,10 @@ class PlexClient(MediaPlayerDevice): | SUPPORT_STOP | SUPPORT_VOLUME_SET | SUPPORT_PLAY + | SUPPORT_PLAY_MEDIA | SUPPORT_TURN_OFF ) + # Only supports play,pause,stop (and off which really is stop) if self.make.lower().startswith("tivo"): _LOGGER.debug( @@ -572,8 +582,7 @@ class PlexClient(MediaPlayerDevice): self.entity_id, ) return SUPPORT_PAUSE | SUPPORT_PLAY | SUPPORT_STOP | SUPPORT_TURN_OFF - # Not all devices support playback functionality - # Playback includes volume, stop/play/pause, etc. + if self.device and "playback" in self._device_protocol_capabilities: return ( SUPPORT_PAUSE @@ -582,6 +591,7 @@ class PlexClient(MediaPlayerDevice): | SUPPORT_STOP | SUPPORT_VOLUME_SET | SUPPORT_PLAY + | SUPPORT_PLAY_MEDIA | SUPPORT_TURN_OFF | SUPPORT_VOLUME_MUTE ) @@ -669,49 +679,74 @@ class PlexClient(MediaPlayerDevice): return src = json.loads(media_id) + library = src.get("library_name") + shuffle = src.get("shuffle", 0) media = None + if media_type == "MUSIC": - media = ( - self.device.server.library.section(src["library_name"]) - .get(src["artist_name"]) - .album(src["album_name"]) - .get(src["track_name"]) - ) + media = self._get_music_media(library, src) elif media_type == "EPISODE": - media = self._get_tv_media( - src["library_name"], - src["show_name"], - src["season_number"], - src["episode_number"], - ) + media = self._get_tv_media(library, src) elif media_type == "PLAYLIST": - media = self.device.server.playlist(src["playlist_name"]) + media = self.plex_server.playlist(src["playlist_name"]) elif media_type == "VIDEO": - media = self.device.server.library.section(src["library_name"]).get( - src["video_name"] - ) + media = self.plex_server.library.section(library).get(src["video_name"]) - if ( - media - and media_type == "EPISODE" - and isinstance(media, plexapi.playlist.Playlist) - ): - # delete episode playlist after being loaded into a play queue - self._client_play_media(media=media, delete=True, shuffle=src["shuffle"]) - elif media: - self._client_play_media(media=media, shuffle=src["shuffle"]) + if media is None: + _LOGGER.error("Media could not be found: %s", media_id) + return - def _get_tv_media(self, library_name, show_name, season_number, episode_number): + playqueue = self.plex_server.create_playqueue(media, shuffle=shuffle) + try: + self.device.playMedia(playqueue) + except ParseError: + # Temporary workaround for Plexamp / plexapi issue + pass + except requests.exceptions.ConnectTimeout: + _LOGGER.error("Timed out playing on %s", self.name) + + self.update_devices() + + def _get_music_media(self, library_name, src): + """Find music media and return a Plex media object.""" + artist_name = src["artist_name"] + album_name = src.get("album_name") + track_name = src.get("track_name") + track_number = src.get("track_number") + + artist = self.plex_server.library.section(library_name).get(artist_name) + + if album_name: + album = artist.album(album_name) + + if track_name: + return album.track(track_name) + + if track_number: + for track in album.tracks(): + if int(track.index) == int(track_number): + return track + return None + + return album + + if track_name: + return artist.searchTracks(track_name, maxresults=1) + return artist + + def _get_tv_media(self, library_name, src): """Find TV media and return a Plex media object.""" + show_name = src["show_name"] + season_number = src.get("season_number") + episode_number = src.get("episode_number") target_season = None target_episode = None - show = self.device.server.library.section(library_name).get(show_name) + show = self.plex_server.library.section(library_name).get(show_name) if not season_number: - playlist_name = f"{self.entity_id} - {show_name} Episodes" - return self.device.server.createPlaylist(playlist_name, show.episodes()) + return show for season in show.seasons(): if int(season.seasonNumber) == int(season_number): @@ -728,12 +763,7 @@ class PlexClient(MediaPlayerDevice): ) else: if not episode_number: - playlist_name = "{} - {} Season {} Episodes".format( - self.entity_id, show_name, str(season_number) - ) - return self.device.server.createPlaylist( - playlist_name, target_season.episodes() - ) + return target_season for episode in target_season.episodes(): if int(episode.index) == int(episode_number): @@ -751,38 +781,6 @@ class PlexClient(MediaPlayerDevice): return target_episode - def _client_play_media(self, media, delete=False, **params): - """Instruct Plex client to play a piece of media.""" - if not (self.device and "playback" in self._device_protocol_capabilities): - _LOGGER.error("Client cannot play media: %s", self.entity_id) - return - - playqueue = plexapi.playqueue.PlayQueue.create( - self.device.server, media, **params - ) - - # Delete dynamic playlists used to build playqueue (ex. play tv season) - if delete: - media.delete() - - server_url = self.device.server.baseurl.split(":") - self.device.sendCommand( - "playback/playMedia", - **dict( - { - "machineIdentifier": self.device.server.machineIdentifier, - "address": server_url[1].strip("/"), - "port": server_url[-1], - "key": media.key, - "containerKey": "/playQueues/{}?window=100&own=1".format( - playqueue.playQueueID - ), - }, - **params, - ), - ) - self.update_devices() - @property def device_state_attributes(self): """Return the scene state attributes.""" diff --git a/homeassistant/components/plex/sensor.py b/homeassistant/components/plex/sensor.py index f469e95da80..7d5b54356a0 100644 --- a/homeassistant/components/plex/sensor.py +++ b/homeassistant/components/plex/sensor.py @@ -8,21 +8,26 @@ import requests.exceptions from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle -from .const import DOMAIN as PLEX_DOMAIN, SERVERS +from .const import CONF_SERVER_IDENTIFIER, DOMAIN as PLEX_DOMAIN, SERVERS -DEFAULT_NAME = "Plex" _LOGGER = logging.getLogger(__name__) MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=1) -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the Plex sensor.""" - if discovery_info is None: - return +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Set up the Plex sensor platform. - plexserver = list(hass.data[PLEX_DOMAIN][SERVERS].values())[0] - add_entities([PlexSensor(plexserver)], True) + Deprecated. + """ + pass + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up Plex sensor from a config entry.""" + server_id = config_entry.data[CONF_SERVER_IDENTIFIER] + sensor = PlexSensor(hass.data[PLEX_DOMAIN][SERVERS][server_id]) + async_add_entities([sensor], True) class PlexSensor(Entity): @@ -30,10 +35,10 @@ class PlexSensor(Entity): def __init__(self, plex_server): """Initialize the sensor.""" - self._name = DEFAULT_NAME self._state = None self._now_playing = [] self._server = plex_server + self._name = f"Plex ({plex_server.friendly_name})" self._unique_id = f"sensor-{plex_server.machine_identifier}" @property diff --git a/homeassistant/components/plex/server.py b/homeassistant/components/plex/server.py index 962e074996f..6ab11430766 100644 --- a/homeassistant/components/plex/server.py +++ b/homeassistant/components/plex/server.py @@ -1,27 +1,44 @@ """Shared class to maintain Plex server instances.""" -import logging - import plexapi.myplex +import plexapi.playqueue import plexapi.server from requests import Session +from homeassistant.components.media_player import DOMAIN as MP_DOMAIN from homeassistant.const import CONF_TOKEN, CONF_URL, CONF_VERIFY_SSL -from .const import CONF_SERVER, DEFAULT_VERIFY_SSL +from .const import ( + CONF_SERVER, + CONF_SHOW_ALL_CONTROLS, + CONF_USE_EPISODE_ART, + DEFAULT_VERIFY_SSL, + X_PLEX_DEVICE_NAME, + X_PLEX_PLATFORM, + X_PLEX_PRODUCT, + X_PLEX_VERSION, +) +from .errors import NoServersFound, ServerNotSpecified -_LOGGER = logging.getLogger(__package__) +# Set default headers sent by plexapi +plexapi.X_PLEX_DEVICE_NAME = X_PLEX_DEVICE_NAME +plexapi.X_PLEX_PLATFORM = X_PLEX_PLATFORM +plexapi.X_PLEX_PRODUCT = X_PLEX_PRODUCT +plexapi.X_PLEX_VERSION = X_PLEX_VERSION +plexapi.myplex.BASE_HEADERS = plexapi.reset_base_headers() +plexapi.server.BASE_HEADERS = plexapi.reset_base_headers() class PlexServer: """Manages a single Plex server connection.""" - def __init__(self, server_config): + def __init__(self, server_config, options=None): """Initialize a Plex server instance.""" self._plex_server = None self._url = server_config.get(CONF_URL) self._token = server_config.get(CONF_TOKEN) self._server_name = server_config.get(CONF_SERVER) self._verify_ssl = server_config.get(CONF_VERIFY_SSL, DEFAULT_VERIFY_SSL) + self.options = options def connect(self): """Connect to a Plex server directly, obtaining direct URL if necessary.""" @@ -29,10 +46,18 @@ class PlexServer: def _set_missing_url(): account = plexapi.myplex.MyPlexAccount(token=self._token) available_servers = [ - x.name for x in account.resources() if "server" in x.provides + (x.name, x.clientIdentifier) + for x in account.resources() + if "server" in x.provides ] + + if not available_servers: + raise NoServersFound + if not self._server_name and len(available_servers) > 1: + raise ServerNotSpecified(available_servers) + server_choice = ( - self._server_name if self._server_name else available_servers[0] + self._server_name if self._server_name else available_servers[0][0] ) connections = account.resource(server_choice).connections local_url = [x.httpuri for x in connections if x.local] @@ -47,7 +72,6 @@ class PlexServer: self._plex_server = plexapi.server.PlexServer( self._url, self._token, session ) - _LOGGER.debug("Connected to: %s (%s)", self.friendly_name, self.url_in_use) if self._token and not self._url: _set_missing_url() @@ -76,3 +100,26 @@ class PlexServer: def url_in_use(self): """Return URL used for connected Plex server.""" return self._plex_server._baseurl # pylint: disable=W0212 + + @property + def use_episode_art(self): + """Return use_episode_art option.""" + return self.options[MP_DOMAIN][CONF_USE_EPISODE_ART] + + @property + def show_all_controls(self): + """Return show_all_controls option.""" + return self.options[MP_DOMAIN][CONF_SHOW_ALL_CONTROLS] + + @property + def library(self): + """Return library attribute from server object.""" + return self._plex_server.library + + def playlist(self, title): + """Return playlist from server object.""" + return self._plex_server.playlist(title) + + def create_playqueue(self, media, **kwargs): + """Create playqueue on Plex server.""" + return plexapi.playqueue.PlayQueue.create(self._plex_server, media, **kwargs) diff --git a/homeassistant/components/plex/strings.json b/homeassistant/components/plex/strings.json new file mode 100644 index 00000000000..aff79acc2ed --- /dev/null +++ b/homeassistant/components/plex/strings.json @@ -0,0 +1,43 @@ +{ + "config": { + "title": "Plex", + "step": { + "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." + } + }, + "error": { + "faulty_credentials": "Authorization failed", + "no_servers": "No servers linked to account", + "not_found": "Plex server not found" + }, + "abort": { + "all_configured": "All linked servers already configured", + "already_configured": "This Plex server is already configured", + "already_in_progress": "Plex is being configured", + "discovery_no_file": "No legacy config file found", + "invalid_import": "Imported configuration is invalid", + "token_request_timeout": "Timed out obtaining token", + "unknown": "Failed for unknown reason" + } + }, + "options": { + "step": { + "plex_mp_settings": { + "description": "Options for Plex Media Players", + "data": { + "use_episode_art": "Use episode art", + "show_all_controls": "Show all controls" + } + } + } + } +} diff --git a/homeassistant/components/plugwise/manifest.json b/homeassistant/components/plugwise/manifest.json index c399232f315..1069c0bcdf0 100644 --- a/homeassistant/components/plugwise/manifest.json +++ b/homeassistant/components/plugwise/manifest.json @@ -1,7 +1,7 @@ { "domain": "plugwise", "name": "Plugwise", - "documentation": "https://www.home-assistant.io/components/plugwise", + "documentation": "https://www.home-assistant.io/integrations/plugwise", "dependencies": [], "codeowners": ["@laetificat","@CoMPaTech"], "requirements": ["haanna==0.10.1"] diff --git a/homeassistant/components/plum_lightpad/manifest.json b/homeassistant/components/plum_lightpad/manifest.json index 389eca09c42..8c2da090269 100644 --- a/homeassistant/components/plum_lightpad/manifest.json +++ b/homeassistant/components/plum_lightpad/manifest.json @@ -1,7 +1,7 @@ { "domain": "plum_lightpad", "name": "Plum lightpad", - "documentation": "https://www.home-assistant.io/components/plum_lightpad", + "documentation": "https://www.home-assistant.io/integrations/plum_lightpad", "requirements": [ "plumlightpad==0.0.11" ], diff --git a/homeassistant/components/pocketcasts/manifest.json b/homeassistant/components/pocketcasts/manifest.json index 11c20236324..f72984f9fc0 100644 --- a/homeassistant/components/pocketcasts/manifest.json +++ b/homeassistant/components/pocketcasts/manifest.json @@ -1,7 +1,7 @@ { "domain": "pocketcasts", "name": "Pocketcasts", - "documentation": "https://www.home-assistant.io/components/pocketcasts", + "documentation": "https://www.home-assistant.io/integrations/pocketcasts", "requirements": [ "pocketcasts==0.1" ], diff --git a/homeassistant/components/point/.translations/nn.json b/homeassistant/components/point/.translations/nn.json new file mode 100644 index 00000000000..865155c0494 --- /dev/null +++ b/homeassistant/components/point/.translations/nn.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Minut Point" + } +} \ No newline at end of file diff --git a/homeassistant/components/point/.translations/no.json b/homeassistant/components/point/.translations/no.json index 58b6e1e63fd..c87c1a702c8 100644 --- a/homeassistant/components/point/.translations/no.json +++ b/homeassistant/components/point/.translations/no.json @@ -8,11 +8,11 @@ "no_flows": "Du m\u00e5 konfigurere Point f\u00f8r du kan autentisere med den. [Vennligst les instruksjonene](https://www.home-assistant.io/components/point/)." }, "create_entry": { - "default": "Vellykket godkjenning med Minut for din(e) Point enhet(er)" + "default": "Vellykket autentisering med Minut for din(e) Point enhet(er)" }, "error": { - "follow_link": "Vennligst f\u00f8lg lenken og godkjen f\u00f8r du trykker p\u00e5 Send", - "no_token": "Ikke godkjent med Minut" + "follow_link": "Vennligst f\u00f8lg lenken og autentiser f\u00f8r du trykker p\u00e5 Send", + "no_token": "Ikke autentisert med Minut" }, "step": { "auth": { diff --git a/homeassistant/components/point/.translations/zh-Hant.json b/homeassistant/components/point/.translations/zh-Hant.json index 9f688b2e5f9..f1bbb1c872c 100644 --- a/homeassistant/components/point/.translations/zh-Hant.json +++ b/homeassistant/components/point/.translations/zh-Hant.json @@ -8,7 +8,7 @@ "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" }, "create_entry": { - "default": "\u5df2\u6210\u529f\u8a8d\u8b49 Minut Point \u88dd\u7f6e\u3002" + "default": "\u5df2\u6210\u529f\u8a8d\u8b49 Minut Point \u8a2d\u5099\u3002" }, "error": { "follow_link": "\u8acb\u65bc\u50b3\u9001\u524d\uff0c\u5148\u4f7f\u7528\u9023\u7d50\u4e26\u9032\u884c\u8a8d\u8b49\u3002", diff --git a/homeassistant/components/point/manifest.json b/homeassistant/components/point/manifest.json index fcc9265ce9b..0f2faef86df 100644 --- a/homeassistant/components/point/manifest.json +++ b/homeassistant/components/point/manifest.json @@ -2,7 +2,7 @@ "domain": "point", "name": "Point", "config_flow": true, - "documentation": "https://www.home-assistant.io/components/point", + "documentation": "https://www.home-assistant.io/integrations/point", "requirements": [ "pypoint==1.1.1" ], diff --git a/homeassistant/components/postnl/manifest.json b/homeassistant/components/postnl/manifest.json index 9746cb168aa..d07f9746ee8 100644 --- a/homeassistant/components/postnl/manifest.json +++ b/homeassistant/components/postnl/manifest.json @@ -1,7 +1,7 @@ { "domain": "postnl", "name": "Postnl", - "documentation": "https://www.home-assistant.io/components/postnl", + "documentation": "https://www.home-assistant.io/integrations/postnl", "requirements": [ "postnl_api==1.0.2" ], diff --git a/homeassistant/components/prezzibenzina/manifest.json b/homeassistant/components/prezzibenzina/manifest.json index 2427ebbfdb0..99a8a1e2701 100644 --- a/homeassistant/components/prezzibenzina/manifest.json +++ b/homeassistant/components/prezzibenzina/manifest.json @@ -1,7 +1,7 @@ { "domain": "prezzibenzina", "name": "Prezzibenzina", - "documentation": "https://www.home-assistant.io/components/prezzibenzina", + "documentation": "https://www.home-assistant.io/integrations/prezzibenzina", "requirements": [ "prezzibenzina-py==1.1.4" ], diff --git a/homeassistant/components/proliphix/manifest.json b/homeassistant/components/proliphix/manifest.json index 3aa356823c1..a035657a090 100644 --- a/homeassistant/components/proliphix/manifest.json +++ b/homeassistant/components/proliphix/manifest.json @@ -1,7 +1,7 @@ { "domain": "proliphix", "name": "Proliphix", - "documentation": "https://www.home-assistant.io/components/proliphix", + "documentation": "https://www.home-assistant.io/integrations/proliphix", "requirements": [ "proliphix==0.4.1" ], diff --git a/homeassistant/components/prometheus/manifest.json b/homeassistant/components/prometheus/manifest.json index cab1228aa56..309284e5f62 100644 --- a/homeassistant/components/prometheus/manifest.json +++ b/homeassistant/components/prometheus/manifest.json @@ -1,7 +1,7 @@ { "domain": "prometheus", "name": "Prometheus", - "documentation": "https://www.home-assistant.io/components/prometheus", + "documentation": "https://www.home-assistant.io/integrations/prometheus", "requirements": [ "prometheus_client==0.7.1" ], diff --git a/homeassistant/components/prowl/manifest.json b/homeassistant/components/prowl/manifest.json index a8b4893c995..73aa818a2ad 100644 --- a/homeassistant/components/prowl/manifest.json +++ b/homeassistant/components/prowl/manifest.json @@ -1,7 +1,7 @@ { "domain": "prowl", "name": "Prowl", - "documentation": "https://www.home-assistant.io/components/prowl", + "documentation": "https://www.home-assistant.io/integrations/prowl", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/proximity/manifest.json b/homeassistant/components/proximity/manifest.json index 335bea82fc9..708a3fb2616 100644 --- a/homeassistant/components/proximity/manifest.json +++ b/homeassistant/components/proximity/manifest.json @@ -1,7 +1,7 @@ { "domain": "proximity", "name": "Proximity", - "documentation": "https://www.home-assistant.io/components/proximity", + "documentation": "https://www.home-assistant.io/integrations/proximity", "requirements": [], "dependencies": [ "device_tracker", diff --git a/homeassistant/components/proxy/camera.py b/homeassistant/components/proxy/camera.py index 53a4f620dcc..b1ce8ad7ac0 100644 --- a/homeassistant/components/proxy/camera.py +++ b/homeassistant/components/proxy/camera.py @@ -9,7 +9,6 @@ from homeassistant.components.camera import PLATFORM_SCHEMA, Camera from homeassistant.const import CONF_ENTITY_ID, CONF_NAME, CONF_MODE from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_validation as cv -from homeassistant.util.async_ import run_coroutine_threadsafe import homeassistant.util.dt as dt_util _LOGGER = logging.getLogger(__name__) @@ -220,7 +219,7 @@ class ProxyCamera(Camera): def camera_image(self): """Return camera image.""" - return run_coroutine_threadsafe( + return asyncio.run_coroutine_threadsafe( self.async_camera_image(), self.hass.loop ).result() diff --git a/homeassistant/components/proxy/manifest.json b/homeassistant/components/proxy/manifest.json index 54d5ebe5f14..c67fd4afc09 100644 --- a/homeassistant/components/proxy/manifest.json +++ b/homeassistant/components/proxy/manifest.json @@ -1,7 +1,7 @@ { "domain": "proxy", "name": "Proxy", - "documentation": "https://www.home-assistant.io/components/proxy", + "documentation": "https://www.home-assistant.io/integrations/proxy", "requirements": [ "pillow==6.1.0" ], diff --git a/homeassistant/components/ps4/.translations/de.json b/homeassistant/components/ps4/.translations/de.json index 6d152108117..2053d2f4a80 100644 --- a/homeassistant/components/ps4/.translations/de.json +++ b/homeassistant/components/ps4/.translations/de.json @@ -5,7 +5,7 @@ "devices_configured": "Alle gefundenen Ger\u00e4te sind bereits konfiguriert.", "no_devices_found": "Es wurden keine PlayStation 4 im Netzwerk gefunden.", "port_987_bind_error": "Bind to Port 987 nicht m\u00f6glich.", - "port_997_bind_error": "Bind to Port 997 nicht m\u00f6glich. Weitere Informationen finden Sie in der [documentation](https://www.home-assistant.io/components/ps4/)" + "port_997_bind_error": "Bind to Port 997 nicht m\u00f6glich. Weitere Informationen finden Sie in der [Dokumentation](https://www.home-assistant.io/components/ps4/)" }, "error": { "credential_timeout": "Zeit\u00fcberschreitung beim Warten auf den Anmeldedienst. Klicken zum Neustarten auf Senden.", diff --git a/homeassistant/components/ps4/.translations/nn.json b/homeassistant/components/ps4/.translations/nn.json index b3302389c88..86920906003 100644 --- a/homeassistant/components/ps4/.translations/nn.json +++ b/homeassistant/components/ps4/.translations/nn.json @@ -5,9 +5,20 @@ "port_997_bind_error": "Kunne ikkje binde til port 997. Sj\u00e5 [dokumentasjonen] (https://www.home-assistant.io/components/ps4/) for ytterlegare informasjon." }, "step": { + "creds": { + "title": "Playstation 4" + }, + "link": { + "data": { + "code": "PIN", + "name": "Namn" + }, + "title": "Playstation 4" + }, "mode": { "title": "Playstation 4" } - } + }, + "title": "Playstation 4" } } \ No newline at end of file diff --git a/homeassistant/components/ps4/.translations/zh-Hant.json b/homeassistant/components/ps4/.translations/zh-Hant.json index f0a71b4be5b..a786b0c74d3 100644 --- a/homeassistant/components/ps4/.translations/zh-Hant.json +++ b/homeassistant/components/ps4/.translations/zh-Hant.json @@ -2,7 +2,7 @@ "config": { "abort": { "credential_error": "\u53d6\u5f97\u6191\u8b49\u932f\u8aa4\u3002", - "devices_configured": "\u6240\u6709\u88dd\u7f6e\u90fd\u5df2\u8a2d\u5b9a\u5b8c\u6210\u3002", + "devices_configured": "\u6240\u6709\u8a2d\u5099\u90fd\u5df2\u8a2d\u5b9a\u5b8c\u6210\u3002", "no_devices_found": "\u5728\u7db2\u8def\u4e0a\u627e\u4e0d\u5230 PlayStation 4 \u88dd\u7f6e\u3002", "port_987_bind_error": "\u7121\u6cd5\u7d81\u5b9a\u901a\u8a0a\u57e0 987\u3002\u8acb\u53c3\u8003 [documentation](https://www.home-assistant.io/components/ps4/) \u4ee5\u7372\u5f97\u66f4\u591a\u8cc7\u8a0a\u3002", "port_997_bind_error": "\u7121\u6cd5\u7d81\u5b9a\u901a\u8a0a\u57e0 997\u3002\u8acb\u53c3\u8003 [documentation](https://www.home-assistant.io/components/ps4/) \u4ee5\u7372\u5f97\u66f4\u591a\u8cc7\u8a0a\u3002" @@ -15,7 +15,7 @@ }, "step": { "creds": { - "description": "\u9700\u8981\u6191\u8b49\u3002\u6309\u4e0b\u300c\u50b3\u9001\u300d\u5f8c\u3001\u65bc PS4 \u7b2c\u4e8c\u756b\u9762 App\uff0c\u66f4\u65b0\u88dd\u7f6e\u4e26\u9078\u64c7\u300cHome-Assistant\u300d\u4ee5\u7e7c\u7e8c\u3002", + "description": "\u9700\u8981\u6191\u8b49\u3002\u6309\u4e0b\u300c\u50b3\u9001\u300d\u5f8c\u3001\u65bc PS4 \u7b2c\u4e8c\u756b\u9762 App\uff0c\u66f4\u65b0\u8a2d\u5099\u4e26\u9078\u64c7\u300cHome-Assistant\u300d\u4ee5\u7e7c\u7e8c\u3002", "title": "PlayStation 4" }, "link": { @@ -25,7 +25,7 @@ "name": "\u540d\u7a31", "region": "\u5340\u57df" }, - "description": "\u8f38\u5165\u60a8\u7684 PlayStation 4 \u8cc7\u8a0a\uff0c\u300cPIN\u300d\u65bc PlayStation 4 \u4e3b\u6a5f\u7684\u300c\u8a2d\u5b9a\u300d\u5167\uff0c\u4e26\u65bc\u300c\u884c\u52d5\u7a0b\u5f0f\u9023\u7dda\u8a2d\u5b9a\uff08Mobile App Connection Settings\uff09\u300d\u4e2d\u9078\u64c7\u300c\u65b0\u589e\u88dd\u7f6e\u300d\u3002\u8f38\u5165\u6240\u986f\u793a\u7684 PIN \u78bc\u3002\u8acb\u53c3\u8003 [documentation](https://www.home-assistant.io/components/ps4/) \u4ee5\u7372\u5f97\u66f4\u591a\u8cc7\u8a0a\u3002", + "description": "\u8f38\u5165\u60a8\u7684 PlayStation 4 \u8cc7\u8a0a\uff0c\u300cPIN\u300d\u65bc PlayStation 4 \u4e3b\u6a5f\u7684\u300c\u8a2d\u5b9a\u300d\u5167\uff0c\u4e26\u65bc\u300c\u884c\u52d5\u7a0b\u5f0f\u9023\u7dda\u8a2d\u5b9a\uff08Mobile App Connection Settings\uff09\u300d\u4e2d\u9078\u64c7\u300c\u65b0\u589e\u8a2d\u5099\u300d\u3002\u8f38\u5165\u6240\u986f\u793a\u7684 PIN \u78bc\u3002\u8acb\u53c3\u8003 [documentation](https://www.home-assistant.io/components/ps4/) \u4ee5\u7372\u5f97\u66f4\u591a\u8cc7\u8a0a\u3002", "title": "PlayStation 4" }, "mode": { @@ -33,7 +33,7 @@ "ip_address": "IP \u4f4d\u5740\uff08\u5982\u679c\u4f7f\u7528\u81ea\u52d5\u63a2\u7d22\u65b9\u5f0f\uff0c\u8acb\u4fdd\u7559\u7a7a\u767d\uff09\u3002", "mode": "\u8a2d\u5b9a\u6a21\u5f0f" }, - "description": "\u9078\u64c7\u6a21\u5f0f\u4ee5\u9032\u884c\u8a2d\u5b9a\u3002\u5047\u5982\u9078\u64c7\u81ea\u52d5\u63a2\u7d22\u6a21\u5f0f\u7684\u8a71\uff0c\u7531\u65bc\u6703\u81ea\u52d5\u9032\u884c\u88dd\u7f6e\u641c\u5c0b\uff0cIP \u4f4d\u5740\u53ef\u4fdd\u7559\u70ba\u7a7a\u767d\u3002", + "description": "\u9078\u64c7\u6a21\u5f0f\u4ee5\u9032\u884c\u8a2d\u5b9a\u3002\u5047\u5982\u9078\u64c7\u81ea\u52d5\u63a2\u7d22\u6a21\u5f0f\u7684\u8a71\uff0c\u7531\u65bc\u6703\u81ea\u52d5\u9032\u884c\u8a2d\u5099\u641c\u5c0b\uff0cIP \u4f4d\u5740\u53ef\u4fdd\u7559\u70ba\u7a7a\u767d\u3002", "title": "PlayStation 4" } }, diff --git a/homeassistant/components/ps4/manifest.json b/homeassistant/components/ps4/manifest.json index 797a5432f97..98a14d877e8 100644 --- a/homeassistant/components/ps4/manifest.json +++ b/homeassistant/components/ps4/manifest.json @@ -2,7 +2,7 @@ "domain": "ps4", "name": "Ps4", "config_flow": true, - "documentation": "https://www.home-assistant.io/components/ps4", + "documentation": "https://www.home-assistant.io/integrations/ps4", "requirements": [ "pyps4-homeassistant==0.8.7" ], diff --git a/homeassistant/components/ptvsd/manifest.json b/homeassistant/components/ptvsd/manifest.json index 8bd46c3dc32..2f8398531c7 100644 --- a/homeassistant/components/ptvsd/manifest.json +++ b/homeassistant/components/ptvsd/manifest.json @@ -1,7 +1,7 @@ { "domain": "ptvsd", "name": "ptvsd", - "documentation": "https://www.home-assistant.io/components/ptvsd", + "documentation": "https://www.home-assistant.io/integrations/ptvsd", "requirements": [ "ptvsd==4.2.8" ], diff --git a/homeassistant/components/pulseaudio_loopback/manifest.json b/homeassistant/components/pulseaudio_loopback/manifest.json index 58a2871e027..19247e1a7d5 100644 --- a/homeassistant/components/pulseaudio_loopback/manifest.json +++ b/homeassistant/components/pulseaudio_loopback/manifest.json @@ -1,7 +1,7 @@ { "domain": "pulseaudio_loopback", "name": "Pulseaudio loopback", - "documentation": "https://www.home-assistant.io/components/pulseaudio_loopback", + "documentation": "https://www.home-assistant.io/integrations/pulseaudio_loopback", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/push/manifest.json b/homeassistant/components/push/manifest.json index 278638caff8..161ea29b3b3 100644 --- a/homeassistant/components/push/manifest.json +++ b/homeassistant/components/push/manifest.json @@ -1,7 +1,7 @@ { "domain": "push", "name": "Push", - "documentation": "https://www.home-assistant.io/components/push", + "documentation": "https://www.home-assistant.io/integrations/push", "requirements": [], "dependencies": ["webhook"], "codeowners": [ diff --git a/homeassistant/components/pushbullet/manifest.json b/homeassistant/components/pushbullet/manifest.json index 51e77959d7a..f649e5467a4 100644 --- a/homeassistant/components/pushbullet/manifest.json +++ b/homeassistant/components/pushbullet/manifest.json @@ -1,7 +1,7 @@ { "domain": "pushbullet", "name": "Pushbullet", - "documentation": "https://www.home-assistant.io/components/pushbullet", + "documentation": "https://www.home-assistant.io/integrations/pushbullet", "requirements": [ "pushbullet.py==0.11.0" ], diff --git a/homeassistant/components/pushetta/manifest.json b/homeassistant/components/pushetta/manifest.json index b42180c7268..3000f9fd17b 100644 --- a/homeassistant/components/pushetta/manifest.json +++ b/homeassistant/components/pushetta/manifest.json @@ -1,7 +1,7 @@ { "domain": "pushetta", "name": "Pushetta", - "documentation": "https://www.home-assistant.io/components/pushetta", + "documentation": "https://www.home-assistant.io/integrations/pushetta", "requirements": [ "pushetta==1.0.15" ], diff --git a/homeassistant/components/pushover/manifest.json b/homeassistant/components/pushover/manifest.json index 1cdbb4ff48b..01c3fc270fb 100644 --- a/homeassistant/components/pushover/manifest.json +++ b/homeassistant/components/pushover/manifest.json @@ -1,7 +1,7 @@ { "domain": "pushover", "name": "Pushover", - "documentation": "https://www.home-assistant.io/components/pushover", + "documentation": "https://www.home-assistant.io/integrations/pushover", "requirements": [ "python-pushover==0.4" ], diff --git a/homeassistant/components/pushsafer/manifest.json b/homeassistant/components/pushsafer/manifest.json index 300d0ead4a5..18592124c24 100644 --- a/homeassistant/components/pushsafer/manifest.json +++ b/homeassistant/components/pushsafer/manifest.json @@ -1,7 +1,7 @@ { "domain": "pushsafer", "name": "Pushsafer", - "documentation": "https://www.home-assistant.io/components/pushsafer", + "documentation": "https://www.home-assistant.io/integrations/pushsafer", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/pvoutput/manifest.json b/homeassistant/components/pvoutput/manifest.json index b61c7100828..7e6879de9a1 100644 --- a/homeassistant/components/pvoutput/manifest.json +++ b/homeassistant/components/pvoutput/manifest.json @@ -1,7 +1,7 @@ { "domain": "pvoutput", "name": "Pvoutput", - "documentation": "https://www.home-assistant.io/components/pvoutput", + "documentation": "https://www.home-assistant.io/integrations/pvoutput", "requirements": [], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/pyload/manifest.json b/homeassistant/components/pyload/manifest.json index 437bd3bc4d2..4cbcd8321b5 100644 --- a/homeassistant/components/pyload/manifest.json +++ b/homeassistant/components/pyload/manifest.json @@ -1,7 +1,7 @@ { "domain": "pyload", "name": "Pyload", - "documentation": "https://www.home-assistant.io/components/pyload", + "documentation": "https://www.home-assistant.io/integrations/pyload", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/python_script/manifest.json b/homeassistant/components/python_script/manifest.json index 610ec92a2b3..2e5c1208210 100644 --- a/homeassistant/components/python_script/manifest.json +++ b/homeassistant/components/python_script/manifest.json @@ -1,10 +1,10 @@ { "domain": "python_script", "name": "Python script", - "documentation": "https://www.home-assistant.io/components/python_script", + "documentation": "https://www.home-assistant.io/integrations/python_script", "requirements": [ - "restrictedpython==4.0" + "restrictedpython==5.0" ], "dependencies": [], "codeowners": [] -} +} \ No newline at end of file diff --git a/homeassistant/components/qbittorrent/manifest.json b/homeassistant/components/qbittorrent/manifest.json index 5fb850739d8..c41d4ba46d3 100644 --- a/homeassistant/components/qbittorrent/manifest.json +++ b/homeassistant/components/qbittorrent/manifest.json @@ -1,7 +1,7 @@ { "domain": "qbittorrent", "name": "Qbittorrent", - "documentation": "https://www.home-assistant.io/components/qbittorrent", + "documentation": "https://www.home-assistant.io/integrations/qbittorrent", "requirements": [ "python-qbittorrent==0.3.1" ], diff --git a/homeassistant/components/qld_bushfire/manifest.json b/homeassistant/components/qld_bushfire/manifest.json index 47a4a4b5f85..c113e6034cd 100644 --- a/homeassistant/components/qld_bushfire/manifest.json +++ b/homeassistant/components/qld_bushfire/manifest.json @@ -1,7 +1,7 @@ { "domain": "qld_bushfire", "name": "Queensland Bushfire Alert", - "documentation": "https://www.home-assistant.io/components/qld_bushfire", + "documentation": "https://www.home-assistant.io/integrations/qld_bushfire", "requirements": [ "georss_qld_bushfire_alert_client==0.3" ], diff --git a/homeassistant/components/qnap/manifest.json b/homeassistant/components/qnap/manifest.json index f02d416c7e6..a34c49645ce 100644 --- a/homeassistant/components/qnap/manifest.json +++ b/homeassistant/components/qnap/manifest.json @@ -1,7 +1,7 @@ { "domain": "qnap", "name": "Qnap", - "documentation": "https://www.home-assistant.io/components/qnap", + "documentation": "https://www.home-assistant.io/integrations/qnap", "requirements": [ "qnapstats==0.2.7" ], diff --git a/homeassistant/components/qrcode/manifest.json b/homeassistant/components/qrcode/manifest.json index eb8da25bace..87e16f62987 100644 --- a/homeassistant/components/qrcode/manifest.json +++ b/homeassistant/components/qrcode/manifest.json @@ -1,7 +1,7 @@ { "domain": "qrcode", "name": "Qrcode", - "documentation": "https://www.home-assistant.io/components/qrcode", + "documentation": "https://www.home-assistant.io/integrations/qrcode", "requirements": [ "pillow==6.1.0", "pyzbar==0.1.7" diff --git a/homeassistant/components/quantum_gateway/manifest.json b/homeassistant/components/quantum_gateway/manifest.json index 9c062482a4c..da2fc20510f 100644 --- a/homeassistant/components/quantum_gateway/manifest.json +++ b/homeassistant/components/quantum_gateway/manifest.json @@ -1,7 +1,7 @@ { "domain": "quantum_gateway", "name": "Quantum gateway", - "documentation": "https://www.home-assistant.io/components/quantum_gateway", + "documentation": "https://www.home-assistant.io/integrations/quantum_gateway", "requirements": [ "quantum-gateway==0.0.5" ], diff --git a/homeassistant/components/qwikswitch/manifest.json b/homeassistant/components/qwikswitch/manifest.json index 4907cb462b6..6d2944282ba 100644 --- a/homeassistant/components/qwikswitch/manifest.json +++ b/homeassistant/components/qwikswitch/manifest.json @@ -1,7 +1,7 @@ { "domain": "qwikswitch", "name": "Qwikswitch", - "documentation": "https://www.home-assistant.io/components/qwikswitch", + "documentation": "https://www.home-assistant.io/integrations/qwikswitch", "requirements": [ "pyqwikswitch==0.93" ], diff --git a/homeassistant/components/rachio/manifest.json b/homeassistant/components/rachio/manifest.json index 30bde9a297d..79e3677d65e 100644 --- a/homeassistant/components/rachio/manifest.json +++ b/homeassistant/components/rachio/manifest.json @@ -1,7 +1,7 @@ { "domain": "rachio", "name": "Rachio", - "documentation": "https://www.home-assistant.io/components/rachio", + "documentation": "https://www.home-assistant.io/integrations/rachio", "requirements": [ "rachiopy==0.1.3" ], diff --git a/homeassistant/components/radarr/manifest.json b/homeassistant/components/radarr/manifest.json index f12fcf4220c..2683525e7b4 100644 --- a/homeassistant/components/radarr/manifest.json +++ b/homeassistant/components/radarr/manifest.json @@ -1,7 +1,7 @@ { "domain": "radarr", "name": "Radarr", - "documentation": "https://www.home-assistant.io/components/radarr", + "documentation": "https://www.home-assistant.io/integrations/radarr", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/radiotherm/manifest.json b/homeassistant/components/radiotherm/manifest.json index 002fdb63273..c3f8079cccd 100644 --- a/homeassistant/components/radiotherm/manifest.json +++ b/homeassistant/components/radiotherm/manifest.json @@ -1,7 +1,7 @@ { "domain": "radiotherm", "name": "Radiotherm", - "documentation": "https://www.home-assistant.io/components/radiotherm", + "documentation": "https://www.home-assistant.io/integrations/radiotherm", "requirements": [ "radiotherm==2.0.0" ], diff --git a/homeassistant/components/rainbird/__init__.py b/homeassistant/components/rainbird/__init__.py index 1d8ed8e37b1..0b51be1f258 100644 --- a/homeassistant/components/rainbird/__init__.py +++ b/homeassistant/components/rainbird/__init__.py @@ -1,42 +1,91 @@ """Support for Rain Bird Irrigation system LNK WiFi Module.""" import logging +from pyrainbird import RainbirdController import voluptuous as vol +from homeassistant.components import binary_sensor, sensor, switch +from homeassistant.const import ( + CONF_FRIENDLY_NAME, + CONF_HOST, + CONF_PASSWORD, + CONF_TRIGGER_TIME, +) +from homeassistant.helpers import discovery import homeassistant.helpers.config_validation as cv -from homeassistant.const import CONF_HOST, CONF_PASSWORD + +CONF_ZONES = "zones" + +SUPPORTED_PLATFORMS = [switch.DOMAIN, sensor.DOMAIN, binary_sensor.DOMAIN] _LOGGER = logging.getLogger(__name__) +RAINBIRD_CONTROLLER = "controller" DATA_RAINBIRD = "rainbird" DOMAIN = "rainbird" -CONFIG_SCHEMA = vol.Schema( +SENSOR_TYPE_RAINDELAY = "raindelay" +SENSOR_TYPE_RAINSENSOR = "rainsensor" +# sensor_type [ description, unit, icon ] +SENSOR_TYPES = { + SENSOR_TYPE_RAINSENSOR: ["Rainsensor", None, "mdi:water"], + SENSOR_TYPE_RAINDELAY: ["Raindelay", None, "mdi:water-off"], +} + +TRIGGER_TIME_SCHEMA = vol.All( + cv.time_period, cv.positive_timedelta, lambda td: (td.total_seconds() // 60) +) + +ZONE_SCHEMA = vol.Schema( { - DOMAIN: vol.Schema( - {vol.Required(CONF_HOST): cv.string, vol.Required(CONF_PASSWORD): cv.string} - ) - }, + vol.Optional(CONF_FRIENDLY_NAME): cv.string, + vol.Optional(CONF_TRIGGER_TIME): TRIGGER_TIME_SCHEMA, + } +) +CONTROLLER_SCHEMA = vol.Schema( + { + vol.Required(CONF_HOST): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Required(CONF_TRIGGER_TIME): TRIGGER_TIME_SCHEMA, + vol.Optional(CONF_ZONES): vol.Schema({cv.positive_int: ZONE_SCHEMA}), + } +) +CONFIG_SCHEMA = vol.Schema( + {DOMAIN: vol.Schema(vol.All(cv.ensure_list, [CONTROLLER_SCHEMA]))}, extra=vol.ALLOW_EXTRA, ) def setup(hass, config): """Set up the Rain Bird component.""" - conf = config[DOMAIN] - server = conf.get(CONF_HOST) - password = conf.get(CONF_PASSWORD) - from pyrainbird import RainbirdController + hass.data[DATA_RAINBIRD] = [] + success = False + for controller_config in config[DOMAIN]: + success = success or _setup_controller(hass, controller_config, config) + return success + + +def _setup_controller(hass, controller_config, config): + """Set up a controller.""" + server = controller_config[CONF_HOST] + password = controller_config[CONF_PASSWORD] controller = RainbirdController(server, password) - - _LOGGER.debug("Rain Bird Controller set to: %s", server) - - initial_status = controller.currentIrrigation() - if initial_status and initial_status["type"] != "CurrentStationsActiveResponse": - _LOGGER.error("Error getting state. Possible configuration issues") + position = len(hass.data[DATA_RAINBIRD]) + try: + controller.get_serial_number() + except Exception as exc: # pylint: disable=W0703 + _LOGGER.error("Unable to setup controller: %s", exc) return False - - hass.data[DATA_RAINBIRD] = controller + hass.data[DATA_RAINBIRD].append(controller) + _LOGGER.debug("Rain Bird Controller %d set to: %s", position, server) + for platform in SUPPORTED_PLATFORMS: + discovery.load_platform( + hass, + platform, + DOMAIN, + {RAINBIRD_CONTROLLER: position, **controller_config}, + config, + ) return True diff --git a/homeassistant/components/rainbird/binary_sensor.py b/homeassistant/components/rainbird/binary_sensor.py new file mode 100644 index 00000000000..51c5f7a9dbe --- /dev/null +++ b/homeassistant/components/rainbird/binary_sensor.py @@ -0,0 +1,64 @@ +"""Support for Rain Bird Irrigation system LNK WiFi Module.""" +import logging + +from pyrainbird import RainbirdController + +from homeassistant.components.binary_sensor import BinarySensorDevice + +from . import ( + DATA_RAINBIRD, + RAINBIRD_CONTROLLER, + SENSOR_TYPE_RAINDELAY, + SENSOR_TYPE_RAINSENSOR, + SENSOR_TYPES, +) + +_LOGGER = logging.getLogger(__name__) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up a Rain Bird sensor.""" + if discovery_info is None: + return + + controller = hass.data[DATA_RAINBIRD][discovery_info[RAINBIRD_CONTROLLER]] + add_entities( + [RainBirdSensor(controller, sensor_type) for sensor_type in SENSOR_TYPES], True + ) + + +class RainBirdSensor(BinarySensorDevice): + """A sensor implementation for Rain Bird device.""" + + def __init__(self, controller: RainbirdController, sensor_type): + """Initialize the Rain Bird sensor.""" + self._sensor_type = sensor_type + self._controller = controller + self._name = SENSOR_TYPES[self._sensor_type][0] + self._icon = SENSOR_TYPES[self._sensor_type][2] + self._state = None + + @property + def is_on(self): + """Return true if the binary sensor is on.""" + return None if self._state is None else bool(self._state) + + def update(self): + """Get the latest data and updates the states.""" + _LOGGER.debug("Updating sensor: %s", self._name) + state = None + if self._sensor_type == SENSOR_TYPE_RAINSENSOR: + state = self._controller.get_rain_sensor_state() + elif self._sensor_type == SENSOR_TYPE_RAINDELAY: + state = self._controller.get_rain_delay() + self._state = None if state is None else bool(state) + + @property + def name(self): + """Return the name of this camera.""" + return self._name + + @property + def icon(self): + """Return icon.""" + return self._icon diff --git a/homeassistant/components/rainbird/manifest.json b/homeassistant/components/rainbird/manifest.json index 584ea22afe2..bb421c725ca 100644 --- a/homeassistant/components/rainbird/manifest.json +++ b/homeassistant/components/rainbird/manifest.json @@ -1,10 +1,12 @@ { "domain": "rainbird", "name": "Rainbird", - "documentation": "https://www.home-assistant.io/components/rainbird", + "documentation": "https://www.home-assistant.io/integrations/rainbird", "requirements": [ - "pyrainbird==0.2.1" + "pyrainbird==0.4.1" ], "dependencies": [], - "codeowners": [] + "codeowners": [ + "@konikvranik" + ] } diff --git a/homeassistant/components/rainbird/sensor.py b/homeassistant/components/rainbird/sensor.py index 2d4549a21d5..501566de682 100644 --- a/homeassistant/components/rainbird/sensor.py +++ b/homeassistant/components/rainbird/sensor.py @@ -1,44 +1,37 @@ """Support for Rain Bird Irrigation system LNK WiFi Module.""" import logging -import voluptuous as vol +from pyrainbird import RainbirdController -from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import CONF_MONITORED_CONDITIONS -import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity -from . import DATA_RAINBIRD +from . import ( + DATA_RAINBIRD, + RAINBIRD_CONTROLLER, + SENSOR_TYPE_RAINDELAY, + SENSOR_TYPE_RAINSENSOR, + SENSOR_TYPES, +) _LOGGER = logging.getLogger(__name__) -# sensor_type [ description, unit, icon ] -SENSOR_TYPES = {"rainsensor": ["Rainsensor", None, "mdi:water"]} - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Optional(CONF_MONITORED_CONDITIONS, default=list(SENSOR_TYPES)): vol.All( - cv.ensure_list, [vol.In(SENSOR_TYPES)] - ) - } -) - def setup_platform(hass, config, add_entities, discovery_info=None): """Set up a Rain Bird sensor.""" - controller = hass.data[DATA_RAINBIRD] - sensors = [] - for sensor_type in config.get(CONF_MONITORED_CONDITIONS): - sensors.append(RainBirdSensor(controller, sensor_type)) + if discovery_info is None: + return - add_entities(sensors, True) + controller = hass.data[DATA_RAINBIRD][discovery_info[RAINBIRD_CONTROLLER]] + add_entities( + [RainBirdSensor(controller, sensor_type) for sensor_type in SENSOR_TYPES], True + ) class RainBirdSensor(Entity): """A sensor implementation for Rain Bird device.""" - def __init__(self, controller, sensor_type): + def __init__(self, controller: RainbirdController, sensor_type): """Initialize the Rain Bird sensor.""" self._sensor_type = sensor_type self._controller = controller @@ -55,12 +48,10 @@ class RainBirdSensor(Entity): def update(self): """Get the latest data and updates the states.""" _LOGGER.debug("Updating sensor: %s", self._name) - if self._sensor_type == "rainsensor": - result = self._controller.currentRainSensorState() - if result and result["type"] == "CurrentRainSensorStateResponse": - self._state = result["sensorState"] - else: - self._state = None + if self._sensor_type == SENSOR_TYPE_RAINSENSOR: + self._state = self._controller.get_rain_sensor_state() + elif self._sensor_type == SENSOR_TYPE_RAINDELAY: + self._state = self._controller.get_rain_delay() @property def name(self): diff --git a/homeassistant/components/rainbird/services.yaml b/homeassistant/components/rainbird/services.yaml new file mode 100644 index 00000000000..cdac7171a25 --- /dev/null +++ b/homeassistant/components/rainbird/services.yaml @@ -0,0 +1,9 @@ +start_irrigation: + description: Start the irrigation + fields: + entity_id: + description: Name of a single irrigation to turn on + example: 'switch.sprinkler_1' + duration: + description: Duration for this sprinkler to be turned on + example: 1 diff --git a/homeassistant/components/rainbird/switch.py b/homeassistant/components/rainbird/switch.py index 868e8ff4c7d..cb4ac83090f 100644 --- a/homeassistant/components/rainbird/switch.py +++ b/homeassistant/components/rainbird/switch.py @@ -2,61 +2,85 @@ import logging +from pyrainbird import AvailableStations, RainbirdController import voluptuous as vol -from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchDevice -from homeassistant.const import ( - CONF_FRIENDLY_NAME, - CONF_SCAN_INTERVAL, - CONF_SWITCHES, - CONF_TRIGGER_TIME, - CONF_ZONE, -) +from homeassistant.components.switch import SwitchDevice +from homeassistant.const import ATTR_ENTITY_ID, CONF_FRIENDLY_NAME, CONF_TRIGGER_TIME from homeassistant.helpers import config_validation as cv -from . import DATA_RAINBIRD +from . import CONF_ZONES, DATA_RAINBIRD, DOMAIN, RAINBIRD_CONTROLLER -DOMAIN = "rainbird" _LOGGER = logging.getLogger(__name__) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +ATTR_DURATION = "duration" + +SERVICE_START_IRRIGATION = "start_irrigation" + +SERVICE_SCHEMA_IRRIGATION = vol.Schema( { - vol.Required(CONF_SWITCHES, default={}): vol.Schema( - { - cv.string: { - vol.Optional(CONF_FRIENDLY_NAME): cv.string, - vol.Required(CONF_ZONE): cv.string, - vol.Required(CONF_TRIGGER_TIME): cv.string, - vol.Optional(CONF_SCAN_INTERVAL): cv.string, - } - } - ) + vol.Required(ATTR_ENTITY_ID): cv.entity_id, + vol.Required(ATTR_DURATION): vol.All(vol.Coerce(float), vol.Range(min=0)), } ) def setup_platform(hass, config, add_entities, discovery_info=None): """Set up Rain Bird switches over a Rain Bird controller.""" - controller = hass.data[DATA_RAINBIRD] + if discovery_info is None: + return + + controller: RainbirdController = hass.data[DATA_RAINBIRD][ + discovery_info[RAINBIRD_CONTROLLER] + ] + available_stations: AvailableStations = controller.get_available_stations() + if not (available_stations and available_stations.stations): + return devices = [] - for dev_id, switch in config.get(CONF_SWITCHES).items(): - devices.append(RainBirdSwitch(controller, switch, dev_id)) + for zone in range(1, available_stations.stations.count + 1): + if available_stations.stations.active(zone): + zone_config = discovery_info.get(CONF_ZONES, {}).get(zone, {}) + time = zone_config.get(CONF_TRIGGER_TIME, discovery_info[CONF_TRIGGER_TIME]) + name = zone_config.get(CONF_FRIENDLY_NAME) + devices.append( + RainBirdSwitch( + controller, + zone, + time, + name if name else "Sprinkler {}".format(zone), + ) + ) + add_entities(devices, True) + def start_irrigation(service): + entity_id = service.data[ATTR_ENTITY_ID] + duration = service.data[ATTR_DURATION] + + for device in devices: + if device.entity_id == entity_id: + device.turn_on(duration=duration) + + hass.services.register( + DOMAIN, + SERVICE_START_IRRIGATION, + start_irrigation, + schema=SERVICE_SCHEMA_IRRIGATION, + ) + class RainBirdSwitch(SwitchDevice): """Representation of a Rain Bird switch.""" - def __init__(self, rb, dev, dev_id): + def __init__(self, controller: RainbirdController, zone, time, name): """Initialize a Rain Bird Switch Device.""" - self._rainbird = rb - self._devid = dev_id - self._zone = int(dev.get(CONF_ZONE)) - self._name = dev.get(CONF_FRIENDLY_NAME, f"Sprinkler {self._zone}") + self._rainbird = controller + self._zone = zone + self._name = name self._state = None - self._duration = dev.get(CONF_TRIGGER_TIME) - self._attributes = {"duration": self._duration, "zone": self._zone} + self._duration = time + self._attributes = {ATTR_DURATION: self._duration, "zone": self._zone} @property def device_state_attributes(self): @@ -70,27 +94,20 @@ class RainBirdSwitch(SwitchDevice): def turn_on(self, **kwargs): """Turn the switch on.""" - response = self._rainbird.startIrrigation(int(self._zone), int(self._duration)) - if response and response["type"] == "AcknowledgeResponse": + if self._rainbird.irrigate_zone( + int(self._zone), + int(kwargs[ATTR_DURATION] if ATTR_DURATION in kwargs else self._duration), + ): self._state = True def turn_off(self, **kwargs): """Turn the switch off.""" - response = self._rainbird.stopIrrigation() - if response and response["type"] == "AcknowledgeResponse": + if self._rainbird.stop_irrigation(): self._state = False - def get_device_status(self): - """Get the status of the switch from Rain Bird Controller.""" - response = self._rainbird.currentIrrigation() - if response is None: - return None - if isinstance(response, dict) and "sprinklers" in response: - return response["sprinklers"][self._zone] - def update(self): """Update switch status.""" - self._state = self.get_device_status() + self._state = self._rainbird.get_zone_state(self._zone) @property def is_on(self): diff --git a/homeassistant/components/raincloud/manifest.json b/homeassistant/components/raincloud/manifest.json index 4d07f2a3ce4..612fc32c014 100644 --- a/homeassistant/components/raincloud/manifest.json +++ b/homeassistant/components/raincloud/manifest.json @@ -1,7 +1,7 @@ { "domain": "raincloud", "name": "Raincloud", - "documentation": "https://www.home-assistant.io/components/raincloud", + "documentation": "https://www.home-assistant.io/integrations/raincloud", "requirements": [ "raincloudy==0.0.7" ], diff --git a/homeassistant/components/rainforest_eagle/manifest.json b/homeassistant/components/rainforest_eagle/manifest.json index 354eaf8b313..c5503976d3f 100644 --- a/homeassistant/components/rainforest_eagle/manifest.json +++ b/homeassistant/components/rainforest_eagle/manifest.json @@ -1,7 +1,7 @@ { "domain": "rainforest_eagle", "name": "Rainforest Eagle-200", - "documentation": "https://www.home-assistant.io/components/rainforest_eagle", + "documentation": "https://www.home-assistant.io/integrations/rainforest_eagle", "requirements": [ "eagle200_reader==0.2.1" ], diff --git a/homeassistant/components/rainmachine/.translations/pl.json b/homeassistant/components/rainmachine/.translations/pl.json index 9ab6156549d..cf842efe9f6 100644 --- a/homeassistant/components/rainmachine/.translations/pl.json +++ b/homeassistant/components/rainmachine/.translations/pl.json @@ -2,7 +2,7 @@ "config": { "error": { "identifier_exists": "Konto jest ju\u017c zarejestrowane", - "invalid_credentials": "Nieprawid\u0142owe po\u015bwiadczenia" + "invalid_credentials": "Nieprawid\u0142owe dane uwierzytelniaj\u0105ce" }, "step": { "user": { diff --git a/homeassistant/components/rainmachine/.translations/ru.json b/homeassistant/components/rainmachine/.translations/ru.json index 6eec3ef0eba..6248890389d 100644 --- a/homeassistant/components/rainmachine/.translations/ru.json +++ b/homeassistant/components/rainmachine/.translations/ru.json @@ -1,7 +1,7 @@ { "config": { "error": { - "identifier_exists": "\u0423\u0447\u0435\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u0430", + "identifier_exists": "\u0423\u0447\u0435\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u0430.", "invalid_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435" }, "step": { diff --git a/homeassistant/components/rainmachine/manifest.json b/homeassistant/components/rainmachine/manifest.json index 25b36c798c5..37db2b31355 100644 --- a/homeassistant/components/rainmachine/manifest.json +++ b/homeassistant/components/rainmachine/manifest.json @@ -2,7 +2,7 @@ "domain": "rainmachine", "name": "Rainmachine", "config_flow": true, - "documentation": "https://www.home-assistant.io/components/rainmachine", + "documentation": "https://www.home-assistant.io/integrations/rainmachine", "requirements": [ "regenmaschine==1.5.1" ], diff --git a/homeassistant/components/random/manifest.json b/homeassistant/components/random/manifest.json index c184f35734c..cbbaa562abd 100644 --- a/homeassistant/components/random/manifest.json +++ b/homeassistant/components/random/manifest.json @@ -1,7 +1,7 @@ { "domain": "random", "name": "Random", - "documentation": "https://www.home-assistant.io/components/random", + "documentation": "https://www.home-assistant.io/integrations/random", "requirements": [], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/raspihats/manifest.json b/homeassistant/components/raspihats/manifest.json index 8f5040152a2..b241cd6db15 100644 --- a/homeassistant/components/raspihats/manifest.json +++ b/homeassistant/components/raspihats/manifest.json @@ -1,7 +1,7 @@ { "domain": "raspihats", "name": "Raspihats", - "documentation": "https://www.home-assistant.io/components/raspihats", + "documentation": "https://www.home-assistant.io/integrations/raspihats", "requirements": [ "raspihats==2.2.3", "smbus-cffi==0.5.1" diff --git a/homeassistant/components/raspyrfm/manifest.json b/homeassistant/components/raspyrfm/manifest.json index fee815a7e6b..f1330c2abe5 100644 --- a/homeassistant/components/raspyrfm/manifest.json +++ b/homeassistant/components/raspyrfm/manifest.json @@ -1,7 +1,7 @@ { "domain": "raspyrfm", "name": "Raspyrfm", - "documentation": "https://www.home-assistant.io/components/raspyrfm", + "documentation": "https://www.home-assistant.io/integrations/raspyrfm", "requirements": [ "raspyrfm-client==1.2.8" ], diff --git a/homeassistant/components/recollect_waste/manifest.json b/homeassistant/components/recollect_waste/manifest.json index 2cccf32f298..396afe30dcf 100644 --- a/homeassistant/components/recollect_waste/manifest.json +++ b/homeassistant/components/recollect_waste/manifest.json @@ -1,7 +1,7 @@ { "domain": "recollect_waste", "name": "Recollect waste", - "documentation": "https://www.home-assistant.io/components/recollect_waste", + "documentation": "https://www.home-assistant.io/integrations/recollect_waste", "requirements": [ "recollect-waste==1.0.1" ], diff --git a/homeassistant/components/recorder/__init__.py b/homeassistant/components/recorder/__init__.py index 9d34cc6fb79..b36e0a34fa4 100644 --- a/homeassistant/components/recorder/__init__.py +++ b/homeassistant/components/recorder/__init__.py @@ -320,10 +320,10 @@ class Recorder(threading.Thread): purge.purge_old_data(self, event.keep_days, event.repack) self.queue.task_done() continue - elif event.event_type == EVENT_TIME_CHANGED: + if event.event_type == EVENT_TIME_CHANGED: self.queue.task_done() continue - elif event.event_type in self.exclude_t: + if event.event_type in self.exclude_t: self.queue.task_done() continue diff --git a/homeassistant/components/recorder/manifest.json b/homeassistant/components/recorder/manifest.json index 9ecfa88053f..cdb09d66067 100644 --- a/homeassistant/components/recorder/manifest.json +++ b/homeassistant/components/recorder/manifest.json @@ -1,7 +1,7 @@ { "domain": "recorder", "name": "Recorder", - "documentation": "https://www.home-assistant.io/components/recorder", + "documentation": "https://www.home-assistant.io/integrations/recorder", "requirements": [ "sqlalchemy==1.3.8" ], diff --git a/homeassistant/components/recswitch/manifest.json b/homeassistant/components/recswitch/manifest.json index af8e802c5ec..b11eca2b088 100644 --- a/homeassistant/components/recswitch/manifest.json +++ b/homeassistant/components/recswitch/manifest.json @@ -1,7 +1,7 @@ { "domain": "recswitch", "name": "Recswitch", - "documentation": "https://www.home-assistant.io/components/recswitch", + "documentation": "https://www.home-assistant.io/integrations/recswitch", "requirements": [ "pyrecswitch==1.0.2" ], diff --git a/homeassistant/components/reddit/manifest.json b/homeassistant/components/reddit/manifest.json index c6d3b3458e5..55baecc486c 100644 --- a/homeassistant/components/reddit/manifest.json +++ b/homeassistant/components/reddit/manifest.json @@ -1,7 +1,7 @@ { "domain": "reddit", "name": "Reddit", - "documentation": "https://www.home-assistant.io/components/reddit", + "documentation": "https://www.home-assistant.io/integrations/reddit", "requirements": [ "praw==6.3.1" ], diff --git a/homeassistant/components/rejseplanen/manifest.json b/homeassistant/components/rejseplanen/manifest.json index 72562399330..f1f82999188 100644 --- a/homeassistant/components/rejseplanen/manifest.json +++ b/homeassistant/components/rejseplanen/manifest.json @@ -1,7 +1,7 @@ { "domain": "rejseplanen", "name": "Rejseplanen", - "documentation": "https://www.home-assistant.io/components/rejseplanen", + "documentation": "https://www.home-assistant.io/integrations/rejseplanen", "requirements": [ "rjpl==0.3.5" ], diff --git a/homeassistant/components/remember_the_milk/manifest.json b/homeassistant/components/remember_the_milk/manifest.json index c9d35e9d2c9..4979fe29e0e 100644 --- a/homeassistant/components/remember_the_milk/manifest.json +++ b/homeassistant/components/remember_the_milk/manifest.json @@ -1,7 +1,7 @@ { "domain": "remember_the_milk", "name": "Remember the milk", - "documentation": "https://www.home-assistant.io/components/remember_the_milk", + "documentation": "https://www.home-assistant.io/integrations/remember_the_milk", "requirements": [ "RtmAPI==0.7.0", "httplib2==0.10.3" diff --git a/homeassistant/components/remote/manifest.json b/homeassistant/components/remote/manifest.json index 5fe585dcd83..5b9e70ebcf0 100644 --- a/homeassistant/components/remote/manifest.json +++ b/homeassistant/components/remote/manifest.json @@ -1,7 +1,7 @@ { "domain": "remote", "name": "Remote", - "documentation": "https://www.home-assistant.io/components/remote", + "documentation": "https://www.home-assistant.io/integrations/remote", "requirements": [], "dependencies": [ "group" diff --git a/homeassistant/components/remote_rpi_gpio/manifest.json b/homeassistant/components/remote_rpi_gpio/manifest.json index f15defd63dc..c3b93346916 100644 --- a/homeassistant/components/remote_rpi_gpio/manifest.json +++ b/homeassistant/components/remote_rpi_gpio/manifest.json @@ -1,9 +1,9 @@ { "domain": "remote_rpi_gpio", "name": "remote_rpi_gpio", - "documentation": "https://www.home-assistant.io/components/remote_rpi_gpio", + "documentation": "https://www.home-assistant.io/integrations/remote_rpi_gpio", "requirements": [ - "gpiozero==1.4.1" + "gpiozero==1.5.1" ], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/repetier/manifest.json b/homeassistant/components/repetier/manifest.json index 14af98cfb64..a894285f729 100644 --- a/homeassistant/components/repetier/manifest.json +++ b/homeassistant/components/repetier/manifest.json @@ -1,7 +1,7 @@ { "domain": "repetier", "name": "Repetier Server", - "documentation": "https://www.home-assistant.io/components/repetier", + "documentation": "https://www.home-assistant.io/integrations/repetier", "requirements": [ "pyrepetier==3.0.5" ], diff --git a/homeassistant/components/rest/manifest.json b/homeassistant/components/rest/manifest.json index 999f5740715..0b197d104ed 100644 --- a/homeassistant/components/rest/manifest.json +++ b/homeassistant/components/rest/manifest.json @@ -1,7 +1,7 @@ { "domain": "rest", "name": "Rest", - "documentation": "https://www.home-assistant.io/components/rest", + "documentation": "https://www.home-assistant.io/integrations/rest", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/rest_command/__init__.py b/homeassistant/components/rest_command/__init__.py index 038787ac8d4..1607000e8d9 100644 --- a/homeassistant/components/rest_command/__init__.py +++ b/homeassistant/components/rest_command/__init__.py @@ -94,13 +94,11 @@ async def async_setup(hass, config): template_payload.async_render(variables=service.data), "utf-8" ) + request_url = template_url.async_render(variables=service.data) try: with async_timeout.timeout(timeout): request = await getattr(websession, method)( - template_url.async_render(variables=service.data), - data=payload, - auth=auth, - headers=headers, + request_url, data=payload, auth=auth, headers=headers ) if request.status < 400: @@ -112,7 +110,7 @@ async def async_setup(hass, config): _LOGGER.warning("Timeout call %s.", request.url) except aiohttp.ClientError: - _LOGGER.error("Client error %s.", request.url) + _LOGGER.error("Client error %s.", request_url) # register services hass.services.async_register(DOMAIN, name, async_service_handler) diff --git a/homeassistant/components/rest_command/manifest.json b/homeassistant/components/rest_command/manifest.json index ced930fc64f..238191a9343 100644 --- a/homeassistant/components/rest_command/manifest.json +++ b/homeassistant/components/rest_command/manifest.json @@ -1,7 +1,7 @@ { "domain": "rest_command", "name": "Rest command", - "documentation": "https://www.home-assistant.io/components/rest_command", + "documentation": "https://www.home-assistant.io/integrations/rest_command", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/rflink/manifest.json b/homeassistant/components/rflink/manifest.json index bbdb49ad401..bda260bdff2 100644 --- a/homeassistant/components/rflink/manifest.json +++ b/homeassistant/components/rflink/manifest.json @@ -1,7 +1,7 @@ { "domain": "rflink", "name": "Rflink", - "documentation": "https://www.home-assistant.io/components/rflink", + "documentation": "https://www.home-assistant.io/integrations/rflink", "requirements": [ "rflink==0.0.46" ], diff --git a/homeassistant/components/rfxtrx/manifest.json b/homeassistant/components/rfxtrx/manifest.json index 5d6cd4b038c..a75a8ba9eb1 100644 --- a/homeassistant/components/rfxtrx/manifest.json +++ b/homeassistant/components/rfxtrx/manifest.json @@ -1,7 +1,7 @@ { "domain": "rfxtrx", "name": "Rfxtrx", - "documentation": "https://www.home-assistant.io/components/rfxtrx", + "documentation": "https://www.home-assistant.io/integrations/rfxtrx", "requirements": [ "pyRFXtrx==0.23" ], diff --git a/homeassistant/components/ring/binary_sensor.py b/homeassistant/components/ring/binary_sensor.py index 6806df0408f..86d26ec25b4 100644 --- a/homeassistant/components/ring/binary_sensor.py +++ b/homeassistant/components/ring/binary_sensor.py @@ -65,7 +65,7 @@ class RingBinarySensor(BinarySensorDevice): def __init__(self, hass, data, sensor_type): """Initialize a sensor for Ring device.""" - super(RingBinarySensor, self).__init__() + super().__init__() self._sensor_type = sensor_type self._data = data self._name = "{0} {1}".format( diff --git a/homeassistant/components/ring/camera.py b/homeassistant/components/ring/camera.py index ef86ea6734c..461b3a199d7 100644 --- a/homeassistant/components/ring/camera.py +++ b/homeassistant/components/ring/camera.py @@ -75,7 +75,7 @@ class RingCam(Camera): def __init__(self, hass, camera, device_info): """Initialize a Ring Door Bell camera.""" - super(RingCam, self).__init__() + super().__init__() self._camera = camera self._hass = hass self._name = self._camera.name diff --git a/homeassistant/components/ring/light.py b/homeassistant/components/ring/light.py index 5805114252e..697be4d1579 100644 --- a/homeassistant/components/ring/light.py +++ b/homeassistant/components/ring/light.py @@ -1,9 +1,10 @@ """This component provides HA switch support for Ring Door Bell/Chimes.""" import logging -from datetime import datetime, timedelta +from datetime import timedelta from homeassistant.components.light import Light from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.core import callback +import homeassistant.util.dt as dt_util from . import DATA_RING_STICKUP_CAMS, SIGNAL_UPDATE_RING @@ -41,7 +42,7 @@ class RingLight(Light): self._device = device self._unique_id = self._device.id self._light_on = False - self._no_updates_until = datetime.now() + self._no_updates_until = dt_util.utcnow() async def async_added_to_hass(self): """Register callbacks.""" @@ -77,7 +78,7 @@ class RingLight(Light): """Update light state, and causes HASS to correctly update.""" self._device.lights = new_state self._light_on = new_state == ON_STATE - self._no_updates_until = datetime.now() + SKIP_UPDATES_DELAY + self._no_updates_until = dt_util.utcnow() + SKIP_UPDATES_DELAY self.async_schedule_update_ha_state(True) def turn_on(self, **kwargs): @@ -90,7 +91,7 @@ class RingLight(Light): def update(self): """Update current state of the light.""" - if self._no_updates_until > datetime.now(): + if self._no_updates_until > dt_util.utcnow(): _LOGGER.debug("Skipping update...") return diff --git a/homeassistant/components/ring/manifest.json b/homeassistant/components/ring/manifest.json index 9dbedad1ffc..47fc2f3a6a8 100644 --- a/homeassistant/components/ring/manifest.json +++ b/homeassistant/components/ring/manifest.json @@ -1,7 +1,7 @@ { "domain": "ring", "name": "Ring", - "documentation": "https://www.home-assistant.io/components/ring", + "documentation": "https://www.home-assistant.io/integrations/ring", "requirements": [ "ring_doorbell==0.2.3" ], diff --git a/homeassistant/components/ring/sensor.py b/homeassistant/components/ring/sensor.py index af661f4571c..6a64226a053 100644 --- a/homeassistant/components/ring/sensor.py +++ b/homeassistant/components/ring/sensor.py @@ -110,7 +110,7 @@ class RingSensor(Entity): def __init__(self, hass, data, sensor_type): """Initialize a sensor for Ring device.""" - super(RingSensor, self).__init__() + super().__init__() self._sensor_type = sensor_type self._data = data self._extra = None diff --git a/homeassistant/components/ring/switch.py b/homeassistant/components/ring/switch.py index cbbecb1a403..413d2a70aae 100644 --- a/homeassistant/components/ring/switch.py +++ b/homeassistant/components/ring/switch.py @@ -1,9 +1,10 @@ """This component provides HA switch support for Ring Door Bell/Chimes.""" import logging -from datetime import datetime, timedelta +from datetime import timedelta from homeassistant.components.switch import SwitchDevice from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.core import callback +import homeassistant.util.dt as dt_util from . import DATA_RING_STICKUP_CAMS, SIGNAL_UPDATE_RING @@ -72,14 +73,14 @@ class SirenSwitch(BaseRingSwitch): def __init__(self, device): """Initialize the switch for a device with a siren.""" super().__init__(device, "siren") - self._no_updates_until = datetime.now() + self._no_updates_until = dt_util.utcnow() self._siren_on = False def _set_switch(self, new_state): """Update switch state, and causes HASS to correctly update.""" self._device.siren = new_state self._siren_on = new_state > 0 - self._no_updates_until = datetime.now() + SKIP_UPDATES_DELAY + self._no_updates_until = dt_util.utcnow() + SKIP_UPDATES_DELAY self.schedule_update_ha_state() @property @@ -102,7 +103,7 @@ class SirenSwitch(BaseRingSwitch): def update(self): """Update state of the siren.""" - if self._no_updates_until > datetime.now(): + if self._no_updates_until > dt_util.utcnow(): _LOGGER.debug("Skipping update...") return self._siren_on = self._device.siren > 0 diff --git a/homeassistant/components/ripple/manifest.json b/homeassistant/components/ripple/manifest.json index fe93bf02445..b8aa5c74302 100644 --- a/homeassistant/components/ripple/manifest.json +++ b/homeassistant/components/ripple/manifest.json @@ -1,7 +1,7 @@ { "domain": "ripple", "name": "Ripple", - "documentation": "https://www.home-assistant.io/components/ripple", + "documentation": "https://www.home-assistant.io/integrations/ripple", "requirements": [ "python-ripple-api==0.0.3" ], diff --git a/homeassistant/components/rmvtransport/manifest.json b/homeassistant/components/rmvtransport/manifest.json index 3f32a61c081..1f06daf0623 100644 --- a/homeassistant/components/rmvtransport/manifest.json +++ b/homeassistant/components/rmvtransport/manifest.json @@ -1,7 +1,7 @@ { "domain": "rmvtransport", "name": "Rmvtransport", - "documentation": "https://www.home-assistant.io/components/rmvtransport", + "documentation": "https://www.home-assistant.io/integrations/rmvtransport", "requirements": [ "PyRMVtransport==0.1.3" ], diff --git a/homeassistant/components/rocketchat/manifest.json b/homeassistant/components/rocketchat/manifest.json index 3a8959f1be6..924a9b86d47 100644 --- a/homeassistant/components/rocketchat/manifest.json +++ b/homeassistant/components/rocketchat/manifest.json @@ -1,7 +1,7 @@ { "domain": "rocketchat", "name": "Rocketchat", - "documentation": "https://www.home-assistant.io/components/rocketchat", + "documentation": "https://www.home-assistant.io/integrations/rocketchat", "requirements": [ "rocketchat-API==0.6.1" ], diff --git a/homeassistant/components/roku/manifest.json b/homeassistant/components/roku/manifest.json index 477bcb105f7..f2639b31d15 100644 --- a/homeassistant/components/roku/manifest.json +++ b/homeassistant/components/roku/manifest.json @@ -1,7 +1,7 @@ { "domain": "roku", "name": "Roku", - "documentation": "https://www.home-assistant.io/components/roku", + "documentation": "https://www.home-assistant.io/integrations/roku", "requirements": [ "roku==3.1" ], diff --git a/homeassistant/components/roomba/manifest.json b/homeassistant/components/roomba/manifest.json index 058ad0c5e81..5064357a7df 100644 --- a/homeassistant/components/roomba/manifest.json +++ b/homeassistant/components/roomba/manifest.json @@ -1,7 +1,7 @@ { "domain": "roomba", "name": "Roomba", - "documentation": "https://www.home-assistant.io/components/roomba", + "documentation": "https://www.home-assistant.io/integrations/roomba", "requirements": [ "roombapy==1.3.1" ], diff --git a/homeassistant/components/route53/manifest.json b/homeassistant/components/route53/manifest.json index d377ca7dca0..34a296b0f9d 100644 --- a/homeassistant/components/route53/manifest.json +++ b/homeassistant/components/route53/manifest.json @@ -1,9 +1,9 @@ { "domain": "route53", "name": "Route53", - "documentation": "https://www.home-assistant.io/components/route53", + "documentation": "https://www.home-assistant.io/integrations/route53", "requirements": [ - "boto3==1.9.16", + "boto3==1.9.233", "ipify==1.0.0" ], "dependencies": [], diff --git a/homeassistant/components/rova/manifest.json b/homeassistant/components/rova/manifest.json index 71ec8fcbc9b..e033e82d922 100644 --- a/homeassistant/components/rova/manifest.json +++ b/homeassistant/components/rova/manifest.json @@ -1,7 +1,7 @@ { "domain": "rova", "name": "Rova", - "documentation": "https://www.home-assistant.io/components/rova", + "documentation": "https://www.home-assistant.io/integrations/rova", "requirements": [ "rova==0.1.0" ], diff --git a/homeassistant/components/rpi_camera/manifest.json b/homeassistant/components/rpi_camera/manifest.json index 1f905b103fe..d5443787d39 100644 --- a/homeassistant/components/rpi_camera/manifest.json +++ b/homeassistant/components/rpi_camera/manifest.json @@ -1,7 +1,7 @@ { "domain": "rpi_camera", "name": "Rpi camera", - "documentation": "https://www.home-assistant.io/components/rpi_camera", + "documentation": "https://www.home-assistant.io/integrations/rpi_camera", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/rpi_gpio/manifest.json b/homeassistant/components/rpi_gpio/manifest.json index 88322708b27..0bee2baeddf 100644 --- a/homeassistant/components/rpi_gpio/manifest.json +++ b/homeassistant/components/rpi_gpio/manifest.json @@ -1,7 +1,7 @@ { "domain": "rpi_gpio", "name": "Rpi gpio", - "documentation": "https://www.home-assistant.io/components/rpi_gpio", + "documentation": "https://www.home-assistant.io/integrations/rpi_gpio", "requirements": [ "RPi.GPIO==0.6.5" ], diff --git a/homeassistant/components/rpi_gpio_pwm/manifest.json b/homeassistant/components/rpi_gpio_pwm/manifest.json index d2ed380d68a..ccff3d3357f 100644 --- a/homeassistant/components/rpi_gpio_pwm/manifest.json +++ b/homeassistant/components/rpi_gpio_pwm/manifest.json @@ -1,7 +1,7 @@ { "domain": "rpi_gpio_pwm", "name": "Rpi gpio pwm", - "documentation": "https://www.home-assistant.io/components/rpi_gpio_pwm", + "documentation": "https://www.home-assistant.io/integrations/rpi_gpio_pwm", "requirements": [ "pwmled==1.4.1" ], diff --git a/homeassistant/components/rpi_pfio/manifest.json b/homeassistant/components/rpi_pfio/manifest.json index 7fc724bf90a..fd0a0a99f58 100644 --- a/homeassistant/components/rpi_pfio/manifest.json +++ b/homeassistant/components/rpi_pfio/manifest.json @@ -1,7 +1,7 @@ { "domain": "rpi_pfio", "name": "Rpi pfio", - "documentation": "https://www.home-assistant.io/components/rpi_pfio", + "documentation": "https://www.home-assistant.io/integrations/rpi_pfio", "requirements": [ "pifacecommon==4.2.2", "pifacedigitalio==3.0.5" diff --git a/homeassistant/components/rpi_rf/manifest.json b/homeassistant/components/rpi_rf/manifest.json index e5fffee131e..5914f65ef69 100644 --- a/homeassistant/components/rpi_rf/manifest.json +++ b/homeassistant/components/rpi_rf/manifest.json @@ -1,7 +1,7 @@ { "domain": "rpi_rf", "name": "Rpi rf", - "documentation": "https://www.home-assistant.io/components/rpi_rf", + "documentation": "https://www.home-assistant.io/integrations/rpi_rf", "requirements": [ "rpi-rf==0.9.7" ], diff --git a/homeassistant/components/rss_feed_template/manifest.json b/homeassistant/components/rss_feed_template/manifest.json index c92f6b2a0ba..7bd92876456 100644 --- a/homeassistant/components/rss_feed_template/manifest.json +++ b/homeassistant/components/rss_feed_template/manifest.json @@ -1,7 +1,7 @@ { "domain": "rss_feed_template", "name": "Rss feed template", - "documentation": "https://www.home-assistant.io/components/rss_feed_template", + "documentation": "https://www.home-assistant.io/integrations/rss_feed_template", "requirements": [], "dependencies": [ "http" diff --git a/homeassistant/components/rtorrent/manifest.json b/homeassistant/components/rtorrent/manifest.json index ce2dca9e085..899ee3a8ae8 100644 --- a/homeassistant/components/rtorrent/manifest.json +++ b/homeassistant/components/rtorrent/manifest.json @@ -1,7 +1,7 @@ { "domain": "rtorrent", "name": "Rtorrent", - "documentation": "https://www.home-assistant.io/components/rtorrent", + "documentation": "https://www.home-assistant.io/integrations/rtorrent", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/russound_rio/manifest.json b/homeassistant/components/russound_rio/manifest.json index 4667e9b8314..9404153f4e5 100644 --- a/homeassistant/components/russound_rio/manifest.json +++ b/homeassistant/components/russound_rio/manifest.json @@ -1,7 +1,7 @@ { "domain": "russound_rio", "name": "Russound rio", - "documentation": "https://www.home-assistant.io/components/russound_rio", + "documentation": "https://www.home-assistant.io/integrations/russound_rio", "requirements": [ "russound_rio==0.1.7" ], diff --git a/homeassistant/components/russound_rnet/manifest.json b/homeassistant/components/russound_rnet/manifest.json index 716f383040f..687dd05b61d 100644 --- a/homeassistant/components/russound_rnet/manifest.json +++ b/homeassistant/components/russound_rnet/manifest.json @@ -1,7 +1,7 @@ { "domain": "russound_rnet", "name": "Russound rnet", - "documentation": "https://www.home-assistant.io/components/russound_rnet", + "documentation": "https://www.home-assistant.io/integrations/russound_rnet", "requirements": [ "russound==0.1.9" ], diff --git a/homeassistant/components/sabnzbd/manifest.json b/homeassistant/components/sabnzbd/manifest.json index 9424e5f3a1a..e69c227f148 100644 --- a/homeassistant/components/sabnzbd/manifest.json +++ b/homeassistant/components/sabnzbd/manifest.json @@ -1,7 +1,7 @@ { "domain": "sabnzbd", "name": "Sabnzbd", - "documentation": "https://www.home-assistant.io/components/sabnzbd", + "documentation": "https://www.home-assistant.io/integrations/sabnzbd", "requirements": [ "pysabnzbd==1.1.0" ], diff --git a/homeassistant/components/saj/__init__.py b/homeassistant/components/saj/__init__.py new file mode 100644 index 00000000000..03277bba4df --- /dev/null +++ b/homeassistant/components/saj/__init__.py @@ -0,0 +1 @@ +"""The saj component.""" diff --git a/homeassistant/components/saj/manifest.json b/homeassistant/components/saj/manifest.json new file mode 100644 index 00000000000..e42b37195a4 --- /dev/null +++ b/homeassistant/components/saj/manifest.json @@ -0,0 +1,12 @@ +{ + "domain": "saj", + "name": "SAJ", + "documentation": "https://www.home-assistant.io/integrations/saj", + "requirements": [ + "pysaj==0.0.9" + ], + "dependencies": [], + "codeowners": [ + "@fredericvl" + ] +} diff --git a/homeassistant/components/saj/sensor.py b/homeassistant/components/saj/sensor.py new file mode 100644 index 00000000000..fa06b2b9125 --- /dev/null +++ b/homeassistant/components/saj/sensor.py @@ -0,0 +1,202 @@ +"""SAJ solar inverter interface.""" +import asyncio +from datetime import date +import logging + +import pysaj +import voluptuous as vol + +from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import ( + CONF_HOST, + DEVICE_CLASS_POWER, + DEVICE_CLASS_TEMPERATURE, + ENERGY_KILO_WATT_HOUR, + EVENT_HOMEASSISTANT_START, + EVENT_HOMEASSISTANT_STOP, + MASS_KILOGRAMS, + POWER_WATT, + TEMP_CELSIUS, + TEMP_FAHRENHEIT, +) +from homeassistant.core import CALLBACK_TYPE, callback +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.event import async_call_later + +_LOGGER = logging.getLogger(__name__) + +MIN_INTERVAL = 5 +MAX_INTERVAL = 300 + +UNIT_OF_MEASUREMENT_HOURS = "h" + +SAJ_UNIT_MAPPINGS = { + "W": POWER_WATT, + "kWh": ENERGY_KILO_WATT_HOUR, + "h": UNIT_OF_MEASUREMENT_HOURS, + "kg": MASS_KILOGRAMS, + "°C": TEMP_CELSIUS, + "": None, +} + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({vol.Required(CONF_HOST): cv.string}) + + +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Set up SAJ sensors.""" + + remove_interval_update = None + + # Init all sensors + sensor_def = pysaj.Sensors() + + # Use all sensors by default + hass_sensors = [] + + for sensor in sensor_def: + hass_sensors.append(SAJsensor(sensor)) + + saj = pysaj.SAJ(config[CONF_HOST]) + + async_add_entities(hass_sensors) + + async def async_saj(): + """Update all the SAJ sensors.""" + tasks = [] + + values = await saj.read(sensor_def) + + for sensor in hass_sensors: + state_unknown = False + if not values: + # SAJ inverters are powered by DC via solar panels and thus are + # offline after the sun has set. If a sensor resets on a daily + # basis like "today_yield", this reset won't happen automatically. + # Code below checks if today > day when sensor was last updated + # and if so: set state to None. + # Sensors with live values like "temperature" or "current_power" + # will also be reset to None. + if (sensor.per_day_basis and date.today() > sensor.date_updated) or ( + not sensor.per_day_basis and not sensor.per_total_basis + ): + state_unknown = True + task = sensor.async_update_values(unknown_state=state_unknown) + if task: + tasks.append(task) + if tasks: + await asyncio.wait(tasks) + return values + + def start_update_interval(event): + """Start the update interval scheduling.""" + nonlocal remove_interval_update + remove_interval_update = async_track_time_interval_backoff(hass, async_saj) + + def stop_update_interval(event): + """Properly cancel the scheduled update.""" + remove_interval_update() # pylint: disable=not-callable + + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, start_update_interval) + hass.bus.async_listen(EVENT_HOMEASSISTANT_STOP, stop_update_interval) + + +@callback +def async_track_time_interval_backoff(hass, action) -> CALLBACK_TYPE: + """Add a listener that fires repetitively and increases the interval when failed.""" + remove = None + interval = MIN_INTERVAL + + async def interval_listener(now=None): + """Handle elapsed interval with backoff.""" + nonlocal interval, remove + try: + if await action(): + interval = MIN_INTERVAL + else: + interval = min(interval * 2, MAX_INTERVAL) + finally: + remove = async_call_later(hass, interval, interval_listener) + + hass.async_create_task(interval_listener()) + + def remove_listener(): + """Remove interval listener.""" + if remove: + remove() # pylint: disable=not-callable + + return remove_listener + + +class SAJsensor(Entity): + """Representation of a SAJ sensor.""" + + def __init__(self, pysaj_sensor): + """Initialize the sensor.""" + self._sensor = pysaj_sensor + self._state = self._sensor.value + + @property + def name(self): + """Return the name of the sensor.""" + return f"saj_{self._sensor.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 SAJ_UNIT_MAPPINGS[self._sensor.unit] + + @property + def device_class(self): + """Return the device class the sensor belongs to.""" + if self.unit_of_measurement == POWER_WATT: + return DEVICE_CLASS_POWER + if ( + self.unit_of_measurement == TEMP_CELSIUS + or self._sensor.unit == TEMP_FAHRENHEIT + ): + return DEVICE_CLASS_TEMPERATURE + + @property + def should_poll(self) -> bool: + """SAJ sensors are updated & don't poll.""" + return False + + @property + def per_day_basis(self) -> bool: + """Return if the sensors value is on daily basis or not.""" + return self._sensor.per_day_basis + + @property + def per_total_basis(self) -> bool: + """Return if the sensors value is cummulative or not.""" + return self._sensor.per_total_basis + + @property + def date_updated(self) -> date: + """Return the date when the sensor was last updated.""" + return self._sensor.date + + def async_update_values(self, unknown_state=False): + """Update this sensor.""" + update = False + + if self._sensor.value != self._state: + update = True + self._state = self._sensor.value + + if unknown_state and self._state is not None: + update = True + self._state = None + + return self.async_update_ha_state() if update else None + + @property + def unique_id(self): + """Return a unique identifier for this sensor.""" + return f"{self._sensor.name}" diff --git a/homeassistant/components/samsungtv/manifest.json b/homeassistant/components/samsungtv/manifest.json index c8825f4ac3f..a080fac112a 100644 --- a/homeassistant/components/samsungtv/manifest.json +++ b/homeassistant/components/samsungtv/manifest.json @@ -1,7 +1,7 @@ { "domain": "samsungtv", "name": "Samsungtv", - "documentation": "https://www.home-assistant.io/components/samsungtv", + "documentation": "https://www.home-assistant.io/integrations/samsungtv", "requirements": [ "samsungctl[websocket]==0.7.1", "wakeonlan==1.1.6" diff --git a/homeassistant/components/satel_integra/manifest.json b/homeassistant/components/satel_integra/manifest.json index ae56b54ce18..dbbfebeaccb 100644 --- a/homeassistant/components/satel_integra/manifest.json +++ b/homeassistant/components/satel_integra/manifest.json @@ -1,7 +1,7 @@ { "domain": "satel_integra", "name": "Satel integra", - "documentation": "https://www.home-assistant.io/components/satel_integra", + "documentation": "https://www.home-assistant.io/integrations/satel_integra", "requirements": [ "satel_integra==0.3.4" ], diff --git a/homeassistant/components/scene/manifest.json b/homeassistant/components/scene/manifest.json index e1becfd1936..b80e4de5041 100644 --- a/homeassistant/components/scene/manifest.json +++ b/homeassistant/components/scene/manifest.json @@ -1,7 +1,7 @@ { "domain": "scene", "name": "Scene", - "documentation": "https://www.home-assistant.io/components/scene", + "documentation": "https://www.home-assistant.io/integrations/scene", "requirements": [], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/scrape/manifest.json b/homeassistant/components/scrape/manifest.json index ec9807d4e00..989070900ca 100644 --- a/homeassistant/components/scrape/manifest.json +++ b/homeassistant/components/scrape/manifest.json @@ -1,7 +1,7 @@ { "domain": "scrape", "name": "Scrape", - "documentation": "https://www.home-assistant.io/components/scrape", + "documentation": "https://www.home-assistant.io/integrations/scrape", "requirements": [ "beautifulsoup4==4.8.0" ], diff --git a/homeassistant/components/script/manifest.json b/homeassistant/components/script/manifest.json index 56a3c39b7b6..51ce17c500a 100644 --- a/homeassistant/components/script/manifest.json +++ b/homeassistant/components/script/manifest.json @@ -1,7 +1,7 @@ { "domain": "script", "name": "Script", - "documentation": "https://www.home-assistant.io/components/script", + "documentation": "https://www.home-assistant.io/integrations/script", "requirements": [], "dependencies": [ "group" diff --git a/homeassistant/components/scsgate/manifest.json b/homeassistant/components/scsgate/manifest.json index d565a5d336d..b6334272f23 100644 --- a/homeassistant/components/scsgate/manifest.json +++ b/homeassistant/components/scsgate/manifest.json @@ -1,7 +1,7 @@ { "domain": "scsgate", "name": "Scsgate", - "documentation": "https://www.home-assistant.io/components/scsgate", + "documentation": "https://www.home-assistant.io/integrations/scsgate", "requirements": [ "scsgate==0.1.0" ], diff --git a/homeassistant/components/season/manifest.json b/homeassistant/components/season/manifest.json index 74bdb3d8b88..528e9ef35f1 100644 --- a/homeassistant/components/season/manifest.json +++ b/homeassistant/components/season/manifest.json @@ -1,7 +1,7 @@ { "domain": "season", "name": "Season", - "documentation": "https://www.home-assistant.io/components/season", + "documentation": "https://www.home-assistant.io/integrations/season", "requirements": [ "ephem==3.7.6.0" ], diff --git a/homeassistant/components/season/sensor.py b/homeassistant/components/season/sensor.py index f2e0ccac2b0..cdd6af57617 100644 --- a/homeassistant/components/season/sensor.py +++ b/homeassistant/components/season/sensor.py @@ -8,6 +8,7 @@ from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import CONF_TYPE from homeassistant.helpers.entity import Entity from homeassistant import util +import homeassistant.util.dt as dt_util _LOGGER = logging.getLogger(__name__) @@ -104,7 +105,7 @@ class Season(Entity): """Initialize the season.""" self.hass = hass self.hemisphere = hemisphere - self.datetime = datetime.now() + self.datetime = dt_util.utcnow().replace(tzinfo=None) self.type = season_tracking_type self.season = get_season(self.datetime, self.hemisphere, self.type) @@ -125,5 +126,5 @@ class Season(Entity): def update(self): """Update season.""" - self.datetime = datetime.utcnow() + self.datetime = dt_util.utcnow().replace(tzinfo=None) self.season = get_season(self.datetime, self.hemisphere, self.type) diff --git a/homeassistant/components/sendgrid/manifest.json b/homeassistant/components/sendgrid/manifest.json index eb006f408bc..f9bd8cc31b4 100644 --- a/homeassistant/components/sendgrid/manifest.json +++ b/homeassistant/components/sendgrid/manifest.json @@ -1,9 +1,9 @@ { "domain": "sendgrid", "name": "Sendgrid", - "documentation": "https://www.home-assistant.io/components/sendgrid", + "documentation": "https://www.home-assistant.io/integrations/sendgrid", "requirements": [ - "sendgrid==6.0.5" + "sendgrid==6.1.0" ], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/sendgrid/notify.py b/homeassistant/components/sendgrid/notify.py index ac334587b89..f16758a5355 100644 --- a/homeassistant/components/sendgrid/notify.py +++ b/homeassistant/components/sendgrid/notify.py @@ -3,6 +3,8 @@ import logging import voluptuous as vol +from sendgrid import SendGridAPIClient + from homeassistant.const import ( CONF_API_KEY, CONF_RECIPIENT, @@ -45,8 +47,6 @@ class SendgridNotificationService(BaseNotificationService): def __init__(self, config): """Initialize the service.""" - from sendgrid import SendGridAPIClient - self.api_key = config[CONF_API_KEY] self.sender = config[CONF_SENDER] self.sender_name = config[CONF_SENDER_NAME] diff --git a/homeassistant/components/sense/manifest.json b/homeassistant/components/sense/manifest.json index 8763234c5ed..0112ca28469 100644 --- a/homeassistant/components/sense/manifest.json +++ b/homeassistant/components/sense/manifest.json @@ -1,7 +1,7 @@ { "domain": "sense", "name": "Sense", - "documentation": "https://www.home-assistant.io/components/sense", + "documentation": "https://www.home-assistant.io/integrations/sense", "requirements": [ "sense_energy==0.7.0" ], diff --git a/homeassistant/components/sensehat/manifest.json b/homeassistant/components/sensehat/manifest.json index cb148c92198..deec8c23ab7 100644 --- a/homeassistant/components/sensehat/manifest.json +++ b/homeassistant/components/sensehat/manifest.json @@ -1,7 +1,7 @@ { "domain": "sensehat", "name": "Sensehat", - "documentation": "https://www.home-assistant.io/components/sensehat", + "documentation": "https://www.home-assistant.io/integrations/sensehat", "requirements": [ "sense-hat==2.2.0" ], diff --git a/homeassistant/components/sensibo/manifest.json b/homeassistant/components/sensibo/manifest.json index 776b8444b82..9b49f442d15 100644 --- a/homeassistant/components/sensibo/manifest.json +++ b/homeassistant/components/sensibo/manifest.json @@ -1,7 +1,7 @@ { "domain": "sensibo", "name": "Sensibo", - "documentation": "https://www.home-assistant.io/components/sensibo", + "documentation": "https://www.home-assistant.io/integrations/sensibo", "requirements": [ "pysensibo==1.0.3" ], diff --git a/homeassistant/components/sensor/.translations/ca.json b/homeassistant/components/sensor/.translations/ca.json new file mode 100644 index 00000000000..59db5a62f86 --- /dev/null +++ b/homeassistant/components/sensor/.translations/ca.json @@ -0,0 +1,24 @@ +{ + "device_automation": { + "condition_type": { + "is_battery_level": "Nivell de bateria de {entity_name}", + "is_humidity": "Humitat de {entity_name}", + "is_illuminance": "Il\u00b7luminaci\u00f3 de {entity_name}", + "is_pressure": "Pressi\u00f3 de {entity_name}", + "is_signal_strength": "For\u00e7a del senyal de {entity_name}", + "is_temperature": "Temperatura de {entity_name}", + "is_timestamp": "Marca de temps de {entity_name}", + "is_value": "Valor de {entity_name}" + }, + "trigger_type": { + "battery_level": "Nivell de bateria de {entity_name}", + "humidity": "Humitat de {entity_name}", + "illuminance": "Il\u00b7luminaci\u00f3 de {entity_name}", + "pressure": "Pressi\u00f3 de {entity_name}", + "signal_strength": "For\u00e7a del senyal de {entity_name}", + "temperature": "Temperatura de {entity_name}", + "timestamp": "Marca de temps de {entity_name}", + "value": "Valor de {entity_name}" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/da.json b/homeassistant/components/sensor/.translations/da.json new file mode 100644 index 00000000000..df9b9935dc1 --- /dev/null +++ b/homeassistant/components/sensor/.translations/da.json @@ -0,0 +1,26 @@ +{ + "device_automation": { + "condition_type": { + "is_battery_level": "{entity_name} batteriniveau", + "is_humidity": "{entity_name} fugtighed", + "is_illuminance": "{entity_name} belysningsstyrke", + "is_power": "{entity_name} str\u00f8m", + "is_pressure": "{entity_name} tryk", + "is_signal_strength": "{entity_name} signalstyrke", + "is_temperature": "{entity_name} temperatur", + "is_timestamp": "{entity_name} tidsstempel", + "is_value": "{entity_name} v\u00e6rdi" + }, + "trigger_type": { + "battery_level": "{entity_name} batteriniveau", + "humidity": "{entity_name} fugtighed", + "illuminance": "{entity_name} belysningsstyrke", + "power": "{entity_name} str\u00f8m", + "pressure": "{entity_name} tryk", + "signal_strength": "{entity_name} signalstyrke", + "temperature": "{entity_name} temperatur", + "timestamp": "{entity_name} tidsstempel", + "value": "{entity_name} v\u00e6rdi" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/de.json b/homeassistant/components/sensor/.translations/de.json new file mode 100644 index 00000000000..1f248099df3 --- /dev/null +++ b/homeassistant/components/sensor/.translations/de.json @@ -0,0 +1,21 @@ +{ + "device_automation": { + "condition_type": { + "is_humidity": "{entity_name} Feuchtigkeit", + "is_pressure": "{entity_name} Druck", + "is_signal_strength": "{entity_name} Signalst\u00e4rke", + "is_temperature": "{entity_name} Temperatur", + "is_timestamp": "{entity_name} Zeitstempel", + "is_value": "{entity_name} Wert" + }, + "trigger_type": { + "battery_level": "{entity_name} Batteriestatus", + "humidity": "{entity_name} Feuchtigkeit", + "pressure": "{entity_name} Druck", + "signal_strength": "{entity_name} Signalst\u00e4rke", + "temperature": "{entity_name} Temperatur", + "timestamp": "{entity_name} Zeitstempel", + "value": "{entity_name} Wert" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/en.json b/homeassistant/components/sensor/.translations/en.json new file mode 100644 index 00000000000..7bbbe660feb --- /dev/null +++ b/homeassistant/components/sensor/.translations/en.json @@ -0,0 +1,26 @@ +{ + "device_automation": { + "condition_type": { + "is_battery_level": "{entity_name} battery level", + "is_humidity": "{entity_name} humidity", + "is_illuminance": "{entity_name} illuminance", + "is_power": "{entity_name} power", + "is_pressure": "{entity_name} pressure", + "is_signal_strength": "{entity_name} signal strength", + "is_temperature": "{entity_name} temperature", + "is_timestamp": "{entity_name} timestamp", + "is_value": "{entity_name} value" + }, + "trigger_type": { + "battery_level": "{entity_name} battery level", + "humidity": "{entity_name} humidity", + "illuminance": "{entity_name} illuminance", + "power": "{entity_name} power", + "pressure": "{entity_name} pressure", + "signal_strength": "{entity_name} signal strength", + "temperature": "{entity_name} temperature", + "timestamp": "{entity_name} timestamp", + "value": "{entity_name} value" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/es.json b/homeassistant/components/sensor/.translations/es.json new file mode 100644 index 00000000000..a9039d2e410 --- /dev/null +++ b/homeassistant/components/sensor/.translations/es.json @@ -0,0 +1,26 @@ +{ + "device_automation": { + "condition_type": { + "is_battery_level": "{entity_name} nivel de bater\u00eda", + "is_humidity": "{entity_name} humedad", + "is_illuminance": "{entity_name} iluminancia", + "is_power": "{entity_name} alimentaci\u00f3n", + "is_pressure": "{entity_name} presi\u00f3n", + "is_signal_strength": "{entity_name} intensidad de la se\u00f1al", + "is_temperature": "{entity_name} temperatura", + "is_timestamp": "{entity_name} marca de tiempo", + "is_value": "{entity_name} valor" + }, + "trigger_type": { + "battery_level": "{entity_name} nivel de bater\u00eda", + "humidity": "{entity_name} humedad", + "illuminance": "{entity_name} iluminancia", + "power": "{entity_name} alimentaci\u00f3n", + "pressure": "{entity_name} presi\u00f3n", + "signal_strength": "{entity_name} intensidad de la se\u00f1al", + "temperature": "{entity_name} temperatura", + "timestamp": "{entity_name} marca de tiempo", + "value": "{entity_name} valor" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/fr.json b/homeassistant/components/sensor/.translations/fr.json new file mode 100644 index 00000000000..676a5aa413f --- /dev/null +++ b/homeassistant/components/sensor/.translations/fr.json @@ -0,0 +1,26 @@ +{ + "device_automation": { + "condition_type": { + "is_battery_level": "{entity_name} niveau batterie", + "is_humidity": "{entity_name} humidit\u00e9", + "is_illuminance": "{entity_name} \u00e9clairement", + "is_power": "{entity_name} puissance", + "is_pressure": "{entity_name} pression", + "is_signal_strength": "{entity_name} force du signal", + "is_temperature": "{entity_name} temp\u00e9rature", + "is_timestamp": "{entity_name} horodatage", + "is_value": "{entity_name} valeur" + }, + "trigger_type": { + "battery_level": "{entity_name} niveau batterie", + "humidity": "{entity_name} humidit\u00e9", + "illuminance": "{entity_name} \u00e9clairement", + "power": "{entity_name} puissance", + "pressure": "{entity_name} pression", + "signal_strength": "{entity_name} force du signal", + "temperature": "{entity_name} temp\u00e9rature", + "timestamp": "{entity_name} horodatage", + "value": "{entity_name} valeur" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/it.json b/homeassistant/components/sensor/.translations/it.json new file mode 100644 index 00000000000..07b20245c16 --- /dev/null +++ b/homeassistant/components/sensor/.translations/it.json @@ -0,0 +1,26 @@ +{ + "device_automation": { + "condition_type": { + "is_battery_level": "Livello della batteria di {entity_name}", + "is_humidity": "Umidit\u00e0 di {entity_name}", + "is_illuminance": "Illuminazione di {entity_name}", + "is_power": "Potenza di {entity_name}", + "is_pressure": "Pressione di {entity_name}", + "is_signal_strength": "Potenza del segnale di {entity_name}", + "is_temperature": "Temperatura di {entity_name}", + "is_timestamp": "Data di {entity_name}", + "is_value": "Valore di {entity_name}" + }, + "trigger_type": { + "battery_level": "Livello della batteria di {entity_name}", + "humidity": "Umidit\u00e0 di {entity_name}", + "illuminance": "Illuminazione di {entity_name}", + "power": "Potenza di {entity_name}", + "pressure": "Pressione di {entity_name}", + "signal_strength": "Potenza del segnale di {entity_name}", + "temperature": "Temperatura di {entity_name}", + "timestamp": "Data di {entity_name}", + "value": "Valore di {entity_name}" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/lb.json b/homeassistant/components/sensor/.translations/lb.json new file mode 100644 index 00000000000..01a4e89c9f4 --- /dev/null +++ b/homeassistant/components/sensor/.translations/lb.json @@ -0,0 +1,26 @@ +{ + "device_automation": { + "condition_type": { + "is_battery_level": "{entity_name} Batterie niveau", + "is_humidity": "{entity_name} Fiichtegkeet", + "is_illuminance": "{entity_name} Beliichtung", + "is_power": "{entity_name} Leeschtung", + "is_pressure": "{entity_name} Drock", + "is_signal_strength": "{entity_name} Signal St\u00e4erkt", + "is_temperature": "{entity_name} Temperatur", + "is_timestamp": "{entity_name} Z\u00e4itstempel", + "is_value": "{entity_name} W\u00e4ert" + }, + "trigger_type": { + "battery_level": "{entity_name} Batterie niveau", + "humidity": "{entity_name} Fiichtegkeet", + "illuminance": "{entity_name} Beliichtung", + "power": "{entity_name} Leeschtung", + "pressure": "{entity_name} Drock", + "signal_strength": "{entity_name} Signal St\u00e4erkt", + "temperature": "{entity_name} Temperatur", + "timestamp": "{entity_name} Z\u00e4itstempel", + "value": "{entity_name} W\u00e4ert" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/no.json b/homeassistant/components/sensor/.translations/no.json new file mode 100644 index 00000000000..5f5eeaacd11 --- /dev/null +++ b/homeassistant/components/sensor/.translations/no.json @@ -0,0 +1,26 @@ +{ + "device_automation": { + "condition_type": { + "is_battery_level": "{entity_name} batteriniv\u00e5", + "is_humidity": "{entity_name} fuktighet", + "is_illuminance": "{entity_name} belysningsstyrke", + "is_power": "{entity_name} str\u00f8m", + "is_pressure": "{entity_name} trykk", + "is_signal_strength": "{entity_name} signalstyrke", + "is_temperature": "{entity_name} temperatur", + "is_timestamp": "{entity_name} tidsstempel", + "is_value": "{entity_name} verdi" + }, + "trigger_type": { + "battery_level": "{entity_name} batteriniv\u00e5", + "humidity": "{entity_name} fuktighet", + "illuminance": "{entity_name} belysningsstyrke", + "power": "{entity_name} str\u00f8m", + "pressure": "{entity_name} trykk", + "signal_strength": "{entity_name} signalstyrke", + "temperature": "{entity_name} temperatur", + "timestamp": "{entity_name} tidsstempel", + "value": "{entity_name} verdi" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/pl.json b/homeassistant/components/sensor/.translations/pl.json new file mode 100644 index 00000000000..da1dcc1d6fd --- /dev/null +++ b/homeassistant/components/sensor/.translations/pl.json @@ -0,0 +1,9 @@ +{ + "device_automation": { + "condition_type": { + "is_battery_level": "{entity_name} poziom na\u0142adowania baterii", + "is_humidity": "{entity_name} wilgotno\u015b\u0107", + "is_temperature": "{entity_name} temperatura" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/sl.json b/homeassistant/components/sensor/.translations/sl.json new file mode 100644 index 00000000000..e3bc994b6ea --- /dev/null +++ b/homeassistant/components/sensor/.translations/sl.json @@ -0,0 +1,26 @@ +{ + "device_automation": { + "condition_type": { + "is_battery_level": "{entity_name} raven baterije", + "is_humidity": "{entity_name} vla\u017enost", + "is_illuminance": "{entity_name} osvetlitev", + "is_power": "{entity_name} mo\u010d", + "is_pressure": "{entity_name} pritisk", + "is_signal_strength": "{entity_name} jakost signala", + "is_temperature": "{entity_name} temperatura", + "is_timestamp": "{entity_name} \u010dasovni \u017eig", + "is_value": "{entity_name} vrednost" + }, + "trigger_type": { + "battery_level": "{entity_name} raven baterije", + "humidity": "{entity_name} vla\u017enost", + "illuminance": "{entity_name} osvetljenosti", + "power": "{entity_name} mo\u010d", + "pressure": "{entity_name} tlak", + "signal_strength": "{entity_name} jakost signala", + "temperature": "{entity_name} temperatura", + "timestamp": "{entity_name} \u010dasovni \u017eig", + "value": "{entity_name} vrednost" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/zh-Hant.json b/homeassistant/components/sensor/.translations/zh-Hant.json new file mode 100644 index 00000000000..af97681ee76 --- /dev/null +++ b/homeassistant/components/sensor/.translations/zh-Hant.json @@ -0,0 +1,26 @@ +{ + "device_automation": { + "condition_type": { + "is_battery_level": "{entity_name} \u96fb\u91cf", + "is_humidity": "{entity_name} \u6fd5\u5ea6", + "is_illuminance": "{entity_name} \u7167\u5ea6", + "is_power": "{entity_name} \u96fb\u529b", + "is_pressure": "{entity_name} \u58d3\u529b", + "is_signal_strength": "{entity_name} \u8a0a\u865f\u5f37\u5ea6", + "is_temperature": "{entity_name} \u6eab\u5ea6", + "is_timestamp": "{entity_name} \u6642\u9593\u6a19\u8a18", + "is_value": "{entity_name} \u503c" + }, + "trigger_type": { + "battery_level": "{entity_name} \u96fb\u91cf", + "humidity": "{entity_name} \u6fd5\u5ea6", + "illuminance": "{entity_name} \u7167\u5ea6", + "power": "{entity_name} \u96fb\u529b", + "pressure": "{entity_name} \u58d3\u529b", + "signal_strength": "{entity_name} \u8a0a\u865f\u5f37\u5ea6", + "temperature": "{entity_name} \u6eab\u5ea6", + "timestamp": "{entity_name} \u6642\u9593\u6a19\u8a18", + "value": "{entity_name} \u503c" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/device_trigger.py b/homeassistant/components/sensor/device_trigger.py new file mode 100644 index 00000000000..50fb1dd5c14 --- /dev/null +++ b/homeassistant/components/sensor/device_trigger.py @@ -0,0 +1,156 @@ +"""Provides device triggers for sensors.""" +import voluptuous as vol + +import homeassistant.components.automation.numeric_state as numeric_state_automation +from homeassistant.components.device_automation import TRIGGER_BASE_SCHEMA +from homeassistant.const import ( + ATTR_DEVICE_CLASS, + ATTR_UNIT_OF_MEASUREMENT, + CONF_ABOVE, + CONF_BELOW, + CONF_ENTITY_ID, + CONF_FOR, + CONF_TYPE, + DEVICE_CLASS_BATTERY, + DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_ILLUMINANCE, + DEVICE_CLASS_POWER, + DEVICE_CLASS_PRESSURE, + DEVICE_CLASS_SIGNAL_STRENGTH, + DEVICE_CLASS_TEMPERATURE, + DEVICE_CLASS_TIMESTAMP, +) +from homeassistant.helpers.entity_registry import async_entries_for_device +from homeassistant.helpers import config_validation as cv + +from . import DOMAIN + + +# mypy: allow-untyped-defs, no-check-untyped-defs + +DEVICE_CLASS_NONE = "none" + +CONF_BATTERY_LEVEL = "battery_level" +CONF_HUMIDITY = "humidity" +CONF_ILLUMINANCE = "illuminance" +CONF_POWER = "power" +CONF_PRESSURE = "pressure" +CONF_SIGNAL_STRENGTH = "signal_strength" +CONF_TEMPERATURE = "temperature" +CONF_TIMESTAMP = "timestamp" +CONF_VALUE = "value" + +ENTITY_TRIGGERS = { + DEVICE_CLASS_BATTERY: [{CONF_TYPE: CONF_BATTERY_LEVEL}], + DEVICE_CLASS_HUMIDITY: [{CONF_TYPE: CONF_HUMIDITY}], + DEVICE_CLASS_ILLUMINANCE: [{CONF_TYPE: CONF_ILLUMINANCE}], + DEVICE_CLASS_POWER: [{CONF_TYPE: CONF_POWER}], + DEVICE_CLASS_PRESSURE: [{CONF_TYPE: CONF_PRESSURE}], + DEVICE_CLASS_SIGNAL_STRENGTH: [{CONF_TYPE: CONF_SIGNAL_STRENGTH}], + DEVICE_CLASS_TEMPERATURE: [{CONF_TYPE: CONF_TEMPERATURE}], + DEVICE_CLASS_TIMESTAMP: [{CONF_TYPE: CONF_TIMESTAMP}], + DEVICE_CLASS_NONE: [{CONF_TYPE: CONF_VALUE}], +} + + +TRIGGER_SCHEMA = vol.All( + TRIGGER_BASE_SCHEMA.extend( + { + vol.Required(CONF_ENTITY_ID): cv.entity_id, + vol.Required(CONF_TYPE): vol.In( + [ + CONF_BATTERY_LEVEL, + CONF_HUMIDITY, + CONF_ILLUMINANCE, + CONF_POWER, + CONF_PRESSURE, + CONF_SIGNAL_STRENGTH, + CONF_TEMPERATURE, + CONF_TIMESTAMP, + CONF_VALUE, + ] + ), + vol.Optional(CONF_BELOW): vol.Any(vol.Coerce(float)), + vol.Optional(CONF_ABOVE): vol.Any(vol.Coerce(float)), + vol.Optional(CONF_FOR): vol.Any( + vol.All(cv.time_period, cv.positive_timedelta), + cv.template, + cv.template_complex, + ), + vol.Optional(CONF_FOR): cv.positive_time_period_dict, + } + ), + cv.has_at_least_one_key(CONF_BELOW, CONF_ABOVE), +) + + +async def async_attach_trigger(hass, config, action, automation_info): + """Listen for state changes based on configuration.""" + numeric_state_config = { + numeric_state_automation.CONF_ENTITY_ID: config[CONF_ENTITY_ID], + numeric_state_automation.CONF_ABOVE: config.get(CONF_ABOVE), + numeric_state_automation.CONF_BELOW: config.get(CONF_BELOW), + numeric_state_automation.CONF_FOR: config.get(CONF_FOR), + } + if CONF_FOR in config: + numeric_state_config[CONF_FOR] = config[CONF_FOR] + + return await numeric_state_automation.async_attach_trigger( + hass, numeric_state_config, action, automation_info, platform_type="device" + ) + + +async def async_get_triggers(hass, device_id): + """List device triggers.""" + triggers = [] + entity_registry = await hass.helpers.entity_registry.async_get_registry() + + entries = [ + entry + for entry in async_entries_for_device(entity_registry, device_id) + if entry.domain == DOMAIN + ] + + for entry in entries: + device_class = DEVICE_CLASS_NONE + state = hass.states.get(entry.entity_id) + unit_of_measurement = ( + state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) if state else None + ) + + if not state or not unit_of_measurement: + continue + + device_class = state.attributes.get(ATTR_DEVICE_CLASS) + + templates = ENTITY_TRIGGERS.get( + device_class, ENTITY_TRIGGERS[DEVICE_CLASS_NONE] + ) + + triggers.extend( + ( + { + **automation, + "platform": "device", + "device_id": device_id, + "entity_id": entry.entity_id, + "domain": DOMAIN, + } + for automation in templates + ) + ) + + return triggers + + +async def async_get_trigger_capabilities(hass, trigger): + """List trigger capabilities.""" + return { + "extra_fields": vol.Schema( + { + vol.Optional(CONF_ABOVE): vol.Coerce(float), + vol.Optional(CONF_BELOW): vol.Coerce(float), + vol.Optional(CONF_FOR): cv.positive_time_period_dict, + } + ) + } diff --git a/homeassistant/components/sensor/manifest.json b/homeassistant/components/sensor/manifest.json index 813bcc27aca..1813d782d31 100644 --- a/homeassistant/components/sensor/manifest.json +++ b/homeassistant/components/sensor/manifest.json @@ -1,7 +1,7 @@ { "domain": "sensor", "name": "Sensor", - "documentation": "https://www.home-assistant.io/components/sensor", + "documentation": "https://www.home-assistant.io/integrations/sensor", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/sensor/strings.json b/homeassistant/components/sensor/strings.json new file mode 100644 index 00000000000..7df239facde --- /dev/null +++ b/homeassistant/components/sensor/strings.json @@ -0,0 +1,26 @@ +{ + "device_automation": { + "condition_type": { + "is_battery_level": "{entity_name} battery level", + "is_humidity": "{entity_name} humidity", + "is_illuminance": "{entity_name} illuminance", + "is_power": "{entity_name} power", + "is_pressure": "{entity_name} pressure", + "is_signal_strength": "{entity_name} signal strength", + "is_temperature": "{entity_name} temperature", + "is_timestamp": "{entity_name} timestamp", + "is_value": "{entity_name} value" + }, + "trigger_type": { + "battery_level": "{entity_name} battery level", + "humidity": "{entity_name} humidity", + "illuminance": "{entity_name} illuminance", + "power": "{entity_name} power", + "pressure": "{entity_name} pressure", + "signal_strength": "{entity_name} signal strength", + "temperature": "{entity_name} temperature", + "timestamp": "{entity_name} timestamp", + "value": "{entity_name} value" + } + } +} diff --git a/homeassistant/components/serial/manifest.json b/homeassistant/components/serial/manifest.json index 945464dbdec..61da7b4ff30 100644 --- a/homeassistant/components/serial/manifest.json +++ b/homeassistant/components/serial/manifest.json @@ -1,7 +1,7 @@ { "domain": "serial", "name": "Serial", - "documentation": "https://www.home-assistant.io/components/serial", + "documentation": "https://www.home-assistant.io/integrations/serial", "requirements": [ "pyserial-asyncio==0.4" ], diff --git a/homeassistant/components/serial_pm/manifest.json b/homeassistant/components/serial_pm/manifest.json index b2a645c88f3..b326481c55b 100644 --- a/homeassistant/components/serial_pm/manifest.json +++ b/homeassistant/components/serial_pm/manifest.json @@ -1,7 +1,7 @@ { "domain": "serial_pm", "name": "Serial pm", - "documentation": "https://www.home-assistant.io/components/serial_pm", + "documentation": "https://www.home-assistant.io/integrations/serial_pm", "requirements": [ "pmsensor==0.4" ], diff --git a/homeassistant/components/sesame/manifest.json b/homeassistant/components/sesame/manifest.json index ad6c71bd19f..f689cd46858 100644 --- a/homeassistant/components/sesame/manifest.json +++ b/homeassistant/components/sesame/manifest.json @@ -1,7 +1,7 @@ { "domain": "sesame", "name": "Sesame Smart Lock", - "documentation": "https://www.home-assistant.io/components/sesame", + "documentation": "https://www.home-assistant.io/integrations/sesame", "requirements": [ "pysesame2==1.0.1" ], diff --git a/homeassistant/components/seven_segments/manifest.json b/homeassistant/components/seven_segments/manifest.json index 45ce2f6a7a0..0bf49d4b906 100644 --- a/homeassistant/components/seven_segments/manifest.json +++ b/homeassistant/components/seven_segments/manifest.json @@ -1,7 +1,7 @@ { "domain": "seven_segments", "name": "Seven segments", - "documentation": "https://www.home-assistant.io/components/seven_segments", + "documentation": "https://www.home-assistant.io/integrations/seven_segments", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/seventeentrack/manifest.json b/homeassistant/components/seventeentrack/manifest.json index 47b36c12291..6a5ac165291 100644 --- a/homeassistant/components/seventeentrack/manifest.json +++ b/homeassistant/components/seventeentrack/manifest.json @@ -1,7 +1,7 @@ { "domain": "seventeentrack", "name": "Seventeentrack", - "documentation": "https://www.home-assistant.io/components/seventeentrack", + "documentation": "https://www.home-assistant.io/integrations/seventeentrack", "requirements": [ "py17track==2.2.2" ], diff --git a/homeassistant/components/shell_command/manifest.json b/homeassistant/components/shell_command/manifest.json index dfe9a8e8e6f..ca354ff2331 100644 --- a/homeassistant/components/shell_command/manifest.json +++ b/homeassistant/components/shell_command/manifest.json @@ -1,7 +1,7 @@ { "domain": "shell_command", "name": "Shell command", - "documentation": "https://www.home-assistant.io/components/shell_command", + "documentation": "https://www.home-assistant.io/integrations/shell_command", "requirements": [], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/shiftr/manifest.json b/homeassistant/components/shiftr/manifest.json index 02718396e5e..282d86ce419 100644 --- a/homeassistant/components/shiftr/manifest.json +++ b/homeassistant/components/shiftr/manifest.json @@ -1,7 +1,7 @@ { "domain": "shiftr", "name": "Shiftr", - "documentation": "https://www.home-assistant.io/components/shiftr", + "documentation": "https://www.home-assistant.io/integrations/shiftr", "requirements": [ "paho-mqtt==1.4.0" ], diff --git a/homeassistant/components/shodan/manifest.json b/homeassistant/components/shodan/manifest.json index 7ecc298e3f6..3ef01a44315 100644 --- a/homeassistant/components/shodan/manifest.json +++ b/homeassistant/components/shodan/manifest.json @@ -1,12 +1,12 @@ { "domain": "shodan", "name": "Shodan", - "documentation": "https://www.home-assistant.io/components/shodan", + "documentation": "https://www.home-assistant.io/integrations/shodan", "requirements": [ - "shodan==1.15.0" + "shodan==1.19.0" ], "dependencies": [], "codeowners": [ "@fabaff" ] -} +} \ No newline at end of file diff --git a/homeassistant/components/shopping_list/manifest.json b/homeassistant/components/shopping_list/manifest.json index b4ea3c2d388..41fe28defaa 100644 --- a/homeassistant/components/shopping_list/manifest.json +++ b/homeassistant/components/shopping_list/manifest.json @@ -1,7 +1,7 @@ { "domain": "shopping_list", "name": "Shopping list", - "documentation": "https://www.home-assistant.io/components/shopping_list", + "documentation": "https://www.home-assistant.io/integrations/shopping_list", "requirements": [], "dependencies": [ "http" diff --git a/homeassistant/components/sht31/manifest.json b/homeassistant/components/sht31/manifest.json index dfa22fc6e23..6ed947e5c82 100644 --- a/homeassistant/components/sht31/manifest.json +++ b/homeassistant/components/sht31/manifest.json @@ -1,7 +1,7 @@ { "domain": "sht31", "name": "Sht31", - "documentation": "https://www.home-assistant.io/components/sht31", + "documentation": "https://www.home-assistant.io/integrations/sht31", "requirements": [ "Adafruit-GPIO==1.0.3", "Adafruit-SHT31==1.0.2" diff --git a/homeassistant/components/sigfox/manifest.json b/homeassistant/components/sigfox/manifest.json index 1dc8f5255ce..689703302a7 100644 --- a/homeassistant/components/sigfox/manifest.json +++ b/homeassistant/components/sigfox/manifest.json @@ -1,7 +1,7 @@ { "domain": "sigfox", "name": "Sigfox", - "documentation": "https://www.home-assistant.io/components/sigfox", + "documentation": "https://www.home-assistant.io/integrations/sigfox", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/simplepush/manifest.json b/homeassistant/components/simplepush/manifest.json index cbf2833a4f7..d303ac221dd 100644 --- a/homeassistant/components/simplepush/manifest.json +++ b/homeassistant/components/simplepush/manifest.json @@ -1,7 +1,7 @@ { "domain": "simplepush", "name": "Simplepush", - "documentation": "https://www.home-assistant.io/components/simplepush", + "documentation": "https://www.home-assistant.io/integrations/simplepush", "requirements": [ "simplepush==1.1.4" ], diff --git a/homeassistant/components/simplisafe/.translations/nn.json b/homeassistant/components/simplisafe/.translations/nn.json new file mode 100644 index 00000000000..0568cad3f6d --- /dev/null +++ b/homeassistant/components/simplisafe/.translations/nn.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "SimpliSafe" + } +} \ No newline at end of file diff --git a/homeassistant/components/simplisafe/.translations/pl.json b/homeassistant/components/simplisafe/.translations/pl.json index c4d616600f5..ad8a15d06b7 100644 --- a/homeassistant/components/simplisafe/.translations/pl.json +++ b/homeassistant/components/simplisafe/.translations/pl.json @@ -2,7 +2,7 @@ "config": { "error": { "identifier_exists": "Konto jest ju\u017c zarejestrowane", - "invalid_credentials": "Nieprawid\u0142owe po\u015bwiadczenia" + "invalid_credentials": "Nieprawid\u0142owe dane uwierzytelniaj\u0105ce" }, "step": { "user": { diff --git a/homeassistant/components/simplisafe/.translations/ru.json b/homeassistant/components/simplisafe/.translations/ru.json index f685297890e..e82172f92f8 100644 --- a/homeassistant/components/simplisafe/.translations/ru.json +++ b/homeassistant/components/simplisafe/.translations/ru.json @@ -1,7 +1,7 @@ { "config": { "error": { - "identifier_exists": "\u0423\u0447\u0435\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u0430", + "identifier_exists": "\u0423\u0447\u0435\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u0430.", "invalid_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435" }, "step": { diff --git a/homeassistant/components/simplisafe/manifest.json b/homeassistant/components/simplisafe/manifest.json index 8a03ac47402..96b337def55 100644 --- a/homeassistant/components/simplisafe/manifest.json +++ b/homeassistant/components/simplisafe/manifest.json @@ -2,9 +2,9 @@ "domain": "simplisafe", "name": "Simplisafe", "config_flow": true, - "documentation": "https://www.home-assistant.io/components/simplisafe", + "documentation": "https://www.home-assistant.io/integrations/simplisafe", "requirements": [ - "simplisafe-python==4.3.0" + "simplisafe-python==5.0.1" ], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/simulated/manifest.json b/homeassistant/components/simulated/manifest.json index b972152aea4..4dcd0715704 100644 --- a/homeassistant/components/simulated/manifest.json +++ b/homeassistant/components/simulated/manifest.json @@ -1,7 +1,7 @@ { "domain": "simulated", "name": "Simulated", - "documentation": "https://www.home-assistant.io/components/simulated", + "documentation": "https://www.home-assistant.io/integrations/simulated", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/sisyphus/manifest.json b/homeassistant/components/sisyphus/manifest.json index f8e6e1bf14d..d5cfc488151 100644 --- a/homeassistant/components/sisyphus/manifest.json +++ b/homeassistant/components/sisyphus/manifest.json @@ -1,7 +1,7 @@ { "domain": "sisyphus", "name": "Sisyphus", - "documentation": "https://www.home-assistant.io/components/sisyphus", + "documentation": "https://www.home-assistant.io/integrations/sisyphus", "requirements": [ "sisyphus-control==2.2.1" ], diff --git a/homeassistant/components/sky_hub/manifest.json b/homeassistant/components/sky_hub/manifest.json index 46337918f84..6be45690629 100644 --- a/homeassistant/components/sky_hub/manifest.json +++ b/homeassistant/components/sky_hub/manifest.json @@ -1,7 +1,7 @@ { "domain": "sky_hub", "name": "Sky hub", - "documentation": "https://www.home-assistant.io/components/sky_hub", + "documentation": "https://www.home-assistant.io/integrations/sky_hub", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/skybeacon/manifest.json b/homeassistant/components/skybeacon/manifest.json index 893a1f3469e..a3cb97cdc2d 100644 --- a/homeassistant/components/skybeacon/manifest.json +++ b/homeassistant/components/skybeacon/manifest.json @@ -1,7 +1,7 @@ { "domain": "skybeacon", "name": "Skybeacon", - "documentation": "https://www.home-assistant.io/components/skybeacon", + "documentation": "https://www.home-assistant.io/integrations/skybeacon", "requirements": [ "pygatt[GATTTOOL]==4.0.1" ], diff --git a/homeassistant/components/skybell/manifest.json b/homeassistant/components/skybell/manifest.json index 843fd3d13b0..8a005be52e2 100644 --- a/homeassistant/components/skybell/manifest.json +++ b/homeassistant/components/skybell/manifest.json @@ -1,7 +1,7 @@ { "domain": "skybell", "name": "Skybell", - "documentation": "https://www.home-assistant.io/components/skybell", + "documentation": "https://www.home-assistant.io/integrations/skybell", "requirements": [ "skybellpy==0.4.0" ], diff --git a/homeassistant/components/slack/manifest.json b/homeassistant/components/slack/manifest.json index 38fa5fa0894..10dc4bffccf 100644 --- a/homeassistant/components/slack/manifest.json +++ b/homeassistant/components/slack/manifest.json @@ -1,7 +1,7 @@ { "domain": "slack", "name": "Slack", - "documentation": "https://www.home-assistant.io/components/slack", + "documentation": "https://www.home-assistant.io/integrations/slack", "requirements": [ "slacker==0.13.0" ], diff --git a/homeassistant/components/sleepiq/manifest.json b/homeassistant/components/sleepiq/manifest.json index ea16d626af4..ac605f42b0c 100644 --- a/homeassistant/components/sleepiq/manifest.json +++ b/homeassistant/components/sleepiq/manifest.json @@ -1,7 +1,7 @@ { "domain": "sleepiq", "name": "Sleepiq", - "documentation": "https://www.home-assistant.io/components/sleepiq", + "documentation": "https://www.home-assistant.io/integrations/sleepiq", "requirements": [ "sleepyq==0.7" ], diff --git a/homeassistant/components/slide/manifest.json b/homeassistant/components/slide/manifest.json index f9fd7f242b6..3d2836e1809 100644 --- a/homeassistant/components/slide/manifest.json +++ b/homeassistant/components/slide/manifest.json @@ -1,7 +1,7 @@ { "domain": "slide", "name": "Slide", - "documentation": "https://www.home-assistant.io/components/slide", + "documentation": "https://www.home-assistant.io/integrations/slide", "requirements": [ "goslide-api==0.5.1" ], diff --git a/homeassistant/components/sma/manifest.json b/homeassistant/components/sma/manifest.json index ea3a33d55ff..8f0caa7c548 100644 --- a/homeassistant/components/sma/manifest.json +++ b/homeassistant/components/sma/manifest.json @@ -1,7 +1,7 @@ { "domain": "sma", "name": "Sma", - "documentation": "https://www.home-assistant.io/components/sma", + "documentation": "https://www.home-assistant.io/integrations/sma", "requirements": [ "pysma==0.3.4" ], diff --git a/homeassistant/components/smappee/manifest.json b/homeassistant/components/smappee/manifest.json index 361802f312e..6f5a4b7b64b 100644 --- a/homeassistant/components/smappee/manifest.json +++ b/homeassistant/components/smappee/manifest.json @@ -1,7 +1,7 @@ { "domain": "smappee", "name": "Smappee", - "documentation": "https://www.home-assistant.io/components/smappee", + "documentation": "https://www.home-assistant.io/integrations/smappee", "requirements": [ "smappy==0.2.16" ], diff --git a/homeassistant/components/smarthab/__init__.py b/homeassistant/components/smarthab/__init__.py index 198a5e9cabc..7206bea110b 100644 --- a/homeassistant/components/smarthab/__init__.py +++ b/homeassistant/components/smarthab/__init__.py @@ -2,7 +2,7 @@ Support for SmartHab device integration. For more details about this component, please refer to the documentation at -https://home-assistant.io/components/smarthab/ +https://home-assistant.io/integrations/smarthab/ """ import logging diff --git a/homeassistant/components/smarthab/cover.py b/homeassistant/components/smarthab/cover.py index 2ae9cadf1ac..3d5b4259aa9 100644 --- a/homeassistant/components/smarthab/cover.py +++ b/homeassistant/components/smarthab/cover.py @@ -2,7 +2,7 @@ Support for SmartHab device integration. For more details about this component, please refer to the documentation at -https://home-assistant.io/components/smarthab/ +https://home-assistant.io/integrations/smarthab/ """ import logging from datetime import timedelta diff --git a/homeassistant/components/smarthab/light.py b/homeassistant/components/smarthab/light.py index 0f7b3c9ef80..a8a55dea48a 100644 --- a/homeassistant/components/smarthab/light.py +++ b/homeassistant/components/smarthab/light.py @@ -2,7 +2,7 @@ Support for SmartHab device integration. For more details about this component, please refer to the documentation at -https://home-assistant.io/components/smarthab/ +https://home-assistant.io/integrations/smarthab/ """ import logging from datetime import timedelta diff --git a/homeassistant/components/smarthab/manifest.json b/homeassistant/components/smarthab/manifest.json index 18b587bac92..515f3dcbf65 100644 --- a/homeassistant/components/smarthab/manifest.json +++ b/homeassistant/components/smarthab/manifest.json @@ -1,7 +1,7 @@ { "domain": "smarthab", "name": "SmartHab", - "documentation": "https://www.home-assistant.io/components/smarthab", + "documentation": "https://www.home-assistant.io/integrations/smarthab", "requirements": [ "smarthab==0.20" ], diff --git a/homeassistant/components/smartthings/.translations/he.json b/homeassistant/components/smartthings/.translations/he.json index c38afd989d2..7f80900d828 100644 --- a/homeassistant/components/smartthings/.translations/he.json +++ b/homeassistant/components/smartthings/.translations/he.json @@ -16,7 +16,7 @@ "access_token": "\u05d0\u05e1\u05d9\u05de\u05d5\u05df \u05d2\u05d9\u05e9\u05d4" }, "description": "\u05d4\u05d6\u05df SmartThings [\u05d0\u05e1\u05d9\u05de\u05d5\u05df \u05d2\u05d9\u05e9\u05d4 \u05d0\u05d9\u05e9\u05d9\u05ea] ( {token_url} ) \u05e9\u05e0\u05d5\u05e6\u05e8 \u05dc\u05e4\u05d9 [\u05d4\u05d5\u05e8\u05d0\u05d5\u05ea] ( {component_url} ).", - "title": "\u05d4\u05d6\u05df \u05d0\u05e1\u05d9\u05de\u05d5\u05df \u05d2\u05d9\u05e9\u05d4 \u05d0\u05d9\u05e9\u05d9 " + "title": "\u05d4\u05d6\u05df \u05d0\u05e1\u05d9\u05de\u05d5\u05df \u05d2\u05d9\u05e9\u05d4 \u05d0\u05d9\u05e9 " }, "wait_install": { "description": "\u05d4\u05ea\u05e7\u05df \u05d0\u05ea \u05d4- Home Assistant SmartApp \u05dc\u05e4\u05d7\u05d5\u05ea \u05d1\u05de\u05d9\u05e7\u05d5\u05dd \u05d0\u05d7\u05d3 \u05d5\u05dc\u05d7\u05e5 \u05e2\u05dc \u05e9\u05dc\u05d7.", diff --git a/homeassistant/components/smartthings/.translations/no.json b/homeassistant/components/smartthings/.translations/no.json index fe93407b429..b539e315ea3 100644 --- a/homeassistant/components/smartthings/.translations/no.json +++ b/homeassistant/components/smartthings/.translations/no.json @@ -5,7 +5,7 @@ "app_setup_error": "Kan ikke konfigurere SmartApp. Vennligst pr\u00f8v p\u00e5 nytt.", "base_url_not_https": "`base_url` for `http` komponenten m\u00e5 konfigureres og starte med `https://`.", "token_already_setup": "Token har allerede blitt satt opp.", - "token_forbidden": "Tollet har ikke de n\u00f8dvendige OAuth m\u00e5lene.", + "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.", "webhook_error": "SmartThings kunne ikke validere endepunktet konfigurert i `base_url`. Vennligst se komponent krav." diff --git a/homeassistant/components/smartthings/config_flow.py b/homeassistant/components/smartthings/config_flow.py index a3ca8fc7629..54c9f815008 100644 --- a/homeassistant/components/smartthings/config_flow.py +++ b/homeassistant/components/smartthings/config_flow.py @@ -176,7 +176,7 @@ class SmartThingsFlowHandler(config_entries.ConfigFlow): errors=errors, description_placeholders={ "token_url": "https://account.smartthings.com/tokens", - "component_url": "https://www.home-assistant.io/components/smartthings/", + "component_url": "https://www.home-assistant.io/integrations/smartthings/", }, ) diff --git a/homeassistant/components/smartthings/manifest.json b/homeassistant/components/smartthings/manifest.json index 621da91f4f8..8b5bf65afa1 100644 --- a/homeassistant/components/smartthings/manifest.json +++ b/homeassistant/components/smartthings/manifest.json @@ -2,7 +2,7 @@ "domain": "smartthings", "name": "Smartthings", "config_flow": true, - "documentation": "https://www.home-assistant.io/components/smartthings", + "documentation": "https://www.home-assistant.io/integrations/smartthings", "requirements": [ "pysmartapp==0.3.2", "pysmartthings==0.6.9" diff --git a/homeassistant/components/smarty/manifest.json b/homeassistant/components/smarty/manifest.json index b2e3deb4008..003ee804b55 100644 --- a/homeassistant/components/smarty/manifest.json +++ b/homeassistant/components/smarty/manifest.json @@ -1,7 +1,7 @@ { "domain": "smarty", "name": "smarty", - "documentation": "https://www.home-assistant.io/components/smarty", + "documentation": "https://www.home-assistant.io/integrations/smarty", "requirements": [ "pysmarty==0.8" ], diff --git a/homeassistant/components/smhi/.translations/ru.json b/homeassistant/components/smhi/.translations/ru.json index 88ea988ff1b..03b17b3ba8b 100644 --- a/homeassistant/components/smhi/.translations/ru.json +++ b/homeassistant/components/smhi/.translations/ru.json @@ -1,7 +1,7 @@ { "config": { "error": { - "name_exists": "\u0418\u043c\u044f \u0443\u0436\u0435 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u0435\u0442", + "name_exists": "\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.", "wrong_location": "\u0422\u043e\u043b\u044c\u043a\u043e \u0434\u043b\u044f \u0428\u0432\u0435\u0446\u0438\u0438" }, "step": { diff --git a/homeassistant/components/smhi/manifest.json b/homeassistant/components/smhi/manifest.json index 421eadca51c..b3fdc2825ae 100644 --- a/homeassistant/components/smhi/manifest.json +++ b/homeassistant/components/smhi/manifest.json @@ -2,7 +2,7 @@ "domain": "smhi", "name": "Smhi", "config_flow": true, - "documentation": "https://www.home-assistant.io/components/smhi", + "documentation": "https://www.home-assistant.io/integrations/smhi", "requirements": [ "smhi-pkg==1.0.10" ], diff --git a/homeassistant/components/smtp/manifest.json b/homeassistant/components/smtp/manifest.json index 2e1a8d826ce..b6ec6b4ca0d 100644 --- a/homeassistant/components/smtp/manifest.json +++ b/homeassistant/components/smtp/manifest.json @@ -1,7 +1,7 @@ { "domain": "smtp", "name": "Smtp", - "documentation": "https://www.home-assistant.io/components/smtp", + "documentation": "https://www.home-assistant.io/integrations/smtp", "requirements": [], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/snapcast/manifest.json b/homeassistant/components/snapcast/manifest.json index 0bb36c1317a..1314b91f982 100644 --- a/homeassistant/components/snapcast/manifest.json +++ b/homeassistant/components/snapcast/manifest.json @@ -1,7 +1,7 @@ { "domain": "snapcast", "name": "Snapcast", - "documentation": "https://www.home-assistant.io/components/snapcast", + "documentation": "https://www.home-assistant.io/integrations/snapcast", "requirements": [ "snapcast==2.0.10" ], diff --git a/homeassistant/components/snips/manifest.json b/homeassistant/components/snips/manifest.json index 58fddb7a3f4..e15d86e811f 100644 --- a/homeassistant/components/snips/manifest.json +++ b/homeassistant/components/snips/manifest.json @@ -1,7 +1,7 @@ { "domain": "snips", "name": "Snips", - "documentation": "https://www.home-assistant.io/components/snips", + "documentation": "https://www.home-assistant.io/integrations/snips", "requirements": [], "dependencies": [ "mqtt" diff --git a/homeassistant/components/snmp/manifest.json b/homeassistant/components/snmp/manifest.json index a3ac3af985d..d3942ab4a32 100644 --- a/homeassistant/components/snmp/manifest.json +++ b/homeassistant/components/snmp/manifest.json @@ -1,7 +1,7 @@ { "domain": "snmp", "name": "Snmp", - "documentation": "https://www.home-assistant.io/components/snmp", + "documentation": "https://www.home-assistant.io/integrations/snmp", "requirements": [ "pysnmp==4.4.11" ], diff --git a/homeassistant/components/sochain/manifest.json b/homeassistant/components/sochain/manifest.json index 23fad3683cb..93361d41e2b 100644 --- a/homeassistant/components/sochain/manifest.json +++ b/homeassistant/components/sochain/manifest.json @@ -1,7 +1,7 @@ { "domain": "sochain", "name": "Sochain", - "documentation": "https://www.home-assistant.io/components/sochain", + "documentation": "https://www.home-assistant.io/integrations/sochain", "requirements": [ "python-sochain-api==0.0.2" ], diff --git a/homeassistant/components/socialblade/manifest.json b/homeassistant/components/socialblade/manifest.json index e800bd7266a..c90342018f8 100644 --- a/homeassistant/components/socialblade/manifest.json +++ b/homeassistant/components/socialblade/manifest.json @@ -1,7 +1,7 @@ { "domain": "socialblade", "name": "Socialblade", - "documentation": "https://www.home-assistant.io/components/socialblade", + "documentation": "https://www.home-assistant.io/integrations/socialblade", "requirements": [ "socialbladeclient==0.2" ], diff --git a/homeassistant/components/solaredge/.translations/bg.json b/homeassistant/components/solaredge/.translations/bg.json index 72f1ad2a4c7..e4223e373fd 100644 --- a/homeassistant/components/solaredge/.translations/bg.json +++ b/homeassistant/components/solaredge/.translations/bg.json @@ -15,6 +15,7 @@ }, "title": "\u041e\u043f\u0440\u0435\u0434\u0435\u043b\u0435\u0442\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u0438\u0442\u0435 \u043d\u0430 \u043f\u0440\u0438\u043b\u043e\u0436\u043d\u043e \u043f\u0440\u043e\u0433\u0440\u0430\u043c\u043d\u0438\u044f \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441 (API) \u0437\u0430 \u0442\u0430\u0437\u0438 \u0438\u043d\u0441\u0442\u0430\u043b\u0430\u0446\u0438\u044f" } - } + }, + "title": "SolarEdge" } } \ No newline at end of file diff --git a/homeassistant/components/solaredge/.translations/ru.json b/homeassistant/components/solaredge/.translations/ru.json index fe36e4296fe..d8622cdd2c1 100644 --- a/homeassistant/components/solaredge/.translations/ru.json +++ b/homeassistant/components/solaredge/.translations/ru.json @@ -1,10 +1,10 @@ { "config": { "abort": { - "site_exists": "\u042d\u0442\u043e\u0442 site_id \u0443\u0436\u0435 \u0441\u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u043e\u0432\u0430\u043d" + "site_exists": "\u042d\u0442\u043e\u0442 site_id \u0443\u0436\u0435 \u0441\u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u043e\u0432\u0430\u043d." }, "error": { - "site_exists": "\u042d\u0442\u043e\u0442 site_id \u0443\u0436\u0435 \u0441\u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u043e\u0432\u0430\u043d" + "site_exists": "\u042d\u0442\u043e\u0442 site_id \u0443\u0436\u0435 \u0441\u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u043e\u0432\u0430\u043d." }, "step": { "user": { diff --git a/homeassistant/components/solaredge/manifest.json b/homeassistant/components/solaredge/manifest.json index 7452790cd60..f2635936cb5 100644 --- a/homeassistant/components/solaredge/manifest.json +++ b/homeassistant/components/solaredge/manifest.json @@ -1,7 +1,7 @@ { "domain": "solaredge", "name": "Solaredge", - "documentation": "https://www.home-assistant.io/components/solaredge", + "documentation": "https://www.home-assistant.io/integrations/solaredge", "requirements": [ "solaredge==0.0.2", "stringcase==1.2.0" diff --git a/homeassistant/components/solaredge_local/manifest.json b/homeassistant/components/solaredge_local/manifest.json index 5fb07011983..91ed6f55ee1 100644 --- a/homeassistant/components/solaredge_local/manifest.json +++ b/homeassistant/components/solaredge_local/manifest.json @@ -1,8 +1,13 @@ { - "domain": "solaredge_local", - "name": "Solar Edge Local", - "documentation": "", - "dependencies": [], - "codeowners": ["@drobtravels"], - "requirements": ["solaredge-local==0.1.4"] - } \ No newline at end of file + "domain": "solaredge_local", + "name": "Solar Edge Local", + "documentation": "https://www.home-assistant.io/integrations/solaredge_local", + "requirements": [ + "solaredge-local==0.2.0" + ], + "dependencies": [], + "codeowners": [ + "@drobtravels", + "@scheric" + ] +} diff --git a/homeassistant/components/solaredge_local/sensor.py b/homeassistant/components/solaredge_local/sensor.py index 8586d950e39..4fc62e44921 100644 --- a/homeassistant/components/solaredge_local/sensor.py +++ b/homeassistant/components/solaredge_local/sensor.py @@ -1,19 +1,20 @@ -""" -Support for SolarEdge Monitoring API. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/sensor.solaredge_local/ -""" +"""Support for SolarEdge-local Monitoring API.""" import logging from datetime import timedelta +import statistics from requests.exceptions import HTTPError, ConnectTimeout from solaredge_local import SolarEdge import voluptuous as vol - from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import CONF_IP_ADDRESS, CONF_NAME, POWER_WATT, ENERGY_WATT_HOUR +from homeassistant.const import ( + CONF_IP_ADDRESS, + CONF_NAME, + POWER_WATT, + ENERGY_WATT_HOUR, + TEMP_CELSIUS, +) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle @@ -24,9 +25,10 @@ UPDATE_DELAY = timedelta(seconds=10) # Supported sensor types: # Key: ['json_key', 'name', unit, icon] SENSOR_TYPES = { - "lifetime_energy": [ - "energyTotal", - "Lifetime energy", + "current_power": ["currentPower", "Current Power", POWER_WATT, "mdi:solar-power"], + "energy_this_month": [ + "energyThisMonth", + "Energy this month", ENERGY_WATT_HOUR, "mdi:solar-power", ], @@ -36,19 +38,48 @@ SENSOR_TYPES = { ENERGY_WATT_HOUR, "mdi:solar-power", ], - "energy_this_month": [ - "energyThisMonth", - "Energy this month", - ENERGY_WATT_HOUR, - "mdi:solar-power", - ], "energy_today": [ "energyToday", "Energy today", ENERGY_WATT_HOUR, "mdi:solar-power", ], - "current_power": ["currentPower", "Current Power", POWER_WATT, "mdi:solar-power"], + "inverter_temperature": [ + "invertertemperature", + "Inverter Temperature", + TEMP_CELSIUS, + "mdi:thermometer", + ], + "lifetime_energy": [ + "energyTotal", + "Lifetime energy", + ENERGY_WATT_HOUR, + "mdi:solar-power", + ], + "optimizer_current": [ + "optimizercurrent", + "Avrage Optimizer Current", + "A", + "mdi:solar-panel", + ], + "optimizer_power": [ + "optimizerpower", + "Avrage Optimizer Power", + POWER_WATT, + "mdi:solar-panel", + ], + "optimizer_temperature": [ + "optimizertemperature", + "Avrage Optimizer Temperature", + TEMP_CELSIUS, + "mdi:solar-panel", + ], + "optimizer_voltage": [ + "optimizervoltage", + "Avrage Optimizer Voltage", + "V", + "mdi:solar-panel", + ], } PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( @@ -66,18 +97,16 @@ def setup_platform(hass, config, add_entities, discovery_info=None): ip_address = config[CONF_IP_ADDRESS] platform_name = config[CONF_NAME] - # Create new SolarEdge object to retrieve data + # Create new SolarEdge object to retrieve data. api = SolarEdge(f"http://{ip_address}/") - # Check if api can be reached and site is active + # Check if api can be reached and site is active. try: status = api.get_status() - - status.energy # pylint: disable=pointless-statement _LOGGER.debug("Credentials correct and site is active") except AttributeError: - _LOGGER.error("Missing details data in solaredge response") - _LOGGER.debug("Response is: %s", status) + _LOGGER.error("Missing details data in solaredge status") + _LOGGER.debug("Status is: %s", status) return except (ConnectTimeout, HTTPError): _LOGGER.error("Could not retrieve details from SolarEdge API") @@ -111,7 +140,7 @@ class SolarEdgeSensor(Entity): @property def name(self): """Return the name.""" - return "{} ({})".format(self.platform_name, SENSOR_TYPES[self.sensor_key][1]) + return f"{self.platform_name} ({SENSOR_TYPES[self.sensor_key][1]})" @property def unit_of_measurement(self): @@ -147,21 +176,55 @@ class SolarEdgeData: def update(self): """Update the data from the SolarEdge Monitoring API.""" try: - response = self.api.get_status() - _LOGGER.debug("response from SolarEdge: %s", response) - except (ConnectTimeout): + status = self.api.get_status() + _LOGGER.debug("Status from SolarEdge: %s", status) + except ConnectTimeout: _LOGGER.error("Connection timeout, skipping update") return - except (HTTPError): - _LOGGER.error("Could not retrieve data, skipping update") + except HTTPError: + _LOGGER.error("Could not retrieve status, skipping update") return try: - self.data["energyTotal"] = response.energy.total - self.data["energyThisYear"] = response.energy.thisYear - self.data["energyThisMonth"] = response.energy.thisMonth - self.data["energyToday"] = response.energy.today - self.data["currentPower"] = response.powerWatt - _LOGGER.debug("Updated SolarEdge overview data: %s", self.data) - except AttributeError: - _LOGGER.error("Missing details data in SolarEdge response") + maintenance = self.api.get_maintenance() + _LOGGER.debug("Maintenance from SolarEdge: %s", maintenance) + except ConnectTimeout: + _LOGGER.error("Connection timeout, skipping update") + return + except HTTPError: + _LOGGER.error("Could not retrieve maintenance, skipping update") + return + + temperature = [] + voltage = [] + current = [] + power = 0 + + for optimizer in maintenance.diagnostics.inverters.primary.optimizer: + if not optimizer.online: + continue + temperature.append(optimizer.temperature.value) + voltage.append(optimizer.inputV) + current.append(optimizer.inputC) + + if not voltage: + temperature.append(0) + voltage.append(0) + current.append(0) + else: + power = statistics.mean(voltage) * statistics.mean(current) + + if status.sn: + self.data["energyTotal"] = round(status.energy.total, 2) + self.data["energyThisYear"] = round(status.energy.thisYear, 2) + self.data["energyThisMonth"] = round(status.energy.thisMonth, 2) + self.data["energyToday"] = round(status.energy.today, 2) + self.data["currentPower"] = round(status.powerWatt, 2) + self.data[ + "invertertemperature" + ] = status.inverters.primary.temperature.value + if maintenance.system.name: + self.data["optimizertemperature"] = statistics.mean(temperature) + self.data["optimizervoltage"] = statistics.mean(voltage) + self.data["optimizercurrent"] = statistics.mean(current) + self.data["optimizerpower"] = power diff --git a/homeassistant/components/solax/manifest.json b/homeassistant/components/solax/manifest.json index 52e50ab4799..14ac59d4621 100644 --- a/homeassistant/components/solax/manifest.json +++ b/homeassistant/components/solax/manifest.json @@ -1,9 +1,9 @@ { "domain": "solax", "name": "Solax Inverter", - "documentation": "https://www.home-assistant.io/components/solax", + "documentation": "https://www.home-assistant.io/integrations/solax", "requirements": [ - "solax==0.1.2" + "solax==0.2.2" ], "dependencies": [], "codeowners": ["@squishykid"] diff --git a/homeassistant/components/solax/sensor.py b/homeassistant/components/solax/sensor.py index 0c1cfcf21da..a5b4547b344 100644 --- a/homeassistant/components/solax/sensor.py +++ b/homeassistant/components/solax/sensor.py @@ -4,9 +4,11 @@ import asyncio from datetime import timedelta import logging +from solax import real_time_api +from solax.inverter import InverterError import voluptuous as vol -from homeassistant.const import TEMP_CELSIUS, CONF_IP_ADDRESS +from homeassistant.const import TEMP_CELSIUS, CONF_IP_ADDRESS, CONF_PORT from homeassistant.helpers.entity import Entity import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -15,24 +17,28 @@ from homeassistant.helpers.event import async_track_time_interval _LOGGER = logging.getLogger(__name__) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({vol.Required(CONF_IP_ADDRESS): cv.string}) +DEFAULT_PORT = 80 + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_IP_ADDRESS): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + } +) SCAN_INTERVAL = timedelta(seconds=30) async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Platform setup.""" - import solax - - api = solax.RealTimeAPI(config[CONF_IP_ADDRESS]) + api = await real_time_api(config[CONF_IP_ADDRESS], config[CONF_PORT]) endpoint = RealTimeDataEndpoint(hass, api) resp = await api.get_data() serial = resp.serial_number hass.async_add_job(endpoint.async_refresh) async_track_time_interval(hass, endpoint.async_refresh, SCAN_INTERVAL) devices = [] - for sensor in solax.INVERTER_SENSORS: - idx, unit = solax.INVERTER_SENSORS[sensor] + for sensor, (idx, unit) in api.inverter.sensor_map().items(): if unit == "C": unit = TEMP_CELSIUS uid = f"{serial}-{idx}" @@ -56,16 +62,14 @@ class RealTimeDataEndpoint: This is the only method that should fetch new data for Home Assistant. """ - from solax import SolaxRequestError - try: api_response = await self.api.get_data() self.ready.set() - except SolaxRequestError: + except InverterError: if now is not None: self.ready.clear() - else: - raise PlatformNotReady + return + raise PlatformNotReady data = api_response.data for sensor in self.sensors: if sensor.key in data: diff --git a/homeassistant/components/soma/.translations/ca.json b/homeassistant/components/soma/.translations/ca.json new file mode 100644 index 00000000000..6bd4737d6fc --- /dev/null +++ b/homeassistant/components/soma/.translations/ca.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "already_setup": "Nom\u00e9s pots configurar un compte de Soma.", + "authorize_url_timeout": "S'ha acabat el temps d'espera durant la generaci\u00f3 de l'URL d'autoritzaci\u00f3.", + "missing_configuration": "El component Soma no est\u00e0 configurat. Mira'n la documentaci\u00f3." + }, + "create_entry": { + "default": "Autenticaci\u00f3 exitosa amb Soma." + }, + "title": "Soma" + } +} \ No newline at end of file diff --git a/homeassistant/components/soma/.translations/da.json b/homeassistant/components/soma/.translations/da.json new file mode 100644 index 00000000000..a82da0ce24d --- /dev/null +++ b/homeassistant/components/soma/.translations/da.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "already_setup": "Du kan kun konfigurere en Soma-konto.", + "authorize_url_timeout": "Timeout ved generering af autoriseret url.", + "missing_configuration": "Soma-komponenten er ikke konfigureret. F\u00f8lg venligst dokumentationen." + }, + "create_entry": { + "default": "Godkendt med Soma." + }, + "title": "Soma" + } +} \ No newline at end of file diff --git a/homeassistant/components/soma/.translations/de.json b/homeassistant/components/soma/.translations/de.json new file mode 100644 index 00000000000..d93eec8aed7 --- /dev/null +++ b/homeassistant/components/soma/.translations/de.json @@ -0,0 +1,9 @@ +{ + "config": { + "abort": { + "already_setup": "Du kannst nur ein einziges Soma-Konto konfigurieren.", + "authorize_url_timeout": "Zeit\u00fcberschreitung beim Erstellen der Authorisierungs-URL.", + "missing_configuration": "Die Soma-Komponente ist nicht konfiguriert. Bitte folgen Sie der Dokumentation." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/soma/.translations/en.json b/homeassistant/components/soma/.translations/en.json new file mode 100644 index 00000000000..5dea73fcc22 --- /dev/null +++ b/homeassistant/components/soma/.translations/en.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "already_setup": "You can only configure one Soma account.", + "authorize_url_timeout": "Timeout generating authorize url.", + "missing_configuration": "The Soma component is not configured. Please follow the documentation." + }, + "create_entry": { + "default": "Successfully authenticated with Soma." + }, + "title": "Soma" + } +} \ No newline at end of file diff --git a/homeassistant/components/soma/.translations/es.json b/homeassistant/components/soma/.translations/es.json new file mode 100644 index 00000000000..8126b6ea5ae --- /dev/null +++ b/homeassistant/components/soma/.translations/es.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "already_setup": "S\u00f3lo puede configurar una cuenta de Soma.", + "authorize_url_timeout": "Tiempo de espera agotado para la autorizaci\u00f3n de la url.", + "missing_configuration": "El componente Soma no est\u00e1 configurado. Por favor, leer la documentaci\u00f3n." + }, + "create_entry": { + "default": "Autenticado con \u00e9xito con Soma." + }, + "title": "Soma" + } +} \ No newline at end of file diff --git a/homeassistant/components/soma/.translations/fr.json b/homeassistant/components/soma/.translations/fr.json new file mode 100644 index 00000000000..e990fb98dc2 --- /dev/null +++ b/homeassistant/components/soma/.translations/fr.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "already_setup": "Vous ne pouvez configurer qu'un seul compte Soma.", + "authorize_url_timeout": "D\u00e9lai d'attente g\u00e9n\u00e9rant l'autorisation de l'URL.", + "missing_configuration": "Le composant Soma n'est pas configur\u00e9. Veuillez suivre la documentation." + }, + "create_entry": { + "default": "Authentifi\u00e9 avec succ\u00e8s avec Soma." + }, + "title": "Soma" + } +} \ No newline at end of file diff --git a/homeassistant/components/soma/.translations/it.json b/homeassistant/components/soma/.translations/it.json new file mode 100644 index 00000000000..ce8e950dacc --- /dev/null +++ b/homeassistant/components/soma/.translations/it.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "already_setup": "\u00c8 possibile configurare un solo account Soma.", + "authorize_url_timeout": "Timeout durante la generazione dell'URL di autorizzazione.", + "missing_configuration": "Il componente Soma non \u00e8 configurato. Si prega di seguire la documentazione." + }, + "create_entry": { + "default": "Autenticato con successo con Soma." + }, + "title": "Soma" + } +} \ No newline at end of file diff --git a/homeassistant/components/soma/.translations/ko.json b/homeassistant/components/soma/.translations/ko.json new file mode 100644 index 00000000000..53146bebf83 --- /dev/null +++ b/homeassistant/components/soma/.translations/ko.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "already_setup": "\ud558\ub098\uc758 Soma \uacc4\uc815\ub9cc \uad6c\uc131 \ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.", + "authorize_url_timeout": "\uc778\uc99d url \uc0dd\uc131 \uc2dc\uac04\uc774 \ucd08\uacfc\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", + "missing_configuration": "Soma \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": "Soma \ub85c \uc131\uacf5\uc801\uc73c\ub85c \uc778\uc99d\ub418\uc5c8\uc2b5\ub2c8\ub2e4." + }, + "title": "Soma" + } +} \ No newline at end of file diff --git a/homeassistant/components/soma/.translations/lb.json b/homeassistant/components/soma/.translations/lb.json new file mode 100644 index 00000000000..d8aba082537 --- /dev/null +++ b/homeassistant/components/soma/.translations/lb.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "already_setup": "Dir k\u00ebnnt n\u00ebmmen een eenzegen Soma Kont konfigur\u00e9ieren.", + "authorize_url_timeout": "Z\u00e4it Iwwerschreidung beim gener\u00e9ieren vun der Autorisatiouns URL.", + "missing_configuration": "D'Soma Komponent ass nach net konfigur\u00e9iert. Follegt w.e.g der Dokumentatioun." + }, + "create_entry": { + "default": "Erfollegr\u00e4ich mat Soma authentifiz\u00e9iert." + }, + "title": "Soma" + } +} \ No newline at end of file diff --git a/homeassistant/components/soma/.translations/nn.json b/homeassistant/components/soma/.translations/nn.json new file mode 100644 index 00000000000..6eeb4f75a3c --- /dev/null +++ b/homeassistant/components/soma/.translations/nn.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Soma" + } +} \ No newline at end of file diff --git a/homeassistant/components/soma/.translations/no.json b/homeassistant/components/soma/.translations/no.json new file mode 100644 index 00000000000..1ea53b778ea --- /dev/null +++ b/homeassistant/components/soma/.translations/no.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "already_setup": "Du kan bare konfigurere en Soma-konto.", + "authorize_url_timeout": "Tidsavbrudd ved generering av autoriseringsadresse.", + "missing_configuration": "Soma-komponenten er ikke konfigurert. Vennligst f\u00f8lg dokumentasjonen." + }, + "create_entry": { + "default": "Vellykket autentisering med Somfy." + }, + "title": "Soma" + } +} \ No newline at end of file diff --git a/homeassistant/components/soma/.translations/pl.json b/homeassistant/components/soma/.translations/pl.json new file mode 100644 index 00000000000..0ed881853b8 --- /dev/null +++ b/homeassistant/components/soma/.translations/pl.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "already_setup": "Mo\u017cesz skonfigurowa\u0107 tylko jedno konto Soma.", + "authorize_url_timeout": "Min\u0105\u0142 limit czasu generowania url autoryzacji.", + "missing_configuration": "Komponent Soma nie jest skonfigurowany. Post\u0119puj zgodnie z dokumentacj\u0105." + }, + "create_entry": { + "default": "Pomy\u015blnie uwierzytelniono z Soma" + }, + "title": "Soma" + } +} \ No newline at end of file diff --git a/homeassistant/components/soma/.translations/ru.json b/homeassistant/components/soma/.translations/ru.json new file mode 100644 index 00000000000..5ab3af0ecf8 --- /dev/null +++ b/homeassistant/components/soma/.translations/ru.json @@ -0,0 +1,13 @@ +{ + "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.", + "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 Soma \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 \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0435\u0439." + }, + "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." + }, + "title": "Soma" + } +} \ No newline at end of file diff --git a/homeassistant/components/soma/.translations/sl.json b/homeassistant/components/soma/.translations/sl.json new file mode 100644 index 00000000000..7dd523f366c --- /dev/null +++ b/homeassistant/components/soma/.translations/sl.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "already_setup": "Nastavite lahko samo en ra\u010dun Soma.", + "authorize_url_timeout": "\u010casovna omejitev za generiranje potrditvenega URL-ja je potekla.", + "missing_configuration": "Komponenta Soma ni konfigurirana. Upo\u0161tevajte dokumentacijo." + }, + "create_entry": { + "default": "Uspe\u0161no overjen s Soma." + }, + "title": "Soma" + } +} \ No newline at end of file diff --git a/homeassistant/components/soma/.translations/zh-Hant.json b/homeassistant/components/soma/.translations/zh-Hant.json new file mode 100644 index 00000000000..3d28389ff91 --- /dev/null +++ b/homeassistant/components/soma/.translations/zh-Hant.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "already_setup": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44 Soma \u5e33\u865f\u3002", + "authorize_url_timeout": "\u7522\u751f\u8a8d\u8b49 URL \u6642\u903e\u6642", + "missing_configuration": "Soma \u5143\u4ef6\u5c1a\u672a\u8a2d\u7f6e\uff0c\u8acb\u53c3\u95b1\u6587\u4ef6\u8aaa\u660e\u3002" + }, + "create_entry": { + "default": "\u5df2\u6210\u529f\u8a8d\u8b49 Soma \u8a2d\u5099\u3002" + }, + "title": "Soma" + } +} \ No newline at end of file diff --git a/homeassistant/components/soma/__init__.py b/homeassistant/components/soma/__init__.py new file mode 100644 index 00000000000..5bf51e743e9 --- /dev/null +++ b/homeassistant/components/soma/__init__.py @@ -0,0 +1,111 @@ +"""Support for Soma Smartshades.""" +import logging + +import voluptuous as vol +from api.soma_api import SomaApi + +import homeassistant.helpers.config_validation as cv +from homeassistant import config_entries +from homeassistant.config_entries import ConfigEntry +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.typing import HomeAssistantType + +from homeassistant.const import CONF_HOST, CONF_PORT + +from .const import DOMAIN, HOST, PORT, API + + +DEVICES = "devices" + +_LOGGER = logging.getLogger(__name__) + +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: vol.Schema( + {vol.Required(CONF_HOST): cv.string, vol.Required(CONF_PORT): cv.string} + ) + }, + extra=vol.ALLOW_EXTRA, +) + +SOMA_COMPONENTS = ["cover"] + + +async def async_setup(hass, config): + """Set up the Soma component.""" + if DOMAIN not in config: + return True + + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, + data=config[DOMAIN], + context={"source": config_entries.SOURCE_IMPORT}, + ) + ) + + return True + + +async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry): + """Set up Soma from a config entry.""" + hass.data[DOMAIN] = {} + hass.data[DOMAIN][API] = SomaApi(entry.data[HOST], entry.data[PORT]) + devices = await hass.async_add_executor_job(hass.data[DOMAIN][API].list_devices) + hass.data[DOMAIN][DEVICES] = devices["shades"] + + for component in SOMA_COMPONENTS: + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, component) + ) + + return True + + +async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry): + """Unload a config entry.""" + return True + + +class SomaEntity(Entity): + """Representation of a generic Soma device.""" + + def __init__(self, device, api): + """Initialize the Soma device.""" + self.device = device + self.api = api + self.current_position = 50 + + @property + def unique_id(self): + """Return the unique id base on the id returned by pysoma API.""" + return self.device["mac"] + + @property + def name(self): + """Return the name of the device.""" + return self.device["name"] + + @property + def device_info(self): + """Return device specific attributes. + + Implemented by platform classes. + """ + return { + "identifiers": {(DOMAIN, self.unique_id)}, + "name": self.name, + "manufacturer": "Wazombi Labs", + } + + async def async_update(self): + """Update the device with the latest data.""" + response = await self.hass.async_add_executor_job( + self.api.get_shade_state, self.device["mac"] + ) + if response["result"] != "success": + _LOGGER.error( + "Unable to reach device %s (%s)", self.device["name"], response["msg"] + ) + return + self.current_position = 100 - response["position"] diff --git a/homeassistant/components/soma/config_flow.py b/homeassistant/components/soma/config_flow.py new file mode 100644 index 00000000000..e2f89273520 --- /dev/null +++ b/homeassistant/components/soma/config_flow.py @@ -0,0 +1,56 @@ +"""Config flow for Soma.""" +import logging + +import voluptuous as vol +from api.soma_api import SomaApi +from requests import RequestException + +from homeassistant import config_entries +from homeassistant.const import CONF_HOST, CONF_PORT +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + +DEFAULT_PORT = 3000 + + +class SomaFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL + + def __init__(self): + """Instantiate config flow.""" + + async def async_step_user(self, user_input=None): + """Handle a flow start.""" + if user_input is None: + data = { + vol.Required(CONF_HOST): str, + vol.Required(CONF_PORT, default=DEFAULT_PORT): int, + } + + return self.async_show_form(step_id="user", data_schema=vol.Schema(data)) + + return await self.async_step_creation(user_input) + + async def async_step_creation(self, user_input=None): + """Finish config flow.""" + api = SomaApi(user_input["host"], user_input["port"]) + try: + await self.hass.async_add_executor_job(api.list_devices) + _LOGGER.info("Successfully set up Soma Connect") + return self.async_create_entry( + title="Soma Connect", + data={"host": user_input["host"], "port": user_input["port"]}, + ) + except RequestException: + _LOGGER.error("Connection to SOMA Connect failed") + return self.async_abort(reason="connection_error") + + async def async_step_import(self, user_input=None): + """Handle flow start from existing config section.""" + if self.hass.config_entries.async_entries(DOMAIN): + return self.async_abort(reason="already_setup") + return await self.async_step_creation(user_input) diff --git a/homeassistant/components/soma/const.py b/homeassistant/components/soma/const.py new file mode 100644 index 00000000000..815a0176e7e --- /dev/null +++ b/homeassistant/components/soma/const.py @@ -0,0 +1,6 @@ +"""Define constants for the Soma component.""" + +DOMAIN = "soma" +HOST = "host" +PORT = "port" +API = "api" diff --git a/homeassistant/components/soma/cover.py b/homeassistant/components/soma/cover.py new file mode 100644 index 00000000000..1577b7f2911 --- /dev/null +++ b/homeassistant/components/soma/cover.py @@ -0,0 +1,79 @@ +"""Support for Soma Covers.""" + +import logging + +from homeassistant.components.cover import CoverDevice, ATTR_POSITION +from homeassistant.components.soma import DOMAIN, SomaEntity, DEVICES, API + + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the Soma cover platform.""" + + devices = hass.data[DOMAIN][DEVICES] + + async_add_entities( + [SomaCover(cover, hass.data[DOMAIN][API]) for cover in devices], True + ) + + +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Old way of setting up platform. + + Can only be called when a user accidentally mentions the platform in their + config. But even in that case it would have been ignored. + """ + pass + + +class SomaCover(SomaEntity, CoverDevice): + """Representation of a Soma cover device.""" + + def close_cover(self, **kwargs): + """Close the cover.""" + response = self.api.set_shade_position(self.device["mac"], 100) + if response["result"] != "success": + _LOGGER.error( + "Unable to reach device %s (%s)", self.device["name"], response["msg"] + ) + + def open_cover(self, **kwargs): + """Open the cover.""" + response = self.api.set_shade_position(self.device["mac"], 0) + if response["result"] != "success": + _LOGGER.error( + "Unable to reach device %s (%s)", self.device["name"], response["msg"] + ) + + def stop_cover(self, **kwargs): + """Stop the cover.""" + # Set cover position to some value where up/down are both enabled + self.current_position = 50 + response = self.api.stop_shade(self.device["mac"]) + if response["result"] != "success": + _LOGGER.error( + "Unable to reach device %s (%s)", self.device["name"], response["msg"] + ) + + def set_cover_position(self, **kwargs): + """Move the cover shutter to a specific position.""" + self.current_position = kwargs[ATTR_POSITION] + response = self.api.set_shade_position( + self.device["mac"], 100 - kwargs[ATTR_POSITION] + ) + if response["result"] != "success": + _LOGGER.error( + "Unable to reach device %s (%s)", self.device["name"], response["msg"] + ) + + @property + def current_cover_position(self): + """Return the current position of cover shutter.""" + return self.current_position + + @property + def is_closed(self): + """Return if the cover is closed.""" + return self.current_position == 0 diff --git a/homeassistant/components/soma/manifest.json b/homeassistant/components/soma/manifest.json new file mode 100644 index 00000000000..35a77c063b8 --- /dev/null +++ b/homeassistant/components/soma/manifest.json @@ -0,0 +1,13 @@ +{ + "domain": "soma", + "name": "Soma Open API", + "config_flow": true, + "documentation": "", + "dependencies": [], + "codeowners": [ + "@ratsept" + ], + "requirements": [ + "pysoma==0.0.10" + ] +} diff --git a/homeassistant/components/soma/strings.json b/homeassistant/components/soma/strings.json new file mode 100644 index 00000000000..eac817ce119 --- /dev/null +++ b/homeassistant/components/soma/strings.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "already_setup": "You can only configure one Soma account.", + "authorize_url_timeout": "Timeout generating authorize url.", + "missing_configuration": "The Soma component is not configured. Please follow the documentation." + }, + "create_entry": { + "default": "Successfully authenticated with Soma." + }, + "title": "Soma" + } +} diff --git a/homeassistant/components/somfy/.translations/es-419.json b/homeassistant/components/somfy/.translations/es-419.json new file mode 100644 index 00000000000..ff0383c7f01 --- /dev/null +++ b/homeassistant/components/somfy/.translations/es-419.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Somfy" + } +} \ No newline at end of file diff --git a/homeassistant/components/somfy/.translations/nn.json b/homeassistant/components/somfy/.translations/nn.json new file mode 100644 index 00000000000..ff0383c7f01 --- /dev/null +++ b/homeassistant/components/somfy/.translations/nn.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Somfy" + } +} \ No newline at end of file diff --git a/homeassistant/components/somfy/.translations/zh-Hant.json b/homeassistant/components/somfy/.translations/zh-Hant.json index 6b53e943304..f7f7f4a1d8e 100644 --- a/homeassistant/components/somfy/.translations/zh-Hant.json +++ b/homeassistant/components/somfy/.translations/zh-Hant.json @@ -6,7 +6,7 @@ "missing_configuration": "Somfy \u5143\u4ef6\u5c1a\u672a\u8a2d\u7f6e\uff0c\u8acb\u53c3\u95b1\u6587\u4ef6\u8aaa\u660e\u3002" }, "create_entry": { - "default": "\u5df2\u6210\u529f\u8a8d\u8b49 Somfy \u88dd\u7f6e\u3002" + "default": "\u5df2\u6210\u529f\u8a8d\u8b49 Somfy \u8a2d\u5099\u3002" }, "title": "Somfy" } diff --git a/homeassistant/components/somfy/__init__.py b/homeassistant/components/somfy/__init__.py index 8c7edb26d46..2c7c71d7a69 100644 --- a/homeassistant/components/somfy/__init__.py +++ b/homeassistant/components/somfy/__init__.py @@ -2,7 +2,7 @@ Support for Somfy hubs. For more details about this component, please refer to the documentation at -https://home-assistant.io/components/somfy/ +https://home-assistant.io/integrations/somfy/ """ import logging from datetime import timedelta diff --git a/homeassistant/components/somfy/manifest.json b/homeassistant/components/somfy/manifest.json index 02eab03c8bb..83b50684fda 100644 --- a/homeassistant/components/somfy/manifest.json +++ b/homeassistant/components/somfy/manifest.json @@ -2,7 +2,7 @@ "domain": "somfy", "name": "Somfy Open API", "config_flow": true, - "documentation": "https://www.home-assistant.io/components/somfy", + "documentation": "https://www.home-assistant.io/integrations/somfy", "dependencies": [], "codeowners": [ "@tetienne" diff --git a/homeassistant/components/somfy_mylink/manifest.json b/homeassistant/components/somfy_mylink/manifest.json index d4e799c2cf1..35422cf09a1 100644 --- a/homeassistant/components/somfy_mylink/manifest.json +++ b/homeassistant/components/somfy_mylink/manifest.json @@ -1,7 +1,7 @@ { "domain": "somfy_mylink", "name": "Somfy MyLink", - "documentation": "https://www.home-assistant.io/components/somfy_mylink", + "documentation": "https://www.home-assistant.io/integrations/somfy_mylink", "requirements": [ "somfy-mylink-synergy==1.0.6" ], diff --git a/homeassistant/components/sonarr/manifest.json b/homeassistant/components/sonarr/manifest.json index bc0235ec5b3..ae32083da39 100644 --- a/homeassistant/components/sonarr/manifest.json +++ b/homeassistant/components/sonarr/manifest.json @@ -1,7 +1,7 @@ { "domain": "sonarr", "name": "Sonarr", - "documentation": "https://www.home-assistant.io/components/sonarr", + "documentation": "https://www.home-assistant.io/integrations/sonarr", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/songpal/manifest.json b/homeassistant/components/songpal/manifest.json index 5a01d9f9e2f..3160c4cee4b 100644 --- a/homeassistant/components/songpal/manifest.json +++ b/homeassistant/components/songpal/manifest.json @@ -1,7 +1,7 @@ { "domain": "songpal", "name": "Songpal", - "documentation": "https://www.home-assistant.io/components/songpal", + "documentation": "https://www.home-assistant.io/integrations/songpal", "requirements": [ "python-songpal==0.0.9.1" ], diff --git a/homeassistant/components/sonos/.translations/no.json b/homeassistant/components/sonos/.translations/no.json index c837abad499..ae47916ac85 100644 --- a/homeassistant/components/sonos/.translations/no.json +++ b/homeassistant/components/sonos/.translations/no.json @@ -2,7 +2,7 @@ "config": { "abort": { "no_devices_found": "Ingen Sonos enheter funnet p\u00e5 nettverket.", - "single_instance_allowed": "Kun en enkelt konfigurasjon av Sonos er n\u00f8dvendig." + "single_instance_allowed": "Kun en konfigurasjon av Sonos er n\u00f8dvendig." }, "step": { "confirm": { diff --git a/homeassistant/components/sonos/.translations/zh-Hant.json b/homeassistant/components/sonos/.translations/zh-Hant.json index 520a29b7602..c6fb13c3605 100644 --- a/homeassistant/components/sonos/.translations/zh-Hant.json +++ b/homeassistant/components/sonos/.translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "no_devices_found": "\u5728\u7db2\u8def\u4e0a\u627e\u4e0d\u5230 Sonos \u88dd\u7f6e\u3002", + "no_devices_found": "\u5728\u7db2\u8def\u4e0a\u627e\u4e0d\u5230 Sonos \u8a2d\u5099\u3002", "single_instance_allowed": "\u50c5\u9700\u8a2d\u5b9a\u4e00\u6b21 Sonos \u5373\u53ef\u3002" }, "step": { diff --git a/homeassistant/components/sonos/manifest.json b/homeassistant/components/sonos/manifest.json index 8c231ec63e0..6d636f36b3f 100644 --- a/homeassistant/components/sonos/manifest.json +++ b/homeassistant/components/sonos/manifest.json @@ -2,7 +2,7 @@ "domain": "sonos", "name": "Sonos", "config_flow": true, - "documentation": "https://www.home-assistant.io/components/sonos", + "documentation": "https://www.home-assistant.io/integrations/sonos", "requirements": [ "pysonos==0.0.23" ], @@ -12,7 +12,5 @@ "urn:schemas-upnp-org:device:ZonePlayer:1" ] }, - "codeowners": [ - "@amelchio" - ] + "codeowners": [] } diff --git a/homeassistant/components/sony_projector/manifest.json b/homeassistant/components/sony_projector/manifest.json index 1cc25d93f59..497347671a9 100644 --- a/homeassistant/components/sony_projector/manifest.json +++ b/homeassistant/components/sony_projector/manifest.json @@ -1,7 +1,7 @@ { "domain": "sony_projector", "name": "Sony projector", - "documentation": "https://www.home-assistant.io/components/sony_projector", + "documentation": "https://www.home-assistant.io/integrations/sony_projector", "requirements": [ "pysdcp==1" ], diff --git a/homeassistant/components/soundtouch/manifest.json b/homeassistant/components/soundtouch/manifest.json index eba60bc6e34..e3ca9ed72e8 100644 --- a/homeassistant/components/soundtouch/manifest.json +++ b/homeassistant/components/soundtouch/manifest.json @@ -1,7 +1,7 @@ { "domain": "soundtouch", "name": "Soundtouch", - "documentation": "https://www.home-assistant.io/components/soundtouch", + "documentation": "https://www.home-assistant.io/integrations/soundtouch", "requirements": [ "libsoundtouch==0.7.2" ], diff --git a/homeassistant/components/spaceapi/__init__.py b/homeassistant/components/spaceapi/__init__.py index 607d9c45538..ea5a64d97e7 100644 --- a/homeassistant/components/spaceapi/__init__.py +++ b/homeassistant/components/spaceapi/__init__.py @@ -7,9 +7,7 @@ from homeassistant.components.http import HomeAssistantView from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_ICON, - ATTR_LATITUDE, ATTR_LOCATION, - ATTR_LONGITUDE, ATTR_STATE, ATTR_UNIT_OF_MEASUREMENT, CONF_ADDRESS, @@ -26,6 +24,15 @@ import homeassistant.util.dt as dt_util _LOGGER = logging.getLogger(__name__) ATTR_ADDRESS = "address" +ATTR_SPACEFED = "spacefed" +ATTR_CAM = "cam" +ATTR_STREAM = "stream" +ATTR_FEEDS = "feeds" +ATTR_CACHE = "cache" +ATTR_PROJECTS = "projects" +ATTR_RADIO_SHOW = "radio_show" +ATTR_LAT = "lat" +ATTR_LON = "lon" ATTR_API = "api" ATTR_CLOSE = "close" ATTR_CONTACT = "contact" @@ -49,32 +56,135 @@ CONF_ICONS = "icons" CONF_IRC = "irc" CONF_ISSUE_REPORT_CHANNELS = "issue_report_channels" CONF_LOCATION = "location" +CONF_SPACEFED = "spacefed" +CONF_SPACENET = "spacenet" +CONF_SPACESAML = "spacesaml" +CONF_SPACEPHONE = "spacephone" +CONF_CAM = "cam" +CONF_STREAM = "stream" +CONF_M4 = "m4" +CONF_MJPEG = "mjpeg" +CONF_USTREAM = "ustream" +CONF_FEEDS = "feeds" +CONF_FEED_BLOG = "blog" +CONF_FEED_WIKI = "wiki" +CONF_FEED_CALENDAR = "calendar" +CONF_FEED_FLICKER = "flicker" +CONF_FEED_TYPE = "type" +CONF_FEED_URL = "url" +CONF_CACHE = "cache" +CONF_CACHE_SCHEDULE = "schedule" +CONF_PROJECTS = "projects" +CONF_RADIO_SHOW = "radio_show" +CONF_RADIO_SHOW_NAME = "name" +CONF_RADIO_SHOW_URL = "url" +CONF_RADIO_SHOW_TYPE = "type" +CONF_RADIO_SHOW_START = "start" +CONF_RADIO_SHOW_END = "end" CONF_LOGO = "logo" -CONF_MAILING_LIST = "mailing_list" CONF_PHONE = "phone" +CONF_SIP = "sip" +CONF_KEYMASTERS = "keymasters" +CONF_KEYMASTER_NAME = "name" +CONF_KEYMASTER_IRC_NICK = "irc_nick" +CONF_KEYMASTER_PHONE = "phone" +CONF_KEYMASTER_EMAIL = "email" +CONF_KEYMASTER_TWITTER = "twitter" +CONF_TWITTER = "twitter" +CONF_FACEBOOK = "facebook" +CONF_IDENTICA = "identica" +CONF_FOURSQUARE = "foursquare" +CONF_ML = "ml" +CONF_JABBER = "jabber" +CONF_ISSUE_MAIL = "issue_mail" CONF_SPACE = "space" CONF_TEMPERATURE = "temperature" -CONF_TWITTER = "twitter" DATA_SPACEAPI = "data_spaceapi" DOMAIN = "spaceapi" -ISSUE_REPORT_CHANNELS = [CONF_EMAIL, CONF_IRC, CONF_MAILING_LIST, CONF_TWITTER] +ISSUE_REPORT_CHANNELS = [CONF_EMAIL, CONF_ISSUE_MAIL, CONF_ML, CONF_TWITTER] SENSOR_TYPES = [CONF_HUMIDITY, CONF_TEMPERATURE] -SPACEAPI_VERSION = 0.13 +SPACEAPI_VERSION = "0.13" URL_API_SPACEAPI = "/api/spaceapi" -LOCATION_SCHEMA = vol.Schema({vol.Optional(CONF_ADDRESS): cv.string}, required=True) +LOCATION_SCHEMA = vol.Schema({vol.Optional(CONF_ADDRESS): cv.string}) + +SPACEFED_SCHEMA = vol.Schema( + { + vol.Optional(CONF_SPACENET): cv.boolean, + vol.Optional(CONF_SPACESAML): cv.boolean, + vol.Optional(CONF_SPACEPHONE): cv.boolean, + } +) + +STREAM_SCHEMA = vol.Schema( + { + vol.Optional(CONF_M4): cv.url, + vol.Optional(CONF_MJPEG): cv.url, + vol.Optional(CONF_USTREAM): cv.url, + } +) + +FEED_SCHEMA = vol.Schema( + {vol.Optional(CONF_FEED_TYPE): cv.string, vol.Required(CONF_FEED_URL): cv.url} +) + +FEEDS_SCHEMA = vol.Schema( + { + vol.Optional(CONF_FEED_BLOG): FEED_SCHEMA, + vol.Optional(CONF_FEED_WIKI): FEED_SCHEMA, + vol.Optional(CONF_FEED_CALENDAR): FEED_SCHEMA, + vol.Optional(CONF_FEED_FLICKER): FEED_SCHEMA, + } +) + +CACHE_SCHEMA = vol.Schema( + { + vol.Required(CONF_CACHE_SCHEDULE): cv.matches_regex( + r"(m.02|m.05|m.10|m.15|m.30|h.01|h.02|h.04|h.08|h.12|d.01)" + ) + } +) + +RADIO_SHOW_SCHEMA = vol.Schema( + { + vol.Required(CONF_RADIO_SHOW_NAME): cv.string, + vol.Required(CONF_RADIO_SHOW_URL): cv.url, + vol.Required(CONF_RADIO_SHOW_TYPE): cv.matches_regex(r"(mp3|ogg)"), + vol.Required(CONF_RADIO_SHOW_START): cv.string, + vol.Required(CONF_RADIO_SHOW_END): cv.string, + } +) + +KEYMASTER_SCHEMA = vol.Schema( + { + vol.Optional(CONF_KEYMASTER_NAME): cv.string, + vol.Optional(CONF_KEYMASTER_IRC_NICK): cv.string, + vol.Optional(CONF_KEYMASTER_PHONE): cv.string, + vol.Optional(CONF_KEYMASTER_EMAIL): cv.string, + vol.Optional(CONF_KEYMASTER_TWITTER): cv.string, + } +) CONTACT_SCHEMA = vol.Schema( { vol.Optional(CONF_EMAIL): cv.string, vol.Optional(CONF_IRC): cv.string, - vol.Optional(CONF_MAILING_LIST): cv.string, + vol.Optional(CONF_ML): cv.string, vol.Optional(CONF_PHONE): cv.string, vol.Optional(CONF_TWITTER): cv.string, + vol.Optional(CONF_SIP): cv.string, + vol.Optional(CONF_FACEBOOK): cv.string, + vol.Optional(CONF_IDENTICA): cv.string, + vol.Optional(CONF_FOURSQUARE): cv.string, + vol.Optional(CONF_JABBER): cv.string, + vol.Optional(CONF_ISSUE_MAIL): cv.string, + vol.Optional(CONF_KEYMASTERS): vol.All( + cv.ensure_list, [KEYMASTER_SCHEMA], vol.Length(min=1) + ), }, required=False, ) @@ -100,12 +210,23 @@ CONFIG_SCHEMA = vol.Schema( vol.Required(CONF_ISSUE_REPORT_CHANNELS): vol.All( cv.ensure_list, [vol.In(ISSUE_REPORT_CHANNELS)] ), - vol.Required(CONF_LOCATION): LOCATION_SCHEMA, + vol.Optional(CONF_LOCATION): LOCATION_SCHEMA, vol.Required(CONF_LOGO): cv.url, vol.Required(CONF_SPACE): cv.string, vol.Required(CONF_STATE): STATE_SCHEMA, vol.Required(CONF_URL): cv.string, vol.Optional(CONF_SENSORS): SENSOR_SCHEMA, + vol.Optional(CONF_SPACEFED): SPACEFED_SCHEMA, + vol.Optional(CONF_CAM): vol.All( + cv.ensure_list, [cv.url], vol.Length(min=1) + ), + vol.Optional(CONF_STREAM): STREAM_SCHEMA, + vol.Optional(CONF_FEEDS): FEEDS_SCHEMA, + vol.Optional(CONF_CACHE): CACHE_SCHEMA, + vol.Optional(CONF_PROJECTS): vol.All(cv.ensure_list, [cv.url]), + vol.Optional(CONF_RADIO_SHOW): vol.All( + cv.ensure_list, [RADIO_SHOW_SCHEMA] + ), } ) }, @@ -150,11 +271,14 @@ class APISpaceApiView(HomeAssistantView): spaceapi = dict(hass.data[DATA_SPACEAPI]) is_sensors = spaceapi.get("sensors") - location = { - ATTR_ADDRESS: spaceapi[ATTR_LOCATION][CONF_ADDRESS], - ATTR_LATITUDE: hass.config.latitude, - ATTR_LONGITUDE: hass.config.longitude, - } + location = {ATTR_LAT: hass.config.latitude, ATTR_LON: hass.config.longitude} + + try: + location[ATTR_ADDRESS] = spaceapi[ATTR_LOCATION][CONF_ADDRESS] + except KeyError: + pass + except TypeError: + pass state_entity = spaceapi["state"][ATTR_ENTITY_ID] space_state = hass.states.get(state_entity) @@ -186,6 +310,41 @@ class APISpaceApiView(HomeAssistantView): ATTR_URL: spaceapi[CONF_URL], } + try: + data[ATTR_CAM] = spaceapi[CONF_CAM] + except KeyError: + pass + + try: + data[ATTR_SPACEFED] = spaceapi[CONF_SPACEFED] + except KeyError: + pass + + try: + data[ATTR_STREAM] = spaceapi[CONF_STREAM] + except KeyError: + pass + + try: + data[ATTR_FEEDS] = spaceapi[CONF_FEEDS] + except KeyError: + pass + + try: + data[ATTR_CACHE] = spaceapi[CONF_CACHE] + except KeyError: + pass + + try: + data[ATTR_PROJECTS] = spaceapi[CONF_PROJECTS] + except KeyError: + pass + + try: + data[ATTR_RADIO_SHOW] = spaceapi[CONF_RADIO_SHOW] + except KeyError: + pass + if is_sensors is not None: sensors = {} for sensor_type in is_sensors: diff --git a/homeassistant/components/spaceapi/manifest.json b/homeassistant/components/spaceapi/manifest.json index 03aa5c0a1f7..4ab05f170ca 100644 --- a/homeassistant/components/spaceapi/manifest.json +++ b/homeassistant/components/spaceapi/manifest.json @@ -1,7 +1,7 @@ { "domain": "spaceapi", "name": "Spaceapi", - "documentation": "https://www.home-assistant.io/components/spaceapi", + "documentation": "https://www.home-assistant.io/integrations/spaceapi", "requirements": [], "dependencies": [ "http" diff --git a/homeassistant/components/spc/manifest.json b/homeassistant/components/spc/manifest.json index 572d4b04b87..5b59d3bfe29 100644 --- a/homeassistant/components/spc/manifest.json +++ b/homeassistant/components/spc/manifest.json @@ -1,7 +1,7 @@ { "domain": "spc", "name": "Spc", - "documentation": "https://www.home-assistant.io/components/spc", + "documentation": "https://www.home-assistant.io/integrations/spc", "requirements": [ "pyspcwebgw==0.4.0" ], diff --git a/homeassistant/components/speedtestdotnet/manifest.json b/homeassistant/components/speedtestdotnet/manifest.json index 91b7e7c5c0f..b35df8ee7c8 100644 --- a/homeassistant/components/speedtestdotnet/manifest.json +++ b/homeassistant/components/speedtestdotnet/manifest.json @@ -1,7 +1,7 @@ { "domain": "speedtestdotnet", "name": "Speedtestdotnet", - "documentation": "https://www.home-assistant.io/components/speedtestdotnet", + "documentation": "https://www.home-assistant.io/integrations/speedtestdotnet", "requirements": [ "speedtest-cli==2.1.1" ], diff --git a/homeassistant/components/spider/manifest.json b/homeassistant/components/spider/manifest.json index 4cd7a467737..567f6f0e513 100644 --- a/homeassistant/components/spider/manifest.json +++ b/homeassistant/components/spider/manifest.json @@ -1,7 +1,7 @@ { "domain": "spider", "name": "Spider", - "documentation": "https://www.home-assistant.io/components/spider", + "documentation": "https://www.home-assistant.io/integrations/spider", "requirements": [ "spiderpy==1.3.1" ], diff --git a/homeassistant/components/splunk/manifest.json b/homeassistant/components/splunk/manifest.json index 2e81da3409a..a6972e8881d 100644 --- a/homeassistant/components/splunk/manifest.json +++ b/homeassistant/components/splunk/manifest.json @@ -1,7 +1,7 @@ { "domain": "splunk", "name": "Splunk", - "documentation": "https://www.home-assistant.io/components/splunk", + "documentation": "https://www.home-assistant.io/integrations/splunk", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/spotcrime/manifest.json b/homeassistant/components/spotcrime/manifest.json index 5827f307ecf..50d9ba3163d 100644 --- a/homeassistant/components/spotcrime/manifest.json +++ b/homeassistant/components/spotcrime/manifest.json @@ -1,7 +1,7 @@ { "domain": "spotcrime", "name": "Spotcrime", - "documentation": "https://www.home-assistant.io/components/spotcrime", + "documentation": "https://www.home-assistant.io/integrations/spotcrime", "requirements": [ "spotcrime==1.0.4" ], diff --git a/homeassistant/components/spotify/manifest.json b/homeassistant/components/spotify/manifest.json index 366a5eef0ad..f514a95c042 100644 --- a/homeassistant/components/spotify/manifest.json +++ b/homeassistant/components/spotify/manifest.json @@ -1,7 +1,7 @@ { "domain": "spotify", "name": "Spotify", - "documentation": "https://www.home-assistant.io/components/spotify", + "documentation": "https://www.home-assistant.io/integrations/spotify", "requirements": [ "spotipy-homeassistant==2.4.4.dev1" ], diff --git a/homeassistant/components/sql/manifest.json b/homeassistant/components/sql/manifest.json index 38a320543a9..41d80ebccf9 100644 --- a/homeassistant/components/sql/manifest.json +++ b/homeassistant/components/sql/manifest.json @@ -1,7 +1,7 @@ { "domain": "sql", "name": "Sql", - "documentation": "https://www.home-assistant.io/components/sql", + "documentation": "https://www.home-assistant.io/integrations/sql", "requirements": [ "sqlalchemy==1.3.8" ], diff --git a/homeassistant/components/squeezebox/manifest.json b/homeassistant/components/squeezebox/manifest.json index ae124d6c03d..1f651f746e6 100644 --- a/homeassistant/components/squeezebox/manifest.json +++ b/homeassistant/components/squeezebox/manifest.json @@ -1,7 +1,7 @@ { "domain": "squeezebox", "name": "Squeezebox", - "documentation": "https://www.home-assistant.io/components/squeezebox", + "documentation": "https://www.home-assistant.io/integrations/squeezebox", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/squeezebox/media_player.py b/homeassistant/components/squeezebox/media_player.py index 9e62e7ee0db..6540fca1405 100644 --- a/homeassistant/components/squeezebox/media_player.py +++ b/homeassistant/components/squeezebox/media_player.py @@ -246,7 +246,7 @@ class SqueezeBoxDevice(MediaPlayerDevice): def __init__(self, lms, player_id, name): """Initialize the SqueezeBox device.""" - super(SqueezeBoxDevice, self).__init__() + super().__init__() self._lms = lms self._id = player_id self._status = {} diff --git a/homeassistant/components/srp_energy/__init__.py b/homeassistant/components/srp_energy/__init__.py deleted file mode 100644 index 71e04d7b8c9..00000000000 --- a/homeassistant/components/srp_energy/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""The srp_energy component.""" diff --git a/homeassistant/components/srp_energy/manifest.json b/homeassistant/components/srp_energy/manifest.json deleted file mode 100644 index 050a78223c1..00000000000 --- a/homeassistant/components/srp_energy/manifest.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "domain": "srp_energy", - "name": "Srp energy", - "documentation": "https://www.home-assistant.io/components/srp_energy", - "requirements": [ - "srpenergy==1.0.6" - ], - "dependencies": [], - "codeowners": [] -} diff --git a/homeassistant/components/srp_energy/sensor.py b/homeassistant/components/srp_energy/sensor.py deleted file mode 100644 index f1d1787b7b4..00000000000 --- a/homeassistant/components/srp_energy/sensor.py +++ /dev/null @@ -1,156 +0,0 @@ -"""Platform for retrieving energy data from SRP.""" -from datetime import datetime, timedelta -import logging - -from requests.exceptions import ConnectionError as ConnectError, HTTPError, Timeout -import voluptuous as vol - -from homeassistant.const import ( - CONF_NAME, - CONF_PASSWORD, - ENERGY_KILO_WATT_HOUR, - CONF_USERNAME, - CONF_ID, -) -import homeassistant.helpers.config_validation as cv -from homeassistant.util import Throttle -from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.helpers.entity import Entity - -_LOGGER = logging.getLogger(__name__) - -ATTRIBUTION = "Powered by SRP Energy" - -DEFAULT_NAME = "SRP Energy" -MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=1440) -ENERGY_KWH = ENERGY_KILO_WATT_HOUR - -ATTR_READING_COST = "reading_cost" -ATTR_READING_TIME = "datetime" -ATTR_READING_USAGE = "reading_usage" -ATTR_DAILY_USAGE = "daily_usage" -ATTR_USAGE_HISTORY = "usage_history" - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Required(CONF_ID): cv.string, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - } -) - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the SRP energy.""" - _LOGGER.warning( - "The srp_energy integration is deprecated and will be removed " - "in Home Assistant 0.100.0. For more information see ADR-0004:" - "https://github.com/home-assistant/architecture/blob/master/adr/0004-webscraping.md" - ) - - name = config[CONF_NAME] - username = config[CONF_USERNAME] - password = config[CONF_PASSWORD] - account_id = config[CONF_ID] - - from srpenergy.client import SrpEnergyClient - - srp_client = SrpEnergyClient(account_id, username, password) - - if not srp_client.validate(): - _LOGGER.error("Couldn't connect to %s. Check credentials", name) - return - - add_entities([SrpEnergy(name, srp_client)], True) - - -class SrpEnergy(Entity): - """Representation of an srp usage.""" - - def __init__(self, name, client): - """Initialize SRP Usage.""" - self._state = None - self._name = name - self._client = client - self._history = None - self._usage = None - - @property - def attribution(self): - """Return the attribution.""" - return ATTRIBUTION - - @property - def state(self): - """Return the current state.""" - if self._state is None: - return None - - return f"{self._state:.2f}" - - @property - def name(self): - """Return the name of the sensor.""" - return self._name - - @property - def unit_of_measurement(self): - """Return the unit of measurement of this entity, if any.""" - return ENERGY_KWH - - @property - def history(self): - """Return the energy usage history of this entity, if any.""" - if self._usage is None: - return None - - history = [ - { - ATTR_READING_TIME: isodate, - ATTR_READING_USAGE: kwh, - ATTR_READING_COST: cost, - } - for _, _, isodate, kwh, cost in self._usage - ] - - return history - - @property - def device_state_attributes(self): - """Return the state attributes.""" - attributes = {ATTR_USAGE_HISTORY: self.history} - - return attributes - - @Throttle(MIN_TIME_BETWEEN_UPDATES) - def update(self): - """Get the latest usage from SRP Energy.""" - start_date = datetime.now() + timedelta(days=-1) - end_date = datetime.now() - - try: - - usage = self._client.usage(start_date, end_date) - - daily_usage = 0.0 - for _, _, _, kwh, _ in usage: - daily_usage += float(kwh) - - if usage: - - self._state = daily_usage - self._usage = usage - - else: - _LOGGER.error("Unable to fetch data from SRP. No data") - - except (ConnectError, HTTPError, Timeout) as error: - _LOGGER.error("Unable to connect to SRP. %s", error) - except ValueError as error: - _LOGGER.error("Value error connecting to SRP. %s", error) - except TypeError as error: - _LOGGER.error( - "Type error connecting to SRP. " "Check username and password. %s", - error, - ) diff --git a/homeassistant/components/ssdp/manifest.json b/homeassistant/components/ssdp/manifest.json index ce00bcbc888..1c3d56fe7fe 100644 --- a/homeassistant/components/ssdp/manifest.json +++ b/homeassistant/components/ssdp/manifest.json @@ -1,7 +1,7 @@ { "domain": "ssdp", "name": "SSDP", - "documentation": "https://www.home-assistant.io/components/ssdp", + "documentation": "https://www.home-assistant.io/integrations/ssdp", "requirements": [ "netdisco==2.6.0" ], diff --git a/homeassistant/components/starlingbank/manifest.json b/homeassistant/components/starlingbank/manifest.json index 1314fda5099..33dbb40f78a 100644 --- a/homeassistant/components/starlingbank/manifest.json +++ b/homeassistant/components/starlingbank/manifest.json @@ -1,7 +1,7 @@ { "domain": "starlingbank", "name": "Starlingbank", - "documentation": "https://www.home-assistant.io/components/starlingbank", + "documentation": "https://www.home-assistant.io/integrations/starlingbank", "requirements": [ "starlingbank==3.1" ], diff --git a/homeassistant/components/startca/manifest.json b/homeassistant/components/startca/manifest.json index d2f9e90c41a..79637bfb124 100644 --- a/homeassistant/components/startca/manifest.json +++ b/homeassistant/components/startca/manifest.json @@ -1,7 +1,7 @@ { "domain": "startca", "name": "Startca", - "documentation": "https://www.home-assistant.io/components/startca", + "documentation": "https://www.home-assistant.io/integrations/startca", "requirements": [ "xmltodict==0.12.0" ], diff --git a/homeassistant/components/statistics/manifest.json b/homeassistant/components/statistics/manifest.json index 49e476a6876..3dab05942b9 100644 --- a/homeassistant/components/statistics/manifest.json +++ b/homeassistant/components/statistics/manifest.json @@ -1,7 +1,7 @@ { "domain": "statistics", "name": "Statistics", - "documentation": "https://www.home-assistant.io/components/statistics", + "documentation": "https://www.home-assistant.io/integrations/statistics", "requirements": [], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/statsd/manifest.json b/homeassistant/components/statsd/manifest.json index 20f4cc7f544..387576f8845 100644 --- a/homeassistant/components/statsd/manifest.json +++ b/homeassistant/components/statsd/manifest.json @@ -1,7 +1,7 @@ { "domain": "statsd", "name": "Statsd", - "documentation": "https://www.home-assistant.io/components/statsd", + "documentation": "https://www.home-assistant.io/integrations/statsd", "requirements": [ "statsd==3.2.1" ], diff --git a/homeassistant/components/steam_online/manifest.json b/homeassistant/components/steam_online/manifest.json index 735a1869c34..ecc6198cb88 100644 --- a/homeassistant/components/steam_online/manifest.json +++ b/homeassistant/components/steam_online/manifest.json @@ -1,7 +1,7 @@ { "domain": "steam_online", "name": "Steam online", - "documentation": "https://www.home-assistant.io/components/steam_online", + "documentation": "https://www.home-assistant.io/integrations/steam_online", "requirements": [ "steamodd==4.21" ], diff --git a/homeassistant/components/stiebel_eltron/manifest.json b/homeassistant/components/stiebel_eltron/manifest.json index 0f8b586a9c2..385df6a5063 100644 --- a/homeassistant/components/stiebel_eltron/manifest.json +++ b/homeassistant/components/stiebel_eltron/manifest.json @@ -1,7 +1,7 @@ { "domain": "stiebel_eltron", "name": "STIEBEL ELTRON", - "documentation": "https://www.home-assistant.io/components/stiebel_eltron", + "documentation": "https://www.home-assistant.io/integrations/stiebel_eltron", "requirements": [ "pystiebeleltron==0.0.1.dev2" ], diff --git a/homeassistant/components/stream/manifest.json b/homeassistant/components/stream/manifest.json index f285f81f27f..a00315e1064 100644 --- a/homeassistant/components/stream/manifest.json +++ b/homeassistant/components/stream/manifest.json @@ -1,7 +1,7 @@ { "domain": "stream", "name": "Stream", - "documentation": "https://www.home-assistant.io/components/stream", + "documentation": "https://www.home-assistant.io/integrations/stream", "requirements": [ "av==6.1.2" ], diff --git a/homeassistant/components/streamlabswater/manifest.json b/homeassistant/components/streamlabswater/manifest.json index b4173ebf0e9..83b6314c4ab 100644 --- a/homeassistant/components/streamlabswater/manifest.json +++ b/homeassistant/components/streamlabswater/manifest.json @@ -1,7 +1,7 @@ { "domain": "streamlabswater", "name": "Streamlabs Water", - "documentation": "https://www.home-assistant.io/components/streamlabswater", + "documentation": "https://www.home-assistant.io/integrations/streamlabswater", "requirements": [ "streamlabswater==1.0.1" ], diff --git a/homeassistant/components/stride/manifest.json b/homeassistant/components/stride/manifest.json index 307f4c929cf..840984ad073 100644 --- a/homeassistant/components/stride/manifest.json +++ b/homeassistant/components/stride/manifest.json @@ -1,7 +1,7 @@ { "domain": "stride", "name": "Stride", - "documentation": "https://www.home-assistant.io/components/stride", + "documentation": "https://www.home-assistant.io/integrations/stride", "requirements": [ "pystride==0.1.7" ], diff --git a/homeassistant/components/suez_water/manifest.json b/homeassistant/components/suez_water/manifest.json index 8ee3de2d77f..654f99f7ea4 100644 --- a/homeassistant/components/suez_water/manifest.json +++ b/homeassistant/components/suez_water/manifest.json @@ -1,7 +1,7 @@ { "domain": "suez_water", "name": "Suez Water Consumption Sensor", - "documentation": "https://www.home-assistant.io/components/suez_water", + "documentation": "https://www.home-assistant.io/integrations/suez_water", "dependencies": [], "codeowners": ["@ooii"], "requirements": ["pysuez==0.1.17"] diff --git a/homeassistant/components/sun/__init__.py b/homeassistant/components/sun/__init__.py index 3e6048e1532..7d883e273e5 100644 --- a/homeassistant/components/sun/__init__.py +++ b/homeassistant/components/sun/__init__.py @@ -17,6 +17,9 @@ from homeassistant.helpers.sun import ( ) from homeassistant.util import dt as dt_util + +# mypy: allow-untyped-calls, allow-untyped-defs + _LOGGER = logging.getLogger(__name__) DOMAIN = "sun" diff --git a/homeassistant/components/sun/manifest.json b/homeassistant/components/sun/manifest.json index e55131306dc..31fcb1d7c2e 100644 --- a/homeassistant/components/sun/manifest.json +++ b/homeassistant/components/sun/manifest.json @@ -1,7 +1,7 @@ { "domain": "sun", "name": "Sun", - "documentation": "https://www.home-assistant.io/components/sun", + "documentation": "https://www.home-assistant.io/integrations/sun", "requirements": [], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/supervisord/manifest.json b/homeassistant/components/supervisord/manifest.json index 1fc849165ef..eaf1e66cff4 100644 --- a/homeassistant/components/supervisord/manifest.json +++ b/homeassistant/components/supervisord/manifest.json @@ -1,7 +1,7 @@ { "domain": "supervisord", "name": "Supervisord", - "documentation": "https://www.home-assistant.io/components/supervisord", + "documentation": "https://www.home-assistant.io/integrations/supervisord", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/supla/manifest.json b/homeassistant/components/supla/manifest.json index cac1a5f18ab..72635ca641a 100644 --- a/homeassistant/components/supla/manifest.json +++ b/homeassistant/components/supla/manifest.json @@ -1,7 +1,7 @@ { "domain": "supla", "name": "Supla", - "documentation": "https://www.home-assistant.io/components/supla", + "documentation": "https://www.home-assistant.io/integrations/supla", "requirements": [ "pysupla==0.0.3" ], diff --git a/homeassistant/components/swiss_hydrological_data/manifest.json b/homeassistant/components/swiss_hydrological_data/manifest.json index d6b18d6cba8..f54ef55ce17 100644 --- a/homeassistant/components/swiss_hydrological_data/manifest.json +++ b/homeassistant/components/swiss_hydrological_data/manifest.json @@ -1,7 +1,7 @@ { "domain": "swiss_hydrological_data", "name": "Swiss hydrological data", - "documentation": "https://www.home-assistant.io/components/swiss_hydrological_data", + "documentation": "https://www.home-assistant.io/integrations/swiss_hydrological_data", "requirements": [ "swisshydrodata==0.0.3" ], diff --git a/homeassistant/components/swiss_public_transport/manifest.json b/homeassistant/components/swiss_public_transport/manifest.json index 99dcdbd0c88..c91d85fecd7 100644 --- a/homeassistant/components/swiss_public_transport/manifest.json +++ b/homeassistant/components/swiss_public_transport/manifest.json @@ -1,7 +1,7 @@ { "domain": "swiss_public_transport", "name": "Swiss public transport", - "documentation": "https://www.home-assistant.io/components/swiss_public_transport", + "documentation": "https://www.home-assistant.io/integrations/swiss_public_transport", "requirements": [ "python_opendata_transport==0.1.4" ], diff --git a/homeassistant/components/swisscom/manifest.json b/homeassistant/components/swisscom/manifest.json index e52fda34083..27e33c81607 100644 --- a/homeassistant/components/swisscom/manifest.json +++ b/homeassistant/components/swisscom/manifest.json @@ -1,7 +1,7 @@ { "domain": "swisscom", "name": "Swisscom", - "documentation": "https://www.home-assistant.io/components/swisscom", + "documentation": "https://www.home-assistant.io/integrations/swisscom", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/switch/.translations/bg.json b/homeassistant/components/switch/.translations/bg.json index efccc652d5b..64a3ea94e1b 100644 --- a/homeassistant/components/switch/.translations/bg.json +++ b/homeassistant/components/switch/.translations/bg.json @@ -6,6 +6,8 @@ "turn_on": "\u0412\u043a\u043b\u044e\u0447\u0438 {entity_name}" }, "condition_type": { + "is_off": "{entity_name} \u0435 \u0438\u0437\u043a\u043b\u044e\u0447\u0435\u043d", + "is_on": "{entity_name} \u0435 \u0432\u043a\u043b\u044e\u0447\u0435\u043d", "turn_off": "\u0418\u0437\u043a\u043b\u044e\u0447\u0432\u0430\u043d\u0435 \u043d\u0430 {entity_name}", "turn_on": "\u0412\u043a\u043b\u044e\u0447\u0432\u0430\u043d\u0435 \u043d\u0430 {entity_name}" }, diff --git a/homeassistant/components/switch/.translations/es-419.json b/homeassistant/components/switch/.translations/es-419.json new file mode 100644 index 00000000000..f9607852036 --- /dev/null +++ b/homeassistant/components/switch/.translations/es-419.json @@ -0,0 +1,18 @@ +{ + "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", + "turn_off": "{entity_name} apagado", + "turn_on": "{entity_name} encendido" + }, + "trigger_type": { + "turned_off": "{entity_name} apagado", + "turned_on": "{entity_name} encendido" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/switch/.translations/fr.json b/homeassistant/components/switch/.translations/fr.json index 4775d62bce3..807b85c5fb5 100644 --- a/homeassistant/components/switch/.translations/fr.json +++ b/homeassistant/components/switch/.translations/fr.json @@ -6,6 +6,8 @@ "turn_on": "Allumer {entity_name}" }, "condition_type": { + "is_off": "{entity_name} est \u00e9teint", + "is_on": "{entity_name} est allum\u00e9", "turn_off": "{entity_name} \u00e9teint", "turn_on": "{entity_name} allum\u00e9" }, diff --git a/homeassistant/components/switch/.translations/hu.json b/homeassistant/components/switch/.translations/hu.json new file mode 100644 index 00000000000..c3ea3190694 --- /dev/null +++ b/homeassistant/components/switch/.translations/hu.json @@ -0,0 +1,19 @@ +{ + "device_automation": { + "action_type": { + "toggle": "{entity_name} be/kikapcsol\u00e1sa", + "turn_off": "{entity_name} kikapcsol\u00e1sa", + "turn_on": "{entity_name} bekapcsol\u00e1sa" + }, + "condition_type": { + "is_off": "{entity_name} ki van kapcsolva", + "is_on": "{entity_name} be van kapcsolva", + "turn_off": "{entity_name} ki lett kapcsolva", + "turn_on": "{entity_name} be lett kapcsolva" + }, + "trigger_type": { + "turned_off": "{entity_name} ki lett kapcsolva", + "turned_on": "{entity_name} be lett kapcsolva" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/switch/.translations/it.json b/homeassistant/components/switch/.translations/it.json index 254c09380c1..ec742e4113b 100644 --- a/homeassistant/components/switch/.translations/it.json +++ b/homeassistant/components/switch/.translations/it.json @@ -6,6 +6,8 @@ "turn_on": "Attivare {entity_name}" }, "condition_type": { + "is_off": "{entity_name} \u00e8 disattivato", + "is_on": "{entity_name} \u00e8 attivo", "turn_off": "{entity_name} disattivato", "turn_on": "{entity_name} attivato" }, diff --git a/homeassistant/components/switch/.translations/no.json b/homeassistant/components/switch/.translations/no.json index adc128991c5..3469079f230 100644 --- a/homeassistant/components/switch/.translations/no.json +++ b/homeassistant/components/switch/.translations/no.json @@ -6,6 +6,8 @@ "turn_on": "Sl\u00e5 p\u00e5 {entity_name}" }, "condition_type": { + "is_off": "{entity_name} er av", + "is_on": "{entity_name} er p\u00e5", "turn_off": "{entity_name} sl\u00e5tt av", "turn_on": "{entity_name} sl\u00e5tt p\u00e5" }, diff --git a/homeassistant/components/switch/.translations/pl.json b/homeassistant/components/switch/.translations/pl.json index c63799bf783..09b43f4100d 100644 --- a/homeassistant/components/switch/.translations/pl.json +++ b/homeassistant/components/switch/.translations/pl.json @@ -1,17 +1,19 @@ { "device_automation": { "action_type": { - "toggle": "Prze\u0142\u0105cz {entity_name}", - "turn_off": "Wy\u0142\u0105cz {entity_name}", - "turn_on": "W\u0142\u0105cz {entity_name}" + "toggle": "prze\u0142\u0105cz {entity_name}", + "turn_off": "wy\u0142\u0105cz {entity_name}", + "turn_on": "w\u0142\u0105cz {entity_name}" }, "condition_type": { - "turn_off": "{entity_name} wy\u0142\u0105czone", - "turn_on": "{entity_name} w\u0142\u0105czone" + "is_off": "prze\u0142\u0105cznik {entity_name} jest wy\u0142\u0105czony", + "is_on": "prze\u0142\u0105cznik {entity_name} jest w\u0142\u0105czony", + "turn_off": "prze\u0142\u0105cznik {entity_name} wy\u0142\u0105czony", + "turn_on": "prze\u0142\u0105cznik {entity_name} w\u0142\u0105czony" }, "trigger_type": { - "turned_off": "{entity_name} wy\u0142\u0105czone", - "turned_on": "{entity_name} w\u0142\u0105czone" + "turned_off": "wy\u0142\u0105czenie {entity_name}", + "turned_on": "w\u0142\u0105czenie {entity_name}" } } } \ No newline at end of file diff --git a/homeassistant/components/switch/.translations/ru.json b/homeassistant/components/switch/.translations/ru.json index 45a941b665d..cd5cbc0d6a1 100644 --- a/homeassistant/components/switch/.translations/ru.json +++ b/homeassistant/components/switch/.translations/ru.json @@ -6,12 +6,14 @@ "turn_on": "\u0412\u043a\u043b\u044e\u0447\u0438\u0442\u044c {entity_name}" }, "condition_type": { + "is_off": "{entity_name} \u0432\u044b\u043a\u043b\u044e\u0447\u0435\u043d\u043e", + "is_on": "{entity_name} \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u043e", "turn_off": "{entity_name} \u0432\u044b\u043a\u043b\u044e\u0447\u0435\u043d\u043e", "turn_on": "{entity_name} \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u043e" }, "trigger_type": { - "turned_off": "{entity_name} \u0432\u044b\u043a\u043b\u044e\u0447\u0435\u043d\u043e", - "turned_on": "{entity_name} \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u043e" + "turned_off": "{entity_name} \u0432\u044b\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435", + "turned_on": "{entity_name} \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435" } } } \ No newline at end of file diff --git a/homeassistant/components/switch/.translations/sl.json b/homeassistant/components/switch/.translations/sl.json index 89423e071fd..f1b851b05b6 100644 --- a/homeassistant/components/switch/.translations/sl.json +++ b/homeassistant/components/switch/.translations/sl.json @@ -6,6 +6,8 @@ "turn_on": "Vklopite {entity_name}" }, "condition_type": { + "is_off": "{entity_name} je izklopljen", + "is_on": "{entity_name} je vklopljen", "turn_off": "{entity_name} izklopljen", "turn_on": "{entity_name} vklopljen" }, diff --git a/homeassistant/components/switch/.translations/zh-Hant.json b/homeassistant/components/switch/.translations/zh-Hant.json index c1a67897b16..517d48354dc 100644 --- a/homeassistant/components/switch/.translations/zh-Hant.json +++ b/homeassistant/components/switch/.translations/zh-Hant.json @@ -6,6 +6,8 @@ "turn_on": "\u958b\u555f {entity_name}" }, "condition_type": { + "is_off": "{entity_name} \u5df2\u95dc\u9589", + "is_on": "{entity_name} \u5df2\u958b\u555f", "turn_off": "{entity_name} \u5df2\u95dc\u9589", "turn_on": "{entity_name} \u5df2\u958b\u555f" }, diff --git a/homeassistant/components/switch/device_action.py b/homeassistant/components/switch/device_action.py new file mode 100644 index 00000000000..a65c1acc512 --- /dev/null +++ b/homeassistant/components/switch/device_action.py @@ -0,0 +1,29 @@ +"""Provides device actions for switches.""" +from typing import List +import voluptuous as vol + +from homeassistant.core import HomeAssistant, Context +from homeassistant.components.device_automation import toggle_entity +from homeassistant.const import CONF_DOMAIN +from homeassistant.helpers.typing import TemplateVarsType, ConfigType +from . import DOMAIN + + +ACTION_SCHEMA = toggle_entity.ACTION_SCHEMA.extend({vol.Required(CONF_DOMAIN): DOMAIN}) + + +async def async_call_action_from_config( + hass: HomeAssistant, + config: ConfigType, + variables: TemplateVarsType, + context: Context, +) -> None: + """Change state based on configuration.""" + await toggle_entity.async_call_action_from_config( + hass, config, variables, context, DOMAIN + ) + + +async def async_get_actions(hass: HomeAssistant, device_id: str) -> List[dict]: + """List device actions.""" + return await toggle_entity.async_get_actions(hass, device_id, DOMAIN) diff --git a/homeassistant/components/switch/device_automation.py b/homeassistant/components/switch/device_automation.py deleted file mode 100644 index 61292d47449..00000000000 --- a/homeassistant/components/switch/device_automation.py +++ /dev/null @@ -1,56 +0,0 @@ -"""Provides device automations for lights.""" -import voluptuous as vol - -from homeassistant.components.device_automation import toggle_entity -from homeassistant.const import CONF_DOMAIN -from . import DOMAIN - - -# mypy: allow-untyped-defs, no-check-untyped-defs - -ACTION_SCHEMA = toggle_entity.ACTION_SCHEMA.extend({vol.Required(CONF_DOMAIN): DOMAIN}) - -CONDITION_SCHEMA = toggle_entity.CONDITION_SCHEMA.extend( - {vol.Required(CONF_DOMAIN): DOMAIN} -) - -TRIGGER_SCHEMA = toggle_entity.TRIGGER_SCHEMA.extend( - {vol.Required(CONF_DOMAIN): DOMAIN} -) - - -async def async_call_action_from_config(hass, config, variables, context): - """Change state based on configuration.""" - config = ACTION_SCHEMA(config) - await toggle_entity.async_call_action_from_config( - hass, config, variables, context, DOMAIN - ) - - -def async_condition_from_config(config, config_validation): - """Evaluate state based on configuration.""" - config = CONDITION_SCHEMA(config) - return toggle_entity.async_condition_from_config(config, config_validation) - - -async def async_trigger(hass, config, action, automation_info): - """Listen for state changes based on configuration.""" - config = TRIGGER_SCHEMA(config) - return await toggle_entity.async_attach_trigger( - hass, config, action, automation_info - ) - - -async def async_get_actions(hass, device_id): - """List device actions.""" - return await toggle_entity.async_get_actions(hass, device_id, DOMAIN) - - -async def async_get_conditions(hass, device_id): - """List device conditions.""" - return await toggle_entity.async_get_conditions(hass, device_id, DOMAIN) - - -async def async_get_triggers(hass, device_id): - """List device triggers.""" - return await toggle_entity.async_get_triggers(hass, device_id, DOMAIN) diff --git a/homeassistant/components/switch/device_condition.py b/homeassistant/components/switch/device_condition.py new file mode 100644 index 00000000000..5825a3ba91a --- /dev/null +++ b/homeassistant/components/switch/device_condition.py @@ -0,0 +1,29 @@ +"""Provides device conditions for switches.""" +from typing import List +import voluptuous as vol + +from homeassistant.core import HomeAssistant +from homeassistant.components.device_automation import toggle_entity +from homeassistant.const import CONF_DOMAIN +from homeassistant.helpers.typing import ConfigType +from homeassistant.helpers.condition import ConditionCheckerType +from . import DOMAIN + + +CONDITION_SCHEMA = toggle_entity.CONDITION_SCHEMA.extend( + {vol.Required(CONF_DOMAIN): DOMAIN} +) + + +def async_condition_from_config( + config: ConfigType, config_validation: bool +) -> ConditionCheckerType: + """Evaluate state based on configuration.""" + if config_validation: + config = CONDITION_SCHEMA(config) + return toggle_entity.async_condition_from_config(config, config_validation) + + +async def async_get_conditions(hass: HomeAssistant, device_id: str) -> List[dict]: + """List device conditions.""" + return await toggle_entity.async_get_conditions(hass, device_id, DOMAIN) diff --git a/homeassistant/components/switch/device_trigger.py b/homeassistant/components/switch/device_trigger.py new file mode 100644 index 00000000000..22a016e49b9 --- /dev/null +++ b/homeassistant/components/switch/device_trigger.py @@ -0,0 +1,37 @@ +"""Provides device triggers for switches.""" +from typing import List +import voluptuous as vol + +from homeassistant.core import HomeAssistant, CALLBACK_TYPE +from homeassistant.components.automation import AutomationActionType +from homeassistant.components.device_automation import toggle_entity +from homeassistant.const import CONF_DOMAIN +from homeassistant.helpers.typing import ConfigType +from . import DOMAIN + + +TRIGGER_SCHEMA = toggle_entity.TRIGGER_SCHEMA.extend( + {vol.Required(CONF_DOMAIN): DOMAIN} +) + + +async def async_attach_trigger( + hass: HomeAssistant, + config: ConfigType, + action: AutomationActionType, + automation_info: dict, +) -> CALLBACK_TYPE: + """Listen for state changes based on configuration.""" + return await toggle_entity.async_attach_trigger( + hass, config, action, automation_info + ) + + +async def async_get_triggers(hass: HomeAssistant, device_id: str) -> List[dict]: + """List device triggers.""" + return await toggle_entity.async_get_triggers(hass, device_id, DOMAIN) + + +async def async_get_trigger_capabilities(hass: HomeAssistant, trigger: dict) -> dict: + """List trigger capabilities.""" + return await toggle_entity.async_get_trigger_capabilities(hass, trigger) diff --git a/homeassistant/components/switch/light.py b/homeassistant/components/switch/light.py index 2027a8fc458..8f3b5d87f8c 100644 --- a/homeassistant/components/switch/light.py +++ b/homeassistant/components/switch/light.py @@ -1,6 +1,6 @@ """Light support for switch entities.""" import logging -from typing import cast +from typing import cast, Callable, Dict, Optional, Sequence import voluptuous as vol @@ -14,13 +14,14 @@ from homeassistant.const import ( ) from homeassistant.core import State, callback import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import async_track_state_change from homeassistant.helpers.typing import ConfigType, HomeAssistantType from homeassistant.components.light import PLATFORM_SCHEMA, Light -# mypy: allow-incomplete-defs, allow-untyped-calls, allow-untyped-defs +# mypy: allow-untyped-calls, allow-untyped-defs _LOGGER = logging.getLogger(__name__) @@ -35,7 +36,10 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( async def async_setup_platform( - hass: HomeAssistantType, config: ConfigType, async_add_entities, discovery_info=None + hass: HomeAssistantType, + config: ConfigType, + async_add_entities: Callable[[Sequence[Entity], bool], None], + discovery_info: Optional[Dict] = None, ) -> None: """Initialize Light Switch platform.""" async_add_entities( @@ -105,7 +109,7 @@ class LightSwitch(Light): @callback def async_state_changed_listener( entity_id: str, old_state: State, new_state: State - ): + ) -> None: """Handle child updates.""" self.async_schedule_update_ha_state(True) diff --git a/homeassistant/components/switch/manifest.json b/homeassistant/components/switch/manifest.json index 0f287251582..73daca18c6a 100644 --- a/homeassistant/components/switch/manifest.json +++ b/homeassistant/components/switch/manifest.json @@ -1,7 +1,7 @@ { "domain": "switch", "name": "Switch", - "documentation": "https://www.home-assistant.io/components/switch", + "documentation": "https://www.home-assistant.io/integrations/switch", "requirements": [], "dependencies": [ "group" diff --git a/homeassistant/components/switchbot/manifest.json b/homeassistant/components/switchbot/manifest.json index b9ea4eb276e..204a3605bb8 100644 --- a/homeassistant/components/switchbot/manifest.json +++ b/homeassistant/components/switchbot/manifest.json @@ -1,7 +1,7 @@ { "domain": "switchbot", "name": "Switchbot", - "documentation": "https://www.home-assistant.io/components/switchbot", + "documentation": "https://www.home-assistant.io/integrations/switchbot", "requirements": [ "PySwitchbot==0.6.2" ], diff --git a/homeassistant/components/switcher_kis/manifest.json b/homeassistant/components/switcher_kis/manifest.json index 2f3b3b6e84a..453ae542b2c 100644 --- a/homeassistant/components/switcher_kis/manifest.json +++ b/homeassistant/components/switcher_kis/manifest.json @@ -1,7 +1,7 @@ { "domain": "switcher_kis", "name": "Switcher", - "documentation": "https://www.home-assistant.io/components/switcher_kis/", + "documentation": "https://www.home-assistant.io/integrations/switcher_kis/", "codeowners": [ "@tomerfi" ], diff --git a/homeassistant/components/switchmate/manifest.json b/homeassistant/components/switchmate/manifest.json index 94f100abe86..b15024b545c 100644 --- a/homeassistant/components/switchmate/manifest.json +++ b/homeassistant/components/switchmate/manifest.json @@ -1,7 +1,7 @@ { "domain": "switchmate", "name": "Switchmate", - "documentation": "https://www.home-assistant.io/components/switchmate", + "documentation": "https://www.home-assistant.io/integrations/switchmate", "requirements": [ "pySwitchmate==0.4.6" ], diff --git a/homeassistant/components/syncthru/manifest.json b/homeassistant/components/syncthru/manifest.json index a2a45826d9d..79e6f8e5571 100644 --- a/homeassistant/components/syncthru/manifest.json +++ b/homeassistant/components/syncthru/manifest.json @@ -1,7 +1,7 @@ { "domain": "syncthru", "name": "Syncthru", - "documentation": "https://www.home-assistant.io/components/syncthru", + "documentation": "https://www.home-assistant.io/integrations/syncthru", "requirements": [ "pysyncthru==0.4.3" ], diff --git a/homeassistant/components/synology/manifest.json b/homeassistant/components/synology/manifest.json index a108f5fa983..ea743836c74 100644 --- a/homeassistant/components/synology/manifest.json +++ b/homeassistant/components/synology/manifest.json @@ -1,7 +1,7 @@ { "domain": "synology", "name": "Synology", - "documentation": "https://www.home-assistant.io/components/synology", + "documentation": "https://www.home-assistant.io/integrations/synology", "requirements": [ "py-synology==0.2.0" ], diff --git a/homeassistant/components/synology_chat/manifest.json b/homeassistant/components/synology_chat/manifest.json index d35b1d8c902..3522ba0405c 100644 --- a/homeassistant/components/synology_chat/manifest.json +++ b/homeassistant/components/synology_chat/manifest.json @@ -1,7 +1,7 @@ { "domain": "synology_chat", "name": "Synology chat", - "documentation": "https://www.home-assistant.io/components/synology_chat", + "documentation": "https://www.home-assistant.io/integrations/synology_chat", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/synology_srm/manifest.json b/homeassistant/components/synology_srm/manifest.json index a790a6c453c..c507e07c717 100644 --- a/homeassistant/components/synology_srm/manifest.json +++ b/homeassistant/components/synology_srm/manifest.json @@ -1,7 +1,7 @@ { "domain": "synology_srm", "name": "Synology SRM", - "documentation": "https://www.home-assistant.io/components/synology_srm", + "documentation": "https://www.home-assistant.io/integrations/synology_srm", "requirements": [ "synology-srm==0.0.7" ], diff --git a/homeassistant/components/synologydsm/manifest.json b/homeassistant/components/synologydsm/manifest.json index fcce2e52a21..d7dc76b6ac9 100644 --- a/homeassistant/components/synologydsm/manifest.json +++ b/homeassistant/components/synologydsm/manifest.json @@ -1,7 +1,7 @@ { "domain": "synologydsm", "name": "Synologydsm", - "documentation": "https://www.home-assistant.io/components/synologydsm", + "documentation": "https://www.home-assistant.io/integrations/synologydsm", "requirements": [ "python-synology==0.2.0" ], diff --git a/homeassistant/components/syslog/manifest.json b/homeassistant/components/syslog/manifest.json index 19836ffa67f..00dda3ebc02 100644 --- a/homeassistant/components/syslog/manifest.json +++ b/homeassistant/components/syslog/manifest.json @@ -1,7 +1,7 @@ { "domain": "syslog", "name": "Syslog", - "documentation": "https://www.home-assistant.io/components/syslog", + "documentation": "https://www.home-assistant.io/integrations/syslog", "requirements": [], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/system_health/manifest.json b/homeassistant/components/system_health/manifest.json index 9c2b7bcae39..4de7af3f862 100644 --- a/homeassistant/components/system_health/manifest.json +++ b/homeassistant/components/system_health/manifest.json @@ -1,7 +1,7 @@ { "domain": "system_health", "name": "System health", - "documentation": "https://www.home-assistant.io/components/system_health", + "documentation": "https://www.home-assistant.io/integrations/system_health", "requirements": [], "dependencies": [ "http" diff --git a/homeassistant/components/system_log/manifest.json b/homeassistant/components/system_log/manifest.json index 01f70af4a15..0bc14a56e71 100644 --- a/homeassistant/components/system_log/manifest.json +++ b/homeassistant/components/system_log/manifest.json @@ -1,7 +1,7 @@ { "domain": "system_log", "name": "System log", - "documentation": "https://www.home-assistant.io/components/system_log", + "documentation": "https://www.home-assistant.io/integrations/system_log", "requirements": [], "dependencies": [ "http" diff --git a/homeassistant/components/systemmonitor/manifest.json b/homeassistant/components/systemmonitor/manifest.json index 565a459818f..a45a59e410a 100644 --- a/homeassistant/components/systemmonitor/manifest.json +++ b/homeassistant/components/systemmonitor/manifest.json @@ -1,7 +1,7 @@ { "domain": "systemmonitor", "name": "Systemmonitor", - "documentation": "https://www.home-assistant.io/components/systemmonitor", + "documentation": "https://www.home-assistant.io/integrations/systemmonitor", "requirements": [ "psutil==5.6.3" ], diff --git a/homeassistant/components/systemmonitor/sensor.py b/homeassistant/components/systemmonitor/sensor.py index 446a36ec350..ad2072baaa5 100644 --- a/homeassistant/components/systemmonitor/sensor.py +++ b/homeassistant/components/systemmonitor/sensor.py @@ -1,5 +1,4 @@ """Support for monitoring the local system.""" -from datetime import datetime import logging import os import socket @@ -193,7 +192,7 @@ class SystemMonitorSensor(Entity): counters = psutil.net_io_counters(pernic=True) if self.argument in counters: counter = counters[self.argument][IO_COUNTER[self.type]] - now = datetime.now() + now = dt_util.utcnow() if self._last_value and self._last_value < counter: self._state = round( (counter - self._last_value) diff --git a/homeassistant/components/sytadin/__init__.py b/homeassistant/components/sytadin/__init__.py deleted file mode 100644 index 5243fe379a7..00000000000 --- a/homeassistant/components/sytadin/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""The sytadin component.""" diff --git a/homeassistant/components/sytadin/manifest.json b/homeassistant/components/sytadin/manifest.json deleted file mode 100644 index c1453d88d81..00000000000 --- a/homeassistant/components/sytadin/manifest.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "domain": "sytadin", - "name": "Sytadin", - "documentation": "https://www.home-assistant.io/components/sytadin", - "requirements": [ - "beautifulsoup4==4.8.0" - ], - "dependencies": [], - "codeowners": [ - "@gautric" - ] -} diff --git a/homeassistant/components/sytadin/sensor.py b/homeassistant/components/sytadin/sensor.py deleted file mode 100644 index b7c94933a39..00000000000 --- a/homeassistant/components/sytadin/sensor.py +++ /dev/null @@ -1,151 +0,0 @@ -"""Support for Sytadin Traffic, French Traffic Supervision.""" -import logging -import re -from datetime import timedelta - -import requests -import voluptuous as vol - -import homeassistant.helpers.config_validation as cv -from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import ( - LENGTH_KILOMETERS, - CONF_MONITORED_CONDITIONS, - CONF_NAME, - ATTR_ATTRIBUTION, -) -from homeassistant.helpers.entity import Entity -from homeassistant.util import Throttle - -_LOGGER = logging.getLogger(__name__) - -URL = "http://www.sytadin.fr/sys/barometres_de_la_circulation.jsp.html" - -ATTRIBUTION = "Data provided by Direction des routes Île-de-France (DiRIF)" - -DEFAULT_NAME = "Sytadin" -REGEX = r"(\d*\.\d+|\d+)" - -OPTION_TRAFFIC_JAM = "traffic_jam" -OPTION_MEAN_VELOCITY = "mean_velocity" -OPTION_CONGESTION = "congestion" - -SENSOR_TYPES = { - OPTION_CONGESTION: ["Congestion", ""], - OPTION_MEAN_VELOCITY: ["Mean Velocity", LENGTH_KILOMETERS + "/h"], - OPTION_TRAFFIC_JAM: ["Traffic Jam", LENGTH_KILOMETERS], -} - -MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=5) - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Optional(CONF_MONITORED_CONDITIONS, default=[OPTION_TRAFFIC_JAM]): vol.All( - cv.ensure_list, [vol.In(SENSOR_TYPES)] - ), - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - } -) - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up of the Sytadin Traffic sensor platform.""" - _LOGGER.warning( - "The sytadin integration is deprecated and will be removed " - "in Home Assistant 0.100.0. For more information see ADR-0004:" - "https://github.com/home-assistant/architecture/blob/master/adr/0004-webscraping.md" - ) - - name = config.get(CONF_NAME) - - sytadin = SytadinData(URL) - - dev = [] - for option in config.get(CONF_MONITORED_CONDITIONS): - _LOGGER.debug("Sensor device - %s", option) - dev.append( - SytadinSensor( - sytadin, name, option, SENSOR_TYPES[option][0], SENSOR_TYPES[option][1] - ) - ) - add_entities(dev, True) - - -class SytadinSensor(Entity): - """Representation of a Sytadin Sensor.""" - - def __init__(self, data, name, sensor_type, option, unit): - """Initialize the sensor.""" - self.data = data - self._state = None - self._name = name - self._option = option - self._type = sensor_type - self._unit = unit - - @property - def name(self): - """Return the name of the sensor.""" - return f"{self._name} {self._option}" - - @property - def state(self): - """Return the state of the sensor.""" - return self._state - - @property - def unit_of_measurement(self): - """Return the unit of measurement.""" - return self._unit - - @property - def device_state_attributes(self): - """Return the state attributes.""" - return {ATTR_ATTRIBUTION: ATTRIBUTION} - - def update(self): - """Fetch new state data for the sensor.""" - self.data.update() - - if self.data is None: - return - - if self._type == OPTION_TRAFFIC_JAM: - self._state = self.data.traffic_jam - elif self._type == OPTION_MEAN_VELOCITY: - self._state = self.data.mean_velocity - elif self._type == OPTION_CONGESTION: - self._state = self.data.congestion - - -class SytadinData: - """The class for handling the data retrieval.""" - - def __init__(self, resource): - """Initialize the data object.""" - self._resource = resource - self.data = None - self.traffic_jam = self.mean_velocity = self.congestion = None - - @Throttle(MIN_TIME_BETWEEN_UPDATES) - def update(self): - """Get the latest data from the Sytadin.""" - from bs4 import BeautifulSoup - - try: - raw_html = requests.get(self._resource, timeout=10).text - data = BeautifulSoup(raw_html, "html.parser") - - values = data.select(".barometre_valeur") - parse_traffic_jam = re.search(REGEX, values[0].text) - if parse_traffic_jam: - self.traffic_jam = parse_traffic_jam.group() - parse_mean_velocity = re.search(REGEX, values[1].text) - if parse_mean_velocity: - self.mean_velocity = parse_mean_velocity.group() - parse_congestion = re.search(REGEX, values[2].text) - if parse_congestion: - self.congestion = parse_congestion.group() - except requests.exceptions.ConnectionError: - _LOGGER.error("Connection error") - self.data = None diff --git a/homeassistant/components/tado/manifest.json b/homeassistant/components/tado/manifest.json index 8d42cde1c05..9a884fa010c 100644 --- a/homeassistant/components/tado/manifest.json +++ b/homeassistant/components/tado/manifest.json @@ -1,7 +1,7 @@ { "domain": "tado", "name": "Tado", - "documentation": "https://www.home-assistant.io/components/tado", + "documentation": "https://www.home-assistant.io/integrations/tado", "requirements": [ "python-tado==0.2.9" ], diff --git a/homeassistant/components/tahoma/cover.py b/homeassistant/components/tahoma/cover.py index a189199bfb2..7448eb27ae0 100644 --- a/homeassistant/components/tahoma/cover.py +++ b/homeassistant/components/tahoma/cover.py @@ -137,14 +137,13 @@ class TahomaCover(TahomaDevice, CoverDevice): if self._closure is not None: if self.tahoma_device.type == HORIZONTAL_AWNING: self._position = self._closure - self._closed = self._position == 0 else: self._position = 100 - self._closure - self._closed = self._position == 100 if self._position <= 5: self._position = 0 if self._position >= 95: self._position = 100 + self._closed = self._position == 0 else: self._position = None if "core:OpenClosedState" in self.tahoma_device.active_states: diff --git a/homeassistant/components/tahoma/manifest.json b/homeassistant/components/tahoma/manifest.json index ca3ab0bc882..1e99d4b288d 100644 --- a/homeassistant/components/tahoma/manifest.json +++ b/homeassistant/components/tahoma/manifest.json @@ -1,7 +1,7 @@ { "domain": "tahoma", "name": "Tahoma", - "documentation": "https://www.home-assistant.io/components/tahoma", + "documentation": "https://www.home-assistant.io/integrations/tahoma", "requirements": [ "tahoma-api==0.0.14" ], diff --git a/homeassistant/components/tank_utility/manifest.json b/homeassistant/components/tank_utility/manifest.json index 04ffb48f396..328285ab67b 100644 --- a/homeassistant/components/tank_utility/manifest.json +++ b/homeassistant/components/tank_utility/manifest.json @@ -1,7 +1,7 @@ { "domain": "tank_utility", "name": "Tank utility", - "documentation": "https://www.home-assistant.io/components/tank_utility", + "documentation": "https://www.home-assistant.io/integrations/tank_utility", "requirements": [ "tank_utility==1.4.0" ], diff --git a/homeassistant/components/tapsaff/manifest.json b/homeassistant/components/tapsaff/manifest.json index 1c8e8476987..79fff1b2b4c 100644 --- a/homeassistant/components/tapsaff/manifest.json +++ b/homeassistant/components/tapsaff/manifest.json @@ -1,7 +1,7 @@ { "domain": "tapsaff", "name": "Tapsaff", - "documentation": "https://www.home-assistant.io/components/tapsaff", + "documentation": "https://www.home-assistant.io/integrations/tapsaff", "requirements": [ "tapsaff==0.2.1" ], diff --git a/homeassistant/components/tautulli/manifest.json b/homeassistant/components/tautulli/manifest.json index d49b5280181..cc635082c35 100644 --- a/homeassistant/components/tautulli/manifest.json +++ b/homeassistant/components/tautulli/manifest.json @@ -1,7 +1,7 @@ { "domain": "tautulli", "name": "Tautulli", - "documentation": "https://www.home-assistant.io/components/tautulli", + "documentation": "https://www.home-assistant.io/integrations/tautulli", "requirements": [ "pytautulli==0.5.0" ], diff --git a/homeassistant/components/tcp/manifest.json b/homeassistant/components/tcp/manifest.json index 2ff29a27f31..53f3f6c6463 100644 --- a/homeassistant/components/tcp/manifest.json +++ b/homeassistant/components/tcp/manifest.json @@ -1,7 +1,7 @@ { "domain": "tcp", "name": "Tcp", - "documentation": "https://www.home-assistant.io/components/tcp", + "documentation": "https://www.home-assistant.io/integrations/tcp", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/tcp/sensor.py b/homeassistant/components/tcp/sensor.py index 70301f203f8..a387b3fc0bb 100644 --- a/homeassistant/components/tcp/sensor.py +++ b/homeassistant/components/tcp/sensor.py @@ -81,7 +81,7 @@ class TcpSensor(Entity): name = self._config[CONF_NAME] if name is not None: return name - return super(TcpSensor, self).name + return super().name @property def state(self): diff --git a/homeassistant/components/ted5000/manifest.json b/homeassistant/components/ted5000/manifest.json index 9cc50405bad..b02a9db3fdc 100644 --- a/homeassistant/components/ted5000/manifest.json +++ b/homeassistant/components/ted5000/manifest.json @@ -1,7 +1,7 @@ { "domain": "ted5000", "name": "Ted5000", - "documentation": "https://www.home-assistant.io/components/ted5000", + "documentation": "https://www.home-assistant.io/integrations/ted5000", "requirements": [ "xmltodict==0.12.0" ], diff --git a/homeassistant/components/teksavvy/manifest.json b/homeassistant/components/teksavvy/manifest.json index 14afdec3b71..220a086e0be 100644 --- a/homeassistant/components/teksavvy/manifest.json +++ b/homeassistant/components/teksavvy/manifest.json @@ -1,7 +1,7 @@ { "domain": "teksavvy", "name": "Teksavvy", - "documentation": "https://www.home-assistant.io/components/teksavvy", + "documentation": "https://www.home-assistant.io/integrations/teksavvy", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/telegram/manifest.json b/homeassistant/components/telegram/manifest.json index 8a6dd7fb369..392c45ea886 100644 --- a/homeassistant/components/telegram/manifest.json +++ b/homeassistant/components/telegram/manifest.json @@ -1,7 +1,7 @@ { "domain": "telegram", "name": "Telegram", - "documentation": "https://www.home-assistant.io/components/telegram", + "documentation": "https://www.home-assistant.io/integrations/telegram", "requirements": [], "dependencies": [ "telegram_bot" diff --git a/homeassistant/components/telegram_bot/manifest.json b/homeassistant/components/telegram_bot/manifest.json index f341fd587ca..afc83879cb0 100644 --- a/homeassistant/components/telegram_bot/manifest.json +++ b/homeassistant/components/telegram_bot/manifest.json @@ -1,7 +1,7 @@ { "domain": "telegram_bot", "name": "Telegram bot", - "documentation": "https://www.home-assistant.io/components/telegram_bot", + "documentation": "https://www.home-assistant.io/integrations/telegram_bot", "requirements": [ "python-telegram-bot==11.1.0" ], diff --git a/homeassistant/components/tellduslive/.translations/es-419.json b/homeassistant/components/tellduslive/.translations/es-419.json index 503530e728a..1281784dceb 100644 --- a/homeassistant/components/tellduslive/.translations/es-419.json +++ b/homeassistant/components/tellduslive/.translations/es-419.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_setup": "TelldusLive ya est\u00e1 configurado", + "authorize_url_fail": "Error desconocido al generar una URL de autorizaci\u00f3n.", "unknown": "Se produjo un error desconocido" }, "error": { @@ -17,6 +18,7 @@ "host": "Host" } } - } + }, + "title": "Telldus Live" } } \ No newline at end of file diff --git a/homeassistant/components/tellduslive/.translations/nn.json b/homeassistant/components/tellduslive/.translations/nn.json new file mode 100644 index 00000000000..934f56a420b --- /dev/null +++ b/homeassistant/components/tellduslive/.translations/nn.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Telldus Live" + } +} \ No newline at end of file diff --git a/homeassistant/components/tellduslive/.translations/no.json b/homeassistant/components/tellduslive/.translations/no.json index d311b3b0d38..3258cf2ddca 100644 --- a/homeassistant/components/tellduslive/.translations/no.json +++ b/homeassistant/components/tellduslive/.translations/no.json @@ -12,12 +12,13 @@ "step": { "auth": { "description": "For \u00e5 koble TelldusLive-kontoen din:\n 1. Klikk p\u00e5 linken under\n 2. Logg inn p\u00e5 Telldus Live \n 3. Tillat **{app_name}** (klikk**Ja**). \n 4. Kom tilbake hit og klikk **SUBMIT**. \n\n [Link TelldusLive-konto]({auth_url})", - "title": "Godkjen mot TelldusLive" + "title": "Godkjenn mot TelldusLive" }, "user": { "data": { "host": "Vert" }, + "description": "Tom", "title": "Velg endepunkt." } }, diff --git a/homeassistant/components/tellduslive/.translations/ru.json b/homeassistant/components/tellduslive/.translations/ru.json index afaaf4edbf5..9d3c97ad902 100644 --- a/homeassistant/components/tellduslive/.translations/ru.json +++ b/homeassistant/components/tellduslive/.translations/ru.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_setup": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\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.", "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.", "unknown": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430" diff --git a/homeassistant/components/tellduslive/manifest.json b/homeassistant/components/tellduslive/manifest.json index 7f431ba92b1..777138d44ab 100644 --- a/homeassistant/components/tellduslive/manifest.json +++ b/homeassistant/components/tellduslive/manifest.json @@ -2,7 +2,7 @@ "domain": "tellduslive", "name": "Tellduslive", "config_flow": true, - "documentation": "https://www.home-assistant.io/components/tellduslive", + "documentation": "https://www.home-assistant.io/integrations/tellduslive", "requirements": [ "tellduslive==0.10.10" ], diff --git a/homeassistant/components/tellstick/manifest.json b/homeassistant/components/tellstick/manifest.json index c50ba514f2a..3391533b081 100644 --- a/homeassistant/components/tellstick/manifest.json +++ b/homeassistant/components/tellstick/manifest.json @@ -1,7 +1,7 @@ { "domain": "tellstick", "name": "Tellstick", - "documentation": "https://www.home-assistant.io/components/tellstick", + "documentation": "https://www.home-assistant.io/integrations/tellstick", "requirements": [ "tellcore-net==0.4", "tellcore-py==1.1.2" diff --git a/homeassistant/components/telnet/manifest.json b/homeassistant/components/telnet/manifest.json index 58f5e15cc1a..afba0e38301 100644 --- a/homeassistant/components/telnet/manifest.json +++ b/homeassistant/components/telnet/manifest.json @@ -1,7 +1,7 @@ { "domain": "telnet", "name": "Telnet", - "documentation": "https://www.home-assistant.io/components/telnet", + "documentation": "https://www.home-assistant.io/integrations/telnet", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/temper/manifest.json b/homeassistant/components/temper/manifest.json index 0e60c957d9d..76656e9936f 100644 --- a/homeassistant/components/temper/manifest.json +++ b/homeassistant/components/temper/manifest.json @@ -1,7 +1,7 @@ { "domain": "temper", "name": "Temper", - "documentation": "https://www.home-assistant.io/components/temper", + "documentation": "https://www.home-assistant.io/integrations/temper", "requirements": [ "temperusb==1.5.3" ], diff --git a/homeassistant/components/template/binary_sensor.py b/homeassistant/components/template/binary_sensor.py index e0fc8677200..d5ade703c97 100644 --- a/homeassistant/components/template/binary_sensor.py +++ b/homeassistant/components/template/binary_sensor.py @@ -26,6 +26,7 @@ from homeassistant.exceptions import TemplateError import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import async_generate_entity_id from homeassistant.helpers.event import async_track_state_change, async_track_same_state +from .const import CONF_AVAILABILITY_TEMPLATE _LOGGER = logging.getLogger(__name__) @@ -38,6 +39,7 @@ SENSOR_SCHEMA = vol.Schema( vol.Required(CONF_VALUE_TEMPLATE): cv.template, vol.Optional(CONF_ICON_TEMPLATE): cv.template, vol.Optional(CONF_ENTITY_PICTURE_TEMPLATE): cv.template, + vol.Optional(CONF_AVAILABILITY_TEMPLATE): cv.template, vol.Optional(CONF_ATTRIBUTE_TEMPLATES): vol.Schema({cv.string: cv.template}), vol.Optional(ATTR_FRIENDLY_NAME): cv.string, vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, @@ -60,6 +62,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= value_template = device_config[CONF_VALUE_TEMPLATE] icon_template = device_config.get(CONF_ICON_TEMPLATE) entity_picture_template = device_config.get(CONF_ENTITY_PICTURE_TEMPLATE) + availability_template = device_config.get(CONF_AVAILABILITY_TEMPLATE) entity_ids = set() manual_entity_ids = device_config.get(ATTR_ENTITY_ID) attribute_templates = device_config.get(CONF_ATTRIBUTE_TEMPLATES, {}) @@ -70,6 +73,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= CONF_VALUE_TEMPLATE: value_template, CONF_ICON_TEMPLATE: icon_template, CONF_ENTITY_PICTURE_TEMPLATE: entity_picture_template, + CONF_AVAILABILITY_TEMPLATE: availability_template, } for tpl_name, template in chain(templates.items(), attribute_templates.items()): @@ -117,6 +121,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= value_template, icon_template, entity_picture_template, + availability_template, entity_ids, delay_on, delay_off, @@ -143,6 +148,7 @@ class BinarySensorTemplate(BinarySensorDevice): value_template, icon_template, entity_picture_template, + availability_template, entity_ids, delay_on, delay_off, @@ -156,12 +162,14 @@ class BinarySensorTemplate(BinarySensorDevice): self._template = value_template self._state = None self._icon_template = icon_template + self._availability_template = availability_template self._entity_picture_template = entity_picture_template self._icon = None self._entity_picture = None self._entities = entity_ids self._delay_on = delay_on self._delay_off = delay_off + self._available = True self._attribute_templates = attribute_templates self._attributes = {} @@ -223,6 +231,11 @@ class BinarySensorTemplate(BinarySensorDevice): """No polling needed.""" return False + @property + def available(self): + """Availability indicator.""" + return self._available + @callback def _async_render(self): """Get the state of template.""" @@ -240,11 +253,6 @@ class BinarySensorTemplate(BinarySensorDevice): return _LOGGER.error("Could not render template %s: %s", self._name, ex) - templates = { - "_icon": self._icon_template, - "_entity_picture": self._entity_picture_template, - } - attrs = {} if self._attribute_templates is not None: for key, value in self._attribute_templates.items(): @@ -254,12 +262,21 @@ class BinarySensorTemplate(BinarySensorDevice): _LOGGER.error("Error rendering attribute %s: %s", key, err) self._attributes = attrs + templates = { + "_icon": self._icon_template, + "_entity_picture": self._entity_picture_template, + "_available": self._availability_template, + } + for property_name, template in templates.items(): if template is None: continue try: - setattr(self, property_name, template.async_render()) + value = template.async_render() + if property_name == "_available": + value = value.lower() == "true" + setattr(self, property_name, value) except TemplateError as ex: friendly_property_name = property_name[1:].replace("_", " ") if ex.args and ex.args[0].startswith( diff --git a/homeassistant/components/template/const.py b/homeassistant/components/template/const.py new file mode 100644 index 00000000000..e6cf69341f9 --- /dev/null +++ b/homeassistant/components/template/const.py @@ -0,0 +1,3 @@ +"""Constants for the Template Platform Components.""" + +CONF_AVAILABILITY_TEMPLATE = "availability_template" diff --git a/homeassistant/components/template/cover.py b/homeassistant/components/template/cover.py index 51b9a523b3b..483ee1ae872 100644 --- a/homeassistant/components/template/cover.py +++ b/homeassistant/components/template/cover.py @@ -38,6 +38,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import async_generate_entity_id from homeassistant.helpers.event import async_track_state_change from homeassistant.helpers.script import Script +from .const import CONF_AVAILABILITY_TEMPLATE _LOGGER = logging.getLogger(__name__) _VALID_STATES = [STATE_OPEN, STATE_CLOSED, "true", "false"] @@ -74,6 +75,7 @@ COVER_SCHEMA = vol.Schema( vol.Exclusive( CONF_VALUE_TEMPLATE, CONF_VALUE_OR_POSITION_TEMPLATE ): cv.template, + vol.Optional(CONF_AVAILABILITY_TEMPLATE): cv.template, vol.Optional(CONF_POSITION_TEMPLATE): cv.template, vol.Optional(CONF_TILT_TEMPLATE): cv.template, vol.Optional(CONF_ICON_TEMPLATE): cv.template, @@ -103,6 +105,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= position_template = device_config.get(CONF_POSITION_TEMPLATE) tilt_template = device_config.get(CONF_TILT_TEMPLATE) icon_template = device_config.get(CONF_ICON_TEMPLATE) + availability_template = device_config.get(CONF_AVAILABILITY_TEMPLATE) entity_picture_template = device_config.get(CONF_ENTITY_PICTURE_TEMPLATE) device_class = device_config.get(CONF_DEVICE_CLASS) open_action = device_config.get(OPEN_ACTION) @@ -144,6 +147,11 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= if str(temp_ids) != MATCH_ALL: template_entity_ids |= set(temp_ids) + if availability_template is not None: + temp_ids = availability_template.extract_entities() + if str(temp_ids) != MATCH_ALL: + template_entity_ids |= set(temp_ids) + if not template_entity_ids: template_entity_ids = MATCH_ALL @@ -160,6 +168,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= tilt_template, icon_template, entity_picture_template, + availability_template, open_action, close_action, stop_action, @@ -192,6 +201,7 @@ class CoverTemplate(CoverDevice): tilt_template, icon_template, entity_picture_template, + availability_template, open_action, close_action, stop_action, @@ -213,6 +223,7 @@ class CoverTemplate(CoverDevice): self._icon_template = icon_template self._device_class = device_class self._entity_picture_template = entity_picture_template + self._availability_template = availability_template self._open_script = None if open_action is not None: self._open_script = Script(hass, open_action) @@ -235,6 +246,7 @@ class CoverTemplate(CoverDevice): self._position = None self._tilt_value = None self._entities = entity_ids + self._available = True if self._template is not None: self._template.hass = self.hass @@ -246,6 +258,8 @@ class CoverTemplate(CoverDevice): self._icon_template.hass = self.hass if self._entity_picture_template is not None: self._entity_picture_template.hass = self.hass + if self._availability_template is not None: + self._availability_template.hass = self.hass async def async_added_to_hass(self): """Register callbacks.""" @@ -332,6 +346,11 @@ class CoverTemplate(CoverDevice): """Return the polling state.""" return False + @property + def available(self) -> bool: + """Return if the device is available.""" + return self._available + async def async_open_cover(self, **kwargs): """Move the cover up.""" if self._open_script: @@ -430,11 +449,8 @@ class CoverTemplate(CoverDevice): ) else: self._position = state - except TemplateError as ex: - _LOGGER.error(ex) - self._position = None - except ValueError as ex: - _LOGGER.error(ex) + except (TemplateError, ValueError) as err: + _LOGGER.error(err) self._position = None if self._tilt_template is not None: try: @@ -447,22 +463,23 @@ class CoverTemplate(CoverDevice): ) else: self._tilt_value = state - except TemplateError as ex: - _LOGGER.error(ex) - self._tilt_value = None - except ValueError as ex: - _LOGGER.error(ex) + except (TemplateError, ValueError) as err: + _LOGGER.error(err) self._tilt_value = None for property_name, template in ( ("_icon", self._icon_template), ("_entity_picture", self._entity_picture_template), + ("_available", self._availability_template), ): if template is None: continue try: - setattr(self, property_name, template.async_render()) + value = template.async_render() + if property_name == "_available": + value = value.lower() == "true" + setattr(self, property_name, value) except TemplateError as ex: friendly_property_name = property_name[1:].replace("_", " ") if ex.args and ex.args[0].startswith( diff --git a/homeassistant/components/template/fan.py b/homeassistant/components/template/fan.py index 7fd8c4d9b3c..42790e618d9 100644 --- a/homeassistant/components/template/fan.py +++ b/homeassistant/components/template/fan.py @@ -33,6 +33,7 @@ from homeassistant.core import callback from homeassistant.exceptions import TemplateError from homeassistant.helpers.entity import async_generate_entity_id from homeassistant.helpers.script import Script +from .const import CONF_AVAILABILITY_TEMPLATE _LOGGER = logging.getLogger(__name__) @@ -58,6 +59,7 @@ FAN_SCHEMA = vol.Schema( vol.Optional(CONF_SPEED_TEMPLATE): cv.template, vol.Optional(CONF_OSCILLATING_TEMPLATE): cv.template, vol.Optional(CONF_DIRECTION_TEMPLATE): cv.template, + vol.Optional(CONF_AVAILABILITY_TEMPLATE): cv.template, vol.Required(CONF_ON_ACTION): cv.SCRIPT_SCHEMA, vol.Required(CONF_OFF_ACTION): cv.SCRIPT_SCHEMA, vol.Optional(CONF_SET_SPEED_ACTION): cv.SCRIPT_SCHEMA, @@ -86,6 +88,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= speed_template = device_config.get(CONF_SPEED_TEMPLATE) oscillating_template = device_config.get(CONF_OSCILLATING_TEMPLATE) direction_template = device_config.get(CONF_DIRECTION_TEMPLATE) + availability_template = device_config.get(CONF_AVAILABILITY_TEMPLATE) on_action = device_config[CONF_ON_ACTION] off_action = device_config[CONF_OFF_ACTION] @@ -103,6 +106,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= speed_template, oscillating_template, direction_template, + availability_template, ): if template is None: continue @@ -131,6 +135,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= speed_template, oscillating_template, direction_template, + availability_template, on_action, off_action, set_speed_action, @@ -156,6 +161,7 @@ class TemplateFan(FanEntity): speed_template, oscillating_template, direction_template, + availability_template, on_action, off_action, set_speed_action, @@ -175,6 +181,8 @@ class TemplateFan(FanEntity): self._speed_template = speed_template self._oscillating_template = oscillating_template self._direction_template = direction_template + self._availability_template = availability_template + self._available = True self._supported_features = 0 self._on_script = Script(hass, on_action) @@ -207,6 +215,8 @@ class TemplateFan(FanEntity): if self._direction_template: self._direction_template.hass = self.hass self._supported_features |= SUPPORT_DIRECTION + if self._availability_template: + self._availability_template.hass = self.hass self._entities = entity_ids # List of valid speeds @@ -252,6 +262,11 @@ class TemplateFan(FanEntity): """Return the polling state.""" return False + @property + def available(self): + """Return availability of Device.""" + return self._available + # pylint: disable=arguments-differ async def async_turn_on(self, speed: str = None) -> None: """Turn on the fan.""" @@ -422,3 +437,17 @@ class TemplateFan(FanEntity): ", ".join(_VALID_DIRECTIONS), ) self._direction = None + + # Update Availability if 'availability_template' is defined + if self._availability_template is not None: + try: + self._available = ( + self._availability_template.async_render().lower() == "true" + ) + except (TemplateError, ValueError) as ex: + _LOGGER.error( + "Could not render %s template %s: %s", + CONF_AVAILABILITY_TEMPLATE, + self._name, + ex, + ) diff --git a/homeassistant/components/template/light.py b/homeassistant/components/template/light.py index 320dcd2e22f..552c21f170d 100644 --- a/homeassistant/components/template/light.py +++ b/homeassistant/components/template/light.py @@ -28,6 +28,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import async_generate_entity_id from homeassistant.helpers.event import async_track_state_change from homeassistant.helpers.script import Script +from .const import CONF_AVAILABILITY_TEMPLATE _LOGGER = logging.getLogger(__name__) _VALID_STATES = [STATE_ON, STATE_OFF, "true", "false"] @@ -44,6 +45,7 @@ LIGHT_SCHEMA = vol.Schema( vol.Optional(CONF_VALUE_TEMPLATE): cv.template, vol.Optional(CONF_ICON_TEMPLATE): cv.template, vol.Optional(CONF_ENTITY_PICTURE_TEMPLATE): cv.template, + vol.Optional(CONF_AVAILABILITY_TEMPLATE): cv.template, vol.Optional(CONF_LEVEL_ACTION): cv.SCRIPT_SCHEMA, vol.Optional(CONF_LEVEL_TEMPLATE): cv.template, vol.Optional(CONF_FRIENDLY_NAME): cv.string, @@ -65,6 +67,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= state_template = device_config.get(CONF_VALUE_TEMPLATE) icon_template = device_config.get(CONF_ICON_TEMPLATE) entity_picture_template = device_config.get(CONF_ENTITY_PICTURE_TEMPLATE) + availability_template = device_config.get(CONF_AVAILABILITY_TEMPLATE) on_action = device_config[CONF_ON_ACTION] off_action = device_config[CONF_OFF_ACTION] level_action = device_config.get(CONF_LEVEL_ACTION) @@ -92,6 +95,11 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= if str(temp_ids) != MATCH_ALL: template_entity_ids |= set(temp_ids) + if availability_template is not None: + temp_ids = availability_template.extract_entities() + if str(temp_ids) != MATCH_ALL: + template_entity_ids |= set(temp_ids) + if not template_entity_ids: template_entity_ids = MATCH_ALL @@ -105,6 +113,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= state_template, icon_template, entity_picture_template, + availability_template, on_action, off_action, level_action, @@ -132,6 +141,7 @@ class LightTemplate(Light): state_template, icon_template, entity_picture_template, + availability_template, on_action, off_action, level_action, @@ -147,6 +157,7 @@ class LightTemplate(Light): self._template = state_template self._icon_template = icon_template self._entity_picture_template = entity_picture_template + self._availability_template = availability_template self._on_script = Script(hass, on_action) self._off_script = Script(hass, off_action) self._level_script = None @@ -159,6 +170,7 @@ class LightTemplate(Light): self._entity_picture = None self._brightness = None self._entities = entity_ids + self._available = True if self._template is not None: self._template.hass = self.hass @@ -168,6 +180,8 @@ class LightTemplate(Light): self._icon_template.hass = self.hass if self._entity_picture_template is not None: self._entity_picture_template.hass = self.hass + if self._availability_template is not None: + self._availability_template.hass = self.hass @property def brightness(self): @@ -207,6 +221,11 @@ class LightTemplate(Light): """Return the entity picture to use in the frontend, if any.""" return self._entity_picture + @property + def available(self) -> bool: + """Return if the device is available.""" + return self._available + async def async_added_to_hass(self): """Register callbacks.""" @@ -218,7 +237,11 @@ class LightTemplate(Light): @callback def template_light_startup(event): """Update template on startup.""" - if self._template is not None or self._level_template is not None: + if ( + self._template is not None + or self._level_template is not None + or self._availability_template is not None + ): async_track_state_change( self.hass, self._entities, template_light_state_listener ) @@ -298,12 +321,16 @@ class LightTemplate(Light): for property_name, template in ( ("_icon", self._icon_template), ("_entity_picture", self._entity_picture_template), + ("_available", self._availability_template), ): if template is None: continue try: - setattr(self, property_name, template.async_render()) + value = template.async_render() + if property_name == "_available": + value = value.lower() == "true" + setattr(self, property_name, value) except TemplateError as ex: friendly_property_name = property_name[1:].replace("_", " ") if ex.args and ex.args[0].startswith( diff --git a/homeassistant/components/template/lock.py b/homeassistant/components/template/lock.py index d7f501987f9..aa8cc8b1224 100644 --- a/homeassistant/components/template/lock.py +++ b/homeassistant/components/template/lock.py @@ -19,6 +19,7 @@ from homeassistant.const import ( from homeassistant.exceptions import TemplateError from homeassistant.helpers.event import async_track_state_change from homeassistant.helpers.script import Script +from .const import CONF_AVAILABILITY_TEMPLATE _LOGGER = logging.getLogger(__name__) @@ -34,6 +35,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( vol.Required(CONF_LOCK): cv.SCRIPT_SCHEMA, vol.Required(CONF_UNLOCK): cv.SCRIPT_SCHEMA, vol.Required(CONF_VALUE_TEMPLATE): cv.template, + vol.Optional(CONF_AVAILABILITY_TEMPLATE): cv.template, vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean, } ) @@ -48,21 +50,32 @@ async def async_setup_platform(hass, config, async_add_devices, discovery_info=N if value_template_entity_ids == MATCH_ALL: _LOGGER.warning( - "Template lock %s has no entity ids configured to track nor " - "were we able to extract the entities to track from the %s " + "Template lock '%s' has no entity ids configured to track nor " + "were we able to extract the entities to track from the '%s' " "template. This entity will only be able to be updated " "manually.", name, CONF_VALUE_TEMPLATE, ) + template_entity_ids = set() + template_entity_ids |= set(value_template_entity_ids) + + availability_template = config.get(CONF_AVAILABILITY_TEMPLATE) + if availability_template is not None: + availability_template.hass = hass + temp_ids = availability_template.extract_entities() + if str(temp_ids) != MATCH_ALL: + template_entity_ids |= set(temp_ids) + async_add_devices( [ TemplateLock( hass, name, value_template, - value_template_entity_ids, + availability_template, + template_entity_ids, config.get(CONF_LOCK), config.get(CONF_UNLOCK), config.get(CONF_OPTIMISTIC), @@ -79,6 +92,7 @@ class TemplateLock(LockDevice): hass, name, value_template, + availability_template, entity_ids, command_lock, command_unlock, @@ -89,10 +103,12 @@ class TemplateLock(LockDevice): self._hass = hass self._name = name self._state_template = value_template + self._availability_template = availability_template self._state_entities = entity_ids self._command_lock = Script(hass, command_lock) self._command_unlock = Script(hass, command_unlock) self._optimistic = optimistic + self._available = True async def async_added_to_hass(self): """Register callbacks.""" @@ -136,6 +152,11 @@ class TemplateLock(LockDevice): """Return true if lock is locked.""" return self._state + @property + def available(self) -> bool: + """Return if the device is available.""" + return self._available + async def async_update(self): """Update the state from the template.""" try: @@ -148,6 +169,19 @@ class TemplateLock(LockDevice): self._state = None _LOGGER.error("Could not render template %s: %s", self._name, ex) + if self._availability_template is not None: + try: + self._available = ( + self._availability_template.async_render().lower() == "true" + ) + except (TemplateError, ValueError) as ex: + _LOGGER.error( + "Could not render %s template %s: %s", + CONF_AVAILABILITY_TEMPLATE, + self._name, + ex, + ) + async def async_lock(self, **kwargs): """Lock the device.""" if self._optimistic: diff --git a/homeassistant/components/template/manifest.json b/homeassistant/components/template/manifest.json index c8406c9d084..20a35f1afe7 100644 --- a/homeassistant/components/template/manifest.json +++ b/homeassistant/components/template/manifest.json @@ -1,7 +1,7 @@ { "domain": "template", "name": "Template", - "documentation": "https://www.home-assistant.io/components/template", + "documentation": "https://www.home-assistant.io/integrations/template", "requirements": [], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/template/sensor.py b/homeassistant/components/template/sensor.py index b77528e0c32..a8768193736 100644 --- a/homeassistant/components/template/sensor.py +++ b/homeassistant/components/template/sensor.py @@ -24,10 +24,12 @@ from homeassistant.const import ( MATCH_ALL, CONF_DEVICE_CLASS, ) + from homeassistant.exceptions import TemplateError import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity, async_generate_entity_id from homeassistant.helpers.event import async_track_state_change +from .const import CONF_AVAILABILITY_TEMPLATE CONF_ATTRIBUTE_TEMPLATES = "attribute_templates" @@ -39,6 +41,7 @@ SENSOR_SCHEMA = vol.Schema( vol.Optional(CONF_ICON_TEMPLATE): cv.template, vol.Optional(CONF_ENTITY_PICTURE_TEMPLATE): cv.template, vol.Optional(CONF_FRIENDLY_NAME_TEMPLATE): cv.template, + vol.Optional(CONF_AVAILABILITY_TEMPLATE): cv.template, vol.Optional(CONF_ATTRIBUTE_TEMPLATES, default={}): vol.Schema( {cv.string: cv.template} ), @@ -62,6 +65,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= state_template = device_config[CONF_VALUE_TEMPLATE] icon_template = device_config.get(CONF_ICON_TEMPLATE) entity_picture_template = device_config.get(CONF_ENTITY_PICTURE_TEMPLATE) + availability_template = device_config.get(CONF_AVAILABILITY_TEMPLATE) friendly_name = device_config.get(ATTR_FRIENDLY_NAME, device) friendly_name_template = device_config.get(CONF_FRIENDLY_NAME_TEMPLATE) unit_of_measurement = device_config.get(ATTR_UNIT_OF_MEASUREMENT) @@ -77,6 +81,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= CONF_ICON_TEMPLATE: icon_template, CONF_ENTITY_PICTURE_TEMPLATE: entity_picture_template, CONF_FRIENDLY_NAME_TEMPLATE: friendly_name_template, + CONF_AVAILABILITY_TEMPLATE: availability_template, } for tpl_name, template in chain(templates.items(), attribute_templates.items()): @@ -120,15 +125,12 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= state_template, icon_template, entity_picture_template, + availability_template, entity_ids, device_class, attribute_templates, ) ) - if not sensors: - _LOGGER.error("No sensors added") - return False - async_add_entities(sensors) return True @@ -146,6 +148,7 @@ class SensorTemplate(Entity): state_template, icon_template, entity_picture_template, + availability_template, entity_ids, device_class, attribute_templates, @@ -162,10 +165,12 @@ class SensorTemplate(Entity): self._state = None self._icon_template = icon_template self._entity_picture_template = entity_picture_template + self._availability_template = availability_template self._icon = None self._entity_picture = None self._entities = entity_ids self._device_class = device_class + self._available = True self._attribute_templates = attribute_templates self._attributes = {} @@ -222,6 +227,11 @@ class SensorTemplate(Entity): """Return the unit_of_measurement of the device.""" return self._unit_of_measurement + @property + def available(self) -> bool: + """Return if the device is available.""" + return self._available + @property def device_state_attributes(self): """Return the state attributes.""" @@ -236,7 +246,9 @@ class SensorTemplate(Entity): """Update the state from the template.""" try: self._state = self._template.async_render() + self._available = True except TemplateError as ex: + self._available = False if ex.args and ex.args[0].startswith( "UndefinedError: 'None' has no attribute" ): @@ -248,12 +260,6 @@ class SensorTemplate(Entity): self._state = None _LOGGER.error("Could not render template %s: %s", self._name, ex) - templates = { - "_icon": self._icon_template, - "_entity_picture": self._entity_picture_template, - "_name": self._friendly_name_template, - } - attrs = {} for key, value in self._attribute_templates.items(): try: @@ -263,12 +269,22 @@ class SensorTemplate(Entity): self._attributes = attrs + templates = { + "_icon": self._icon_template, + "_entity_picture": self._entity_picture_template, + "_name": self._friendly_name_template, + "_available": self._availability_template, + } + for property_name, template in templates.items(): if template is None: continue try: - setattr(self, property_name, template.async_render()) + value = template.async_render() + if property_name == "_available": + value = value.lower() == "true" + setattr(self, property_name, value) except TemplateError as ex: friendly_property_name = property_name[1:].replace("_", " ") if ex.args and ex.args[0].startswith( diff --git a/homeassistant/components/template/switch.py b/homeassistant/components/template/switch.py index c77a90c1f8b..2d4dda032ca 100644 --- a/homeassistant/components/template/switch.py +++ b/homeassistant/components/template/switch.py @@ -19,12 +19,14 @@ from homeassistant.const import ( ATTR_ENTITY_ID, CONF_SWITCHES, EVENT_HOMEASSISTANT_START, + MATCH_ALL, ) from homeassistant.exceptions import TemplateError import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import async_generate_entity_id from homeassistant.helpers.event import async_track_state_change from homeassistant.helpers.script import Script +from .const import CONF_AVAILABILITY_TEMPLATE _LOGGER = logging.getLogger(__name__) _VALID_STATES = [STATE_ON, STATE_OFF, "true", "false"] @@ -37,6 +39,7 @@ SWITCH_SCHEMA = vol.Schema( vol.Required(CONF_VALUE_TEMPLATE): cv.template, vol.Optional(CONF_ICON_TEMPLATE): cv.template, vol.Optional(CONF_ENTITY_PICTURE_TEMPLATE): cv.template, + vol.Optional(CONF_AVAILABILITY_TEMPLATE): cv.template, vol.Required(ON_ACTION): cv.SCRIPT_SCHEMA, vol.Required(OFF_ACTION): cv.SCRIPT_SCHEMA, vol.Optional(ATTR_FRIENDLY_NAME): cv.string, @@ -58,19 +61,47 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= state_template = device_config[CONF_VALUE_TEMPLATE] icon_template = device_config.get(CONF_ICON_TEMPLATE) entity_picture_template = device_config.get(CONF_ENTITY_PICTURE_TEMPLATE) + availability_template = device_config.get(CONF_AVAILABILITY_TEMPLATE) on_action = device_config[ON_ACTION] off_action = device_config[OFF_ACTION] - entity_ids = ( - device_config.get(ATTR_ENTITY_ID) or state_template.extract_entities() - ) + manual_entity_ids = device_config.get(ATTR_ENTITY_ID) + entity_ids = set() - state_template.hass = hass + templates = { + CONF_VALUE_TEMPLATE: state_template, + CONF_ICON_TEMPLATE: icon_template, + CONF_ENTITY_PICTURE_TEMPLATE: entity_picture_template, + CONF_AVAILABILITY_TEMPLATE: availability_template, + } + invalid_templates = [] - if icon_template is not None: - icon_template.hass = hass + for template_name, template in templates.items(): + if template is not None: + template.hass = hass - if entity_picture_template is not None: - entity_picture_template.hass = hass + if manual_entity_ids is not None: + continue + + template_entity_ids = template.extract_entities() + if template_entity_ids == MATCH_ALL: + invalid_templates.append(template_name.replace("_template", "")) + entity_ids = MATCH_ALL + elif entity_ids != MATCH_ALL: + entity_ids |= set(template_entity_ids) + if invalid_templates: + _LOGGER.warning( + "Template sensor %s has no entity ids configured to track nor" + " were we able to extract the entities to track from the %s " + "template(s). This entity will only be able to be updated " + "manually.", + device, + ", ".join(invalid_templates), + ) + else: + if manual_entity_ids is None: + entity_ids = list(entity_ids) + else: + entity_ids = manual_entity_ids switches.append( SwitchTemplate( @@ -80,6 +111,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= state_template, icon_template, entity_picture_template, + availability_template, on_action, off_action, entity_ids, @@ -104,6 +136,7 @@ class SwitchTemplate(SwitchDevice): state_template, icon_template, entity_picture_template, + availability_template, on_action, off_action, entity_ids, @@ -120,9 +153,11 @@ class SwitchTemplate(SwitchDevice): self._state = False self._icon_template = icon_template self._entity_picture_template = entity_picture_template + self._availability_template = availability_template self._icon = None self._entity_picture = None self._entities = entity_ids + self._available = True async def async_added_to_hass(self): """Register callbacks.""" @@ -160,11 +195,6 @@ class SwitchTemplate(SwitchDevice): """Return the polling state.""" return False - @property - def available(self): - """If switch is available.""" - return self._state is not None - @property def icon(self): """Return the icon to use in the frontend, if any.""" @@ -175,6 +205,11 @@ class SwitchTemplate(SwitchDevice): """Return the entity_picture to use in the frontend, if any.""" return self._entity_picture + @property + def available(self) -> bool: + """Return if the device is available.""" + return self._available + async def async_turn_on(self, **kwargs): """Fire the on action.""" await self._on_script.async_run(context=self._context) @@ -205,12 +240,16 @@ class SwitchTemplate(SwitchDevice): for property_name, template in ( ("_icon", self._icon_template), ("_entity_picture", self._entity_picture_template), + ("_available", self._availability_template), ): if template is None: continue try: - setattr(self, property_name, template.async_render()) + value = template.async_render() + if property_name == "_available": + value = value.lower() == "true" + setattr(self, property_name, value) except TemplateError as ex: friendly_property_name = property_name[1:].replace("_", " ") if ex.args and ex.args[0].startswith( diff --git a/homeassistant/components/template/vacuum.py b/homeassistant/components/template/vacuum.py index 5374247dacc..6a6523514c4 100644 --- a/homeassistant/components/template/vacuum.py +++ b/homeassistant/components/template/vacuum.py @@ -44,6 +44,8 @@ from homeassistant.exceptions import TemplateError from homeassistant.helpers.entity import async_generate_entity_id from homeassistant.helpers.script import Script +from .const import CONF_AVAILABILITY_TEMPLATE + _LOGGER = logging.getLogger(__name__) CONF_VACUUMS = "vacuums" @@ -67,6 +69,7 @@ VACUUM_SCHEMA = vol.Schema( vol.Optional(CONF_VALUE_TEMPLATE): cv.template, vol.Optional(CONF_BATTERY_LEVEL_TEMPLATE): cv.template, vol.Optional(CONF_FAN_SPEED_TEMPLATE): cv.template, + vol.Optional(CONF_AVAILABILITY_TEMPLATE): cv.template, vol.Required(SERVICE_START): cv.SCRIPT_SCHEMA, vol.Optional(SERVICE_PAUSE): cv.SCRIPT_SCHEMA, vol.Optional(SERVICE_STOP): cv.SCRIPT_SCHEMA, @@ -94,6 +97,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= state_template = device_config.get(CONF_VALUE_TEMPLATE) battery_level_template = device_config.get(CONF_BATTERY_LEVEL_TEMPLATE) fan_speed_template = device_config.get(CONF_FAN_SPEED_TEMPLATE) + availability_template = device_config.get(CONF_AVAILABILITY_TEMPLATE) start_action = device_config[SERVICE_START] pause_action = device_config.get(SERVICE_PAUSE) @@ -113,6 +117,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= (CONF_VALUE_TEMPLATE, state_template), (CONF_BATTERY_LEVEL_TEMPLATE, battery_level_template), (CONF_FAN_SPEED_TEMPLATE, fan_speed_template), + (CONF_AVAILABILITY_TEMPLATE, availability_template), ): if template is None: continue @@ -152,6 +157,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= state_template, battery_level_template, fan_speed_template, + availability_template, start_action, pause_action, stop_action, @@ -178,6 +184,7 @@ class TemplateVacuum(StateVacuumDevice): state_template, battery_level_template, fan_speed_template, + availability_template, start_action, pause_action, stop_action, @@ -198,6 +205,7 @@ class TemplateVacuum(StateVacuumDevice): self._template = state_template self._battery_level_template = battery_level_template self._fan_speed_template = fan_speed_template + self._availability_template = availability_template self._supported_features = SUPPORT_START self._start_script = Script(hass, start_action) @@ -235,6 +243,7 @@ class TemplateVacuum(StateVacuumDevice): self._state = None self._battery_level = None self._fan_speed = None + self._available = True if self._template: self._supported_features |= SUPPORT_STATE @@ -280,6 +289,11 @@ class TemplateVacuum(StateVacuumDevice): """Return the polling state.""" return False + @property + def available(self) -> bool: + """Return if the device is available.""" + return self._available + async def async_start(self): """Start or resume the cleaning task.""" await self._start_script.async_run(context=self._context) @@ -421,3 +435,16 @@ class TemplateVacuum(StateVacuumDevice): self._fan_speed_list, ) self._fan_speed = None + # Update availability if availability template is defined + if self._availability_template is not None: + try: + self._available = ( + self._availability_template.async_render().lower() == "true" + ) + except (TemplateError, ValueError) as ex: + _LOGGER.error( + "Could not render %s template %s: %s", + CONF_AVAILABILITY_TEMPLATE, + self._name, + ex, + ) diff --git a/homeassistant/components/tensorflow/image_processing.py b/homeassistant/components/tensorflow/image_processing.py index b977a087eae..65e20f558a7 100644 --- a/homeassistant/components/tensorflow/image_processing.py +++ b/homeassistant/components/tensorflow/image_processing.py @@ -12,6 +12,7 @@ from homeassistant.components.image_processing import ( CONF_SOURCE, PLATFORM_SCHEMA, ImageProcessingEntity, + draw_box, ) from homeassistant.core import split_entity_id from homeassistant.helpers import template @@ -67,24 +68,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( ) -def draw_box(draw, box, img_width, img_height, text="", color=(255, 255, 0)): - """Draw bounding box on image.""" - ymin, xmin, ymax, xmax = box - (left, right, top, bottom) = ( - xmin * img_width, - xmax * img_width, - ymin * img_height, - ymax * img_height, - ) - draw.line( - [(left, top), (left, bottom), (right, bottom), (right, top), (left, top)], - width=5, - fill=color, - ) - if text: - draw.text((left, abs(top - 15)), text, fill=color) - - def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the TensorFlow image processing platform.""" model_config = config.get(CONF_MODEL) diff --git a/homeassistant/components/tensorflow/manifest.json b/homeassistant/components/tensorflow/manifest.json index 9419cbaaefb..e7d35829ffb 100644 --- a/homeassistant/components/tensorflow/manifest.json +++ b/homeassistant/components/tensorflow/manifest.json @@ -1,11 +1,10 @@ { "domain": "tensorflow", "name": "Tensorflow", - "documentation": "https://www.home-assistant.io/components/tensorflow", + "documentation": "https://www.home-assistant.io/integrations/tensorflow", "requirements": [ "tensorflow==1.13.2", "numpy==1.17.1", - "pillow==6.1.0", "protobuf==3.6.1" ], "dependencies": [], diff --git a/homeassistant/components/tesla/manifest.json b/homeassistant/components/tesla/manifest.json index ab32a64e670..4071178c7c3 100644 --- a/homeassistant/components/tesla/manifest.json +++ b/homeassistant/components/tesla/manifest.json @@ -1,7 +1,7 @@ { "domain": "tesla", "name": "Tesla", - "documentation": "https://www.home-assistant.io/components/tesla", + "documentation": "https://www.home-assistant.io/integrations/tesla", "requirements": [ "teslajsonpy==0.0.25" ], diff --git a/homeassistant/components/tfiac/manifest.json b/homeassistant/components/tfiac/manifest.json index 9997ae00f0a..7c93000790c 100644 --- a/homeassistant/components/tfiac/manifest.json +++ b/homeassistant/components/tfiac/manifest.json @@ -1,9 +1,9 @@ { "domain": "tfiac", "name": "Tfiac", - "documentation": "https://www.home-assistant.io/components/tfiac", + "documentation": "https://www.home-assistant.io/integrations/tfiac", "requirements": [ - "pytfiac==0.3" + "pytfiac==0.4" ], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/thermoworks_smoke/manifest.json b/homeassistant/components/thermoworks_smoke/manifest.json index fab670627ba..93a334fc986 100644 --- a/homeassistant/components/thermoworks_smoke/manifest.json +++ b/homeassistant/components/thermoworks_smoke/manifest.json @@ -1,7 +1,7 @@ { "domain": "thermoworks_smoke", "name": "Thermoworks smoke", - "documentation": "https://www.home-assistant.io/components/thermoworks_smoke", + "documentation": "https://www.home-assistant.io/integrations/thermoworks_smoke", "requirements": [ "stringcase==1.2.0", "thermoworks_smoke==0.1.8" diff --git a/homeassistant/components/thethingsnetwork/manifest.json b/homeassistant/components/thethingsnetwork/manifest.json index 8d6082d74bf..98a852dea08 100644 --- a/homeassistant/components/thethingsnetwork/manifest.json +++ b/homeassistant/components/thethingsnetwork/manifest.json @@ -1,7 +1,7 @@ { "domain": "thethingsnetwork", "name": "Thethingsnetwork", - "documentation": "https://www.home-assistant.io/components/thethingsnetwork", + "documentation": "https://www.home-assistant.io/integrations/thethingsnetwork", "requirements": [], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/thingspeak/manifest.json b/homeassistant/components/thingspeak/manifest.json index 482bb94ac2a..689a6678cab 100644 --- a/homeassistant/components/thingspeak/manifest.json +++ b/homeassistant/components/thingspeak/manifest.json @@ -1,7 +1,7 @@ { "domain": "thingspeak", "name": "Thingspeak", - "documentation": "https://www.home-assistant.io/components/thingspeak", + "documentation": "https://www.home-assistant.io/integrations/thingspeak", "requirements": [ "thingspeak==0.4.1" ], diff --git a/homeassistant/components/thinkingcleaner/manifest.json b/homeassistant/components/thinkingcleaner/manifest.json index 4e43270a5e0..c7c8aaaf16a 100644 --- a/homeassistant/components/thinkingcleaner/manifest.json +++ b/homeassistant/components/thinkingcleaner/manifest.json @@ -1,7 +1,7 @@ { "domain": "thinkingcleaner", "name": "Thinkingcleaner", - "documentation": "https://www.home-assistant.io/components/thinkingcleaner", + "documentation": "https://www.home-assistant.io/integrations/thinkingcleaner", "requirements": [ "pythinkingcleaner==0.0.3" ], diff --git a/homeassistant/components/thomson/manifest.json b/homeassistant/components/thomson/manifest.json index 063c84d4ff7..ac07a2f77ad 100644 --- a/homeassistant/components/thomson/manifest.json +++ b/homeassistant/components/thomson/manifest.json @@ -1,7 +1,7 @@ { "domain": "thomson", "name": "Thomson", - "documentation": "https://www.home-assistant.io/components/thomson", + "documentation": "https://www.home-assistant.io/integrations/thomson", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/threshold/manifest.json b/homeassistant/components/threshold/manifest.json index 107b4351505..f3aa5405278 100644 --- a/homeassistant/components/threshold/manifest.json +++ b/homeassistant/components/threshold/manifest.json @@ -1,7 +1,7 @@ { "domain": "threshold", "name": "Threshold", - "documentation": "https://www.home-assistant.io/components/threshold", + "documentation": "https://www.home-assistant.io/integrations/threshold", "requirements": [], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/tibber/manifest.json b/homeassistant/components/tibber/manifest.json index d0f358c5902..11c5a676bf6 100644 --- a/homeassistant/components/tibber/manifest.json +++ b/homeassistant/components/tibber/manifest.json @@ -1,7 +1,7 @@ { "domain": "tibber", "name": "Tibber", - "documentation": "https://www.home-assistant.io/components/tibber", + "documentation": "https://www.home-assistant.io/integrations/tibber", "requirements": [ "pyTibber==0.11.7" ], diff --git a/homeassistant/components/tibber/sensor.py b/homeassistant/components/tibber/sensor.py index 3dfe0265bde..a5a7f320d93 100644 --- a/homeassistant/components/tibber/sensor.py +++ b/homeassistant/components/tibber/sensor.py @@ -44,8 +44,8 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async_add_entities(dev, True) -class TibberSensorElPrice(Entity): - """Representation of an Tibber sensor for el price.""" +class TibberSensor(Entity): + """Representation of a generic Tibber sensor.""" def __init__(self, tibber_home): """Initialize the sensor.""" @@ -54,10 +54,25 @@ class TibberSensorElPrice(Entity): self._state = None self._is_available = False self._device_state_attributes = {} - self._unit_of_measurement = self._tibber_home.price_unit - self._name = "Electricity price {}".format( - tibber_home.info["viewer"]["home"]["appNickname"] - ) + self._name = tibber_home.info["viewer"]["home"]["appNickname"] + if self._name is None: + self._name = tibber_home.info["viewer"]["home"]["address"].get( + "address1", "" + ) + + @property + def device_state_attributes(self): + """Return the state attributes.""" + return self._device_state_attributes + + @property + def state(self): + """Return the state of the device.""" + return self._state + + +class TibberSensorElPrice(TibberSensor): + """Representation of a Tibber sensor for el price.""" async def async_update(self): """Get the latest data and updates the states.""" @@ -86,11 +101,6 @@ class TibberSensorElPrice(Entity): self._device_state_attributes.update(attrs) self._is_available = self._state is not None - @property - def device_state_attributes(self): - """Return the state attributes.""" - return self._device_state_attributes - @property def available(self): """Return True if entity is available.""" @@ -99,12 +109,7 @@ class TibberSensorElPrice(Entity): @property def name(self): """Return the name of the sensor.""" - return self._name - - @property - def state(self): - """Return the state of the device.""" - return self._state + return "Electricity price {}".format(self._name) @property def icon(self): @@ -114,7 +119,7 @@ class TibberSensorElPrice(Entity): @property def unit_of_measurement(self): """Return the unit of measurement of this entity.""" - return self._unit_of_measurement + return self._tibber_home.price_unit @property def unique_id(self): @@ -139,17 +144,8 @@ class TibberSensorElPrice(Entity): ]["estimatedAnnualConsumption"] -class TibberSensorRT(Entity): - """Representation of an Tibber sensor for real time consumption.""" - - def __init__(self, tibber_home): - """Initialize the sensor.""" - self._tibber_home = tibber_home - self._state = None - self._device_state_attributes = {} - self._unit_of_measurement = "W" - nickname = tibber_home.info["viewer"]["home"]["appNickname"] - self._name = f"Real time consumption {nickname}" +class TibberSensorRT(TibberSensor): + """Representation of a Tibber sensor for real time consumption.""" async def async_added_to_hass(self): """Start unavailability tracking.""" @@ -175,11 +171,6 @@ class TibberSensorRT(Entity): self.async_schedule_update_ha_state() - @property - def device_state_attributes(self): - """Return the state attributes.""" - return self._device_state_attributes - @property def available(self): """Return True if entity is available.""" @@ -188,18 +179,13 @@ class TibberSensorRT(Entity): @property def name(self): """Return the name of the sensor.""" - return self._name + return "Real time consumption {}".format(self._name) @property def should_poll(self): """Return the polling state.""" return False - @property - def state(self): - """Return the state of the device.""" - return self._state - @property def icon(self): """Return the icon to use in the frontend.""" @@ -208,7 +194,7 @@ class TibberSensorRT(Entity): @property def unit_of_measurement(self): """Return the unit of measurement of this entity.""" - return self._unit_of_measurement + return "W" @property def unique_id(self): diff --git a/homeassistant/components/tikteck/manifest.json b/homeassistant/components/tikteck/manifest.json index 7edaf9ba978..c81b6f0a0b1 100644 --- a/homeassistant/components/tikteck/manifest.json +++ b/homeassistant/components/tikteck/manifest.json @@ -1,7 +1,7 @@ { "domain": "tikteck", "name": "Tikteck", - "documentation": "https://www.home-assistant.io/components/tikteck", + "documentation": "https://www.home-assistant.io/integrations/tikteck", "requirements": [ "tikteck==0.4" ], diff --git a/homeassistant/components/tile/manifest.json b/homeassistant/components/tile/manifest.json index 3d26e8315ae..5e40c89369a 100644 --- a/homeassistant/components/tile/manifest.json +++ b/homeassistant/components/tile/manifest.json @@ -1,7 +1,7 @@ { "domain": "tile", "name": "Tile", - "documentation": "https://www.home-assistant.io/components/tile", + "documentation": "https://www.home-assistant.io/integrations/tile", "requirements": [ "pytile==2.0.6" ], diff --git a/homeassistant/components/time_date/manifest.json b/homeassistant/components/time_date/manifest.json index bd620d4a18f..1824ea2db41 100644 --- a/homeassistant/components/time_date/manifest.json +++ b/homeassistant/components/time_date/manifest.json @@ -1,7 +1,7 @@ { "domain": "time_date", "name": "Time date", - "documentation": "https://www.home-assistant.io/components/time_date", + "documentation": "https://www.home-assistant.io/integrations/time_date", "requirements": [], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/timer/manifest.json b/homeassistant/components/timer/manifest.json index 76a506faee8..1cf2c610014 100644 --- a/homeassistant/components/timer/manifest.json +++ b/homeassistant/components/timer/manifest.json @@ -1,7 +1,7 @@ { "domain": "timer", "name": "Timer", - "documentation": "https://www.home-assistant.io/components/timer", + "documentation": "https://www.home-assistant.io/integrations/timer", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/tod/manifest.json b/homeassistant/components/tod/manifest.json index ff67748d64c..ff582b33cef 100644 --- a/homeassistant/components/tod/manifest.json +++ b/homeassistant/components/tod/manifest.json @@ -1,7 +1,7 @@ { "domain": "tod", "name": "Tod", - "documentation": "https://www.home-assistant.io/components/tod", + "documentation": "https://www.home-assistant.io/integrations/tod", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/todoist/calendar.py b/homeassistant/components/todoist/calendar.py index 7d2f51f29af..1179fd90868 100644 --- a/homeassistant/components/todoist/calendar.py +++ b/homeassistant/components/todoist/calendar.py @@ -36,6 +36,7 @@ CONTENT = "content" DESCRIPTION = "description" # Calendar Platform: Used in the '_get_date()' method DATETIME = "dateTime" +DUE = "due" # Service Call: When is this task due (in natural language)? DUE_DATE_STRING = "due_date_string" # Service Call: The language of DUE_DATE_STRING @@ -206,7 +207,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): project_id = project_id_lookup[project_name] # Create the task - item = api.items.add(call.data[CONTENT], project_id) + item = api.items.add(call.data[CONTENT], project_id=project_id) if LABELS in call.data: task_labels = call.data[LABELS] @@ -216,11 +217,12 @@ def setup_platform(hass, config, add_entities, discovery_info=None): if PRIORITY in call.data: item.update(priority=call.data[PRIORITY]) + _due: dict = {} if DUE_DATE_STRING in call.data: - item.update(date_string=call.data[DUE_DATE_STRING]) + _due["string"] = call.data[DUE_DATE_STRING] if DUE_DATE_LANG in call.data: - item.update(date_lang=call.data[DUE_DATE_LANG]) + _due["lang"] = call.data[DUE_DATE_LANG] if DUE_DATE in call.data: due_date = dt.parse_datetime(call.data[DUE_DATE]) @@ -231,7 +233,11 @@ def setup_platform(hass, config, add_entities, discovery_info=None): due_date = dt.as_utc(due_date) date_format = "%Y-%m-%dT%H:%M" due_date = datetime.strftime(due_date, date_format) - item.update(due_date_utc=due_date) + _due["date"] = due_date + + if _due: + item.update(due=_due) + # Commit changes api.commit() _LOGGER.debug("Created Todoist task: %s", call.data[CONTENT]) @@ -241,6 +247,17 @@ def setup_platform(hass, config, add_entities, discovery_info=None): ) +def _parse_due_date(data: dict) -> datetime: + """Parse the due date dict into a datetime object.""" + # Add time information to date only strings. + if len(data["date"]) == 10: + data["date"] += "T00:00:00" + # If there is no timezone provided, use UTC. + if data["timezone"] is None: + data["date"] += "Z" + return dt.parse_datetime(data["date"]) + + class TodoistProjectDevice(CalendarEventDevice): """A device for getting the next Task from a Todoist Project.""" @@ -412,16 +429,8 @@ class TodoistProjectData: # complete the task. # Generally speaking, that means right now. task[START] = dt.utcnow() - if data[DUE_DATE_UTC] is not None: - due_date = data[DUE_DATE_UTC] - - # Due dates are represented in RFC3339 format, in UTC. - # Home Assistant exclusively uses UTC, so it'll - # handle the conversion. - time_format = "%a %d %b %Y %H:%M:%S %z" - # HASS' built-in parse time function doesn't like - # Todoist's time format; strptime has to be used. - task[END] = datetime.strptime(due_date, time_format) + if data[DUE] is not None: + task[END] = _parse_due_date(data[DUE]) if self._latest_due_date is not None and ( task[END] > self._latest_due_date @@ -484,39 +493,44 @@ class TodoistProjectData: for proposed_event in project_tasks: if event == proposed_event: continue + if proposed_event[COMPLETED]: # Event is complete! continue + if proposed_event[END] is None: # No end time: if event[END] is None and (proposed_event[PRIORITY] < event[PRIORITY]): # They also have no end time, # but we have a higher priority. event = proposed_event - continue - else: - continue - elif event[END] is None: + continue + + if event[END] is None: # We have an end time, they do not. event = proposed_event continue + if proposed_event[END].date() > event[END].date(): # Event is too late. continue - elif proposed_event[END].date() < event[END].date(): + + if proposed_event[END].date() < event[END].date(): # Event is earlier than current, select it. event = proposed_event continue - else: - if proposed_event[PRIORITY] > event[PRIORITY]: - # Proposed event has a higher priority. - event = proposed_event - continue - elif proposed_event[PRIORITY] == event[PRIORITY] and ( - proposed_event[END] < event[END] - ): - event = proposed_event - continue + + if proposed_event[PRIORITY] > event[PRIORITY]: + # Proposed event has a higher priority. + event = proposed_event + continue + + if proposed_event[PRIORITY] == event[PRIORITY] and ( + proposed_event[END] < event[END] + ): + event = proposed_event + continue + return event async def async_get_events(self, hass, start_date, end_date): @@ -535,9 +549,8 @@ class TodoistProjectData: project_task_data = project_data[TASKS] events = [] - time_format = "%a %d %b %Y %H:%M:%S %z" for task in project_task_data: - due_date = datetime.strptime(task["due_date_utc"], time_format) + due_date = _parse_due_date(task["due"]) if start_date < due_date < end_date: event = { "uid": task["id"], diff --git a/homeassistant/components/todoist/manifest.json b/homeassistant/components/todoist/manifest.json index 7a6b4e2efab..e7876c953cc 100644 --- a/homeassistant/components/todoist/manifest.json +++ b/homeassistant/components/todoist/manifest.json @@ -1,10 +1,10 @@ { "domain": "todoist", "name": "Todoist", - "documentation": "https://www.home-assistant.io/components/todoist", + "documentation": "https://www.home-assistant.io/integrations/todoist", "requirements": [ - "todoist-python==7.0.17" + "todoist-python==8.0.0" ], "dependencies": [], - "codeowners": [] + "codeowners": ["@boralyl"] } diff --git a/homeassistant/components/todoist/services.yaml b/homeassistant/components/todoist/services.yaml index e69de29bb2d..c2d23cc4bec 100644 --- a/homeassistant/components/todoist/services.yaml +++ b/homeassistant/components/todoist/services.yaml @@ -0,0 +1,25 @@ +new_task: + description: Create a new task and add it to a project. + fields: + content: + description: The name of the task. + example: Pick up the mail. + project: + description: The name of the project this task should belong to. Defaults to Inbox. + example: Errands + labels: + description: Any labels that you want to apply to this task, separated by a comma. + example: Chores,Delivieries + priority: + description: The priority of this task, from 1 (normal) to 4 (urgent). + example: 2 + due_date_string: + description: The day this task is due, in natural language. + example: Tomorrow + due_date_lang: + description: The language of due_date_string. + example: en + due_date: + description: The day this task is due, in format YYYY-MM-DD. + example: 2019-10-22 + diff --git a/homeassistant/components/tof/manifest.json b/homeassistant/components/tof/manifest.json index 5f64e661a9a..dc87b6c2fc7 100644 --- a/homeassistant/components/tof/manifest.json +++ b/homeassistant/components/tof/manifest.json @@ -1,7 +1,7 @@ { "domain": "tof", "name": "Tof", - "documentation": "https://www.home-assistant.io/components/tof", + "documentation": "https://www.home-assistant.io/integrations/tof", "requirements": [ "VL53L1X2==0.1.5" ], diff --git a/homeassistant/components/tomato/manifest.json b/homeassistant/components/tomato/manifest.json index 615ea9ecd7e..5f6584ce250 100644 --- a/homeassistant/components/tomato/manifest.json +++ b/homeassistant/components/tomato/manifest.json @@ -1,7 +1,7 @@ { "domain": "tomato", "name": "Tomato", - "documentation": "https://www.home-assistant.io/components/tomato", + "documentation": "https://www.home-assistant.io/integrations/tomato", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/toon/.translations/bg.json b/homeassistant/components/toon/.translations/bg.json index e4aa0d8c088..0de9452b3cd 100644 --- a/homeassistant/components/toon/.translations/bg.json +++ b/homeassistant/components/toon/.translations/bg.json @@ -15,6 +15,7 @@ "authenticate": { "data": { "password": "\u041f\u0430\u0440\u043e\u043b\u0430", + "tenant": "\u041d\u0430\u0435\u043c\u0430\u0442\u0435\u043b", "username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435" }, "description": "\u0423\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u0441 \u0412\u0430\u0448\u0438\u044f Eneco Toon \u043f\u0440\u043e\u0444\u0438\u043b (\u043d\u0435 \u043f\u0440\u043e\u0444\u0438\u043b\u0430 \u0437\u0430 \u0440\u0430\u0437\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u0446\u0438).", diff --git a/homeassistant/components/toon/.translations/es-419.json b/homeassistant/components/toon/.translations/es-419.json index a0ce81495a8..598bc77aee9 100644 --- a/homeassistant/components/toon/.translations/es-419.json +++ b/homeassistant/components/toon/.translations/es-419.json @@ -1,17 +1,27 @@ { "config": { "abort": { + "no_agreements": "Esta cuenta no tiene pantallas Toon.", "unknown_auth_fail": "Ocurri\u00f3 un error inesperado, mientras se autenticaba." }, "error": { - "credentials": "Las credenciales proporcionadas no son v\u00e1lidas." + "credentials": "Las credenciales proporcionadas no son v\u00e1lidas.", + "display_exists": "La pantalla seleccionada ya est\u00e1 configurada." }, "step": { "authenticate": { "data": { "password": "Contrase\u00f1a", "username": "Nombre de usuario" - } + }, + "title": "Vincula tu cuenta de Toon" + }, + "display": { + "data": { + "display": "Elegir pantalla" + }, + "description": "Seleccione la pantalla Toon para conectarse.", + "title": "Seleccionar pantalla" } }, "title": "Toon" diff --git a/homeassistant/components/toon/.translations/nn.json b/homeassistant/components/toon/.translations/nn.json index b8dbeff27ca..eed288a5e39 100644 --- a/homeassistant/components/toon/.translations/nn.json +++ b/homeassistant/components/toon/.translations/nn.json @@ -1,5 +1,12 @@ { "config": { + "step": { + "authenticate": { + "data": { + "username": "Brukarnamn" + } + } + }, "title": "Toon" } } \ No newline at end of file diff --git a/homeassistant/components/toon/.translations/no.json b/homeassistant/components/toon/.translations/no.json index 37dcd8ac22f..a033d2954d9 100644 --- a/homeassistant/components/toon/.translations/no.json +++ b/homeassistant/components/toon/.translations/no.json @@ -8,7 +8,7 @@ "unknown_auth_fail": "Uventet feil oppstod under autentisering." }, "error": { - "credentials": "De oppgitte legitimasjonene er ugyldige.", + "credentials": "Den oppgitte kontoinformasjonen er ugyldig.", "display_exists": "Den valgte skjermen er allerede konfigurert." }, "step": { @@ -18,7 +18,7 @@ "tenant": "Leietaker", "username": "Brukernavn" }, - "description": "Godkjen med Eneco Toon kontoen din (ikke utviklerkontoen).", + "description": "Godkjenn med Eneco Toon kontoen din (ikke utviklerkontoen).", "title": "Linken din Toon konto" }, "display": { diff --git a/homeassistant/components/toon/manifest.json b/homeassistant/components/toon/manifest.json index 3fd00e88a0c..721f7037fce 100644 --- a/homeassistant/components/toon/manifest.json +++ b/homeassistant/components/toon/manifest.json @@ -2,7 +2,7 @@ "domain": "toon", "name": "Toon", "config_flow": true, - "documentation": "https://www.home-assistant.io/components/toon", + "documentation": "https://www.home-assistant.io/integrations/toon", "requirements": [ "toonapilib==3.2.4" ], diff --git a/homeassistant/components/torque/manifest.json b/homeassistant/components/torque/manifest.json index 9ce41b59861..fbc788d252d 100644 --- a/homeassistant/components/torque/manifest.json +++ b/homeassistant/components/torque/manifest.json @@ -1,7 +1,7 @@ { "domain": "torque", "name": "Torque", - "documentation": "https://www.home-assistant.io/components/torque", + "documentation": "https://www.home-assistant.io/integrations/torque", "requirements": [], "dependencies": [ "http" diff --git a/homeassistant/components/torque/sensor.py b/homeassistant/components/torque/sensor.py index 0806ba0799c..10161856a47 100644 --- a/homeassistant/components/torque/sensor.py +++ b/homeassistant/components/torque/sensor.py @@ -88,7 +88,12 @@ class TorqueReceiveDataView(HomeAssistantView): names[pid] = data[key] elif is_unit: pid = convert_pid(is_unit.group(1)) - units[pid] = data[key] + + temp_unit = data[key] + if "\\xC2\\xB0" in temp_unit: + temp_unit = temp_unit.replace("\\xC2\\xB0", "°") + + units[pid] = temp_unit elif is_value: pid = convert_pid(is_value.group(1)) if pid in self.sensors: diff --git a/homeassistant/components/totalconnect/manifest.json b/homeassistant/components/totalconnect/manifest.json index e6bcd6dd00a..39cd0e0f1e6 100644 --- a/homeassistant/components/totalconnect/manifest.json +++ b/homeassistant/components/totalconnect/manifest.json @@ -1,7 +1,7 @@ { "domain": "totalconnect", "name": "Totalconnect", - "documentation": "https://www.home-assistant.io/components/totalconnect", + "documentation": "https://www.home-assistant.io/integrations/totalconnect", "requirements": [ "total_connect_client==0.28" ], diff --git a/homeassistant/components/touchline/manifest.json b/homeassistant/components/touchline/manifest.json index 5b8b4f521ee..7c0b36b50fe 100644 --- a/homeassistant/components/touchline/manifest.json +++ b/homeassistant/components/touchline/manifest.json @@ -1,7 +1,7 @@ { "domain": "touchline", "name": "Touchline", - "documentation": "https://www.home-assistant.io/components/touchline", + "documentation": "https://www.home-assistant.io/integrations/touchline", "requirements": [ "pytouchline==0.7" ], diff --git a/homeassistant/components/tplink/.translations/nn.json b/homeassistant/components/tplink/.translations/nn.json new file mode 100644 index 00000000000..1d9fb41fc8c --- /dev/null +++ b/homeassistant/components/tplink/.translations/nn.json @@ -0,0 +1,10 @@ +{ + "config": { + "step": { + "confirm": { + "title": "TP-Link Smart Home" + } + }, + "title": "TP-Link Smart Home" + } +} \ No newline at end of file diff --git a/homeassistant/components/tplink/.translations/no.json b/homeassistant/components/tplink/.translations/no.json index 4946eb81f02..41148475a5f 100644 --- a/homeassistant/components/tplink/.translations/no.json +++ b/homeassistant/components/tplink/.translations/no.json @@ -2,7 +2,7 @@ "config": { "abort": { "no_devices_found": "Ingen TP-Link enheter funnet p\u00e5 nettverket.", - "single_instance_allowed": "Kun en enkelt konfigurasjon av TP-Link er n\u00f8dvendig." + "single_instance_allowed": "Kun en konfigurasjon av TP-Link er n\u00f8dvendig." }, "step": { "confirm": { diff --git a/homeassistant/components/tplink/.translations/zh-Hant.json b/homeassistant/components/tplink/.translations/zh-Hant.json index d44faf195e5..250a5509c4c 100644 --- a/homeassistant/components/tplink/.translations/zh-Hant.json +++ b/homeassistant/components/tplink/.translations/zh-Hant.json @@ -6,7 +6,7 @@ }, "step": { "confirm": { - "description": "\u662f\u5426\u8981\u8a2d\u5b9a TP-Link \u667a\u80fd\u88dd\u7f6e\uff1f", + "description": "\u662f\u5426\u8981\u8a2d\u5b9a TP-Link \u667a\u80fd\u8a2d\u5099\uff1f", "title": "TP-Link Smart Home" } }, diff --git a/homeassistant/components/tplink/device_tracker.py b/homeassistant/components/tplink/device_tracker.py index 60d49573833..e7f87074cb4 100644 --- a/homeassistant/components/tplink/device_tracker.py +++ b/homeassistant/components/tplink/device_tracker.py @@ -245,7 +245,7 @@ class Tplink3DeviceScanner(Tplink1DeviceScanner): """Initialize the scanner.""" self.stok = "" self.sysauth = "" - super(Tplink3DeviceScanner, self).__init__(config) + super().__init__(config) def scan_devices(self): """Scan for new devices and return a list with found device IDs.""" @@ -365,7 +365,7 @@ class Tplink4DeviceScanner(Tplink1DeviceScanner): """Initialize the scanner.""" self.credentials = "" self.token = "" - super(Tplink4DeviceScanner, self).__init__(config) + super().__init__(config) def scan_devices(self): """Scan for new devices and return a list with found device IDs.""" diff --git a/homeassistant/components/tplink/manifest.json b/homeassistant/components/tplink/manifest.json index e0f85757afd..f299e02e2d3 100644 --- a/homeassistant/components/tplink/manifest.json +++ b/homeassistant/components/tplink/manifest.json @@ -2,7 +2,7 @@ "domain": "tplink", "name": "Tplink", "config_flow": true, - "documentation": "https://www.home-assistant.io/components/tplink", + "documentation": "https://www.home-assistant.io/integrations/tplink", "requirements": [ "pyHS100==0.3.5", "tplink==0.2.1" diff --git a/homeassistant/components/tplink_lte/manifest.json b/homeassistant/components/tplink_lte/manifest.json index e3efd8c8331..e1cdacde6d8 100644 --- a/homeassistant/components/tplink_lte/manifest.json +++ b/homeassistant/components/tplink_lte/manifest.json @@ -1,7 +1,7 @@ { "domain": "tplink_lte", "name": "Tplink lte", - "documentation": "https://www.home-assistant.io/components/tplink_lte", + "documentation": "https://www.home-assistant.io/integrations/tplink_lte", "requirements": [ "tp-connected==0.0.4" ], diff --git a/homeassistant/components/traccar/.translations/bg.json b/homeassistant/components/traccar/.translations/bg.json new file mode 100644 index 00000000000..7fe89d491c9 --- /dev/null +++ b/homeassistant/components/traccar/.translations/bg.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "Home Assistant \u0442\u0440\u044f\u0431\u0432\u0430 \u0434\u0430 \u0435 \u0434\u043e\u0441\u0442\u044a\u043f\u0435\u043d \u043e\u0442 \u0438\u043d\u0442\u0435\u0440\u043d\u0435\u0442 \u0437\u0430 \u0434\u0430 \u043f\u043e\u043b\u0443\u0447\u0430\u0432\u0430 \u0441\u044a\u043e\u0431\u0449\u0435\u043d\u0438\u044f \u043e\u0442 Traccar.", + "one_instance_allowed": "\u041d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u0430 \u0435 \u0441\u0430\u043c\u043e \u0435\u0434\u043d\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f." + }, + "create_entry": { + "default": "\u0417\u0430 \u0434\u0430 \u0438\u0437\u043f\u0440\u0430\u0449\u0430\u0442\u0435 \u0441\u044a\u0431\u0438\u0442\u0438\u044f \u0434\u043e Home Assistant, \u0449\u0435 \u0442\u0440\u044f\u0431\u0432\u0430 \u0434\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u0435 \u0444\u0443\u043d\u043a\u0446\u0438\u044f\u0442\u0430 webhook \u0432 Traccar. \n\n \u0418\u0437\u043f\u043e\u043b\u0437\u0432\u0430\u0439\u0442\u0435 \u0441\u043b\u0435\u0434\u043d\u0438\u044f \u0430\u0434\u0440\u0435\u0441: ` {webhook_url} ` \n\n \u0412\u0438\u0436\u0442\u0435 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u044f\u0442\u0430]({docs_url}) \u0437\u0430 \u043f\u043e\u0432\u0435\u0447\u0435 \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u043e\u0441\u0442\u0438." + }, + "step": { + "user": { + "description": "\u0421\u0438\u0433\u0443\u0440\u043d\u0438 \u043b\u0438 \u0441\u0442\u0435, \u0447\u0435 \u0438\u0441\u043a\u0430\u0442\u0435 \u0434\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u0435 Traccar?", + "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043d\u0430 Traccar" + } + }, + "title": "Traccar" + } +} \ No newline at end of file diff --git a/homeassistant/components/traccar/.translations/de.json b/homeassistant/components/traccar/.translations/de.json index c835ddf76b2..dccd39b6997 100644 --- a/homeassistant/components/traccar/.translations/de.json +++ b/homeassistant/components/traccar/.translations/de.json @@ -5,7 +5,7 @@ "one_instance_allowed": "Es ist nur eine einzelne Instanz erforderlich." }, "create_entry": { - "default": "Um Ereignisse an den Heimassistenten zu senden, m\u00fcssen die Webhook-Funktionen in Traccar eingerichtet werden.\n\nVerwende die folgende URL: `{webhook_url}}`\n\nSiehe [die Dokumentation]({docs_url}) f\u00fcr weitere Details." + "default": "Um Ereignisse an den Heimassistenten zu senden, m\u00fcssen die Webhook-Funktionen in Traccar eingerichtet werden.\n\nVerwende die folgende URL: `{webhook_url}}`\n\nSiehe [die Dokumentation]( {docs_url} ) f\u00fcr weitere Details." }, "step": { "user": { diff --git a/homeassistant/components/traccar/.translations/es-419.json b/homeassistant/components/traccar/.translations/es-419.json new file mode 100644 index 00000000000..bfe62cc4e78 --- /dev/null +++ b/homeassistant/components/traccar/.translations/es-419.json @@ -0,0 +1,8 @@ +{ + "config": { + "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." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/traccar/.translations/nn.json b/homeassistant/components/traccar/.translations/nn.json new file mode 100644 index 00000000000..9fc23b3e394 --- /dev/null +++ b/homeassistant/components/traccar/.translations/nn.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Traccar" + } +} \ No newline at end of file diff --git a/homeassistant/components/traccar/.translations/no.json b/homeassistant/components/traccar/.translations/no.json index dea146b649a..805b952690e 100644 --- a/homeassistant/components/traccar/.translations/no.json +++ b/homeassistant/components/traccar/.translations/no.json @@ -2,7 +2,7 @@ "config": { "abort": { "not_internet_accessible": "Din Home Assistant-forekomst m\u00e5 v\u00e6re tilgjengelig fra Internett for \u00e5 motta meldinger fra Traccar.", - "one_instance_allowed": "Kun en enkelt forekomst er n\u00f8dvendig." + "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." diff --git a/homeassistant/components/traccar/.translations/pt-BR.json b/homeassistant/components/traccar/.translations/pt-BR.json new file mode 100644 index 00000000000..4fa0c4e6714 --- /dev/null +++ b/homeassistant/components/traccar/.translations/pt-BR.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "Sua inst\u00e2ncia do Home Assistant precisa estar acess\u00edvel na Internet para receber mensagens do Traccar.", + "one_instance_allowed": "Apenas uma \u00fanica inst\u00e2ncia \u00e9 necess\u00e1ria." + }, + "create_entry": { + "default": "Para enviar eventos ao Home Assistant, voc\u00ea precisar\u00e1 configurar o recurso de webhook no Traccar. \n\n Use o seguinte URL: ` {webhook_url} ` \n\n Veja [a documenta\u00e7\u00e3o] ({docs_url}) para mais detalhes." + }, + "step": { + "user": { + "description": "Tem certeza de que deseja configurar o Traccar?", + "title": "Configurar Traccar" + } + }, + "title": "Traccar" + } +} \ No newline at end of file diff --git a/homeassistant/components/traccar/.translations/zh-Hans.json b/homeassistant/components/traccar/.translations/zh-Hans.json new file mode 100644 index 00000000000..248e8f9f44e --- /dev/null +++ b/homeassistant/components/traccar/.translations/zh-Hans.json @@ -0,0 +1,8 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "\u60a8\u7684 Home Assistant \u5b9e\u4f8b\u9700\u8981\u53ef\u4ece\u4e92\u8054\u7f51\u8bbf\u95ee\u4ee5\u63a5\u6536Traccar\u6d88\u606f\u3002", + "one_instance_allowed": "\u53ea\u6709\u4e00\u4e2a\u5b9e\u4f8b\u662f\u5fc5\u9700\u7684\u3002" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/traccar/.translations/zh-Hant.json b/homeassistant/components/traccar/.translations/zh-Hant.json index f5402454294..85e8994dc55 100644 --- a/homeassistant/components/traccar/.translations/zh-Hant.json +++ b/homeassistant/components/traccar/.translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "not_internet_accessible": "Home Assistant \u5be6\u4f8b\u5fc5\u9808\u80fd\u5920\u7531\u7db2\u969b\u7db2\u8def\u5b58\u53d6\uff0c\u65b9\u80fd\u63a5\u53d7 Traccar \u8a0a\u606f\u3002", + "not_internet_accessible": "Home Assistant \u7269\u4ef6\u5fc5\u9808\u80fd\u5920\u7531\u7db2\u969b\u7db2\u8def\u5b58\u53d6\uff0c\u65b9\u80fd\u63a5\u53d7 Traccar \u8a0a\u606f\u3002", "one_instance_allowed": "\u50c5\u9700\u8a2d\u5b9a\u4e00\u7d44\u7269\u4ef6\u5373\u53ef\u3002" }, "create_entry": { diff --git a/homeassistant/components/traccar/config_flow.py b/homeassistant/components/traccar/config_flow.py index cc3f1f23727..4bd75910163 100644 --- a/homeassistant/components/traccar/config_flow.py +++ b/homeassistant/components/traccar/config_flow.py @@ -6,5 +6,5 @@ from .const import DOMAIN config_entry_flow.register_webhook_flow( DOMAIN, "Traccar Webhook", - {"docs_url": "https://www.home-assistant.io/components/traccar/"}, + {"docs_url": "https://www.home-assistant.io/integrations/traccar/"}, ) diff --git a/homeassistant/components/traccar/manifest.json b/homeassistant/components/traccar/manifest.json index 7d3e2f22d65..ffc82ee13e8 100644 --- a/homeassistant/components/traccar/manifest.json +++ b/homeassistant/components/traccar/manifest.json @@ -2,7 +2,7 @@ "domain": "traccar", "name": "Traccar", "config_flow": true, - "documentation": "https://www.home-assistant.io/components/traccar", + "documentation": "https://www.home-assistant.io/integrations/traccar", "requirements": [ "pytraccar==0.9.0", "stringcase==1.2.0" diff --git a/homeassistant/components/trackr/manifest.json b/homeassistant/components/trackr/manifest.json index 6ad348176ba..638d63cb487 100644 --- a/homeassistant/components/trackr/manifest.json +++ b/homeassistant/components/trackr/manifest.json @@ -1,7 +1,7 @@ { "domain": "trackr", "name": "Trackr", - "documentation": "https://www.home-assistant.io/components/trackr", + "documentation": "https://www.home-assistant.io/integrations/trackr", "requirements": [ "pytrackr==0.0.5" ], diff --git a/homeassistant/components/tradfri/__init__.py b/homeassistant/components/tradfri/__init__.py index 87b073db052..bca91134bed 100644 --- a/homeassistant/components/tradfri/__init__.py +++ b/homeassistant/components/tradfri/__init__.py @@ -131,6 +131,9 @@ async def async_setup_entry(hass, entry): sw_version=gateway_info.firmware_version, ) + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, "cover") + ) hass.async_create_task( hass.config_entries.async_forward_entry_setup(entry, "light") ) diff --git a/homeassistant/components/tradfri/cover.py b/homeassistant/components/tradfri/cover.py new file mode 100644 index 00000000000..3dea978044f --- /dev/null +++ b/homeassistant/components/tradfri/cover.py @@ -0,0 +1,149 @@ +"""Support for IKEA Tradfri covers.""" +import logging + +from pytradfri.error import PytradfriError + +from homeassistant.components.cover import ( + CoverDevice, + ATTR_POSITION, + SUPPORT_OPEN, + SUPPORT_CLOSE, + SUPPORT_SET_POSITION, +) +from homeassistant.core import callback +from . import DOMAIN as TRADFRI_DOMAIN, KEY_API, KEY_GATEWAY +from .const import CONF_GATEWAY_ID + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Load Tradfri covers based on a config entry.""" + gateway_id = config_entry.data[CONF_GATEWAY_ID] + api = hass.data[KEY_API][config_entry.entry_id] + gateway = hass.data[KEY_GATEWAY][config_entry.entry_id] + + devices_commands = await api(gateway.get_devices()) + devices = await api(devices_commands) + covers = [dev for dev in devices if dev.has_blind_control] + if covers: + async_add_entities(TradfriCover(cover, api, gateway_id) for cover in covers) + + +class TradfriCover(CoverDevice): + """The platform class required by Home Assistant.""" + + def __init__(self, cover, api, gateway_id): + """Initialize a cover.""" + self._api = api + self._unique_id = f"{gateway_id}-{cover.id}" + self._cover = None + self._cover_control = None + self._cover_data = None + self._name = None + self._available = True + self._gateway_id = gateway_id + + self._refresh(cover) + + @property + def supported_features(self): + """Flag supported features.""" + return SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_SET_POSITION + + @property + def unique_id(self): + """Return unique ID for cover.""" + return self._unique_id + + @property + def device_info(self): + """Return the device info.""" + info = self._cover.device_info + + return { + "identifiers": {(TRADFRI_DOMAIN, self._cover.id)}, + "name": self._name, + "manufacturer": info.manufacturer, + "model": info.model_number, + "sw_version": info.firmware_version, + "via_device": (TRADFRI_DOMAIN, self._gateway_id), + } + + async def async_added_to_hass(self): + """Start thread when added to hass.""" + self._async_start_observe() + + @property + def available(self): + """Return True if entity is available.""" + return self._available + + @property + def should_poll(self): + """No polling needed for tradfri cover.""" + return False + + @property + def name(self): + """Return the display name of this cover.""" + return self._name + + @property + def current_cover_position(self): + """Return current position of cover. + + None is unknown, 0 is closed, 100 is fully open. + """ + return 100 - self._cover_data.current_cover_position + + async def async_set_cover_position(self, **kwargs): + """Move the cover to a specific position.""" + await self._api(self._cover_control.set_state(100 - kwargs[ATTR_POSITION])) + + async def async_open_cover(self, **kwargs): + """Open the cover.""" + await self._api(self._cover_control.set_state(0)) + + async def async_close_cover(self, **kwargs): + """Close cover.""" + await self._api(self._cover_control.set_state(100)) + + @property + def is_closed(self): + """Return if the cover is closed or not.""" + return self.current_cover_position == 0 + + @callback + def _async_start_observe(self, exc=None): + """Start observation of cover.""" + if exc: + self._available = False + self.async_schedule_update_ha_state() + _LOGGER.warning("Observation failed for %s", self._name, exc_info=exc) + try: + cmd = self._cover.observe( + callback=self._observe_update, + err_callback=self._async_start_observe, + duration=0, + ) + self.hass.async_create_task(self._api(cmd)) + except PytradfriError as err: + _LOGGER.warning("Observation failed, trying again", exc_info=err) + self._async_start_observe() + + def _refresh(self, cover): + """Refresh the cover data.""" + self._cover = cover + + # Caching of BlindControl and cover object + self._available = cover.reachable + self._cover_control = cover.blind_control + self._cover_data = cover.blind_control.blinds[0] + self._name = cover.name + + @callback + def _observe_update(self, tradfri_device): + """Receive new state data for this cover.""" + self._refresh(tradfri_device) + self.async_schedule_update_ha_state() diff --git a/homeassistant/components/tradfri/light.py b/homeassistant/components/tradfri/light.py index 97fdfd9d36d..615899a98c8 100644 --- a/homeassistant/components/tradfri/light.py +++ b/homeassistant/components/tradfri/light.py @@ -1,6 +1,9 @@ """Support for IKEA Tradfri lights.""" import logging +from pytradfri.error import PytradfriError + +import homeassistant.util.color as color_util from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, @@ -14,8 +17,6 @@ from homeassistant.components.light import ( Light, ) from homeassistant.core import callback -import homeassistant.util.color as color_util - from . import DOMAIN as TRADFRI_DOMAIN, KEY_API, KEY_GATEWAY from .const import CONF_GATEWAY_ID, CONF_IMPORT_GROUPS @@ -26,7 +27,6 @@ ATTR_HUE = "hue" ATTR_SAT = "saturation" ATTR_TRANSITION_TIME = "transition_time" PLATFORM_SCHEMA = LIGHT_PLATFORM_SCHEMA -IKEA = "IKEA of Sweden" TRADFRI_LIGHT_MANAGER = "Tradfri Light Manager" SUPPORTED_FEATURES = SUPPORT_TRANSITION SUPPORTED_GROUP_FEATURES = SUPPORT_BRIGHTNESS | SUPPORT_TRANSITION @@ -113,9 +113,6 @@ class TradfriGroup(Light): @callback def _async_start_observe(self, exc=None): """Start observation of light.""" - # pylint: disable=import-error - from pytradfri.error import PytradfriError - if exc: _LOGGER.warning("Observation failed for %s", self._name, exc_info=exc) @@ -339,8 +336,6 @@ class TradfriLight(Light): @callback def _async_start_observe(self, exc=None): """Start observation of light.""" - # pylint: disable=import-error - from pytradfri.error import PytradfriError if exc: self._available = False diff --git a/homeassistant/components/tradfri/manifest.json b/homeassistant/components/tradfri/manifest.json index d847c6df24f..d9fa4ad5696 100644 --- a/homeassistant/components/tradfri/manifest.json +++ b/homeassistant/components/tradfri/manifest.json @@ -2,7 +2,7 @@ "domain": "tradfri", "name": "Tradfri", "config_flow": true, - "documentation": "https://www.home-assistant.io/components/tradfri", + "documentation": "https://www.home-assistant.io/integrations/tradfri", "requirements": ["pytradfri[async]==6.3.1"], "homekit": { "models": ["TRADFRI"] diff --git a/homeassistant/components/tradfri/sensor.py b/homeassistant/components/tradfri/sensor.py index 627a9882154..4877dbbb541 100644 --- a/homeassistant/components/tradfri/sensor.py +++ b/homeassistant/components/tradfri/sensor.py @@ -1,10 +1,11 @@ """Support for IKEA Tradfri sensors.""" -from datetime import timedelta import logging +from datetime import timedelta + +from pytradfri.error import PytradfriError from homeassistant.core import callback from homeassistant.helpers.entity import Entity - from . import KEY_API, KEY_GATEWAY _LOGGER = logging.getLogger(__name__) @@ -79,9 +80,6 @@ class TradfriDevice(Entity): @callback def _async_start_observe(self, exc=None): """Start observation of light.""" - # pylint: disable=import-error - from pytradfri.error import PytradfriError - if exc: _LOGGER.warning("Observation failed for %s", self._name, exc_info=exc) diff --git a/homeassistant/components/tradfri/switch.py b/homeassistant/components/tradfri/switch.py index 4be72eb7359..545c1ad93ce 100644 --- a/homeassistant/components/tradfri/switch.py +++ b/homeassistant/components/tradfri/switch.py @@ -1,15 +1,15 @@ """Support for IKEA Tradfri switches.""" import logging +from pytradfri.error import PytradfriError + from homeassistant.components.switch import SwitchDevice from homeassistant.core import callback - from . import DOMAIN as TRADFRI_DOMAIN, KEY_API, KEY_GATEWAY from .const import CONF_GATEWAY_ID _LOGGER = logging.getLogger(__name__) -IKEA = "IKEA of Sweden" TRADFRI_SWITCH_MANAGER = "Tradfri Switch Manager" @@ -98,8 +98,6 @@ class TradfriSwitch(SwitchDevice): @callback def _async_start_observe(self, exc=None): """Start observation of switch.""" - from pytradfri.error import PytradfriError - if exc: self._available = False self.async_schedule_update_ha_state() diff --git a/homeassistant/components/trafikverket_train/manifest.json b/homeassistant/components/trafikverket_train/manifest.json index 2e24100edd0..1e24895af44 100644 --- a/homeassistant/components/trafikverket_train/manifest.json +++ b/homeassistant/components/trafikverket_train/manifest.json @@ -1,7 +1,7 @@ { "domain": "trafikverket_train", "name": "Trafikverket train information", - "documentation": "https://www.home-assistant.io/components/trafikverket_train", + "documentation": "https://www.home-assistant.io/integrations/trafikverket_train", "requirements": [ "pytrafikverket==0.1.5.9" ], diff --git a/homeassistant/components/trafikverket_weatherstation/manifest.json b/homeassistant/components/trafikverket_weatherstation/manifest.json index 9bd734fe094..64f1d636a1a 100644 --- a/homeassistant/components/trafikverket_weatherstation/manifest.json +++ b/homeassistant/components/trafikverket_weatherstation/manifest.json @@ -1,7 +1,7 @@ { "domain": "trafikverket_weatherstation", "name": "Trafikverket weatherstation", - "documentation": "https://www.home-assistant.io/components/trafikverket_weatherstation", + "documentation": "https://www.home-assistant.io/integrations/trafikverket_weatherstation", "requirements": [ "pytrafikverket==0.1.5.9" ], diff --git a/homeassistant/components/transmission/.translations/ca.json b/homeassistant/components/transmission/.translations/ca.json new file mode 100644 index 00000000000..395f6e2d681 --- /dev/null +++ b/homeassistant/components/transmission/.translations/ca.json @@ -0,0 +1,40 @@ +{ + "config": { + "abort": { + "one_instance_allowed": "Nom\u00e9s cal una sola inst\u00e0ncia." + }, + "error": { + "cannot_connect": "No s'ha pogut connectar amb l'amfitri\u00f3", + "wrong_credentials": "Nom d'usuari o contrasenya incorrectes" + }, + "step": { + "options": { + "data": { + "scan_interval": "Freq\u00fc\u00e8ncia d\u2019actualitzaci\u00f3" + }, + "title": "Opcions de configuraci\u00f3" + }, + "user": { + "data": { + "host": "Amfitri\u00f3", + "name": "Nom", + "password": "Contrasenya", + "port": "Port", + "username": "Nom d'usuari" + }, + "title": "Configuraci\u00f3 del client de Transmission" + } + }, + "title": "Transmission" + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Freq\u00fc\u00e8ncia d\u2019actualitzaci\u00f3" + }, + "description": "Opcions de configuraci\u00f3 per a Transmission" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/transmission/.translations/da.json b/homeassistant/components/transmission/.translations/da.json new file mode 100644 index 00000000000..3a619d8154a --- /dev/null +++ b/homeassistant/components/transmission/.translations/da.json @@ -0,0 +1,40 @@ +{ + "config": { + "abort": { + "one_instance_allowed": "Det er kun n\u00f8dvendigt med en ops\u00e6tning." + }, + "error": { + "cannot_connect": "Kunne ikke oprette forbindelse til v\u00e6rt", + "wrong_credentials": "Ugyldigt brugernavn eller adgangskode" + }, + "step": { + "options": { + "data": { + "scan_interval": "Opdateringsfrekvens" + }, + "title": "Konfigurer indstillinger" + }, + "user": { + "data": { + "host": "V\u00e6rt", + "name": "Navn", + "password": "Adgangskode", + "port": "Port", + "username": "Brugernavn" + }, + "title": "Konfigurer Transmission klient" + } + }, + "title": "Transmission" + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Opdateringsfrekvens" + }, + "description": "Konfigurationsindstillinger for Transmission" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/transmission/.translations/de.json b/homeassistant/components/transmission/.translations/de.json new file mode 100644 index 00000000000..ed0342b9430 --- /dev/null +++ b/homeassistant/components/transmission/.translations/de.json @@ -0,0 +1,37 @@ +{ + "config": { + "abort": { + "one_instance_allowed": "Nur eine einzige Instanz ist notwendig." + }, + "error": { + "cannot_connect": "Verbindung zum Host nicht m\u00f6glich", + "wrong_credentials": "Falscher Benutzername oder Kennwort" + }, + "step": { + "options": { + "data": { + "scan_interval": "Aktualisierungsfrequenz" + }, + "title": "Konfigurationsoptionen" + }, + "user": { + "data": { + "host": "Host", + "name": "Name", + "password": "Passwort", + "port": "Port", + "username": "Benutzername" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Aktualisierungsfrequenz" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/transmission/.translations/en.json b/homeassistant/components/transmission/.translations/en.json new file mode 100644 index 00000000000..67461d1a3e8 --- /dev/null +++ b/homeassistant/components/transmission/.translations/en.json @@ -0,0 +1,40 @@ +{ + "config": { + "abort": { + "one_instance_allowed": "Only a single instance is necessary." + }, + "error": { + "cannot_connect": "Unable to Connect to host", + "wrong_credentials": "Wrong username or password" + }, + "step": { + "options": { + "data": { + "scan_interval": "Update frequency" + }, + "title": "Configure Options" + }, + "user": { + "data": { + "host": "Host", + "name": "Name", + "password": "Password", + "port": "Port", + "username": "Username" + }, + "title": "Setup Transmission Client" + } + }, + "title": "Transmission" + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Update frequency" + }, + "description": "Configure options for Transmission" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/transmission/.translations/es.json b/homeassistant/components/transmission/.translations/es.json new file mode 100644 index 00000000000..f210eb42f8a --- /dev/null +++ b/homeassistant/components/transmission/.translations/es.json @@ -0,0 +1,40 @@ +{ + "config": { + "abort": { + "one_instance_allowed": "S\u00f3lo se necesita una sola instancia." + }, + "error": { + "cannot_connect": "No se puede conectar al host", + "wrong_credentials": "Nombre de usuario o contrase\u00f1a incorrectos" + }, + "step": { + "options": { + "data": { + "scan_interval": "Frecuencia de actualizaci\u00f3n" + }, + "title": "Configurar opciones" + }, + "user": { + "data": { + "host": "Host", + "name": "Nombre", + "password": "Contrase\u00f1a", + "port": "Puerto", + "username": "Nombre de usuario" + }, + "title": "Configuraci\u00f3n del cliente de transmisi\u00f3n" + } + }, + "title": "Transmisi\u00f3n" + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Frecuencia de actualizaci\u00f3n" + }, + "description": "Configurar opciones para la transmisi\u00f3n" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/transmission/.translations/fr.json b/homeassistant/components/transmission/.translations/fr.json new file mode 100644 index 00000000000..e2360c016ca --- /dev/null +++ b/homeassistant/components/transmission/.translations/fr.json @@ -0,0 +1,40 @@ +{ + "config": { + "abort": { + "one_instance_allowed": "Une seule instance est n\u00e9cessaire." + }, + "error": { + "cannot_connect": "Impossible de se connecter \u00e0 l'h\u00f4te", + "wrong_credentials": "Mauvais nom d'utilisateur ou mot de passe" + }, + "step": { + "options": { + "data": { + "scan_interval": "Fr\u00e9quence de mise \u00e0 jour" + }, + "title": "Configurer les options" + }, + "user": { + "data": { + "host": "H\u00f4te", + "name": "Nom", + "password": "Mot de passe", + "port": "Port", + "username": "Nom d'utilisateur" + }, + "title": "Configuration du client Transmission" + } + }, + "title": "Transmission" + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Fr\u00e9quence de mise \u00e0 jour" + }, + "description": "Configurer les options pour Transmission" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/transmission/.translations/it.json b/homeassistant/components/transmission/.translations/it.json new file mode 100644 index 00000000000..17a03b6dba1 --- /dev/null +++ b/homeassistant/components/transmission/.translations/it.json @@ -0,0 +1,40 @@ +{ + "config": { + "abort": { + "one_instance_allowed": "\u00c8 necessaria solo una singola istanza." + }, + "error": { + "cannot_connect": "Impossibile connettersi all'host", + "wrong_credentials": "Nome utente o password non validi" + }, + "step": { + "options": { + "data": { + "scan_interval": "Frequenza di aggiornamento" + }, + "title": "Configura opzioni" + }, + "user": { + "data": { + "host": "Host", + "name": "Nome", + "password": "Password", + "port": "Porta", + "username": "Nome utente" + }, + "title": "Configura client di Trasmissione" + } + }, + "title": "Trasmissione" + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Frequenza di aggiornamento" + }, + "description": "Configurare le opzioni per Trasmissione" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/transmission/.translations/lb.json b/homeassistant/components/transmission/.translations/lb.json new file mode 100644 index 00000000000..6cc611fcb71 --- /dev/null +++ b/homeassistant/components/transmission/.translations/lb.json @@ -0,0 +1,40 @@ +{ + "config": { + "abort": { + "one_instance_allowed": "N\u00ebmmen eng eenzeg Instanz ass n\u00e9ideg." + }, + "error": { + "cannot_connect": "Kann sech net mam Server verbannen.", + "wrong_credentials": "Falsche Benotzernumm oder Passwuert" + }, + "step": { + "options": { + "data": { + "scan_interval": "Intervalle vun de Mise \u00e0 jour" + }, + "title": "Optioune konfigur\u00e9ieren" + }, + "user": { + "data": { + "host": "Server", + "name": "Numm", + "password": "Passwuert", + "port": "Port", + "username": "Benotzernumm" + }, + "title": "Transmission Client ariichten" + } + }, + "title": "Transmission" + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Intervalle vun de Mise \u00e0 jour" + }, + "description": "Optioune fir Transmission konfigur\u00e9ieren" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/transmission/.translations/nn.json b/homeassistant/components/transmission/.translations/nn.json new file mode 100644 index 00000000000..7622ac1b459 --- /dev/null +++ b/homeassistant/components/transmission/.translations/nn.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "name": "Namn", + "username": "Brukarnamn" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/transmission/.translations/no.json b/homeassistant/components/transmission/.translations/no.json new file mode 100644 index 00000000000..f6ddce2a4a7 --- /dev/null +++ b/homeassistant/components/transmission/.translations/no.json @@ -0,0 +1,40 @@ +{ + "config": { + "abort": { + "one_instance_allowed": "Bare en enkel instans er n\u00f8dvendig." + }, + "error": { + "cannot_connect": "Kan ikke koble til vert", + "wrong_credentials": "Ugyldig brukernavn eller passord" + }, + "step": { + "options": { + "data": { + "scan_interval": "Oppdater frekvens" + }, + "title": "Konfigurer alternativer" + }, + "user": { + "data": { + "host": "Vert", + "name": "Navn", + "password": "Passord", + "port": "Port", + "username": "Brukernavn" + }, + "title": "Oppsett av klient for Transmission" + } + }, + "title": "Transmission" + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Oppdater frekvens" + }, + "description": "Konfigurer alternativer for Transmission" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/transmission/.translations/pl.json b/homeassistant/components/transmission/.translations/pl.json new file mode 100644 index 00000000000..9b45e310767 --- /dev/null +++ b/homeassistant/components/transmission/.translations/pl.json @@ -0,0 +1,40 @@ +{ + "config": { + "abort": { + "one_instance_allowed": "Wymagana jest tylko jedna instancja." + }, + "error": { + "cannot_connect": "Nie mo\u017cna po\u0142\u0105czy\u0107 si\u0119 z hostem", + "wrong_credentials": "Nieprawid\u0142owa nazwa u\u017cytkownika lub has\u0142o" + }, + "step": { + "options": { + "data": { + "scan_interval": "Cz\u0119stotliwo\u015b\u0107 aktualizacji" + }, + "title": "Opcje" + }, + "user": { + "data": { + "host": "Host", + "name": "Nazwa", + "password": "Has\u0142o", + "port": "Port", + "username": "Nazwa u\u017cytkownika" + }, + "title": "Konfiguracja klienta Transmission" + } + }, + "title": "Transmission" + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Cz\u0119stotliwo\u015b\u0107 aktualizacji" + }, + "description": "Konfiguracja opcji dla Transmission" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/transmission/.translations/pt-BR.json b/homeassistant/components/transmission/.translations/pt-BR.json new file mode 100644 index 00000000000..cabbb6d9149 --- /dev/null +++ b/homeassistant/components/transmission/.translations/pt-BR.json @@ -0,0 +1,40 @@ +{ + "config": { + "abort": { + "one_instance_allowed": "Apenas uma \u00fanica inst\u00e2ncia \u00e9 necess\u00e1ria." + }, + "error": { + "cannot_connect": "N\u00e3o foi poss\u00edvel conectar ao host", + "wrong_credentials": "Nome de usu\u00e1rio ou senha incorretos" + }, + "step": { + "options": { + "data": { + "scan_interval": "Frequ\u00eancia de atualiza\u00e7\u00e3o" + }, + "title": "Op\u00e7\u00f5es de configura\u00e7\u00e3o" + }, + "user": { + "data": { + "host": "Host", + "name": "Nome", + "password": "Senha", + "port": "Porta", + "username": "Usu\u00e1rio" + }, + "title": "Configurar o cliente de transmiss\u00e3o" + } + }, + "title": "Transmiss\u00e3o" + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Frequ\u00eancia de atualiza\u00e7\u00e3o" + }, + "description": "Configurar op\u00e7\u00f5es para transmiss\u00e3o" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/transmission/.translations/ru.json b/homeassistant/components/transmission/.translations/ru.json new file mode 100644 index 00000000000..e7a438cae11 --- /dev/null +++ b/homeassistant/components/transmission/.translations/ru.json @@ -0,0 +1,40 @@ +{ + "config": { + "abort": { + "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." + }, + "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 \u0445\u043e\u0441\u0442\u0443", + "wrong_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043b\u043e\u0433\u0438\u043d \u0438\u043b\u0438 \u043f\u0430\u0440\u043e\u043b\u044c" + }, + "step": { + "options": { + "data": { + "scan_interval": "\u0427\u0430\u0441\u0442\u043e\u0442\u0430 \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u044f" + }, + "title": "\u041f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b" + }, + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "port": "\u041f\u043e\u0440\u0442", + "username": "\u041b\u043e\u0433\u0438\u043d" + }, + "title": "Transmission" + } + }, + "title": "Transmission" + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "\u0427\u0430\u0441\u0442\u043e\u0442\u0430 \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u044f" + }, + "description": "\u0414\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b Transmission" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/transmission/.translations/sl.json b/homeassistant/components/transmission/.translations/sl.json new file mode 100644 index 00000000000..122c332f429 --- /dev/null +++ b/homeassistant/components/transmission/.translations/sl.json @@ -0,0 +1,40 @@ +{ + "config": { + "abort": { + "one_instance_allowed": "Potrebna je samo ena instanca." + }, + "error": { + "cannot_connect": "Ni mogo\u010de vzpostaviti povezave z gostiteljem", + "wrong_credentials": "Napa\u010dno uporabni\u0161ko ime ali geslo" + }, + "step": { + "options": { + "data": { + "scan_interval": "Pogostost posodabljanja" + }, + "title": "Nastavite mo\u017enosti" + }, + "user": { + "data": { + "host": "Gostitelj", + "name": "Ime", + "password": "Geslo", + "port": "Vrata", + "username": "Uporabni\u0161ko ime" + }, + "title": "Namestitev odjemalca Transmission" + } + }, + "title": "Transmission" + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Pogostost posodabljanja" + }, + "description": "Nastavite mo\u017enosti za Transmission" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/transmission/.translations/sv.json b/homeassistant/components/transmission/.translations/sv.json new file mode 100644 index 00000000000..30004af17db --- /dev/null +++ b/homeassistant/components/transmission/.translations/sv.json @@ -0,0 +1,37 @@ +{ + "config": { + "abort": { + "one_instance_allowed": "Endast en enda instans \u00e4r n\u00f6dv\u00e4ndig." + }, + "error": { + "cannot_connect": "Det g\u00e5r inte att ansluta till v\u00e4rden", + "wrong_credentials": "Fel anv\u00e4ndarnamn eller l\u00f6senord" + }, + "step": { + "options": { + "data": { + "scan_interval": "Uppdateringsfrekvens" + }, + "title": "Konfigurera alternativ" + }, + "user": { + "data": { + "host": "V\u00e4rd", + "name": "Namn", + "password": "L\u00f6senord", + "port": "Port", + "username": "Anv\u00e4ndarnamn" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Uppdateringsfrekvens" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/transmission/.translations/zh-Hant.json b/homeassistant/components/transmission/.translations/zh-Hant.json new file mode 100644 index 00000000000..479e25c6d8a --- /dev/null +++ b/homeassistant/components/transmission/.translations/zh-Hant.json @@ -0,0 +1,40 @@ +{ + "config": { + "abort": { + "one_instance_allowed": "\u50c5\u9700\u8a2d\u5b9a\u4e00\u7d44\u7269\u4ef6\u5373\u53ef\u3002" + }, + "error": { + "cannot_connect": "\u7121\u6cd5\u9023\u7dda\u81f3\u4e3b\u6a5f\u7aef", + "wrong_credentials": "\u4f7f\u7528\u8005\u540d\u7a31\u6216\u5bc6\u78bc\u932f\u8aa4" + }, + "step": { + "options": { + "data": { + "scan_interval": "\u66f4\u65b0\u983b\u7387" + }, + "title": "\u8a2d\u5b9a\u9078\u9805" + }, + "user": { + "data": { + "host": "\u4e3b\u6a5f\u7aef", + "name": "\u540d\u7a31", + "password": "\u5bc6\u78bc", + "port": "\u901a\u8a0a\u57e0", + "username": "\u4f7f\u7528\u8005\u540d\u7a31" + }, + "title": "\u8a2d\u5b9a Transmission \u5ba2\u6236\u7aef" + } + }, + "title": "Transmission" + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "\u66f4\u65b0\u983b\u7387" + }, + "description": "Transmission \u8a2d\u5b9a\u9078\u9805" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/transmission/__init__.py b/homeassistant/components/transmission/__init__.py index e7f9b94046d..e6ddd87bdf5 100644 --- a/homeassistant/components/transmission/__init__.py +++ b/homeassistant/components/transmission/__init__.py @@ -2,47 +2,38 @@ from datetime import timedelta import logging +import transmissionrpc +from transmissionrpc.error import TransmissionError import voluptuous as vol +from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.const import ( CONF_HOST, - CONF_MONITORED_CONDITIONS, CONF_NAME, CONF_PASSWORD, CONF_PORT, CONF_SCAN_INTERVAL, CONF_USERNAME, ) -from homeassistant.helpers import config_validation as cv, discovery +from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers import config_validation as cv from homeassistant.helpers.dispatcher import dispatcher_send -from homeassistant.helpers.event import track_time_interval +from homeassistant.helpers.event import async_track_time_interval + +from .const import ( + ATTR_TORRENT, + DATA_TRANSMISSION, + DATA_UPDATED, + DEFAULT_NAME, + DEFAULT_PORT, + DEFAULT_SCAN_INTERVAL, + DOMAIN, + SERVICE_ADD_TORRENT, +) +from .errors import AuthenticationError, CannotConnect, UnknownError _LOGGER = logging.getLogger(__name__) -DOMAIN = "transmission" -DATA_UPDATED = "transmission_data_updated" -DATA_TRANSMISSION = "data_transmission" - -DEFAULT_NAME = "Transmission" -DEFAULT_PORT = 9091 -TURTLE_MODE = "turtle_mode" - -SENSOR_TYPES = { - "active_torrents": ["Active Torrents", None], - "current_status": ["Status", None], - "download_speed": ["Down Speed", "MB/s"], - "paused_torrents": ["Paused Torrents", None], - "total_torrents": ["Total Torrents", None], - "upload_speed": ["Up Speed", "MB/s"], - "completed_torrents": ["Completed Torrents", None], - "started_torrents": ["Started Torrents", None], -} - -DEFAULT_SCAN_INTERVAL = timedelta(seconds=120) - -ATTR_TORRENT = "torrent" - -SERVICE_ADD_TORRENT = "add_torrent" SERVICE_ADD_TORRENT_SCHEMA = vol.Schema({vol.Required(ATTR_TORRENT): cv.string}) @@ -55,13 +46,9 @@ CONFIG_SCHEMA = vol.Schema( vol.Optional(CONF_USERNAME): cv.string, vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(TURTLE_MODE, default=False): cv.boolean, vol.Optional( CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL ): cv.time_period, - vol.Optional( - CONF_MONITORED_CONDITIONS, default=["current_status"] - ): vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]), } ) }, @@ -69,70 +56,165 @@ CONFIG_SCHEMA = vol.Schema( ) -def setup(hass, config): - """Set up the Transmission Component.""" - host = config[DOMAIN][CONF_HOST] - username = config[DOMAIN].get(CONF_USERNAME) - password = config[DOMAIN].get(CONF_PASSWORD) - port = config[DOMAIN][CONF_PORT] - scan_interval = config[DOMAIN][CONF_SCAN_INTERVAL] - - import transmissionrpc - from transmissionrpc.error import TransmissionError - - try: - api = transmissionrpc.Client(host, port=port, user=username, password=password) - api.session_stats() - except TransmissionError as error: - if str(error).find("401: Unauthorized"): - _LOGGER.error("Credentials for" " Transmission client are not valid") - return False - - tm_data = hass.data[DATA_TRANSMISSION] = TransmissionData(hass, config, api) - - tm_data.update() - tm_data.init_torrent_list() - - def refresh(event_time): - """Get the latest data from Transmission.""" - tm_data.update() - - track_time_interval(hass, refresh, scan_interval) - - def add_torrent(service): - """Add new torrent to download.""" - torrent = service.data[ATTR_TORRENT] - if torrent.startswith( - ("http", "ftp:", "magnet:") - ) or hass.config.is_allowed_path(torrent): - api.add_torrent(torrent) - else: - _LOGGER.warning( - "Could not add torrent: " "unsupported type or no permission" +async def async_setup(hass, config): + """Import the Transmission Component from config.""" + if not hass.config_entries.async_entries(DOMAIN) and DOMAIN in config: + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=config[DOMAIN] ) - - hass.services.register( - DOMAIN, SERVICE_ADD_TORRENT, add_torrent, schema=SERVICE_ADD_TORRENT_SCHEMA - ) - - sensorconfig = { - "sensors": config[DOMAIN][CONF_MONITORED_CONDITIONS], - "client_name": config[DOMAIN][CONF_NAME], - } - - discovery.load_platform(hass, "sensor", DOMAIN, sensorconfig, config) - - if config[DOMAIN][TURTLE_MODE]: - discovery.load_platform(hass, "switch", DOMAIN, sensorconfig, config) + ) return True +async def async_setup_entry(hass, config_entry): + """Set up the Transmission Component.""" + if DOMAIN not in hass.data: + hass.data[DOMAIN] = {} + + if not config_entry.options: + await async_populate_options(hass, config_entry) + + client = TransmissionClient(hass, config_entry) + client_id = config_entry.entry_id + hass.data[DOMAIN][client_id] = client + if not await client.async_setup(): + return False + + return True + + +async def async_unload_entry(hass, entry): + """Unload Transmission Entry from config_entry.""" + hass.services.async_remove(DOMAIN, SERVICE_ADD_TORRENT) + if hass.data[DOMAIN][entry.entry_id].unsub_timer: + hass.data[DOMAIN][entry.entry_id].unsub_timer() + + for component in "sensor", "switch": + await hass.config_entries.async_forward_entry_unload(entry, component) + + del hass.data[DOMAIN] + + return True + + +async def get_api(hass, host, port, username=None, password=None): + """Get Transmission client.""" + try: + api = await hass.async_add_executor_job( + transmissionrpc.Client, host, port, username, password + ) + return api + + except TransmissionError as error: + if "401: Unauthorized" in str(error): + _LOGGER.error("Credentials for Transmission client are not valid") + raise AuthenticationError + if "111: Connection refused" in str(error): + _LOGGER.error("Connecting to the Transmission client failed") + raise CannotConnect + + _LOGGER.error(error) + raise UnknownError + + +async def async_populate_options(hass, config_entry): + """Populate default options for Transmission Client.""" + options = {CONF_SCAN_INTERVAL: config_entry.data["options"][CONF_SCAN_INTERVAL]} + + hass.config_entries.async_update_entry(config_entry, options=options) + + +class TransmissionClient: + """Transmission Client Object.""" + + def __init__(self, hass, config_entry): + """Initialize the Transmission RPC API.""" + self.hass = hass + self.config_entry = config_entry + self.scan_interval = self.config_entry.options[CONF_SCAN_INTERVAL] + self.tm_data = None + self.unsub_timer = None + + async def async_setup(self): + """Set up the Transmission client.""" + + config = { + CONF_HOST: self.config_entry.data[CONF_HOST], + CONF_PORT: self.config_entry.data[CONF_PORT], + CONF_USERNAME: self.config_entry.data.get(CONF_USERNAME), + CONF_PASSWORD: self.config_entry.data.get(CONF_PASSWORD), + } + try: + api = await get_api(self.hass, **config) + except CannotConnect: + raise ConfigEntryNotReady + except (AuthenticationError, UnknownError): + return False + + self.tm_data = self.hass.data[DOMAIN][DATA_TRANSMISSION] = TransmissionData( + self.hass, self.config_entry, api + ) + + await self.hass.async_add_executor_job(self.tm_data.init_torrent_list) + await self.hass.async_add_executor_job(self.tm_data.update) + self.set_scan_interval(self.scan_interval) + + for platform in ["sensor", "switch"]: + self.hass.async_create_task( + self.hass.config_entries.async_forward_entry_setup( + self.config_entry, platform + ) + ) + + def add_torrent(service): + """Add new torrent to download.""" + torrent = service.data[ATTR_TORRENT] + if torrent.startswith( + ("http", "ftp:", "magnet:") + ) or self.hass.config.is_allowed_path(torrent): + api.add_torrent(torrent) + else: + _LOGGER.warning( + "Could not add torrent: unsupported type or no permission" + ) + + self.hass.services.async_register( + DOMAIN, SERVICE_ADD_TORRENT, add_torrent, schema=SERVICE_ADD_TORRENT_SCHEMA + ) + + self.config_entry.add_update_listener(self.async_options_updated) + + return True + + def set_scan_interval(self, scan_interval): + """Update scan interval.""" + + def refresh(event_time): + """Get the latest data from Transmission.""" + self.tm_data.update() + + if self.unsub_timer is not None: + self.unsub_timer() + self.unsub_timer = async_track_time_interval( + self.hass, refresh, timedelta(seconds=scan_interval) + ) + + @staticmethod + async def async_options_updated(hass, entry): + """Triggered by config entry options updates.""" + hass.data[DOMAIN][entry.entry_id].set_scan_interval( + entry.options[CONF_SCAN_INTERVAL] + ) + + class TransmissionData: """Get the latest data and update the states.""" def __init__(self, hass, config, api): """Initialize the Transmission RPC API.""" + self.hass = hass self.data = None self.torrents = None self.session = None @@ -140,12 +222,9 @@ class TransmissionData: self._api = api self.completed_torrents = [] self.started_torrents = [] - self.hass = hass def update(self): """Get the latest data from Transmission instance.""" - from transmissionrpc.error import TransmissionError - try: self.data = self._api.session_stats() self.torrents = self._api.get_torrents() @@ -153,15 +232,15 @@ class TransmissionData: self.check_completed_torrent() self.check_started_torrent() + _LOGGER.debug("Torrent Data Updated") - dispatcher_send(self.hass, DATA_UPDATED) - - _LOGGER.debug("Torrent Data updated") self.available = True except TransmissionError: self.available = False _LOGGER.error("Unable to connect to Transmission client") + dispatcher_send(self.hass, DATA_UPDATED) + def init_torrent_list(self): """Initialize torrent lists.""" self.torrents = self._api.get_torrents() @@ -211,6 +290,15 @@ class TransmissionData: """Get the number of completed torrents.""" return len(self.completed_torrents) + def start_torrents(self): + """Start all torrents.""" + self._api.start_all() + + def stop_torrents(self): + """Stop all active torrents.""" + torrent_ids = [torrent.id for torrent in self.torrents] + self._api.stop_torrent(torrent_ids) + def set_alt_speed_enabled(self, is_enabled): """Set the alternative speed flag.""" self._api.set_session(alt_speed_enabled=is_enabled) diff --git a/homeassistant/components/transmission/config_flow.py b/homeassistant/components/transmission/config_flow.py new file mode 100644 index 00000000000..99376f4b6e0 --- /dev/null +++ b/homeassistant/components/transmission/config_flow.py @@ -0,0 +1,104 @@ +"""Config flow for Transmission Bittorent Client.""" +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import ( + CONF_HOST, + CONF_NAME, + CONF_PASSWORD, + CONF_PORT, + CONF_SCAN_INTERVAL, + CONF_USERNAME, +) +from homeassistant.core import callback + +from . import get_api +from .const import DEFAULT_NAME, DEFAULT_PORT, DEFAULT_SCAN_INTERVAL, DOMAIN +from .errors import AuthenticationError, CannotConnect, UnknownError + + +class TransmissionFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a UniFi 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 TransmissionOptionsFlowHandler(config_entry) + + def __init__(self): + """Initialize the Transmission flow.""" + self.config = {} + self.errors = {} + + async def async_step_user(self, user_input=None): + """Handle a flow initialized by the user.""" + if self.hass.config_entries.async_entries(DOMAIN): + return self.async_abort(reason="one_instance_allowed") + + if user_input is not None: + + self.config[CONF_NAME] = user_input.pop(CONF_NAME) + try: + await get_api(self.hass, **user_input) + self.config.update(user_input) + if "options" not in self.config: + self.config["options"] = {CONF_SCAN_INTERVAL: DEFAULT_SCAN_INTERVAL} + return self.async_create_entry( + title=self.config[CONF_NAME], data=self.config + ) + except AuthenticationError: + self.errors[CONF_USERNAME] = "wrong_credentials" + self.errors[CONF_PASSWORD] = "wrong_credentials" + except (CannotConnect, UnknownError): + self.errors["base"] = "cannot_connect" + + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + { + vol.Required(CONF_NAME, default=DEFAULT_NAME): str, + vol.Required(CONF_HOST): str, + vol.Optional(CONF_USERNAME): str, + vol.Optional(CONF_PASSWORD): str, + vol.Required(CONF_PORT, default=DEFAULT_PORT): int, + } + ), + errors=self.errors, + ) + + async def async_step_import(self, import_config): + """Import from Transmission client config.""" + self.config["options"] = { + CONF_SCAN_INTERVAL: import_config.pop(CONF_SCAN_INTERVAL).seconds + } + + return await self.async_step_user(user_input=import_config) + + +class TransmissionOptionsFlowHandler(config_entries.OptionsFlow): + """Handle Transmission client options.""" + + def __init__(self, config_entry): + """Initialize Transmission options flow.""" + self.config_entry = config_entry + + async def async_step_init(self, user_input=None): + """Manage the Transmission options.""" + if user_input is not None: + return self.async_create_entry(title="", data=user_input) + + options = { + vol.Optional( + CONF_SCAN_INTERVAL, + default=self.config_entry.options.get( + CONF_SCAN_INTERVAL, + self.config_entry.data["options"][CONF_SCAN_INTERVAL], + ), + ): int + } + + return self.async_show_form(step_id="init", data_schema=vol.Schema(options)) diff --git a/homeassistant/components/transmission/const.py b/homeassistant/components/transmission/const.py new file mode 100644 index 00000000000..e4a8b1490c2 --- /dev/null +++ b/homeassistant/components/transmission/const.py @@ -0,0 +1,24 @@ +"""Constants for the Transmission Bittorent Client component.""" +DOMAIN = "transmission" + +SENSOR_TYPES = { + "active_torrents": ["Active Torrents", None], + "current_status": ["Status", None], + "download_speed": ["Down Speed", "MB/s"], + "paused_torrents": ["Paused Torrents", None], + "total_torrents": ["Total Torrents", None], + "upload_speed": ["Up Speed", "MB/s"], + "completed_torrents": ["Completed Torrents", None], + "started_torrents": ["Started Torrents", None], +} +SWITCH_TYPES = {"on_off": "Switch", "turtle_mode": "Turtle Mode"} + +DEFAULT_NAME = "Transmission" +DEFAULT_PORT = 9091 +DEFAULT_SCAN_INTERVAL = 120 + +ATTR_TORRENT = "torrent" +SERVICE_ADD_TORRENT = "add_torrent" + +DATA_UPDATED = "transmission_data_updated" +DATA_TRANSMISSION = "data_transmission" diff --git a/homeassistant/components/transmission/errors.py b/homeassistant/components/transmission/errors.py new file mode 100644 index 00000000000..b5f74f7bf40 --- /dev/null +++ b/homeassistant/components/transmission/errors.py @@ -0,0 +1,14 @@ +"""Errors for the Transmission component.""" +from homeassistant.exceptions import HomeAssistantError + + +class AuthenticationError(HomeAssistantError): + """Wrong Username or Password.""" + + +class CannotConnect(HomeAssistantError): + """Unable to connect to client.""" + + +class UnknownError(HomeAssistantError): + """Unknown Error.""" diff --git a/homeassistant/components/transmission/manifest.json b/homeassistant/components/transmission/manifest.json index bc5da64fcac..c2fa31d7b50 100644 --- a/homeassistant/components/transmission/manifest.json +++ b/homeassistant/components/transmission/manifest.json @@ -1,10 +1,13 @@ { "domain": "transmission", "name": "Transmission", - "documentation": "https://www.home-assistant.io/components/transmission", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/transmission", "requirements": [ "transmissionrpc==0.11" ], "dependencies": [], - "codeowners": [] -} + "codeowners": [ + "@engrbm87" + ] +} \ No newline at end of file diff --git a/homeassistant/components/transmission/sensor.py b/homeassistant/components/transmission/sensor.py index ac2e64ce92f..30dfa4a3cbe 100644 --- a/homeassistant/components/transmission/sensor.py +++ b/homeassistant/components/transmission/sensor.py @@ -1,32 +1,29 @@ """Support for monitoring the Transmission BitTorrent client API.""" -from datetime import timedelta import logging -from homeassistant.const import STATE_IDLE +from homeassistant.const import CONF_NAME, STATE_IDLE from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import Entity -from . import DATA_TRANSMISSION, DATA_UPDATED, SENSOR_TYPES +from .const import DATA_TRANSMISSION, DATA_UPDATED, DOMAIN, SENSOR_TYPES _LOGGER = logging.getLogger(__name__) -DEFAULT_NAME = "Transmission" - -SCAN_INTERVAL = timedelta(seconds=120) - async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Set up the Transmission sensors.""" - if discovery_info is None: - return + """Import config from configuration.yaml.""" + pass - transmission_api = hass.data[DATA_TRANSMISSION] - monitored_variables = discovery_info["sensors"] - name = discovery_info["client_name"] + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the Transmission sensors.""" + + transmission_api = hass.data[DOMAIN][DATA_TRANSMISSION] + name = config_entry.data[CONF_NAME] dev = [] - for sensor_type in monitored_variables: + for sensor_type in SENSOR_TYPES: dev.append( TransmissionSensor( sensor_type, diff --git a/homeassistant/components/transmission/services.yaml b/homeassistant/components/transmission/services.yaml index e049f89b3c6..ab383584e83 100644 --- a/homeassistant/components/transmission/services.yaml +++ b/homeassistant/components/transmission/services.yaml @@ -3,4 +3,4 @@ add_torrent: fields: torrent: description: URL, magnet link or Base64 encoded file. - example: http://releases.ubuntu.com/19.04/ubuntu-19.04-desktop-amd64.iso.torrent} + example: http://releases.ubuntu.com/19.04/ubuntu-19.04-desktop-amd64.iso.torrent diff --git a/homeassistant/components/transmission/strings.json b/homeassistant/components/transmission/strings.json new file mode 100644 index 00000000000..7160cd109c4 --- /dev/null +++ b/homeassistant/components/transmission/strings.json @@ -0,0 +1,40 @@ +{ + "config": { + "title": "Transmission", + "step": { + "user": { + "title": "Setup Transmission Client", + "data": { + "name": "Name", + "host": "Host", + "username": "User name", + "password": "Password", + "port": "Port" + } + }, + "options": { + "title": "Configure Options", + "data": { + "scan_interval": "Update frequency" + } + } + }, + "error": { + "wrong_credentials": "Wrong username or password", + "cannot_connect": "Unable to Connect to host" + }, + "abort": { + "one_instance_allowed": "Only a single instance is necessary." + } + }, + "options": { + "step": { + "init": { + "description": "Configure options for Transmission", + "data": { + "scan_interval": "Update frequency" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/transmission/switch.py b/homeassistant/components/transmission/switch.py index df490cdbe47..0bb43f715ac 100644 --- a/homeassistant/components/transmission/switch.py +++ b/homeassistant/components/transmission/switch.py @@ -1,43 +1,50 @@ """Support for setting the Transmission BitTorrent client Turtle Mode.""" import logging -from homeassistant.const import STATE_OFF, STATE_ON +from homeassistant.const import CONF_NAME, STATE_OFF, STATE_ON from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import ToggleEntity -from . import DATA_TRANSMISSION, DATA_UPDATED +from .const import DATA_TRANSMISSION, DATA_UPDATED, DOMAIN, SWITCH_TYPES _LOGGING = logging.getLogger(__name__) -DEFAULT_NAME = "Transmission Turtle Mode" - async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Import config from configuration.yaml.""" + pass + + +async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the Transmission switch.""" - if discovery_info is None: - return - component_name = DATA_TRANSMISSION - transmission_api = hass.data[component_name] - name = discovery_info["client_name"] + transmission_api = hass.data[DOMAIN][DATA_TRANSMISSION] + name = config_entry.data[CONF_NAME] - async_add_entities([TransmissionSwitch(transmission_api, name)], True) + dev = [] + for switch_type, switch_name in SWITCH_TYPES.items(): + dev.append(TransmissionSwitch(switch_type, switch_name, transmission_api, name)) + + async_add_entities(dev, True) class TransmissionSwitch(ToggleEntity): """Representation of a Transmission switch.""" - def __init__(self, transmission_client, name): + def __init__(self, switch_type, switch_name, transmission_api, name): """Initialize the Transmission switch.""" - self._name = name - self.transmission_client = transmission_client + self._name = switch_name + self.client_name = name + self.type = switch_type + self._transmission_api = transmission_api self._state = STATE_OFF + self._data = None @property def name(self): """Return the name of the switch.""" - return self._name + return f"{self.client_name} {self._name}" @property def state(self): @@ -54,15 +61,30 @@ class TransmissionSwitch(ToggleEntity): """Return true if device is on.""" return self._state == STATE_ON + @property + def available(self): + """Could the device be accessed during the last update call.""" + return self._transmission_api.available + def turn_on(self, **kwargs): """Turn the device on.""" - _LOGGING.debug("Turning Turtle Mode of Transmission on") - self.transmission_client.set_alt_speed_enabled(True) + if self.type == "on_off": + _LOGGING.debug("Starting all torrents") + self._transmission_api.start_torrents() + elif self.type == "turtle_mode": + _LOGGING.debug("Turning Turtle Mode of Transmission on") + self._transmission_api.set_alt_speed_enabled(True) + self._transmission_api.update() def turn_off(self, **kwargs): """Turn the device off.""" - _LOGGING.debug("Turning Turtle Mode of Transmission off") - self.transmission_client.set_alt_speed_enabled(False) + if self.type == "on_off": + _LOGGING.debug("Stoping all torrents") + self._transmission_api.stop_torrents() + if self.type == "turtle_mode": + _LOGGING.debug("Turning Turtle Mode of Transmission off") + self._transmission_api.set_alt_speed_enabled(False) + self._transmission_api.update() async def async_added_to_hass(self): """Handle entity which will be added.""" @@ -76,7 +98,14 @@ class TransmissionSwitch(ToggleEntity): def update(self): """Get the latest data from Transmission and updates the state.""" - active = self.transmission_client.get_alt_speed_enabled() + active = None + if self.type == "on_off": + self._data = self._transmission_api.data + if self._data: + active = self._data.activeTorrentCount > 0 + + elif self.type == "turtle_mode": + active = self._transmission_api.get_alt_speed_enabled() if active is None: return diff --git a/homeassistant/components/transport_nsw/manifest.json b/homeassistant/components/transport_nsw/manifest.json index 491cce7407f..9d2e675c757 100644 --- a/homeassistant/components/transport_nsw/manifest.json +++ b/homeassistant/components/transport_nsw/manifest.json @@ -1,7 +1,7 @@ { "domain": "transport_nsw", "name": "Transport nsw", - "documentation": "https://www.home-assistant.io/components/transport_nsw", + "documentation": "https://www.home-assistant.io/integrations/transport_nsw", "requirements": [ "PyTransportNSW==0.1.1" ], diff --git a/homeassistant/components/transport_nsw/sensor.py b/homeassistant/components/transport_nsw/sensor.py index 5f08d0a4750..9d0610c139e 100644 --- a/homeassistant/components/transport_nsw/sensor.py +++ b/homeassistant/components/transport_nsw/sensor.py @@ -7,7 +7,7 @@ import voluptuous as vol import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import CONF_NAME, CONF_API_KEY, ATTR_ATTRIBUTION +from homeassistant.const import ATTR_MODE, CONF_NAME, CONF_API_KEY, ATTR_ATTRIBUTION _LOGGER = logging.getLogger(__name__) @@ -17,7 +17,6 @@ ATTR_DUE_IN = "due" ATTR_DELAY = "delay" ATTR_REAL_TIME = "real_time" ATTR_DESTINATION = "destination" -ATTR_MODE = "mode" ATTRIBUTION = "Data provided by Transport NSW" diff --git a/homeassistant/components/travisci/manifest.json b/homeassistant/components/travisci/manifest.json index eb553fbe73c..f0d5d54c8cf 100644 --- a/homeassistant/components/travisci/manifest.json +++ b/homeassistant/components/travisci/manifest.json @@ -1,7 +1,7 @@ { "domain": "travisci", "name": "Travisci", - "documentation": "https://www.home-assistant.io/components/travisci", + "documentation": "https://www.home-assistant.io/integrations/travisci", "requirements": [ "TravisPy==0.3.5" ], diff --git a/homeassistant/components/trend/manifest.json b/homeassistant/components/trend/manifest.json index 8719138f3ac..1432d2d21a0 100644 --- a/homeassistant/components/trend/manifest.json +++ b/homeassistant/components/trend/manifest.json @@ -1,7 +1,7 @@ { "domain": "trend", "name": "Trend", - "documentation": "https://www.home-assistant.io/components/trend", + "documentation": "https://www.home-assistant.io/integrations/trend", "requirements": [ "numpy==1.17.1" ], diff --git a/homeassistant/components/tts/manifest.json b/homeassistant/components/tts/manifest.json index ce600473cc5..ca2059a4d19 100644 --- a/homeassistant/components/tts/manifest.json +++ b/homeassistant/components/tts/manifest.json @@ -1,7 +1,7 @@ { "domain": "tts", "name": "Tts", - "documentation": "https://www.home-assistant.io/components/tts", + "documentation": "https://www.home-assistant.io/integrations/tts", "requirements": [ "mutagen==1.42.0" ], diff --git a/homeassistant/components/tuya/manifest.json b/homeassistant/components/tuya/manifest.json index 9c83056f6ac..cf16d587e87 100644 --- a/homeassistant/components/tuya/manifest.json +++ b/homeassistant/components/tuya/manifest.json @@ -1,7 +1,7 @@ { "domain": "tuya", "name": "Tuya", - "documentation": "https://www.home-assistant.io/components/tuya", + "documentation": "https://www.home-assistant.io/integrations/tuya", "requirements": [ "tuyaha==0.0.4" ], diff --git a/homeassistant/components/twentemilieu/.translations/bg.json b/homeassistant/components/twentemilieu/.translations/bg.json new file mode 100644 index 00000000000..df36ab070d7 --- /dev/null +++ b/homeassistant/components/twentemilieu/.translations/bg.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "address_exists": "\u0410\u0434\u0440\u0435\u0441\u044a\u0442 \u0432\u0435\u0447\u0435 \u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d." + }, + "error": { + "connection_error": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435.", + "invalid_address": "\u0410\u0434\u0440\u0435\u0441\u044a\u0442 \u043d\u0435 \u0435 \u043d\u0430\u043c\u0435\u0440\u0435\u043d \u0432 \u0437\u043e\u043d\u0430 \u0437\u0430 \u043e\u0431\u0441\u043b\u0443\u0436\u0432\u0430\u043d\u0435 \u043d\u0430 Twente Milieu." + }, + "step": { + "user": { + "data": { + "house_letter": "\u0414\u043e\u043c\u0430\u0448\u043d\u043e \u043f\u0438\u0441\u043c\u043e/\u0434\u043e\u043f\u044a\u043b\u043d\u0438\u0442\u0435\u043b\u043d\u043e", + "house_number": "\u041d\u043e\u043c\u0435\u0440 \u043d\u0430 \u043a\u044a\u0449\u0430", + "post_code": "\u041f\u043e\u0449\u0435\u043d\u0441\u043a\u0438 \u043a\u043e\u0434" + }, + "description": "\u0421\u044a\u0437\u0434\u0430\u0439\u0442\u0435 Twente Milieu, \u043f\u0440\u0435\u0434\u043e\u0441\u0442\u0430\u0432\u044f\u0449\u0430 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044f \u0437\u0430 \u0441\u044a\u0431\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 \u043e\u0442\u043f\u0430\u0434\u044a\u0446\u0438 \u043d\u0430 \u0432\u0430\u0448\u0438\u044f \u0430\u0434\u0440\u0435\u0441.", + "title": "Twente Milieu" + } + }, + "title": "Twente Milieu" + } +} \ No newline at end of file diff --git a/homeassistant/components/twentemilieu/.translations/es-419.json b/homeassistant/components/twentemilieu/.translations/es-419.json new file mode 100644 index 00000000000..02ac8ecf27a --- /dev/null +++ b/homeassistant/components/twentemilieu/.translations/es-419.json @@ -0,0 +1,10 @@ +{ + "config": { + "step": { + "user": { + "title": "Twente Milieu" + } + }, + "title": "Twente Milieu" + } +} \ No newline at end of file diff --git a/homeassistant/components/twentemilieu/.translations/lb.json b/homeassistant/components/twentemilieu/.translations/lb.json index 0b07c5003ef..b6f10842b4d 100644 --- a/homeassistant/components/twentemilieu/.translations/lb.json +++ b/homeassistant/components/twentemilieu/.translations/lb.json @@ -4,7 +4,8 @@ "address_exists": "Adresse ass scho ageriicht." }, "error": { - "connection_error": "Feeler beim verbannen." + "connection_error": "Feeler beim verbannen.", + "invalid_address": "Adresse net am Twente Milieu Service Ber\u00e4ich fonnt" }, "step": { "user": { @@ -13,6 +14,7 @@ "house_number": "Haus Nummer", "post_code": "Postleitzuel" }, + "description": "Offallsammlung Informatiounen vun Twente Milieu zu \u00e4erer Adresse ariichten.", "title": "Twente Milieu" } }, diff --git a/homeassistant/components/twentemilieu/.translations/nn.json b/homeassistant/components/twentemilieu/.translations/nn.json new file mode 100644 index 00000000000..02ac8ecf27a --- /dev/null +++ b/homeassistant/components/twentemilieu/.translations/nn.json @@ -0,0 +1,10 @@ +{ + "config": { + "step": { + "user": { + "title": "Twente Milieu" + } + }, + "title": "Twente Milieu" + } +} \ No newline at end of file diff --git a/homeassistant/components/twentemilieu/.translations/pt-BR.json b/homeassistant/components/twentemilieu/.translations/pt-BR.json new file mode 100644 index 00000000000..73735dda1d9 --- /dev/null +++ b/homeassistant/components/twentemilieu/.translations/pt-BR.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "address_exists": "Endere\u00e7o j\u00e1 configurado." + }, + "error": { + "connection_error": "Falha ao conectar.", + "invalid_address": "Endere\u00e7o n\u00e3o encontrado na \u00e1rea de servi\u00e7o de Twente Milieu." + }, + "step": { + "user": { + "data": { + "house_letter": "Carta da casa/adicional", + "house_number": "N\u00famero da casa", + "post_code": "C\u00f3digo postal" + }, + "description": "Configure o Twente Milieu, fornecendo informa\u00e7\u00f5es de coleta de lixo em seu endere\u00e7o.", + "title": "Twente Milieu" + } + }, + "title": "Twente Milieu" + } +} \ No newline at end of file diff --git a/homeassistant/components/twentemilieu/.translations/zh-Hans.json b/homeassistant/components/twentemilieu/.translations/zh-Hans.json new file mode 100644 index 00000000000..80301cfd57b --- /dev/null +++ b/homeassistant/components/twentemilieu/.translations/zh-Hans.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "address_exists": "\u5730\u5740\u5df2\u7ecf\u8bbe\u7f6e\u597d\u4e86\u3002" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/twentemilieu/manifest.json b/homeassistant/components/twentemilieu/manifest.json index d1acf936a24..4eebba28ef6 100644 --- a/homeassistant/components/twentemilieu/manifest.json +++ b/homeassistant/components/twentemilieu/manifest.json @@ -2,7 +2,7 @@ "domain": "twentemilieu", "name": "Twente Milieu", "config_flow": true, - "documentation": "https://www.home-assistant.io/components/twentemilieu", + "documentation": "https://www.home-assistant.io/integrations/twentemilieu", "requirements": [ "twentemilieu==0.1.0" ], diff --git a/homeassistant/components/twilio/.translations/nn.json b/homeassistant/components/twilio/.translations/nn.json new file mode 100644 index 00000000000..86e5d9051b3 --- /dev/null +++ b/homeassistant/components/twilio/.translations/nn.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Twilio" + } +} \ No newline at end of file diff --git a/homeassistant/components/twilio/.translations/no.json b/homeassistant/components/twilio/.translations/no.json index 0d28b094340..c3d6ff16e2d 100644 --- a/homeassistant/components/twilio/.translations/no.json +++ b/homeassistant/components/twilio/.translations/no.json @@ -2,7 +2,7 @@ "config": { "abort": { "not_internet_accessible": "Din Home Assistant forekomst m\u00e5 v\u00e6re tilgjengelig fra internett for \u00e5 motta Twilio-meldinger.", - "one_instance_allowed": "Kun \u00e9n enkelt forekomst er n\u00f8dvendig." + "one_instance_allowed": "Kun en forekomst er n\u00f8dvendig." }, "create_entry": { "default": "For \u00e5 sende hendelser til Home Assistant, m\u00e5 du sette opp [Webhooks with Twilio]({twilio_url}). \n\nFyll ut f\u00f8lgende informasjon: \n\n- URL: `{webhook_url}` \n- Metode: POST\n- Innholdstype: application/x-www-form-urlencoded \n\n Se [dokumentasjonen]({docs_url}) om hvordan du konfigurerer automatiseringer for \u00e5 h\u00e5ndtere innkommende data." diff --git a/homeassistant/components/twilio/.translations/zh-Hant.json b/homeassistant/components/twilio/.translations/zh-Hant.json index 2e85ef7b2de..858970539cd 100644 --- a/homeassistant/components/twilio/.translations/zh-Hant.json +++ b/homeassistant/components/twilio/.translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "not_internet_accessible": "Home Assistant \u5be6\u4f8b\u5fc5\u9808\u80fd\u5920\u7531\u7db2\u969b\u7db2\u8def\u5b58\u53d6\uff0c\u65b9\u80fd\u63a5\u53d7 Twilio \u8a0a\u606f\u3002", + "not_internet_accessible": "Home Assistant \u7269\u4ef6\u5fc5\u9808\u80fd\u5920\u7531\u7db2\u969b\u7db2\u8def\u5b58\u53d6\uff0c\u65b9\u80fd\u63a5\u53d7 Twilio \u8a0a\u606f\u3002", "one_instance_allowed": "\u50c5\u9700\u8a2d\u5b9a\u4e00\u7d44\u7269\u4ef6\u5373\u53ef\u3002" }, "create_entry": { diff --git a/homeassistant/components/twilio/config_flow.py b/homeassistant/components/twilio/config_flow.py index 1408e05e738..dad8e0bf496 100644 --- a/homeassistant/components/twilio/config_flow.py +++ b/homeassistant/components/twilio/config_flow.py @@ -9,6 +9,6 @@ config_entry_flow.register_webhook_flow( "Twilio Webhook", { "twilio_url": "https://www.twilio.com/docs/glossary/what-is-a-webhook", - "docs_url": "https://www.home-assistant.io/components/twilio/", + "docs_url": "https://www.home-assistant.io/integrations/twilio/", }, ) diff --git a/homeassistant/components/twilio/manifest.json b/homeassistant/components/twilio/manifest.json index f96afa18115..23fac51a347 100644 --- a/homeassistant/components/twilio/manifest.json +++ b/homeassistant/components/twilio/manifest.json @@ -2,7 +2,7 @@ "domain": "twilio", "name": "Twilio", "config_flow": true, - "documentation": "https://www.home-assistant.io/components/twilio", + "documentation": "https://www.home-assistant.io/integrations/twilio", "requirements": [ "twilio==6.19.1" ], diff --git a/homeassistant/components/twilio_call/manifest.json b/homeassistant/components/twilio_call/manifest.json index b235385396b..3fe8f129b5d 100644 --- a/homeassistant/components/twilio_call/manifest.json +++ b/homeassistant/components/twilio_call/manifest.json @@ -1,7 +1,7 @@ { "domain": "twilio_call", "name": "Twilio call", - "documentation": "https://www.home-assistant.io/components/twilio_call", + "documentation": "https://www.home-assistant.io/integrations/twilio_call", "requirements": [], "dependencies": [ "twilio" diff --git a/homeassistant/components/twilio_sms/manifest.json b/homeassistant/components/twilio_sms/manifest.json index 2174dc275b5..00c843d2dff 100644 --- a/homeassistant/components/twilio_sms/manifest.json +++ b/homeassistant/components/twilio_sms/manifest.json @@ -1,7 +1,7 @@ { "domain": "twilio_sms", "name": "Twilio sms", - "documentation": "https://www.home-assistant.io/components/twilio_sms", + "documentation": "https://www.home-assistant.io/integrations/twilio_sms", "requirements": [], "dependencies": [ "twilio" diff --git a/homeassistant/components/twitch/manifest.json b/homeassistant/components/twitch/manifest.json index 80bc795b536..f64182f6e4d 100644 --- a/homeassistant/components/twitch/manifest.json +++ b/homeassistant/components/twitch/manifest.json @@ -1,7 +1,7 @@ { "domain": "twitch", "name": "Twitch", - "documentation": "https://www.home-assistant.io/components/twitch", + "documentation": "https://www.home-assistant.io/integrations/twitch", "requirements": [ "python-twitch-client==0.6.0" ], diff --git a/homeassistant/components/twitter/manifest.json b/homeassistant/components/twitter/manifest.json index e721bb669ed..2713004343b 100644 --- a/homeassistant/components/twitter/manifest.json +++ b/homeassistant/components/twitter/manifest.json @@ -1,7 +1,7 @@ { "domain": "twitter", "name": "Twitter", - "documentation": "https://www.home-assistant.io/components/twitter", + "documentation": "https://www.home-assistant.io/integrations/twitter", "requirements": [ "TwitterAPI==2.5.9" ], diff --git a/homeassistant/components/ubee/manifest.json b/homeassistant/components/ubee/manifest.json index 39ffe768657..0bf29beb0dc 100644 --- a/homeassistant/components/ubee/manifest.json +++ b/homeassistant/components/ubee/manifest.json @@ -1,7 +1,7 @@ { "domain": "ubee", "name": "Ubee", - "documentation": "https://www.home-assistant.io/components/ubee", + "documentation": "https://www.home-assistant.io/integrations/ubee", "requirements": [ "pyubee==0.7" ], diff --git a/homeassistant/components/ubus/device_tracker.py b/homeassistant/components/ubus/device_tracker.py index f14ea5af02c..8c83de202a4 100644 --- a/homeassistant/components/ubus/device_tracker.py +++ b/homeassistant/components/ubus/device_tracker.py @@ -146,7 +146,7 @@ class DnsmasqUbusDeviceScanner(UbusDeviceScanner): def __init__(self, config): """Initialize the scanner.""" - super(DnsmasqUbusDeviceScanner, self).__init__(config) + super().__init__(config) self.leasefile = None def _generate_mac2name(self): diff --git a/homeassistant/components/ubus/manifest.json b/homeassistant/components/ubus/manifest.json index f886e84254b..664ae861442 100644 --- a/homeassistant/components/ubus/manifest.json +++ b/homeassistant/components/ubus/manifest.json @@ -1,7 +1,7 @@ { "domain": "ubus", "name": "Ubus", - "documentation": "https://www.home-assistant.io/components/ubus", + "documentation": "https://www.home-assistant.io/integrations/ubus", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/ue_smart_radio/manifest.json b/homeassistant/components/ue_smart_radio/manifest.json index 189ac690758..3711d7cbeb4 100644 --- a/homeassistant/components/ue_smart_radio/manifest.json +++ b/homeassistant/components/ue_smart_radio/manifest.json @@ -1,7 +1,7 @@ { "domain": "ue_smart_radio", "name": "Ue smart radio", - "documentation": "https://www.home-assistant.io/components/ue_smart_radio", + "documentation": "https://www.home-assistant.io/integrations/ue_smart_radio", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/uk_transport/manifest.json b/homeassistant/components/uk_transport/manifest.json index be44a9d8cc8..cf349a20571 100644 --- a/homeassistant/components/uk_transport/manifest.json +++ b/homeassistant/components/uk_transport/manifest.json @@ -1,7 +1,7 @@ { "domain": "uk_transport", "name": "Uk transport", - "documentation": "https://www.home-assistant.io/components/uk_transport", + "documentation": "https://www.home-assistant.io/integrations/uk_transport", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/unifi/.translations/bg.json b/homeassistant/components/unifi/.translations/bg.json index d8f571c968e..df5654ff78b 100644 --- a/homeassistant/components/unifi/.translations/bg.json +++ b/homeassistant/components/unifi/.translations/bg.json @@ -22,5 +22,17 @@ } }, "title": "UniFi \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u0435\u0440" + }, + "options": { + "step": { + "device_tracker": { + "data": { + "detection_time": "\u0412\u0440\u0435\u043c\u0435 \u0432 \u0441\u0435\u043a\u0443\u043d\u0434\u0438 \u043e\u0442 \u043f\u043e\u0441\u043b\u0435\u0434\u043d\u043e\u0442\u043e \u0432\u0438\u0436\u0434\u0430\u043d\u0435 \u0437\u0430 \u0434\u0430 \u0441\u0435 \u0441\u0447\u0435\u0442\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e, \u043a\u0430\u0442\u043e \u043e\u0442\u0441\u044a\u0441\u0442\u0432\u0430\u0449\u043e", + "track_clients": "\u041f\u0440\u043e\u0441\u043b\u0435\u0434\u044f\u0432\u0430\u043d\u0435 \u043d\u0430 \u043c\u0440\u0435\u0436\u043e\u0432\u0438 \u043a\u043b\u0438\u0435\u043d\u0442\u0438", + "track_devices": "\u041f\u0440\u043e\u0441\u043b\u0435\u0434\u044f\u0432\u0430\u043d\u0435 \u043d\u0430 \u043c\u0440\u0435\u0436\u043e\u0432\u0438 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 (Ubiquiti \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430)", + "track_wired_clients": "\u0412\u043a\u043b\u044e\u0447\u0435\u0442\u0435 \u043d\u0430 \u043a\u043b\u0438\u0435\u043d\u0442\u0438 \u0441\u0432\u044a\u0440\u0437\u0430\u043d\u0438 \u0441 \u043a\u0430\u0431\u0435\u043b" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/unifi/.translations/ca.json b/homeassistant/components/unifi/.translations/ca.json index 3741b035d7a..899b532290e 100644 --- a/homeassistant/components/unifi/.translations/ca.json +++ b/homeassistant/components/unifi/.translations/ca.json @@ -38,6 +38,11 @@ "one": "un", "other": "altre" } + }, + "statistics_sensors": { + "data": { + "allow_bandwidth_sensors": "Crea sensors d'\u00fas d'ample de banda per a clients de la xarxa" + } } } } diff --git a/homeassistant/components/unifi/.translations/da.json b/homeassistant/components/unifi/.translations/da.json index 53b794ed435..0d0315e49c7 100644 --- a/homeassistant/components/unifi/.translations/da.json +++ b/homeassistant/components/unifi/.translations/da.json @@ -32,6 +32,11 @@ "track_devices": "Spor netv\u00e6rksenheder (Ubiquiti-enheder)", "track_wired_clients": "Inkluder kablede netv\u00e6rksklienter" } + }, + "statistics_sensors": { + "data": { + "allow_bandwidth_sensors": "Opret b\u00e5ndbredde sensorer for netv\u00e6rksklienter" + } } } } diff --git a/homeassistant/components/unifi/.translations/de.json b/homeassistant/components/unifi/.translations/de.json index e447e89644f..32a378b7c00 100644 --- a/homeassistant/components/unifi/.translations/de.json +++ b/homeassistant/components/unifi/.translations/de.json @@ -38,6 +38,11 @@ "one": "eins", "other": "andere" } + }, + "statistics_sensors": { + "data": { + "allow_bandwidth_sensors": "Erstellen von Bandbreiten-Nutzungssensoren f\u00fcr Netzwerk-Clients" + } } } } diff --git a/homeassistant/components/unifi/.translations/en.json b/homeassistant/components/unifi/.translations/en.json index 2025bad6246..d9b65b6d1da 100644 --- a/homeassistant/components/unifi/.translations/en.json +++ b/homeassistant/components/unifi/.translations/en.json @@ -32,6 +32,11 @@ "track_devices": "Track network devices (Ubiquiti devices)", "track_wired_clients": "Include wired network clients" } + }, + "statistics_sensors": { + "data": { + "allow_bandwidth_sensors": "Create bandwidth usage sensors for network clients" + } } } } diff --git a/homeassistant/components/unifi/.translations/es.json b/homeassistant/components/unifi/.translations/es.json index 0539f5607b4..1db6712142d 100644 --- a/homeassistant/components/unifi/.translations/es.json +++ b/homeassistant/components/unifi/.translations/es.json @@ -38,6 +38,11 @@ "one": "uno", "other": "otro" } + }, + "statistics_sensors": { + "data": { + "allow_bandwidth_sensors": "Crear sensores para monitorizar uso de ancho de banda de clientes de red" + } } } } diff --git a/homeassistant/components/unifi/.translations/fr.json b/homeassistant/components/unifi/.translations/fr.json index 8c2526f8a15..c40b7822073 100644 --- a/homeassistant/components/unifi/.translations/fr.json +++ b/homeassistant/components/unifi/.translations/fr.json @@ -32,6 +32,11 @@ "track_devices": "Suivre les p\u00e9riph\u00e9riques r\u00e9seau (p\u00e9riph\u00e9riques Ubiquiti)", "track_wired_clients": "Inclure les clients du r\u00e9seau filaire" } + }, + "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 5285ed21873..80b546ebcf8 100644 --- a/homeassistant/components/unifi/.translations/it.json +++ b/homeassistant/components/unifi/.translations/it.json @@ -38,6 +38,11 @@ "one": "uno", "other": "altro" } + }, + "statistics_sensors": { + "data": { + "allow_bandwidth_sensors": "Creare sensori di utilizzo della larghezza di banda per i client di rete" + } } } } diff --git a/homeassistant/components/unifi/.translations/lb.json b/homeassistant/components/unifi/.translations/lb.json index 3bef273b83e..4fa1f62c602 100644 --- a/homeassistant/components/unifi/.translations/lb.json +++ b/homeassistant/components/unifi/.translations/lb.json @@ -22,5 +22,28 @@ } }, "title": "Unifi Kontroller" + }, + "options": { + "step": { + "device_tracker": { + "data": { + "detection_time": "Z\u00e4it a Sekonne vum leschten Z\u00e4itpunkt un bis den Apparat als \u00ebnnerwee consider\u00e9iert g\u00ebtt", + "track_clients": "Netzwierk Cliente verfollegen", + "track_devices": "Netzwierk Apparater (Ubiquiti Apparater) verfollegen", + "track_wired_clients": "Kabel Netzwierk Cliente abez\u00e9ien" + } + }, + "init": { + "data": { + "one": "Een", + "other": "M\u00e9i" + } + }, + "statistics_sensors": { + "data": { + "allow_bandwidth_sensors": "Bandbreet Benotzung Sensore fir Netzwierk Cliente erstellen" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/unifi/.translations/nn.json b/homeassistant/components/unifi/.translations/nn.json new file mode 100644 index 00000000000..7c129cba3af --- /dev/null +++ b/homeassistant/components/unifi/.translations/nn.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "username": "Brukarnamn" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/unifi/.translations/no.json b/homeassistant/components/unifi/.translations/no.json index 068f4341544..9041f018423 100644 --- a/homeassistant/components/unifi/.translations/no.json +++ b/homeassistant/components/unifi/.translations/no.json @@ -32,6 +32,17 @@ "track_devices": "Spore nettverksenheter (Ubiquiti-enheter)", "track_wired_clients": "Inkluder kablede nettverksklienter" } + }, + "init": { + "data": { + "one": "en", + "other": "andre" + } + }, + "statistics_sensors": { + "data": { + "allow_bandwidth_sensors": "Opprett b\u00e5ndbreddesensorer for nettverksklienter" + } } } } diff --git a/homeassistant/components/unifi/.translations/pl.json b/homeassistant/components/unifi/.translations/pl.json index 6366f82b3da..5887460a8a5 100644 --- a/homeassistant/components/unifi/.translations/pl.json +++ b/homeassistant/components/unifi/.translations/pl.json @@ -40,6 +40,11 @@ "one": "Jeden", "other": "Inne" } + }, + "statistics_sensors": { + "data": { + "allow_bandwidth_sensors": "Stw\u00f3rz sensory wykorzystania przepustowo\u015bci przez klient\u00f3w sieciowych" + } } } } diff --git a/homeassistant/components/unifi/.translations/pt-BR.json b/homeassistant/components/unifi/.translations/pt-BR.json index a7eac61bab3..113eaa000fe 100644 --- a/homeassistant/components/unifi/.translations/pt-BR.json +++ b/homeassistant/components/unifi/.translations/pt-BR.json @@ -22,5 +22,17 @@ } }, "title": "Controlador UniFi" + }, + "options": { + "step": { + "device_tracker": { + "data": { + "detection_time": "Tempo em segundos desde a \u00faltima vez que foi visto at\u00e9 ser considerado afastado", + "track_clients": "Rastrear clientes da rede", + "track_devices": "Rastrear dispositivos de rede (dispositivos Ubiquiti)", + "track_wired_clients": "Incluir clientes de rede com fio" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/unifi/.translations/ru.json b/homeassistant/components/unifi/.translations/ru.json index 76802a96367..d7451bd81a0 100644 --- a/homeassistant/components/unifi/.translations/ru.json +++ b/homeassistant/components/unifi/.translations/ru.json @@ -1,7 +1,7 @@ { "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.", "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": { @@ -32,6 +32,11 @@ "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)", "track_wired_clients": "\u0412\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u043a\u043b\u0438\u0435\u043d\u0442\u043e\u0432 \u043f\u0440\u043e\u0432\u043e\u0434\u043d\u043e\u0439 \u0441\u0435\u0442\u0438" } + }, + "statistics_sensors": { + "data": { + "allow_bandwidth_sensors": "\u0421\u043e\u0437\u0434\u0430\u0432\u0430\u0442\u044c \u0434\u0430\u0442\u0447\u0438\u043a\u0438 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u044f \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 35000bf4e1f..7084c4609c5 100644 --- a/homeassistant/components/unifi/.translations/sl.json +++ b/homeassistant/components/unifi/.translations/sl.json @@ -40,6 +40,11 @@ "other": "OSTALO", "two": "DVA" } + }, + "statistics_sensors": { + "data": { + "allow_bandwidth_sensors": "Ustvarite senzorje porabe pasovne \u0161irine za omre\u017ene odjemalce" + } } } } diff --git a/homeassistant/components/unifi/.translations/zh-Hant.json b/homeassistant/components/unifi/.translations/zh-Hant.json index 2d5bd9027ac..5e0b881af15 100644 --- a/homeassistant/components/unifi/.translations/zh-Hant.json +++ b/homeassistant/components/unifi/.translations/zh-Hant.json @@ -29,9 +29,14 @@ "data": { "detection_time": "\u6700\u7d42\u51fa\u73fe\u5f8c\u8996\u70ba\u96e2\u958b\u7684\u6642\u9593\uff08\u4ee5\u79d2\u70ba\u55ae\u4f4d\uff09", "track_clients": "\u8ffd\u8e64\u7db2\u8def\u5ba2\u6236\u7aef", - "track_devices": "\u8ffd\u8e64\u7db2\u8def\u88dd\u7f6e\uff08Ubiquiti \u88dd\u7f6e\uff09", + "track_devices": "\u8ffd\u8e64\u7db2\u8def\u8a2d\u5099\uff08Ubiquiti \u8a2d\u5099\uff09", "track_wired_clients": "\u5305\u542b\u6709\u7dda\u7db2\u8def\u5ba2\u6236\u7aef" } + }, + "statistics_sensors": { + "data": { + "allow_bandwidth_sensors": "\u65b0\u589e\u7db2\u8def\u5ba2\u6236\u7aef\u983b\u5bec\u7528\u91cf\u611f\u61c9\u5668" + } } } } diff --git a/homeassistant/components/unifi/__init__.py b/homeassistant/components/unifi/__init__.py index db635828529..5b43289e403 100644 --- a/homeassistant/components/unifi/__init__.py +++ b/homeassistant/components/unifi/__init__.py @@ -1,14 +1,13 @@ """Support for devices connected to UniFi POE.""" import voluptuous as vol -from homeassistant.components.unifi.config_flow import ( - get_controller_id_from_config_entry, -) from homeassistant.const import CONF_HOST +from homeassistant.core import callback from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC import homeassistant.helpers.config_validation as cv +from .config_flow import get_controller_id_from_config_entry from .const import ( ATTR_MANUFACTURER, CONF_BLOCK_CLIENT, @@ -20,9 +19,14 @@ from .const import ( CONF_SSID_FILTER, DOMAIN, UNIFI_CONFIG, + UNIFI_WIRELESS_CLIENTS, ) from .controller import UniFiController +SAVE_DELAY = 10 +STORAGE_KEY = "unifi_data" +STORAGE_VERSION = 1 + CONF_CONTROLLERS = "controllers" CONTROLLER_SCHEMA = vol.Schema( @@ -61,6 +65,9 @@ async def async_setup(hass, config): if DOMAIN in config: hass.data[UNIFI_CONFIG] = config[DOMAIN][CONF_CONTROLLERS] + hass.data[UNIFI_WIRELESS_CLIENTS] = wireless_clients = UnifiWirelessClients(hass) + await wireless_clients.async_load() + return True @@ -70,9 +77,7 @@ async def async_setup_entry(hass, config_entry): hass.data[DOMAIN] = {} controller = UniFiController(hass, config_entry) - controller_id = get_controller_id_from_config_entry(config_entry) - hass.data[DOMAIN][controller_id] = controller if not await controller.async_setup(): @@ -99,3 +104,43 @@ async def async_unload_entry(hass, config_entry): controller_id = get_controller_id_from_config_entry(config_entry) controller = hass.data[DOMAIN].pop(controller_id) return await controller.async_reset() + + +class UnifiWirelessClients: + """Class to store clients known to be wireless. + + This is needed since wireless devices going offline might get marked as wired by UniFi. + """ + + def __init__(self, hass): + """Set up client storage.""" + self.hass = hass + self.data = {} + self._store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY) + + async def async_load(self): + """Load data from file.""" + data = await self._store.async_load() + + if data is not None: + self.data = data + + @callback + 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": []}) + 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)} + + self._store.async_delay_save(self._data_to_save, SAVE_DELAY) + + @callback + def _data_to_save(self): + """Return data of UniFi wireless clients to store in a file.""" + return self.data diff --git a/homeassistant/components/unifi/const.py b/homeassistant/components/unifi/const.py index 4522ac4254a..eac14735074 100644 --- a/homeassistant/components/unifi/const.py +++ b/homeassistant/components/unifi/const.py @@ -10,6 +10,7 @@ CONF_CONTROLLER = "controller" CONF_SITE_ID = "site" UNIFI_CONFIG = "unifi_config" +UNIFI_WIRELESS_CLIENTS = "unifi_wireless_clients" CONF_BLOCK_CLIENT = "block_client" CONF_DETECTION_TIME = "detection_time" diff --git a/homeassistant/components/unifi/controller.py b/homeassistant/components/unifi/controller.py index b29b088a815..ffea98b9050 100644 --- a/homeassistant/components/unifi/controller.py +++ b/homeassistant/components/unifi/controller.py @@ -36,6 +36,7 @@ from .const import ( DOMAIN, LOGGER, UNIFI_CONFIG, + UNIFI_WIRELESS_CLIENTS, ) from .errors import AuthenticationRequired, CannotConnect @@ -50,6 +51,7 @@ class UniFiController: self.available = True self.api = None self.progress = None + self.wireless_clients = None self._site_name = None self._site_role = None @@ -128,6 +130,22 @@ class UniFiController: """Event specific per UniFi entry to signal new options.""" return f"unifi-options-{CONTROLLER_ID.format(host=self.host, site=self.site)}" + def update_wireless_clients(self): + """Update set of known to be wireless clients.""" + new_wireless_clients = set() + + for client_id in self.api.clients: + if ( + client_id not in self.wireless_clients + and not self.api.clients[client_id].is_wired + ): + new_wireless_clients.add(client_id) + + if new_wireless_clients: + self.wireless_clients |= new_wireless_clients + unifi_wireless_clients = self.hass.data[UNIFI_WIRELESS_CLIENTS] + unifi_wireless_clients.update_data(self.wireless_clients, self.config_entry) + async def request_update(self): """Request an update.""" if self.progress is not None: @@ -170,6 +188,8 @@ class UniFiController: LOGGER.info("Reconnected to controller %s", self.host) self.available = True + self.update_wireless_clients() + async_dispatcher_send(self.hass, self.signal_update) async def async_setup(self): @@ -197,6 +217,10 @@ class UniFiController: LOGGER.error("Unknown error connecting with UniFi controller: %s", err) return False + wireless_clients = hass.data[UNIFI_WIRELESS_CLIENTS] + self.wireless_clients = wireless_clients.get_data(self.config_entry) + self.update_wireless_clients() + self.import_configuration() self.config_entry.add_update_listener(self.async_options_updated) diff --git a/homeassistant/components/unifi/device_tracker.py b/homeassistant/components/unifi/device_tracker.py index b3982e7327d..48b19d7bada 100644 --- a/homeassistant/components/unifi/device_tracker.py +++ b/homeassistant/components/unifi/device_tracker.py @@ -1,9 +1,8 @@ """Track devices using UniFi controllers.""" import logging -import voluptuous as vol from homeassistant.components.unifi.config_flow import get_controller_from_config_entry -from homeassistant.components.device_tracker import DOMAIN, PLATFORM_SCHEMA +from homeassistant.components.device_tracker import DOMAIN as DEVICE_TRACKER_DOMAIN from homeassistant.components.device_tracker.config_entry import ScannerEntity from homeassistant.components.device_tracker.const import SOURCE_TYPE_ROUTER from homeassistant.core import callback @@ -27,7 +26,6 @@ DEVICE_ATTRIBUTES = [ "ip", "is_11r", "is_guest", - "is_wired", "mac", "name", "noted", @@ -39,13 +37,6 @@ DEVICE_ATTRIBUTES = [ "vlan", ] -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({}, extra=vol.ALLOW_EXTRA) - - -async def async_setup_scanner(hass, config, sync_see, discovery_info): - """Set up the Unifi integration.""" - return True - async def async_setup_entry(hass, config_entry, async_add_entities): """Set up device tracker for UniFi component.""" @@ -59,7 +50,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): if ( entity.config_entry_id == config_entry.entry_id - and entity.domain == DOMAIN + and entity.domain == DEVICE_TRACKER_DOMAIN and "-" in entity.unique_id ): @@ -129,6 +120,7 @@ class UniFiClientTracker(ScannerEntity): """Set up tracked client.""" self.client = client self.controller = controller + self.is_wired = self.client.mac not in controller.wireless_clients @property def entity_registry_enabled_default(self): @@ -137,13 +129,13 @@ class UniFiClientTracker(ScannerEntity): return False if ( - not self.client.is_wired + not self.is_wired and self.controller.option_ssid_filter and self.client.essid not in self.controller.option_ssid_filter ): return False - if not self.controller.option_track_wired_clients and self.client.is_wired: + if not self.controller.option_track_wired_clients and self.is_wired: return False return True @@ -153,18 +145,31 @@ class UniFiClientTracker(ScannerEntity): LOGGER.debug("New UniFi client tracker %s (%s)", self.name, self.client.mac) async def async_update(self): - """Synchronize state with controller.""" + """Synchronize state with controller. + + Make sure to update self.is_wired if client is wireless, there is an issue when clients go offline that they get marked as wired. + """ LOGGER.debug( "Updating UniFi tracked client %s (%s)", self.entity_id, self.client.mac ) await self.controller.request_update() + if self.is_wired and self.client.mac in self.controller.wireless_clients: + self.is_wired = False + @property def is_connected(self): - """Return true if the client is connected to the network.""" - if ( - dt_util.utcnow() - dt_util.utc_from_timestamp(float(self.client.last_seen)) - ) < self.controller.option_detection_time: + """Return true if the client is connected to the network. + + If is_wired and client.is_wired differ it means that the device is offline and UniFi bug shows device as wired. + """ + if self.is_wired == self.client.is_wired and ( + ( + dt_util.utcnow() + - dt_util.utc_from_timestamp(float(self.client.last_seen)) + ) + < self.controller.option_detection_time + ): return True return False @@ -203,6 +208,8 @@ class UniFiClientTracker(ScannerEntity): if variable in self.client.raw: attributes[variable] = self.client.raw[variable] + attributes["is_wired"] = self.is_wired + return attributes diff --git a/homeassistant/components/unifi/manifest.json b/homeassistant/components/unifi/manifest.json index d182806c4ac..ecbeb002f04 100644 --- a/homeassistant/components/unifi/manifest.json +++ b/homeassistant/components/unifi/manifest.json @@ -2,7 +2,7 @@ "domain": "unifi", "name": "Unifi", "config_flow": true, - "documentation": "https://www.home-assistant.io/components/unifi", + "documentation": "https://www.home-assistant.io/integrations/unifi", "requirements": [ "aiounifi==11" ], diff --git a/homeassistant/components/unifi/switch.py b/homeassistant/components/unifi/switch.py index 4f757102d53..f0183a7ecb3 100644 --- a/homeassistant/components/unifi/switch.py +++ b/homeassistant/components/unifi/switch.py @@ -88,7 +88,7 @@ def update_items(controller, async_add_entities, switches, switches_off): new_switches.append(switches[block_client_id]) LOGGER.debug("New UniFi Block switch %s (%s)", client.hostname, client.mac) - # control poe + # control POE for client_id in controller.api.clients: poe_client_id = f"poe-{client_id}" @@ -108,9 +108,10 @@ def update_items(controller, async_add_entities, switches, switches_off): pass # Network device with active POE elif ( - not client.is_wired + client_id 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 diff --git a/homeassistant/components/unifi_direct/manifest.json b/homeassistant/components/unifi_direct/manifest.json index 515bd68d011..805dc6638bb 100644 --- a/homeassistant/components/unifi_direct/manifest.json +++ b/homeassistant/components/unifi_direct/manifest.json @@ -1,7 +1,7 @@ { "domain": "unifi_direct", "name": "Unifi direct", - "documentation": "https://www.home-assistant.io/components/unifi_direct", + "documentation": "https://www.home-assistant.io/integrations/unifi_direct", "requirements": [ "pexpect==4.6.0" ], diff --git a/homeassistant/components/universal/manifest.json b/homeassistant/components/universal/manifest.json index ac72d10f07f..3e066b48598 100644 --- a/homeassistant/components/universal/manifest.json +++ b/homeassistant/components/universal/manifest.json @@ -1,7 +1,7 @@ { "domain": "universal", "name": "Universal", - "documentation": "https://www.home-assistant.io/components/universal", + "documentation": "https://www.home-assistant.io/integrations/universal", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/upc_connect/manifest.json b/homeassistant/components/upc_connect/manifest.json index efa38286e7e..cd5d327f2c2 100644 --- a/homeassistant/components/upc_connect/manifest.json +++ b/homeassistant/components/upc_connect/manifest.json @@ -1,8 +1,8 @@ { "domain": "upc_connect", "name": "Upc connect", - "documentation": "https://www.home-assistant.io/components/upc_connect", - "requirements": ["connect-box==0.2.4"], + "documentation": "https://www.home-assistant.io/integrations/upc_connect", + "requirements": ["connect-box==0.2.5"], "dependencies": [], "codeowners": ["@pvizeli"] } diff --git a/homeassistant/components/upcloud/manifest.json b/homeassistant/components/upcloud/manifest.json index 3a58d80f64a..62ce608a911 100644 --- a/homeassistant/components/upcloud/manifest.json +++ b/homeassistant/components/upcloud/manifest.json @@ -1,7 +1,7 @@ { "domain": "upcloud", "name": "Upcloud", - "documentation": "https://www.home-assistant.io/components/upcloud", + "documentation": "https://www.home-assistant.io/integrations/upcloud", "requirements": [ "upcloud-api==0.4.3" ], diff --git a/homeassistant/components/updater/manifest.json b/homeassistant/components/updater/manifest.json index 9275ef34968..eb26d6e36b7 100644 --- a/homeassistant/components/updater/manifest.json +++ b/homeassistant/components/updater/manifest.json @@ -1,7 +1,7 @@ { "domain": "updater", "name": "Updater", - "documentation": "https://www.home-assistant.io/components/updater", + "documentation": "https://www.home-assistant.io/integrations/updater", "requirements": [ "distro==1.4.0" ], diff --git a/homeassistant/components/upnp/.translations/nn.json b/homeassistant/components/upnp/.translations/nn.json index 286efcf0353..cfbedd994af 100644 --- a/homeassistant/components/upnp/.translations/nn.json +++ b/homeassistant/components/upnp/.translations/nn.json @@ -11,6 +11,7 @@ "init": { "title": "UPnP / IGD" } - } + }, + "title": "UPnP / IGD" } } \ No newline at end of file diff --git a/homeassistant/components/upnp/.translations/no.json b/homeassistant/components/upnp/.translations/no.json index 813509121e3..fb1508a1aab 100644 --- a/homeassistant/components/upnp/.translations/no.json +++ b/homeassistant/components/upnp/.translations/no.json @@ -6,7 +6,7 @@ "no_devices_discovered": "Ingen UPnP / IGDs oppdaget", "no_devices_found": "Ingen UPnP / IGD-enheter funnet p\u00e5 nettverket.", "no_sensors_or_port_mapping": "Aktiver minst sensorer eller port mapping", - "single_instance_allowed": "Bare en enkelt konfigurasjon av UPnP / IGD er n\u00f8dvendig." + "single_instance_allowed": "Bare en konfigurasjon av UPnP / IGD er n\u00f8dvendig." }, "error": { "few": "f\u00e5", diff --git a/homeassistant/components/upnp/.translations/ru.json b/homeassistant/components/upnp/.translations/ru.json index 8d41ec1d5de..3351f0d5d8a 100644 --- a/homeassistant/components/upnp/.translations/ru.json +++ b/homeassistant/components/upnp/.translations/ru.json @@ -1,7 +1,7 @@ { "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.", "incomplete_device": "\u0418\u0433\u043d\u043e\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435 \u043d\u0435\u043f\u043e\u043b\u043d\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 UPnP", "no_devices_discovered": "\u041d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u043e UPnP / IGD", "no_devices_found": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 UPnP / IGD \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b \u0432 \u0441\u0435\u0442\u0438.", diff --git a/homeassistant/components/upnp/.translations/zh-Hant.json b/homeassistant/components/upnp/.translations/zh-Hant.json index 2a036a1d2f3..1611dac2721 100644 --- a/homeassistant/components/upnp/.translations/zh-Hant.json +++ b/homeassistant/components/upnp/.translations/zh-Hant.json @@ -2,9 +2,9 @@ "config": { "abort": { "already_configured": "UPnP/IGD \u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", - "incomplete_device": "\u5ffd\u7565\u4e0d\u76f8\u5bb9 UPnP \u88dd\u7f6e", + "incomplete_device": "\u5ffd\u7565\u4e0d\u76f8\u5bb9 UPnP \u8a2d\u5099", "no_devices_discovered": "\u672a\u641c\u5c0b\u5230 UPnP/IGD", - "no_devices_found": "\u5728\u7db2\u8def\u4e0a\u627e\u4e0d\u5230 UPnP/IGD \u88dd\u7f6e\u3002", + "no_devices_found": "\u5728\u7db2\u8def\u4e0a\u627e\u4e0d\u5230 UPnP/IGD \u8a2d\u5099\u3002", "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" }, diff --git a/homeassistant/components/upnp/manifest.json b/homeassistant/components/upnp/manifest.json index 9aec23a687c..d4446b271f9 100644 --- a/homeassistant/components/upnp/manifest.json +++ b/homeassistant/components/upnp/manifest.json @@ -2,7 +2,7 @@ "domain": "upnp", "name": "Upnp", "config_flow": true, - "documentation": "https://www.home-assistant.io/components/upnp", + "documentation": "https://www.home-assistant.io/integrations/upnp", "requirements": [ "async-upnp-client==0.14.11" ], diff --git a/homeassistant/components/upnp/sensor.py b/homeassistant/components/upnp/sensor.py index e5746e088f8..b721fa29cdd 100644 --- a/homeassistant/components/upnp/sensor.py +++ b/homeassistant/components/upnp/sensor.py @@ -1,11 +1,11 @@ """Support for UPnP/IGD Sensors.""" -from datetime import datetime import logging from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import Entity from homeassistant.helpers.typing import HomeAssistantType +import homeassistant.util.dt as dt_util from .const import DOMAIN as DOMAIN_UPNP, SIGNAL_REMOVE_SENSOR @@ -199,10 +199,10 @@ class PerSecondUPnPIGDSensor(UpnpSensor): if self._last_value is None: self._last_value = new_value - self._last_update_time = datetime.now() + self._last_update_time = dt_util.utcnow() return - now = datetime.now() + now = dt_util.utcnow() if self._is_overflowed(new_value): self._state = None # temporarily report nothing else: diff --git a/homeassistant/components/ups/__init__.py b/homeassistant/components/ups/__init__.py deleted file mode 100644 index 690d3102f9c..00000000000 --- a/homeassistant/components/ups/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""The ups component.""" diff --git a/homeassistant/components/ups/manifest.json b/homeassistant/components/ups/manifest.json deleted file mode 100644 index 98db00c3094..00000000000 --- a/homeassistant/components/ups/manifest.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "domain": "ups", - "name": "Ups", - "documentation": "https://www.home-assistant.io/components/ups", - "requirements": [ - "upsmychoice==1.0.6" - ], - "dependencies": [], - "codeowners": [] -} diff --git a/homeassistant/components/ups/sensor.py b/homeassistant/components/ups/sensor.py deleted file mode 100644 index cfe35a9a63f..00000000000 --- a/homeassistant/components/ups/sensor.py +++ /dev/null @@ -1,126 +0,0 @@ -"""Sensor for UPS packages.""" -import logging -from collections import defaultdict -from datetime import timedelta - -import voluptuous as vol - -import homeassistant.helpers.config_validation as cv -from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import ( - ATTR_ATTRIBUTION, - CONF_NAME, - CONF_PASSWORD, - CONF_SCAN_INTERVAL, - CONF_USERNAME, -) -from homeassistant.helpers.entity import Entity -from homeassistant.util import Throttle, slugify -from homeassistant.util.dt import now, parse_date - -_LOGGER = logging.getLogger(__name__) - -DOMAIN = "ups" -COOKIE = "upsmychoice_cookies.pickle" -ICON = "mdi:package-variant-closed" -STATUS_DELIVERED = "delivered" - -SCAN_INTERVAL = timedelta(seconds=1800) - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Optional(CONF_NAME): cv.string, - } -) - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the UPS platform.""" - import upsmychoice - - _LOGGER.warning( - "The ups integration is deprecated and will be removed " - "in Home Assistant 0.100.0. For more information see ADR-0004:" - "https://github.com/home-assistant/architecture/blob/master/adr/0004-webscraping.md" - ) - - try: - cookie = hass.config.path(COOKIE) - session = upsmychoice.get_session( - config.get(CONF_USERNAME), config.get(CONF_PASSWORD), cookie_path=cookie - ) - except upsmychoice.UPSError: - _LOGGER.exception("Could not connect to UPS My Choice") - return False - - add_entities( - [ - UPSSensor( - session, - config.get(CONF_NAME), - config.get(CONF_SCAN_INTERVAL, SCAN_INTERVAL), - ) - ], - True, - ) - - -class UPSSensor(Entity): - """UPS Sensor.""" - - def __init__(self, session, name, interval): - """Initialize the sensor.""" - self._session = session - self._name = name - self._attributes = None - self._state = None - self.update = Throttle(interval)(self._update) - - @property - def name(self): - """Return the name of the sensor.""" - return self._name or DOMAIN - - @property - def state(self): - """Return the state of the sensor.""" - return self._state - - @property - def unit_of_measurement(self): - """Return the unit of measurement of this entity, if any.""" - return "packages" - - def _update(self): - """Update device state.""" - import upsmychoice - - status_counts = defaultdict(int) - try: - for package in upsmychoice.get_packages(self._session): - status = slugify(package["status"]) - skip = ( - status == STATUS_DELIVERED - and parse_date(package["delivery_date"]) < now().date() - ) - if skip: - continue - status_counts[status] += 1 - except upsmychoice.UPSError: - _LOGGER.error("Could not connect to UPS My Choice account") - - self._attributes = {ATTR_ATTRIBUTION: upsmychoice.ATTRIBUTION} - self._attributes.update(status_counts) - self._state = sum(status_counts.values()) - - @property - def device_state_attributes(self): - """Return the state attributes.""" - return self._attributes - - @property - def icon(self): - """Icon to use in the frontend.""" - return ICON diff --git a/homeassistant/components/uptime/manifest.json b/homeassistant/components/uptime/manifest.json index 10197178381..5997916e2c3 100644 --- a/homeassistant/components/uptime/manifest.json +++ b/homeassistant/components/uptime/manifest.json @@ -1,7 +1,7 @@ { "domain": "uptime", "name": "Uptime", - "documentation": "https://www.home-assistant.io/components/uptime", + "documentation": "https://www.home-assistant.io/integrations/uptime", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/uptimerobot/manifest.json b/homeassistant/components/uptimerobot/manifest.json index 375baf12565..cc2d1b6c2e8 100644 --- a/homeassistant/components/uptimerobot/manifest.json +++ b/homeassistant/components/uptimerobot/manifest.json @@ -1,7 +1,7 @@ { "domain": "uptimerobot", "name": "Uptimerobot", - "documentation": "https://www.home-assistant.io/components/uptimerobot", + "documentation": "https://www.home-assistant.io/integrations/uptimerobot", "requirements": [ "pyuptimerobot==0.0.5" ], diff --git a/homeassistant/components/uscis/manifest.json b/homeassistant/components/uscis/manifest.json index f2ffcfbf8a3..707b7f27860 100644 --- a/homeassistant/components/uscis/manifest.json +++ b/homeassistant/components/uscis/manifest.json @@ -1,7 +1,7 @@ { "domain": "uscis", "name": "Uscis", - "documentation": "https://www.home-assistant.io/components/uscis", + "documentation": "https://www.home-assistant.io/integrations/uscis", "requirements": [ "uscisstatus==0.1.1" ], diff --git a/homeassistant/components/usgs_earthquakes_feed/manifest.json b/homeassistant/components/usgs_earthquakes_feed/manifest.json index 0d1c116786a..d1ae97b550a 100644 --- a/homeassistant/components/usgs_earthquakes_feed/manifest.json +++ b/homeassistant/components/usgs_earthquakes_feed/manifest.json @@ -1,7 +1,7 @@ { "domain": "usgs_earthquakes_feed", "name": "Usgs earthquakes feed", - "documentation": "https://www.home-assistant.io/components/usgs_earthquakes_feed", + "documentation": "https://www.home-assistant.io/integrations/usgs_earthquakes_feed", "requirements": [ "geojson_client==0.4" ], diff --git a/homeassistant/components/usps/__init__.py b/homeassistant/components/usps/__init__.py deleted file mode 100644 index 61da78fa6d7..00000000000 --- a/homeassistant/components/usps/__init__.py +++ /dev/null @@ -1,96 +0,0 @@ -"""Support for USPS packages and mail.""" -from datetime import timedelta -import logging - -import voluptuous as vol - -from homeassistant.const import CONF_NAME, CONF_USERNAME, CONF_PASSWORD -from homeassistant.helpers import config_validation as cv, discovery -from homeassistant.util import Throttle -from homeassistant.util.dt import now - -_LOGGER = logging.getLogger(__name__) - -DOMAIN = "usps" -DATA_USPS = "data_usps" -MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=30) -COOKIE = "usps_cookies.pickle" -CACHE = "usps_cache" -CONF_DRIVER = "driver" - -USPS_TYPE = ["sensor", "camera"] - -CONFIG_SCHEMA = vol.Schema( - { - DOMAIN: vol.Schema( - { - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Optional(CONF_NAME, default=DOMAIN): cv.string, - vol.Optional(CONF_DRIVER): cv.string, - } - ) - }, - extra=vol.ALLOW_EXTRA, -) - - -def setup(hass, config): - """Use config values to set up a function enabling status retrieval.""" - _LOGGER.warning( - "The usps integration is deprecated and will be removed " - "in Home Assistant 0.100.0. For more information see ADR-0004:" - "https://github.com/home-assistant/architecture/blob/master/adr/0004-webscraping.md" - ) - - conf = config[DOMAIN] - username = conf.get(CONF_USERNAME) - password = conf.get(CONF_PASSWORD) - name = conf.get(CONF_NAME) - driver = conf.get(CONF_DRIVER) - - import myusps - - try: - cookie = hass.config.path(COOKIE) - cache = hass.config.path(CACHE) - session = myusps.get_session( - username, password, cookie_path=cookie, cache_path=cache, driver=driver - ) - except myusps.USPSError: - _LOGGER.exception("Could not connect to My USPS") - return False - - hass.data[DATA_USPS] = USPSData(session, name) - - for component in USPS_TYPE: - discovery.load_platform(hass, component, DOMAIN, {}, config) - - return True - - -class USPSData: - """Stores the data retrieved from USPS. - - For each entity to use, acts as the single point responsible for fetching - updates from the server. - """ - - def __init__(self, session, name): - """Initialize the data object.""" - self.session = session - self.name = name - self.packages = [] - self.mail = [] - self.attribution = None - - @Throttle(MIN_TIME_BETWEEN_UPDATES) - def update(self, **kwargs): - """Fetch the latest info from USPS.""" - import myusps - - self.packages = myusps.get_packages(self.session) - self.mail = myusps.get_mail(self.session, now().date()) - self.attribution = myusps.ATTRIBUTION - _LOGGER.debug("Mail, request date: %s, list: %s", now().date(), self.mail) - _LOGGER.debug("Package list: %s", self.packages) diff --git a/homeassistant/components/usps/camera.py b/homeassistant/components/usps/camera.py deleted file mode 100644 index 3141314b049..00000000000 --- a/homeassistant/components/usps/camera.py +++ /dev/null @@ -1,88 +0,0 @@ -"""Support for a camera made up of USPS mail images.""" -from datetime import timedelta -import logging - -from homeassistant.components.camera import Camera - -from . import DATA_USPS - -_LOGGER = logging.getLogger(__name__) - -SCAN_INTERVAL = timedelta(seconds=10) - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up USPS mail camera.""" - if discovery_info is None: - return - - usps = hass.data[DATA_USPS] - add_entities([USPSCamera(usps)]) - - -class USPSCamera(Camera): - """Representation of the images available from USPS.""" - - def __init__(self, usps): - """Initialize the USPS camera images.""" - super().__init__() - - self._usps = usps - self._name = self._usps.name - self._session = self._usps.session - - self._mail_img = [] - self._last_mail = None - self._mail_index = 0 - self._mail_count = 0 - - self._timer = None - - def camera_image(self): - """Update the camera's image if it has changed.""" - self._usps.update() - try: - self._mail_count = len(self._usps.mail) - except TypeError: - # No mail - return None - - if self._usps.mail != self._last_mail: - # Mail items must have changed - self._mail_img = [] - if len(self._usps.mail) >= 1: - self._last_mail = self._usps.mail - for article in self._usps.mail: - _LOGGER.debug("Fetching article image: %s", article) - img = self._session.get(article["image"]).content - self._mail_img.append(img) - - try: - return self._mail_img[self._mail_index] - except IndexError: - return None - - @property - def name(self): - """Return the name of this camera.""" - return f"{self._name} mail" - - @property - def model(self): - """Return date of mail as model.""" - try: - return "Date: {}".format(str(self._usps.mail[0]["date"])) - except IndexError: - return None - - @property - def should_poll(self): - """Update the mail image index periodically.""" - return True - - def update(self): - """Update mail image index.""" - if self._mail_index < (self._mail_count - 1): - self._mail_index += 1 - else: - self._mail_index = 0 diff --git a/homeassistant/components/usps/manifest.json b/homeassistant/components/usps/manifest.json deleted file mode 100644 index 9e2f8886d3a..00000000000 --- a/homeassistant/components/usps/manifest.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "domain": "usps", - "name": "Usps", - "documentation": "https://www.home-assistant.io/components/usps", - "requirements": [ - "myusps==1.3.2" - ], - "dependencies": [], - "codeowners": [] -} diff --git a/homeassistant/components/usps/sensor.py b/homeassistant/components/usps/sensor.py deleted file mode 100644 index 7e26e6c9e5c..00000000000 --- a/homeassistant/components/usps/sensor.py +++ /dev/null @@ -1,122 +0,0 @@ -"""Sensor for USPS packages.""" -from collections import defaultdict -import logging - -from homeassistant.const import ATTR_ATTRIBUTION, ATTR_DATE -from homeassistant.helpers.entity import Entity -from homeassistant.util import slugify -from homeassistant.util.dt import now - -from . import DATA_USPS - -_LOGGER = logging.getLogger(__name__) - -STATUS_DELIVERED = "delivered" - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the USPS platform.""" - if discovery_info is None: - return - - usps = hass.data[DATA_USPS] - add_entities([USPSPackageSensor(usps), USPSMailSensor(usps)], True) - - -class USPSPackageSensor(Entity): - """USPS Package Sensor.""" - - def __init__(self, usps): - """Initialize the sensor.""" - self._usps = usps - self._name = self._usps.name - self._attributes = None - self._state = None - - @property - def name(self): - """Return the name of the sensor.""" - return f"{self._name} packages" - - @property - def state(self): - """Return the state of the sensor.""" - return self._state - - def update(self): - """Update device state.""" - self._usps.update() - status_counts = defaultdict(int) - for package in self._usps.packages: - status = slugify(package["primary_status"]) - if status == STATUS_DELIVERED and package["delivery_date"] < now().date(): - continue - status_counts[status] += 1 - self._attributes = {ATTR_ATTRIBUTION: self._usps.attribution} - self._attributes.update(status_counts) - self._state = sum(status_counts.values()) - - @property - def device_state_attributes(self): - """Return the state attributes.""" - return self._attributes - - @property - def icon(self): - """Return the icon to use in the frontend.""" - return "mdi:package-variant-closed" - - @property - def unit_of_measurement(self): - """Return the unit of measurement of this entity, if any.""" - return "packages" - - -class USPSMailSensor(Entity): - """USPS Mail Sensor.""" - - def __init__(self, usps): - """Initialize the sensor.""" - self._usps = usps - self._name = self._usps.name - self._attributes = None - self._state = None - - @property - def name(self): - """Return the name of the sensor.""" - return f"{self._name} mail" - - @property - def state(self): - """Return the state of the sensor.""" - return self._state - - def update(self): - """Update device state.""" - self._usps.update() - if self._usps.mail is not None: - self._state = len(self._usps.mail) - else: - self._state = 0 - - @property - def device_state_attributes(self): - """Return the state attributes.""" - attr = {} - attr[ATTR_ATTRIBUTION] = self._usps.attribution - try: - attr[ATTR_DATE] = str(self._usps.mail[0]["date"]) - except IndexError: - pass - return attr - - @property - def icon(self): - """Icon to use in the frontend.""" - return "mdi:mailbox" - - @property - def unit_of_measurement(self): - """Return the unit of measurement of this entity, if any.""" - return "pieces" diff --git a/homeassistant/components/utility_meter/manifest.json b/homeassistant/components/utility_meter/manifest.json index 59f4d1ca21b..7a470037ba4 100644 --- a/homeassistant/components/utility_meter/manifest.json +++ b/homeassistant/components/utility_meter/manifest.json @@ -1,7 +1,7 @@ { "domain": "utility_meter", "name": "Utility meter", - "documentation": "https://www.home-assistant.io/components/utility_meter", + "documentation": "https://www.home-assistant.io/integrations/utility_meter", "requirements": [], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/uvc/camera.py b/homeassistant/components/uvc/camera.py index 01a3338e540..20aae3849ab 100644 --- a/homeassistant/components/uvc/camera.py +++ b/homeassistant/components/uvc/camera.py @@ -78,7 +78,7 @@ class UnifiVideoCamera(Camera): def __init__(self, nvr, uuid, name, password): """Initialize an Unifi camera.""" - super(UnifiVideoCamera, self).__init__() + super().__init__() self._nvr = nvr self._uuid = uuid self._name = name diff --git a/homeassistant/components/uvc/manifest.json b/homeassistant/components/uvc/manifest.json index 5c77f9ecc70..497bdac656e 100644 --- a/homeassistant/components/uvc/manifest.json +++ b/homeassistant/components/uvc/manifest.json @@ -1,7 +1,7 @@ { "domain": "uvc", "name": "Uvc", - "documentation": "https://www.home-assistant.io/components/uvc", + "documentation": "https://www.home-assistant.io/integrations/uvc", "requirements": [ "uvcclient==0.11.0" ], diff --git a/homeassistant/components/vacuum/manifest.json b/homeassistant/components/vacuum/manifest.json index 8dfbb8ed968..69edc40b15b 100644 --- a/homeassistant/components/vacuum/manifest.json +++ b/homeassistant/components/vacuum/manifest.json @@ -1,7 +1,7 @@ { "domain": "vacuum", "name": "Vacuum", - "documentation": "https://www.home-assistant.io/components/vacuum", + "documentation": "https://www.home-assistant.io/integrations/vacuum", "requirements": [], "dependencies": [ "group" diff --git a/homeassistant/components/vallox/manifest.json b/homeassistant/components/vallox/manifest.json index 4f9b0f4d126..51ecda91404 100644 --- a/homeassistant/components/vallox/manifest.json +++ b/homeassistant/components/vallox/manifest.json @@ -1,7 +1,7 @@ { "domain": "vallox", "name": "Vallox", - "documentation": "https://www.home-assistant.io/components/vallox", + "documentation": "https://www.home-assistant.io/integrations/vallox", "requirements": [ "vallox-websocket-api==2.2.0" ], diff --git a/homeassistant/components/vasttrafik/manifest.json b/homeassistant/components/vasttrafik/manifest.json index 47153dcf17f..c4e3a2c97bb 100644 --- a/homeassistant/components/vasttrafik/manifest.json +++ b/homeassistant/components/vasttrafik/manifest.json @@ -1,7 +1,7 @@ { "domain": "vasttrafik", "name": "Vasttrafik", - "documentation": "https://www.home-assistant.io/components/vasttrafik", + "documentation": "https://www.home-assistant.io/integrations/vasttrafik", "requirements": [ "vtjp==0.1.14" ], diff --git a/homeassistant/components/velbus/.translations/bg.json b/homeassistant/components/velbus/.translations/bg.json new file mode 100644 index 00000000000..e769f83d28e --- /dev/null +++ b/homeassistant/components/velbus/.translations/bg.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "port_exists": "\u0422\u043e\u0437\u0438 \u043f\u043e\u0440\u0442 \u0435 \u0432\u0435\u0447\u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d" + }, + "error": { + "connection_failed": "\u0412\u0440\u044a\u0437\u043a\u0430\u0442\u0430 \u0441 velbus \u043d\u0435 \u0431\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u0430", + "port_exists": "\u0422\u043e\u0437\u0438 \u043f\u043e\u0440\u0442 \u0435 \u0432\u0435\u0447\u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d" + }, + "step": { + "user": { + "data": { + "name": "\u041d\u0430\u0438\u043c\u0435\u043d\u043e\u0432\u0430\u043d\u0438\u0435 \u043d\u0430 \u0442\u0430\u0437\u0438 \u0432\u0440\u044a\u0437\u043a\u0430 \u0441 velbus", + "port": "\u0421\u0432\u044a\u0440\u0437\u0432\u0430\u0449 \u043d\u0438\u0437" + }, + "title": "\u0414\u0435\u0444\u0438\u043d\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 \u0442\u0438\u043f\u0430 \u0432\u0440\u044a\u0437\u043a\u0430\u0442\u0430 \u0441 velbus" + } + }, + "title": "Velbus \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441" + } +} \ No newline at end of file diff --git a/homeassistant/components/velbus/.translations/es-419.json b/homeassistant/components/velbus/.translations/es-419.json new file mode 100644 index 00000000000..1e1e8897c30 --- /dev/null +++ b/homeassistant/components/velbus/.translations/es-419.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "port_exists": "Este puerto ya est\u00e1 configurado" + }, + "error": { + "connection_failed": "La conexi\u00f3n velbus fall\u00f3", + "port_exists": "Este puerto ya est\u00e1 configurado" + }, + "step": { + "user": { + "data": { + "name": "El nombre de esta conexi\u00f3n velbus", + "port": "Cadena de conexi\u00f3n" + }, + "title": "Definir el tipo de conexi\u00f3n velbus" + } + }, + "title": "Interfaz Velbus" + } +} \ No newline at end of file diff --git a/homeassistant/components/velbus/.translations/lb.json b/homeassistant/components/velbus/.translations/lb.json index 89e0bd818d2..f38a74e5c1f 100644 --- a/homeassistant/components/velbus/.translations/lb.json +++ b/homeassistant/components/velbus/.translations/lb.json @@ -12,7 +12,8 @@ "data": { "name": "Numm fir d\u00ebs velbus Verbindung", "port": "Verbindungs zeeche-folleg" - } + }, + "title": "D\u00e9fin\u00e9iert den Typ vun der Velbus Verbindung" } }, "title": "Velbus Interface" diff --git a/homeassistant/components/velbus/.translations/pt-BR.json b/homeassistant/components/velbus/.translations/pt-BR.json new file mode 100644 index 00000000000..cb2031dc7e5 --- /dev/null +++ b/homeassistant/components/velbus/.translations/pt-BR.json @@ -0,0 +1,11 @@ +{ + "config": { + "abort": { + "port_exists": "Esta porta j\u00e1 est\u00e1 configurada" + }, + "error": { + "connection_failed": "A conex\u00e3o velbus falhou", + "port_exists": "Esta porta j\u00e1 est\u00e1 configurada" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/velbus/.translations/zh-Hans.json b/homeassistant/components/velbus/.translations/zh-Hans.json new file mode 100644 index 00000000000..7b2bc3b028b --- /dev/null +++ b/homeassistant/components/velbus/.translations/zh-Hans.json @@ -0,0 +1,14 @@ +{ + "config": { + "error": { + "port_exists": "\u6b64\u7aef\u53e3\u5df2\u914d\u7f6e\u5b8c\u6210" + }, + "step": { + "user": { + "data": { + "name": "\u8fd9\u4e2avelbus\u8fde\u63a5\u7684\u540d\u79f0" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/velbus/manifest.json b/homeassistant/components/velbus/manifest.json index b071b354d74..1d9401f6cfe 100644 --- a/homeassistant/components/velbus/manifest.json +++ b/homeassistant/components/velbus/manifest.json @@ -1,7 +1,7 @@ { "domain": "velbus", "name": "Velbus", - "documentation": "https://www.home-assistant.io/components/velbus", + "documentation": "https://www.home-assistant.io/integrations/velbus", "requirements": [ "python-velbus==2.0.27" ], diff --git a/homeassistant/components/velux/__init__.py b/homeassistant/components/velux/__init__.py index 4c21bb7fdef..51f615e68aa 100644 --- a/homeassistant/components/velux/__init__.py +++ b/homeassistant/components/velux/__init__.py @@ -1,11 +1,12 @@ """Support for VELUX KLF 200 devices.""" import logging - import voluptuous as vol +from pyvlx import PyVLX +from pyvlx import PyVLXException from homeassistant.helpers import discovery import homeassistant.helpers.config_validation as cv -from homeassistant.const import CONF_HOST, CONF_PASSWORD +from homeassistant.const import CONF_HOST, CONF_PASSWORD, EVENT_HOMEASSISTANT_STOP DOMAIN = "velux" DATA_VELUX = "data_velux" @@ -24,10 +25,9 @@ CONFIG_SCHEMA = vol.Schema( async def async_setup(hass, config): """Set up the velux component.""" - from pyvlx import PyVLXException - try: - hass.data[DATA_VELUX] = VeluxModule(hass, config) + hass.data[DATA_VELUX] = VeluxModule(hass, config[DOMAIN]) + hass.data[DATA_VELUX].setup() await hass.data[DATA_VELUX].async_start() except PyVLXException as ex: @@ -44,15 +44,27 @@ async def async_setup(hass, config): class VeluxModule: """Abstraction for velux component.""" - def __init__(self, hass, config): + def __init__(self, hass, domain_config): """Initialize for velux component.""" - from pyvlx import PyVLX + self.pyvlx = None + self._hass = hass + self._domain_config = domain_config - host = config[DOMAIN].get(CONF_HOST) - password = config[DOMAIN].get(CONF_PASSWORD) + def setup(self): + """Velux component setup.""" + + async def on_hass_stop(event): + """Close connection when hass stops.""" + _LOGGER.debug("Velux interface terminated") + await self.pyvlx.disconnect() + + self._hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, on_hass_stop) + host = self._domain_config.get(CONF_HOST) + password = self._domain_config.get(CONF_PASSWORD) self.pyvlx = PyVLX(host=host, password=password) async def async_start(self): """Start velux component.""" + _LOGGER.debug("Velux interface started") await self.pyvlx.load_scenes() await self.pyvlx.load_nodes() diff --git a/homeassistant/components/velux/manifest.json b/homeassistant/components/velux/manifest.json index 9f1f4a7200a..783e23a8171 100644 --- a/homeassistant/components/velux/manifest.json +++ b/homeassistant/components/velux/manifest.json @@ -1,7 +1,7 @@ { "domain": "velux", "name": "Velux", - "documentation": "https://www.home-assistant.io/components/velux", + "documentation": "https://www.home-assistant.io/integrations/velux", "requirements": [ "pyvlx==0.2.11" ], diff --git a/homeassistant/components/venstar/climate.py b/homeassistant/components/venstar/climate.py index 7e1ae1ecd60..7be31d56c08 100644 --- a/homeassistant/components/venstar/climate.py +++ b/homeassistant/components/venstar/climate.py @@ -11,14 +11,20 @@ from homeassistant.components.climate.const import ( HVAC_MODE_AUTO, HVAC_MODE_COOL, HVAC_MODE_HEAT, + HVAC_MODE_OFF, + CURRENT_HVAC_HEAT, + CURRENT_HVAC_COOL, + CURRENT_HVAC_IDLE, + CURRENT_HVAC_OFF, SUPPORT_FAN_MODE, + FAN_ON, + FAN_AUTO, SUPPORT_TARGET_HUMIDITY, SUPPORT_PRESET_MODE, SUPPORT_TARGET_TEMPERATURE, PRESET_AWAY, PRESET_NONE, SUPPORT_TARGET_TEMPERATURE_RANGE, - HVAC_MODE_OFF, ) from homeassistant.const import ( ATTR_TEMPERATURE, @@ -156,7 +162,7 @@ class VenstarThermostat(ClimateDevice): @property def hvac_mode(self): - """Return current operation ie. heat, cool, idle.""" + """Return current operation mode ie. heat, cool, auto.""" if self._client.mode == self._client.MODE_HEAT: return HVAC_MODE_HEAT if self._client.mode == self._client.MODE_COOL: @@ -165,12 +171,23 @@ class VenstarThermostat(ClimateDevice): return HVAC_MODE_AUTO return HVAC_MODE_OFF + @property + def hvac_action(self): + """Return current operation mode ie. heat, cool, auto.""" + if self._client.state == self._client.STATE_IDLE: + return CURRENT_HVAC_IDLE + if self._client.state == self._client.STATE_HEATING: + return CURRENT_HVAC_HEAT + if self._client.state == self._client.STATE_COOLING: + return CURRENT_HVAC_COOL + return CURRENT_HVAC_OFF + @property def fan_mode(self): - """Return the fan setting.""" - if self._client.fan == self._client.FAN_AUTO: - return HVAC_MODE_AUTO - return STATE_ON + """Return the current fan mode.""" + if self._client.fan == self._client.FAN_ON: + return FAN_ON + return FAN_AUTO @property def device_state_attributes(self): diff --git a/homeassistant/components/venstar/manifest.json b/homeassistant/components/venstar/manifest.json index 655ba019e8c..e8e36d04467 100644 --- a/homeassistant/components/venstar/manifest.json +++ b/homeassistant/components/venstar/manifest.json @@ -1,7 +1,7 @@ { "domain": "venstar", "name": "Venstar", - "documentation": "https://www.home-assistant.io/components/venstar", + "documentation": "https://www.home-assistant.io/integrations/venstar", "requirements": [ "venstarcolortouch==0.9" ], diff --git a/homeassistant/components/vera/lock.py b/homeassistant/components/vera/lock.py index cf2d6d25c4a..23b62bb0331 100644 --- a/homeassistant/components/vera/lock.py +++ b/homeassistant/components/vera/lock.py @@ -8,6 +8,9 @@ from . import VERA_CONTROLLER, VERA_DEVICES, VeraDevice _LOGGER = logging.getLogger(__name__) +ATTR_LAST_USER_NAME = "changed_by_name" +ATTR_LOW_BATTERY = "low_battery" + def setup_platform(hass, config, add_entities, discovery_info=None): """Find and return Vera locks.""" @@ -44,6 +47,35 @@ class VeraLock(VeraDevice, LockDevice): """Return true if device is on.""" return self._state == STATE_LOCKED + @property + def device_state_attributes(self): + """Who unlocked the lock and did a low battery alert fire. + + Reports on the previous poll cycle. + changed_by_name is a string like 'Bob'. + low_battery is 1 if an alert fired, 0 otherwise. + """ + data = super().device_state_attributes + + last_user = self.vera_device.get_last_user_alert() + if last_user is not None: + data[ATTR_LAST_USER_NAME] = last_user[1] + + data[ATTR_LOW_BATTERY] = self.vera_device.get_low_battery_alert() + return data + + @property + def changed_by(self): + """Who unlocked the lock. + + Reports on the previous poll cycle. + changed_by is an integer user ID. + """ + last_user = self.vera_device.get_last_user_alert() + if last_user is not None: + return last_user[0] + return None + def update(self): """Update state by the Vera device callback.""" self._state = ( diff --git a/homeassistant/components/vera/manifest.json b/homeassistant/components/vera/manifest.json index 8365ca1a765..120ec241d60 100644 --- a/homeassistant/components/vera/manifest.json +++ b/homeassistant/components/vera/manifest.json @@ -1,9 +1,9 @@ { "domain": "vera", "name": "Vera", - "documentation": "https://www.home-assistant.io/components/vera", + "documentation": "https://www.home-assistant.io/integrations/vera", "requirements": [ - "pyvera==0.3.4" + "pyvera==0.3.6" ], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/verisure/manifest.json b/homeassistant/components/verisure/manifest.json index 7c895233f77..38ea8c73147 100644 --- a/homeassistant/components/verisure/manifest.json +++ b/homeassistant/components/verisure/manifest.json @@ -1,7 +1,7 @@ { "domain": "verisure", "name": "Verisure", - "documentation": "https://www.home-assistant.io/components/verisure", + "documentation": "https://www.home-assistant.io/integrations/verisure", "requirements": [ "jsonpath==0.75", "vsure==1.5.2" diff --git a/homeassistant/components/version/manifest.json b/homeassistant/components/version/manifest.json index 815e7ff9a25..3f35b0dc9a5 100644 --- a/homeassistant/components/version/manifest.json +++ b/homeassistant/components/version/manifest.json @@ -1,7 +1,7 @@ { "domain": "version", "name": "Version", - "documentation": "https://www.home-assistant.io/components/version", + "documentation": "https://www.home-assistant.io/integrations/version", "requirements": [ "pyhaversion==3.1.0" ], diff --git a/homeassistant/components/vesync/.translations/bg.json b/homeassistant/components/vesync/.translations/bg.json new file mode 100644 index 00000000000..a12436936e6 --- /dev/null +++ b/homeassistant/components/vesync/.translations/bg.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_setup": "\u0420\u0430\u0437\u0440\u0435\u0448\u0435\u043d\u0430 \u0435 \u0441\u0430\u043c\u043e \u0435\u0434\u043d\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u043d\u0430 Vesync" + }, + "error": { + "invalid_login": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u043e \u043f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435 \u0438\u043b\u0438 \u043f\u0430\u0440\u043e\u043b\u0430" + }, + "step": { + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u0430", + "username": "E-mail \u0430\u0434\u0440\u0435\u0441" + }, + "title": "\u0412\u044a\u0432\u0435\u0434\u0435\u0442\u0435 \u043f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435 \u0438 \u043f\u0430\u0440\u043e\u043b\u0430" + } + }, + "title": "VeSync" + } +} \ No newline at end of file diff --git a/homeassistant/components/vesync/.translations/es-419.json b/homeassistant/components/vesync/.translations/es-419.json new file mode 100644 index 00000000000..58c62fb64b6 --- /dev/null +++ b/homeassistant/components/vesync/.translations/es-419.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_setup": "Solo se permite una instancia de Vesync" + }, + "error": { + "invalid_login": "Nombre de usuario o contrase\u00f1a inv\u00e1lidos" + }, + "step": { + "user": { + "data": { + "password": "Contrase\u00f1a", + "username": "Direcci\u00f3n de correo electr\u00f3nico" + }, + "title": "Ingrese nombre de usuario y contrase\u00f1a" + } + }, + "title": "VeSync" + } +} \ No newline at end of file diff --git a/homeassistant/components/vesync/.translations/nn.json b/homeassistant/components/vesync/.translations/nn.json new file mode 100644 index 00000000000..372e37133b1 --- /dev/null +++ b/homeassistant/components/vesync/.translations/nn.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "VeSync" + } +} \ No newline at end of file diff --git a/homeassistant/components/vesync/.translations/zh-Hans.json b/homeassistant/components/vesync/.translations/zh-Hans.json new file mode 100644 index 00000000000..caa00f36c89 --- /dev/null +++ b/homeassistant/components/vesync/.translations/zh-Hans.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_setup": "\u53ea\u5141\u8bb8\u4e00\u4e2aVesync\u5b9e\u4f8b" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vesync/manifest.json b/homeassistant/components/vesync/manifest.json index 53cc96be388..d52380c1025 100644 --- a/homeassistant/components/vesync/manifest.json +++ b/homeassistant/components/vesync/manifest.json @@ -1,7 +1,7 @@ { "domain": "vesync", "name": "VeSync", - "documentation": "https://www.home-assistant.io/components/vesync", + "documentation": "https://www.home-assistant.io/integrations/vesync", "dependencies": [], "codeowners": ["@markperdue", "@webdjoe"], "requirements": ["pyvesync==1.1.0"], diff --git a/homeassistant/components/viaggiatreno/manifest.json b/homeassistant/components/viaggiatreno/manifest.json index e145b26b0c9..99857fc2f7e 100644 --- a/homeassistant/components/viaggiatreno/manifest.json +++ b/homeassistant/components/viaggiatreno/manifest.json @@ -1,7 +1,7 @@ { "domain": "viaggiatreno", "name": "Viaggiatreno", - "documentation": "https://www.home-assistant.io/components/viaggiatreno", + "documentation": "https://www.home-assistant.io/integrations/viaggiatreno", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/vicare/manifest.json b/homeassistant/components/vicare/manifest.json index e5f55b20dda..9f7c703fe4b 100644 --- a/homeassistant/components/vicare/manifest.json +++ b/homeassistant/components/vicare/manifest.json @@ -1,7 +1,7 @@ { "domain": "vicare", "name": "Viessmann ViCare", - "documentation": "https://www.home-assistant.io/components/vicare", + "documentation": "https://www.home-assistant.io/integrations/vicare", "dependencies": [], "codeowners": ["@oischinger"], "requirements": ["PyViCare==0.1.1"] diff --git a/homeassistant/components/vivotek/camera.py b/homeassistant/components/vivotek/camera.py index bf136731cb6..012c1e1df34 100644 --- a/homeassistant/components/vivotek/camera.py +++ b/homeassistant/components/vivotek/camera.py @@ -55,7 +55,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): config[CONF_IP_ADDRESS], ), ) - add_entities([VivotekCam(**args)]) + add_entities([VivotekCam(**args)], True) class VivotekCam(Camera): @@ -68,6 +68,7 @@ class VivotekCam(Camera): self._cam = cam self._frame_interval = 1 / config[CONF_FRAMERATE] self._motion_detection_enabled = False + self._model_name = None self._name = config[CONF_NAME] self._stream_source = stream_source @@ -117,4 +118,8 @@ class VivotekCam(Camera): @property def model(self): """Return the camera model.""" - return self._cam.model_name + return self._model_name + + def update(self): + """Update entity status.""" + self._model_name = self._cam.model_name diff --git a/homeassistant/components/vivotek/manifest.json b/homeassistant/components/vivotek/manifest.json index 8a6a37762d4..20b2ac347f6 100644 --- a/homeassistant/components/vivotek/manifest.json +++ b/homeassistant/components/vivotek/manifest.json @@ -1,9 +1,9 @@ { "domain": "vivotek", "name": "Vivotek", - "documentation": "https://www.home-assistant.io/components/vivotek", + "documentation": "https://www.home-assistant.io/integrations/vivotek", "requirements": [ - "libpyvivotek==0.2.1" + "libpyvivotek==0.2.2" ], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/vizio/manifest.json b/homeassistant/components/vizio/manifest.json index c65204d78e8..682405a375b 100644 --- a/homeassistant/components/vizio/manifest.json +++ b/homeassistant/components/vizio/manifest.json @@ -1,7 +1,7 @@ { "domain": "vizio", "name": "Vizio", - "documentation": "https://www.home-assistant.io/components/vizio", + "documentation": "https://www.home-assistant.io/integrations/vizio", "requirements": [ "pyvizio==0.0.7" ], diff --git a/homeassistant/components/vlc/manifest.json b/homeassistant/components/vlc/manifest.json index a40b0e8c7d6..0dfc9f1ceb9 100644 --- a/homeassistant/components/vlc/manifest.json +++ b/homeassistant/components/vlc/manifest.json @@ -1,7 +1,7 @@ { "domain": "vlc", "name": "Vlc", - "documentation": "https://www.home-assistant.io/components/vlc", + "documentation": "https://www.home-assistant.io/integrations/vlc", "requirements": [ "python-vlc==1.1.2" ], diff --git a/homeassistant/components/vlc_telnet/manifest.json b/homeassistant/components/vlc_telnet/manifest.json index 1e0f1c71df5..7894eb2982b 100644 --- a/homeassistant/components/vlc_telnet/manifest.json +++ b/homeassistant/components/vlc_telnet/manifest.json @@ -1,7 +1,7 @@ { "domain": "vlc_telnet", "name": "VLC telnet", - "documentation": "https://www.home-assistant.io/components/vlc-telnet", + "documentation": "https://www.home-assistant.io/integrations/vlc-telnet", "requirements": [ "python-telnet-vlc==1.0.4" ], diff --git a/homeassistant/components/voicerss/manifest.json b/homeassistant/components/voicerss/manifest.json index 6f0b4ae5fd2..7a079d9ccf2 100644 --- a/homeassistant/components/voicerss/manifest.json +++ b/homeassistant/components/voicerss/manifest.json @@ -1,7 +1,7 @@ { "domain": "voicerss", "name": "Voicerss", - "documentation": "https://www.home-assistant.io/components/voicerss", + "documentation": "https://www.home-assistant.io/integrations/voicerss", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/volkszaehler/manifest.json b/homeassistant/components/volkszaehler/manifest.json index db068e35056..0b210bc780b 100644 --- a/homeassistant/components/volkszaehler/manifest.json +++ b/homeassistant/components/volkszaehler/manifest.json @@ -1,7 +1,7 @@ { "domain": "volkszaehler", "name": "Volkszaehler", - "documentation": "https://www.home-assistant.io/components/volkszaehler", + "documentation": "https://www.home-assistant.io/integrations/volkszaehler", "requirements": [ "volkszaehler==0.1.2" ], diff --git a/homeassistant/components/volumio/manifest.json b/homeassistant/components/volumio/manifest.json index e7c4bac4abd..a97c9d637ef 100644 --- a/homeassistant/components/volumio/manifest.json +++ b/homeassistant/components/volumio/manifest.json @@ -1,7 +1,7 @@ { "domain": "volumio", "name": "Volumio", - "documentation": "https://www.home-assistant.io/components/volumio", + "documentation": "https://www.home-assistant.io/integrations/volumio", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/volumio/media_player.py b/homeassistant/components/volumio/media_player.py index 8bd1952a650..7c13488c3f5 100644 --- a/homeassistant/components/volumio/media_player.py +++ b/homeassistant/components/volumio/media_player.py @@ -306,7 +306,7 @@ class Volumio(MediaPlayerDevice): def async_set_shuffle(self, shuffle): """Enable/disable shuffle mode.""" return self.send_volumio_msg( - "commands", params={"cmd": "random", "value": str(shuffle)} + "commands", params={"cmd": "random", "value": str(shuffle).lower()} ) def async_select_source(self, source): diff --git a/homeassistant/components/volvooncall/manifest.json b/homeassistant/components/volvooncall/manifest.json index aa691d7766c..3f75c391115 100644 --- a/homeassistant/components/volvooncall/manifest.json +++ b/homeassistant/components/volvooncall/manifest.json @@ -1,7 +1,7 @@ { "domain": "volvooncall", "name": "Volvooncall", - "documentation": "https://www.home-assistant.io/components/volvooncall", + "documentation": "https://www.home-assistant.io/integrations/volvooncall", "requirements": [ "volvooncall==0.8.7" ], diff --git a/homeassistant/components/vultr/manifest.json b/homeassistant/components/vultr/manifest.json index 5f5461f2d63..0a0afe3d71b 100644 --- a/homeassistant/components/vultr/manifest.json +++ b/homeassistant/components/vultr/manifest.json @@ -1,7 +1,7 @@ { "domain": "vultr", "name": "Vultr", - "documentation": "https://www.home-assistant.io/components/vultr", + "documentation": "https://www.home-assistant.io/integrations/vultr", "requirements": [ "vultr==0.1.2" ], diff --git a/homeassistant/components/w800rf32/manifest.json b/homeassistant/components/w800rf32/manifest.json index 920ee1120a7..89b0ac591ea 100644 --- a/homeassistant/components/w800rf32/manifest.json +++ b/homeassistant/components/w800rf32/manifest.json @@ -1,7 +1,7 @@ { "domain": "w800rf32", "name": "W800rf32", - "documentation": "https://www.home-assistant.io/components/w800rf32", + "documentation": "https://www.home-assistant.io/integrations/w800rf32", "requirements": [ "pyW800rf32==0.1" ], diff --git a/homeassistant/components/wake_on_lan/manifest.json b/homeassistant/components/wake_on_lan/manifest.json index dc689f8d617..ef6dbd06470 100644 --- a/homeassistant/components/wake_on_lan/manifest.json +++ b/homeassistant/components/wake_on_lan/manifest.json @@ -1,7 +1,7 @@ { "domain": "wake_on_lan", "name": "Wake on lan", - "documentation": "https://www.home-assistant.io/components/wake_on_lan", + "documentation": "https://www.home-assistant.io/integrations/wake_on_lan", "requirements": [ "wakeonlan==1.1.6" ], diff --git a/homeassistant/components/waqi/manifest.json b/homeassistant/components/waqi/manifest.json index 4b692c669d1..8ce03e2e8e2 100644 --- a/homeassistant/components/waqi/manifest.json +++ b/homeassistant/components/waqi/manifest.json @@ -1,7 +1,7 @@ { "domain": "waqi", "name": "Waqi", - "documentation": "https://www.home-assistant.io/components/waqi", + "documentation": "https://www.home-assistant.io/integrations/waqi", "requirements": [ "waqiasync==1.0.0" ], diff --git a/homeassistant/components/water_heater/manifest.json b/homeassistant/components/water_heater/manifest.json index e291777483e..7ed6493b843 100644 --- a/homeassistant/components/water_heater/manifest.json +++ b/homeassistant/components/water_heater/manifest.json @@ -1,7 +1,7 @@ { "domain": "water_heater", "name": "Water heater", - "documentation": "https://www.home-assistant.io/components/water_heater", + "documentation": "https://www.home-assistant.io/integrations/water_heater", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/waterfurnace/manifest.json b/homeassistant/components/waterfurnace/manifest.json index 57aa663a348..e2982bd0145 100644 --- a/homeassistant/components/waterfurnace/manifest.json +++ b/homeassistant/components/waterfurnace/manifest.json @@ -1,7 +1,7 @@ { "domain": "waterfurnace", "name": "Waterfurnace", - "documentation": "https://www.home-assistant.io/components/waterfurnace", + "documentation": "https://www.home-assistant.io/integrations/waterfurnace", "requirements": [ "waterfurnace==1.1.0" ], diff --git a/homeassistant/components/watson_iot/manifest.json b/homeassistant/components/watson_iot/manifest.json index 8896f34f976..20834bf0bf4 100644 --- a/homeassistant/components/watson_iot/manifest.json +++ b/homeassistant/components/watson_iot/manifest.json @@ -1,7 +1,7 @@ { "domain": "watson_iot", "name": "Watson iot", - "documentation": "https://www.home-assistant.io/components/watson_iot", + "documentation": "https://www.home-assistant.io/integrations/watson_iot", "requirements": [ "ibmiotf==0.3.4" ], diff --git a/homeassistant/components/watson_tts/manifest.json b/homeassistant/components/watson_tts/manifest.json index d40baaca132..4cde3f764e5 100644 --- a/homeassistant/components/watson_tts/manifest.json +++ b/homeassistant/components/watson_tts/manifest.json @@ -1,7 +1,7 @@ { "domain": "watson_tts", "name": "IBM Watson TTS", - "documentation": "https://www.home-assistant.io/components/watson_tts", + "documentation": "https://www.home-assistant.io/integrations/watson_tts", "requirements": [ "ibm-watson==3.0.3" ], diff --git a/homeassistant/components/watson_tts/tts.py b/homeassistant/components/watson_tts/tts.py index 0b7228fb568..a30d08f31f3 100644 --- a/homeassistant/components/watson_tts/tts.py +++ b/homeassistant/components/watson_tts/tts.py @@ -99,6 +99,7 @@ def get_engine(hass, config): supported_languages = list({s[:5] for s in SUPPORTED_VOICES}) default_voice = config[CONF_VOICE] output_format = config[CONF_OUTPUT_FORMAT] + service.set_default_headers({"x-watson-learning-opt-out": "true"}) return WatsonTTSProvider(service, supported_languages, default_voice, output_format) diff --git a/homeassistant/components/waze_travel_time/manifest.json b/homeassistant/components/waze_travel_time/manifest.json index 09ae4f812d7..85bcc19032e 100644 --- a/homeassistant/components/waze_travel_time/manifest.json +++ b/homeassistant/components/waze_travel_time/manifest.json @@ -1,7 +1,7 @@ { "domain": "waze_travel_time", "name": "Waze travel time", - "documentation": "https://www.home-assistant.io/components/waze_travel_time", + "documentation": "https://www.home-assistant.io/integrations/waze_travel_time", "requirements": [ "WazeRouteCalculator==0.10" ], diff --git a/homeassistant/components/weather/manifest.json b/homeassistant/components/weather/manifest.json index 7008c033f95..568ce57ed62 100644 --- a/homeassistant/components/weather/manifest.json +++ b/homeassistant/components/weather/manifest.json @@ -1,7 +1,7 @@ { "domain": "weather", "name": "Weather", - "documentation": "https://www.home-assistant.io/components/weather", + "documentation": "https://www.home-assistant.io/integrations/weather", "requirements": [], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/webhook/manifest.json b/homeassistant/components/webhook/manifest.json index 384e61aed2a..f93d8a5e295 100644 --- a/homeassistant/components/webhook/manifest.json +++ b/homeassistant/components/webhook/manifest.json @@ -1,7 +1,7 @@ { "domain": "webhook", "name": "Webhook", - "documentation": "https://www.home-assistant.io/components/webhook", + "documentation": "https://www.home-assistant.io/integrations/webhook", "requirements": [], "dependencies": [ "http" diff --git a/homeassistant/components/weblink/manifest.json b/homeassistant/components/weblink/manifest.json index 7c30ad6c5d3..7cdf8bea4ff 100644 --- a/homeassistant/components/weblink/manifest.json +++ b/homeassistant/components/weblink/manifest.json @@ -1,7 +1,7 @@ { "domain": "weblink", "name": "Weblink", - "documentation": "https://www.home-assistant.io/components/weblink", + "documentation": "https://www.home-assistant.io/integrations/weblink", "requirements": [], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/webostv/manifest.json b/homeassistant/components/webostv/manifest.json index 4dd2f92628d..dcf908cd603 100644 --- a/homeassistant/components/webostv/manifest.json +++ b/homeassistant/components/webostv/manifest.json @@ -1,7 +1,7 @@ { "domain": "webostv", "name": "Webostv", - "documentation": "https://www.home-assistant.io/components/webostv", + "documentation": "https://www.home-assistant.io/integrations/webostv", "requirements": [ "pylgtv==0.1.9", "websockets==6.0" diff --git a/homeassistant/components/webostv/media_player.py b/homeassistant/components/webostv/media_player.py index 1da70bc60ec..913d193845f 100644 --- a/homeassistant/components/webostv/media_player.py +++ b/homeassistant/components/webostv/media_player.py @@ -396,10 +396,12 @@ class LgWebOSDevice(MediaPlayerDevice): if media_id == channel["channelNumber"]: perfect_match_channel_id = channel["channelId"] continue - elif media_id.lower() == channel["channelName"].lower(): + + if media_id.lower() == channel["channelName"].lower(): perfect_match_channel_id = channel["channelId"] continue - elif media_id.lower() in channel["channelName"].lower(): + + if media_id.lower() in channel["channelName"].lower(): partial_match_channel_id = channel["channelId"] if perfect_match_channel_id is not None: diff --git a/homeassistant/components/websocket_api/__init__.py b/homeassistant/components/websocket_api/__init__.py index 57ab34a6a5a..1ec758ebd4d 100644 --- a/homeassistant/components/websocket_api/__init__.py +++ b/homeassistant/components/websocket_api/__init__.py @@ -4,6 +4,9 @@ from homeassistant.loader import bind_hass from . import commands, connection, const, decorators, http, messages + +# mypy: allow-untyped-calls, allow-untyped-defs + DOMAIN = const.DOMAIN DEPENDENCIES = ("http",) diff --git a/homeassistant/components/websocket_api/auth.py b/homeassistant/components/websocket_api/auth.py index 2d748f5cc6a..716b20f4ca4 100644 --- a/homeassistant/components/websocket_api/auth.py +++ b/homeassistant/components/websocket_api/auth.py @@ -2,6 +2,7 @@ import voluptuous as vol from voluptuous.humanize import humanize_error +from homeassistant.auth.models import RefreshToken, User from homeassistant.auth.providers import legacy_api_password from homeassistant.components.http.ban import process_wrong_login, process_success_login from homeassistant.const import __version__ @@ -9,6 +10,9 @@ from homeassistant.const import __version__ from .connection import ActiveConnection from .error import Disconnect + +# mypy: allow-untyped-calls, allow-untyped-defs + TYPE_AUTH = "auth" TYPE_AUTH_INVALID = "auth_invalid" TYPE_AUTH_OK = "auth_ok" @@ -87,7 +91,9 @@ class AuthPhase: await process_wrong_login(self._request) raise Disconnect - async def _async_finish_auth(self, user, refresh_token) -> ActiveConnection: + async def _async_finish_auth( + self, user: User, refresh_token: RefreshToken + ) -> ActiveConnection: """Create an active connection.""" self._logger.debug("Auth OK") await process_success_login(self._request) diff --git a/homeassistant/components/websocket_api/commands.py b/homeassistant/components/websocket_api/commands.py index deb3600574f..9d46238b241 100644 --- a/homeassistant/components/websocket_api/commands.py +++ b/homeassistant/components/websocket_api/commands.py @@ -12,6 +12,9 @@ from homeassistant.helpers.event import async_track_state_change from . import const, decorators, messages +# mypy: allow-untyped-calls, allow-untyped-defs + + @callback def async_register_commands(hass, async_reg): """Register commands.""" diff --git a/homeassistant/components/websocket_api/connection.py b/homeassistant/components/websocket_api/connection.py index 3886d6c21d0..41232b097d1 100644 --- a/homeassistant/components/websocket_api/connection.py +++ b/homeassistant/components/websocket_api/connection.py @@ -1,5 +1,7 @@ """Connection session.""" import asyncio +from typing import Any, Callable, Dict, Hashable + import voluptuous as vol from homeassistant.core import callback, Context @@ -8,6 +10,9 @@ from homeassistant.exceptions import Unauthorized from . import const, messages +# mypy: allow-untyped-calls, allow-untyped-defs + + class ActiveConnection: """Handle an active websocket client connection.""" @@ -22,7 +27,7 @@ class ActiveConnection: else: self.refresh_token_id = None - self.subscriptions = {} + self.subscriptions: Dict[Hashable, Callable[[], Any]] = {} self.last_id = 0 def context(self, msg): diff --git a/homeassistant/components/websocket_api/decorators.py b/homeassistant/components/websocket_api/decorators.py index ba65d5e19a8..025131643e8 100644 --- a/homeassistant/components/websocket_api/decorators.py +++ b/homeassistant/components/websocket_api/decorators.py @@ -8,6 +8,8 @@ from homeassistant.exceptions import Unauthorized from . import messages +# mypy: allow-untyped-calls, allow-untyped-defs + _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/websocket_api/http.py b/homeassistant/components/websocket_api/http.py index 108fcbc7740..08a0430ee2a 100644 --- a/homeassistant/components/websocket_api/http.py +++ b/homeassistant/components/websocket_api/http.py @@ -25,6 +25,9 @@ from .error import Disconnect from .messages import error_message +# mypy: allow-untyped-calls, allow-untyped-defs + + class WebsocketAPIView(HomeAssistantView): """View to serve a websockets endpoint.""" @@ -45,7 +48,7 @@ class WebSocketHandler: self.hass = hass self.request = request self.wsock = None - self._to_write = asyncio.Queue(maxsize=MAX_PENDING_MSG) + self._to_write: asyncio.Queue = asyncio.Queue(maxsize=MAX_PENDING_MSG) self._handle_task = None self._writer_task = None self._logger = logging.getLogger("{}.connection.{}".format(__name__, id(self))) @@ -58,12 +61,15 @@ class WebSocketHandler: message = await self._to_write.get() if message is None: break + self._logger.debug("Sending %s", message) + + if isinstance(message, str): + await self.wsock.send_str(message) + continue + try: - if isinstance(message, str): - await self.wsock.send_str(message) - else: - await self.wsock.send_json(message, dumps=JSON_DUMP) + dumped = JSON_DUMP(message) except (ValueError, TypeError) as err: self._logger.error( "Unable to serialize to JSON: %s\n%s", err, message @@ -73,6 +79,9 @@ class WebSocketHandler: message["id"], ERR_UNKNOWN_ERROR, "Invalid JSON in response" ) ) + continue + + await self.wsock.send_str(dumped) @callback def _send_message(self, message): @@ -106,7 +115,7 @@ class WebSocketHandler: # Py3.7+ if hasattr(asyncio, "current_task"): # pylint: disable=no-member - self._handle_task = asyncio.current_task() + self._handle_task = asyncio.current_task() # type: ignore else: self._handle_task = asyncio.Task.current_task() @@ -144,13 +153,13 @@ class WebSocketHandler: raise Disconnect try: - msg = msg.json() + msg_data = msg.json() except ValueError: disconnect_warn = "Received invalid JSON." raise Disconnect - self._logger.debug("Received %s", msg) - connection = await auth.async_handle(msg) + self._logger.debug("Received %s", msg_data) + connection = await auth.async_handle(msg_data) self.hass.data[DATA_CONNECTIONS] = ( self.hass.data.get(DATA_CONNECTIONS, 0) + 1 ) @@ -165,18 +174,18 @@ class WebSocketHandler: if msg.type in (WSMsgType.CLOSE, WSMsgType.CLOSING): break - elif msg.type != WSMsgType.TEXT: + if msg.type != WSMsgType.TEXT: disconnect_warn = "Received non-Text message." break try: - msg = msg.json() + msg_data = msg.json() except ValueError: disconnect_warn = "Received invalid JSON." break - self._logger.debug("Received %s", msg) - connection.async_handle(msg) + self._logger.debug("Received %s", msg_data) + connection.async_handle(msg_data) except asyncio.CancelledError: self._logger.info("Connection closed by client") diff --git a/homeassistant/components/websocket_api/manifest.json b/homeassistant/components/websocket_api/manifest.json index bc630b2947f..9826b11ec45 100644 --- a/homeassistant/components/websocket_api/manifest.json +++ b/homeassistant/components/websocket_api/manifest.json @@ -1,7 +1,7 @@ { "domain": "websocket_api", "name": "Websocket api", - "documentation": "https://www.home-assistant.io/components/websocket_api", + "documentation": "https://www.home-assistant.io/integrations/websocket_api", "requirements": [], "dependencies": [ "http" diff --git a/homeassistant/components/websocket_api/messages.py b/homeassistant/components/websocket_api/messages.py index 65291bc55e7..c8c760a6549 100644 --- a/homeassistant/components/websocket_api/messages.py +++ b/homeassistant/components/websocket_api/messages.py @@ -7,6 +7,8 @@ from homeassistant.helpers import config_validation as cv from . import const +# mypy: allow-untyped-defs + # Minimal requirements of a message MINIMAL_MESSAGE_SCHEMA = vol.Schema( {vol.Required("id"): cv.positive_int, vol.Required("type"): cv.string}, diff --git a/homeassistant/components/websocket_api/sensor.py b/homeassistant/components/websocket_api/sensor.py index 1ae76b56252..20a6a90860b 100644 --- a/homeassistant/components/websocket_api/sensor.py +++ b/homeassistant/components/websocket_api/sensor.py @@ -10,6 +10,9 @@ from .const import ( ) +# mypy: allow-untyped-calls, allow-untyped-defs + + async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the API streams platform.""" entity = APICount() diff --git a/homeassistant/components/wemo/.translations/nn.json b/homeassistant/components/wemo/.translations/nn.json new file mode 100644 index 00000000000..c1c8830cb25 --- /dev/null +++ b/homeassistant/components/wemo/.translations/nn.json @@ -0,0 +1,10 @@ +{ + "config": { + "step": { + "confirm": { + "title": "Wemo" + } + }, + "title": "Wemo" + } +} \ No newline at end of file diff --git a/homeassistant/components/wemo/.translations/no.json b/homeassistant/components/wemo/.translations/no.json index 917eb0ef3a9..25a4172f00c 100644 --- a/homeassistant/components/wemo/.translations/no.json +++ b/homeassistant/components/wemo/.translations/no.json @@ -2,7 +2,7 @@ "config": { "abort": { "no_devices_found": "Ingen Sonos enheter funnet p\u00e5 nettverket.", - "single_instance_allowed": "Kun en enkelt konfigurasjon av Wemo er mulig." + "single_instance_allowed": "Kun en konfigurasjon av Wemo er mulig." }, "step": { "confirm": { diff --git a/homeassistant/components/wemo/config_flow.py b/homeassistant/components/wemo/config_flow.py index a1614eb1ce3..21c911a66ce 100644 --- a/homeassistant/components/wemo/config_flow.py +++ b/homeassistant/components/wemo/config_flow.py @@ -1,14 +1,16 @@ """Config flow for Wemo.""" + +import pywemo + from homeassistant.helpers import config_entry_flow from homeassistant import config_entries + from . import DOMAIN async def _async_has_devices(hass): """Return if there are devices that can be discovered.""" - import pywemo - - return bool(pywemo.discover_devices()) + return bool(await hass.async_add_executor_job(pywemo.discover_devices)) config_entry_flow.register_discovery_flow( diff --git a/homeassistant/components/wemo/manifest.json b/homeassistant/components/wemo/manifest.json index 1902df1060b..aa863bcff0d 100644 --- a/homeassistant/components/wemo/manifest.json +++ b/homeassistant/components/wemo/manifest.json @@ -2,7 +2,7 @@ "domain": "wemo", "name": "Wemo", "config_flow": true, - "documentation": "https://www.home-assistant.io/components/wemo", + "documentation": "https://www.home-assistant.io/integrations/wemo", "requirements": [ "pywemo==0.4.34" ], diff --git a/homeassistant/components/whois/manifest.json b/homeassistant/components/whois/manifest.json index dec3e78a503..1566366362a 100644 --- a/homeassistant/components/whois/manifest.json +++ b/homeassistant/components/whois/manifest.json @@ -1,9 +1,9 @@ { "domain": "whois", "name": "Whois", - "documentation": "https://www.home-assistant.io/components/whois", + "documentation": "https://www.home-assistant.io/integrations/whois", "requirements": [ - "python-whois==0.7.1" + "python-whois==0.7.2" ], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/whois/sensor.py b/homeassistant/components/whois/sensor.py index 313a6337a11..09cf40f193f 100644 --- a/homeassistant/components/whois/sensor.py +++ b/homeassistant/components/whois/sensor.py @@ -3,6 +3,7 @@ from datetime import timedelta import logging import voluptuous as vol +import whois from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import CONF_NAME @@ -32,8 +33,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the WHOIS sensor.""" - import whois - domain = config.get(CONF_DOMAIN) name = config.get(CONF_NAME) @@ -41,7 +40,9 @@ def setup_platform(hass, config, add_entities, discovery_info=None): if "expiration_date" in whois.whois(domain): add_entities([WhoisSensor(name, domain)], True) else: - _LOGGER.error("WHOIS lookup for %s didn't contain expiration_date", domain) + _LOGGER.error( + "WHOIS lookup for %s didn't contain an expiration date", domain + ) return except whois.BaseException as ex: _LOGGER.error("Exception %s occurred during WHOIS lookup for %s", ex, domain) @@ -53,8 +54,6 @@ class WhoisSensor(Entity): def __init__(self, name, domain): """Initialize the sensor.""" - import whois - self.whois = whois.whois self._name = name @@ -95,8 +94,6 @@ class WhoisSensor(Entity): def update(self): """Get the current WHOIS data for the domain.""" - import whois - try: response = self.whois(self._domain) except whois.BaseException as ex: diff --git a/homeassistant/components/wink/__init__.py b/homeassistant/components/wink/__init__.py index 5af784359d8..d0bb27c06e1 100644 --- a/homeassistant/components/wink/__init__.py +++ b/homeassistant/components/wink/__init__.py @@ -863,7 +863,7 @@ class WinkSirenDevice(WinkDevice): @property def device_state_attributes(self): """Return the device state attributes.""" - attributes = super(WinkSirenDevice, self).device_state_attributes + attributes = super().device_state_attributes auto_shutoff = self.wink.auto_shutoff() if auto_shutoff is not None: @@ -921,7 +921,7 @@ class WinkNimbusDialDevice(WinkDevice): @property def device_state_attributes(self): """Return the device state attributes.""" - attributes = super(WinkNimbusDialDevice, self).device_state_attributes + attributes = super().device_state_attributes dial_attributes = self.dial_attributes() return {**attributes, **dial_attributes} diff --git a/homeassistant/components/wink/lock.py b/homeassistant/components/wink/lock.py index 0d4d373b2b6..5246fb49eed 100644 --- a/homeassistant/components/wink/lock.py +++ b/homeassistant/components/wink/lock.py @@ -4,7 +4,13 @@ import logging import voluptuous as vol from homeassistant.components.lock import LockDevice -from homeassistant.const import ATTR_CODE, ATTR_ENTITY_ID, ATTR_NAME, STATE_UNKNOWN +from homeassistant.const import ( + ATTR_CODE, + ATTR_ENTITY_ID, + ATTR_MODE, + ATTR_NAME, + STATE_UNKNOWN, +) import homeassistant.helpers.config_validation as cv from . import DOMAIN, WinkDevice @@ -20,7 +26,6 @@ SERVICE_ADD_KEY = "wink_add_new_lock_key_code" ATTR_ENABLED = "enabled" ATTR_SENSITIVITY = "sensitivity" -ATTR_MODE = "mode" ALARM_SENSITIVITY_MAP = { "low": 0.2, diff --git a/homeassistant/components/wink/manifest.json b/homeassistant/components/wink/manifest.json index cddfdc5dc9c..acf9c38e632 100644 --- a/homeassistant/components/wink/manifest.json +++ b/homeassistant/components/wink/manifest.json @@ -1,7 +1,7 @@ { "domain": "wink", "name": "Wink", - "documentation": "https://www.home-assistant.io/components/wink", + "documentation": "https://www.home-assistant.io/integrations/wink", "requirements": [ "pubnubsub-handler==1.0.8", "python-wink==1.10.5" diff --git a/homeassistant/components/wink/switch.py b/homeassistant/components/wink/switch.py index d888fb82528..07d3ff4becc 100644 --- a/homeassistant/components/wink/switch.py +++ b/homeassistant/components/wink/switch.py @@ -53,7 +53,7 @@ class WinkToggleDevice(WinkDevice, ToggleEntity): @property def device_state_attributes(self): """Return the state attributes.""" - attributes = super(WinkToggleDevice, self).device_state_attributes + attributes = super().device_state_attributes try: event = self.wink.last_event() if event is not None: diff --git a/homeassistant/components/wirelesstag/manifest.json b/homeassistant/components/wirelesstag/manifest.json index c3da00ce951..7472898b7ca 100644 --- a/homeassistant/components/wirelesstag/manifest.json +++ b/homeassistant/components/wirelesstag/manifest.json @@ -1,7 +1,7 @@ { "domain": "wirelesstag", "name": "Wirelesstag", - "documentation": "https://www.home-assistant.io/components/wirelesstag", + "documentation": "https://www.home-assistant.io/integrations/wirelesstag", "requirements": [ "wirelesstagpy==0.4.0" ], diff --git a/homeassistant/components/withings/.translations/bg.json b/homeassistant/components/withings/.translations/bg.json new file mode 100644 index 00000000000..e75860d0e16 --- /dev/null +++ b/homeassistant/components/withings/.translations/bg.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "no_flows": "\u0422\u0440\u044f\u0431\u0432\u0430 \u0434\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u0442\u0435 Withings, \u043f\u0440\u0435\u0434\u0438 \u0434\u0430 \u043c\u043e\u0436\u0435\u0442\u0435 \u0434\u0430 \u0441\u0435 \u0438\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u0446\u0438\u0440\u0430\u0442\u0435. \u041c\u043e\u043b\u044f, \u043f\u0440\u043e\u0447\u0435\u0442\u0435\u0442\u0435 \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u044f\u0442\u0430." + }, + "create_entry": { + "default": "\u0423\u0441\u043f\u0435\u0448\u043d\u043e \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u043a\u0438\u0440\u0430\u043d\u0435 \u0441 Withings \u0437\u0430 \u0438\u0437\u0431\u0440\u0430\u043d\u0438\u044f \u043f\u0440\u043e\u0444\u0438\u043b." + }, + "step": { + "user": { + "data": { + "profile": "\u041f\u0440\u043e\u0444\u0438\u043b" + }, + "description": "\u0418\u0437\u0431\u0435\u0440\u0435\u0442\u0435 \u043f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u0438 \u043f\u0440\u043e\u0444\u0438\u043b, \u043a\u044a\u043c \u043a\u043e\u0439\u0442\u043e \u0438\u0441\u043a\u0430\u0442\u0435 \u0434\u0430 \u0441\u0432\u044a\u0440\u0436\u0435\u0442\u0435 Home Assistant \u0441 Withings. \u041d\u0430 \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0430\u0442\u0430 \u043d\u0430 Withings \u043d\u0435 \u0437\u0430\u0431\u0440\u0430\u0432\u044f\u0439\u0442\u0435 \u0434\u0430 \u0438\u0437\u0431\u0435\u0440\u0435\u0442\u0435 \u0435\u0434\u0438\u043d \u0438 \u0441\u044a\u0449 \u043f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b \u0438\u043b\u0438 \u0434\u0430\u043d\u043d\u0438\u0442\u0435 \u043d\u044f\u043c\u0430 \u0434\u0430 \u0431\u044a\u0434\u0430\u0442 \u0441\u0432\u044a\u0440\u0437\u0430\u043d\u0438 \u043f\u0440\u0430\u0432\u0438\u043b\u043d\u043e.", + "title": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u0438 \u043f\u0440\u043e\u0444\u0438\u043b." + } + }, + "title": "Withings" + } +} \ No newline at end of file diff --git a/homeassistant/components/withings/.translations/es-419.json b/homeassistant/components/withings/.translations/es-419.json new file mode 100644 index 00000000000..485150d2928 --- /dev/null +++ b/homeassistant/components/withings/.translations/es-419.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "no_flows": "Debe configurar Withings antes de poder autenticarse con \u00e9l. Por favor lea la documentaci\u00f3n." + }, + "create_entry": { + "default": "Autenticado correctamente con Withings para el perfil seleccionado." + }, + "step": { + "user": { + "data": { + "profile": "Perfil" + }, + "title": "Perfil del usuario." + } + }, + "title": "Withings" + } +} \ No newline at end of file diff --git a/homeassistant/components/withings/.translations/es.json b/homeassistant/components/withings/.translations/es.json index fac325a7097..ee0cf523588 100644 --- a/homeassistant/components/withings/.translations/es.json +++ b/homeassistant/components/withings/.translations/es.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "no_flows": "Debe configurar Withings antes de poder autenticarse con \u00e9l. Por favor, lea la documentaci\u00f3n." + }, "create_entry": { "default": "Autenticado correctamente con Withings para el perfil seleccionado." }, diff --git a/homeassistant/components/withings/.translations/fr.json b/homeassistant/components/withings/.translations/fr.json index b66786cc9e0..ad715d54eb1 100644 --- a/homeassistant/components/withings/.translations/fr.json +++ b/homeassistant/components/withings/.translations/fr.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "no_flows": "Vous devez configurer Withings avant de pouvoir vous authentifier avec celui-ci. Veuillez lire la documentation." + }, "create_entry": { "default": "Authentifi\u00e9 avec succ\u00e8s \u00e0 Withings pour le profil s\u00e9lectionn\u00e9." }, diff --git a/homeassistant/components/withings/.translations/it.json b/homeassistant/components/withings/.translations/it.json index 5bf342836ce..51276869ec6 100644 --- a/homeassistant/components/withings/.translations/it.json +++ b/homeassistant/components/withings/.translations/it.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "no_flows": "\u00c8 necessario configurare Withings prima di potersi autenticare con esso. Si prega di leggere la documentazione." + }, "create_entry": { "default": "Autenticazione completata con Withings per il profilo selezionato." }, diff --git a/homeassistant/components/withings/.translations/lb.json b/homeassistant/components/withings/.translations/lb.json index 9015f490830..5ca969f0391 100644 --- a/homeassistant/components/withings/.translations/lb.json +++ b/homeassistant/components/withings/.translations/lb.json @@ -1,10 +1,17 @@ { "config": { + "abort": { + "no_flows": "Dir musst Withingss konfigur\u00e9ieren, ier Dir d\u00ebs Authentifiz\u00e9ierung k\u00ebnnt benotzen. Liest w.e.g. d'Instruktioune." + }, + "create_entry": { + "default": "Erfollegr\u00e4ich mam ausgewielte Profile mat Withings authentifiz\u00e9iert." + }, "step": { "user": { "data": { "profile": "Profil" }, + "description": "Wielt ee Benotzer Profile aus dee mam Withings Profile soll verbonne ginn. Stellt s\u00e9cher dass dir op der Withings S\u00e4it deeselwechte Benotzer auswielt, soss ginn d'Donn\u00e9e net richteg ugewisen.", "title": "Benotzer Profil." } }, diff --git a/homeassistant/components/withings/.translations/nn.json b/homeassistant/components/withings/.translations/nn.json new file mode 100644 index 00000000000..7d8b268367c --- /dev/null +++ b/homeassistant/components/withings/.translations/nn.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Withings" + } +} \ No newline at end of file diff --git a/homeassistant/components/withings/.translations/no.json b/homeassistant/components/withings/.translations/no.json index 22d8884d66a..d32c9640fd7 100644 --- a/homeassistant/components/withings/.translations/no.json +++ b/homeassistant/components/withings/.translations/no.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "no_flows": "Du m\u00e5 konfigurere Withings f\u00f8r du kan godkjenne med den. Vennligst les dokumentasjonen." + }, "create_entry": { "default": "Vellykket autentisering for Withings og den valgte profilen." }, diff --git a/homeassistant/components/withings/.translations/pl.json b/homeassistant/components/withings/.translations/pl.json index 1643ecb1480..3c345a1a788 100644 --- a/homeassistant/components/withings/.translations/pl.json +++ b/homeassistant/components/withings/.translations/pl.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "no_flows": "Musisz skonfigurowa\u0107 Withings, aby m\u00f3c si\u0119 z nim uwierzytelni\u0107. Przeczytaj prosz\u0119 dokumentacj\u0119." + }, "create_entry": { "default": "Pomy\u015blnie uwierzytelniono z Withings dla wybranego profilu" }, diff --git a/homeassistant/components/withings/.translations/sl.json b/homeassistant/components/withings/.translations/sl.json index d0fcb6a5276..7f32ade8a08 100644 --- a/homeassistant/components/withings/.translations/sl.json +++ b/homeassistant/components/withings/.translations/sl.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "no_flows": "Withings morate prvo konfigurirati, preden ga boste lahko uporabili za overitev. Prosimo, preberite dokumentacijo." + }, "create_entry": { "default": "Uspe\u0161no overjen z Withings za izbrani profil." }, diff --git a/homeassistant/components/withings/.translations/zh-Hans.json b/homeassistant/components/withings/.translations/zh-Hans.json new file mode 100644 index 00000000000..c7485b09248 --- /dev/null +++ b/homeassistant/components/withings/.translations/zh-Hans.json @@ -0,0 +1,10 @@ +{ + "config": { + "step": { + "user": { + "description": "\u8bf7\u9009\u62e9\u4f60\u60f3\u8981Home Assistant\u548cWithings\u5bf9\u5e94\u7684\u7528\u6237\u914d\u7f6e\u6587\u4ef6\u3002\u5728Withings\u9875\u9762\u4e0a\uff0c\u8bf7\u52a1\u5fc5\u9009\u62e9\u76f8\u540c\u7684\u7528\u6237\uff0c\u5426\u5219\u6570\u636e\u5c06\u65e0\u6cd5\u6b63\u786e\u6807\u8bb0\u3002", + "title": "\u7528\u6237\u8d44\u6599" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/withings/.translations/zh-Hant.json b/homeassistant/components/withings/.translations/zh-Hant.json index 30a77102d04..f01109b2d62 100644 --- a/homeassistant/components/withings/.translations/zh-Hant.json +++ b/homeassistant/components/withings/.translations/zh-Hant.json @@ -1,7 +1,10 @@ { "config": { + "abort": { + "no_flows": "\u5fc5\u9808\u5148\u8a2d\u5b9a Withings \u65b9\u80fd\u9032\u884c\u8a8d\u8b49\u3002\u8acb\u53c3\u95b1\u6587\u4ef6\u3002" + }, "create_entry": { - "default": "\u5df2\u6210\u529f\u4f7f\u7528\u6240\u9078\u8a2d\u5b9a\u8a8d\u8b49 Withings \u88dd\u7f6e\u3002" + "default": "\u5df2\u6210\u529f\u4f7f\u7528\u6240\u9078\u8a2d\u5b9a\u8a8d\u8b49 Withings \u8a2d\u5099\u3002" }, "step": { "user": { diff --git a/homeassistant/components/withings/common.py b/homeassistant/components/withings/common.py index f2be849cbc7..9acca6f0cd6 100644 --- a/homeassistant/components/withings/common.py +++ b/homeassistant/components/withings/common.py @@ -4,7 +4,7 @@ import logging import re import time -import nokia +import withings_api as withings from oauthlib.oauth2.rfc6749.errors import MissingTokenError from requests_oauthlib import TokenUpdated @@ -68,7 +68,9 @@ class WithingsDataManager: service_available = None - def __init__(self, hass: HomeAssistantType, profile: str, api: nokia.NokiaApi): + def __init__( + self, hass: HomeAssistantType, profile: str, api: withings.WithingsApi + ): """Constructor.""" self._hass = hass self._api = api @@ -253,7 +255,7 @@ def create_withings_data_manager( """Set up the sensor config entry.""" entry_creds = entry.data.get(const.CREDENTIALS) or {} profile = entry.data[const.PROFILE] - credentials = nokia.NokiaCredentials( + credentials = withings.WithingsCredentials( entry_creds.get("access_token"), entry_creds.get("token_expiry"), entry_creds.get("token_type"), @@ -266,7 +268,7 @@ def create_withings_data_manager( def credentials_saver(credentials_param): _LOGGER.debug("Saving updated credentials of type %s", type(credentials_param)) - # Sanitizing the data as sometimes a NokiaCredentials object + # Sanitizing the data as sometimes a WithingsCredentials object # is passed through from the API. cred_data = credentials_param if not isinstance(credentials_param, dict): @@ -275,8 +277,8 @@ def create_withings_data_manager( entry.data[const.CREDENTIALS] = cred_data hass.config_entries.async_update_entry(entry, data={**entry.data}) - _LOGGER.debug("Creating nokia api instance") - api = nokia.NokiaApi( + _LOGGER.debug("Creating withings api instance") + api = withings.WithingsApi( credentials, refresh_cb=(lambda token: credentials_saver(api.credentials)) ) diff --git a/homeassistant/components/withings/config_flow.py b/homeassistant/components/withings/config_flow.py index f28a4f59d80..c781e785f5e 100644 --- a/homeassistant/components/withings/config_flow.py +++ b/homeassistant/components/withings/config_flow.py @@ -4,7 +4,7 @@ import logging from typing import Optional import aiohttp -import nokia +import withings_api as withings import voluptuous as vol from homeassistant import config_entries, data_entry_flow @@ -75,7 +75,7 @@ class WithingsFlowHandler(config_entries.ConfigFlow): profile, ) - return nokia.NokiaAuth( + return withings.WithingsAuth( client_id, client_secret, callback_uri, diff --git a/homeassistant/components/withings/manifest.json b/homeassistant/components/withings/manifest.json index 726d9f13eda..ae5cd4bcdd9 100644 --- a/homeassistant/components/withings/manifest.json +++ b/homeassistant/components/withings/manifest.json @@ -2,9 +2,9 @@ "domain": "withings", "name": "Withings", "config_flow": true, - "documentation": "https://www.home-assistant.io/components/withings", + "documentation": "https://www.home-assistant.io/integrations/withings", "requirements": [ - "nokia==1.2.0" + "withings-api==2.0.0b7" ], "dependencies": [ "api", diff --git a/homeassistant/components/workday/manifest.json b/homeassistant/components/workday/manifest.json index ba8712f0575..4b407e95235 100644 --- a/homeassistant/components/workday/manifest.json +++ b/homeassistant/components/workday/manifest.json @@ -1,7 +1,7 @@ { "domain": "workday", "name": "Workday", - "documentation": "https://www.home-assistant.io/components/workday", + "documentation": "https://www.home-assistant.io/integrations/workday", "requirements": [ "holidays==0.9.11" ], diff --git a/homeassistant/components/worldclock/manifest.json b/homeassistant/components/worldclock/manifest.json index 2da33f942b8..8f7b72491b8 100644 --- a/homeassistant/components/worldclock/manifest.json +++ b/homeassistant/components/worldclock/manifest.json @@ -1,7 +1,7 @@ { "domain": "worldclock", "name": "Worldclock", - "documentation": "https://www.home-assistant.io/components/worldclock", + "documentation": "https://www.home-assistant.io/integrations/worldclock", "requirements": [], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/worldtidesinfo/manifest.json b/homeassistant/components/worldtidesinfo/manifest.json index dfc116c97db..467a98a6660 100644 --- a/homeassistant/components/worldtidesinfo/manifest.json +++ b/homeassistant/components/worldtidesinfo/manifest.json @@ -1,7 +1,7 @@ { "domain": "worldtidesinfo", "name": "Worldtidesinfo", - "documentation": "https://www.home-assistant.io/components/worldtidesinfo", + "documentation": "https://www.home-assistant.io/integrations/worldtidesinfo", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/worxlandroid/manifest.json b/homeassistant/components/worxlandroid/manifest.json index 3e7c626ddd0..b112bb7771c 100644 --- a/homeassistant/components/worxlandroid/manifest.json +++ b/homeassistant/components/worxlandroid/manifest.json @@ -1,7 +1,7 @@ { "domain": "worxlandroid", "name": "Worxlandroid", - "documentation": "https://www.home-assistant.io/components/worxlandroid", + "documentation": "https://www.home-assistant.io/integrations/worxlandroid", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/wsdot/manifest.json b/homeassistant/components/wsdot/manifest.json index c778ed1049f..1c20b884223 100644 --- a/homeassistant/components/wsdot/manifest.json +++ b/homeassistant/components/wsdot/manifest.json @@ -1,7 +1,7 @@ { "domain": "wsdot", "name": "Wsdot", - "documentation": "https://www.home-assistant.io/components/wsdot", + "documentation": "https://www.home-assistant.io/integrations/wsdot", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/wunderground/manifest.json b/homeassistant/components/wunderground/manifest.json index d14c9db419a..8309f03bca4 100644 --- a/homeassistant/components/wunderground/manifest.json +++ b/homeassistant/components/wunderground/manifest.json @@ -1,7 +1,7 @@ { "domain": "wunderground", "name": "Wunderground", - "documentation": "https://www.home-assistant.io/components/wunderground", + "documentation": "https://www.home-assistant.io/integrations/wunderground", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/wunderlist/manifest.json b/homeassistant/components/wunderlist/manifest.json index 505447f454c..90a55ad48e8 100644 --- a/homeassistant/components/wunderlist/manifest.json +++ b/homeassistant/components/wunderlist/manifest.json @@ -1,7 +1,7 @@ { "domain": "wunderlist", "name": "Wunderlist", - "documentation": "https://www.home-assistant.io/components/wunderlist", + "documentation": "https://www.home-assistant.io/integrations/wunderlist", "requirements": [ "wunderpy2==0.1.6" ], diff --git a/homeassistant/components/wwlln/.translations/ru.json b/homeassistant/components/wwlln/.translations/ru.json index ad553def6c3..3bdaf85498b 100644 --- a/homeassistant/components/wwlln/.translations/ru.json +++ b/homeassistant/components/wwlln/.translations/ru.json @@ -1,7 +1,7 @@ { "config": { "error": { - "identifier_exists": "\u041c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435 \u0443\u0436\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u043e" + "identifier_exists": "\u041c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435 \u0443\u0436\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u043e." }, "step": { "user": { diff --git a/homeassistant/components/wwlln/manifest.json b/homeassistant/components/wwlln/manifest.json index 6d13f7adbfd..a7bf14454da 100644 --- a/homeassistant/components/wwlln/manifest.json +++ b/homeassistant/components/wwlln/manifest.json @@ -2,9 +2,9 @@ "domain": "wwlln", "name": "World Wide Lightning Location Network", "config_flow": true, - "documentation": "https://www.home-assistant.io/components/wwlln", + "documentation": "https://www.home-assistant.io/integrations/wwlln", "requirements": [ - "aiowwlln==2.0.1" + "aiowwlln==2.0.2" ], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/x10/manifest.json b/homeassistant/components/x10/manifest.json index 2fbe16a6e7a..bae5247ffbc 100644 --- a/homeassistant/components/x10/manifest.json +++ b/homeassistant/components/x10/manifest.json @@ -1,7 +1,7 @@ { "domain": "x10", "name": "X10", - "documentation": "https://www.home-assistant.io/components/x10", + "documentation": "https://www.home-assistant.io/integrations/x10", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/xbox_live/manifest.json b/homeassistant/components/xbox_live/manifest.json index 0d80ce770ce..79f4ce6c87f 100644 --- a/homeassistant/components/xbox_live/manifest.json +++ b/homeassistant/components/xbox_live/manifest.json @@ -1,10 +1,12 @@ { "domain": "xbox_live", "name": "Xbox live", - "documentation": "https://www.home-assistant.io/components/xbox_live", + "documentation": "https://www.home-assistant.io/integrations/xbox_live", "requirements": [ "xboxapi==0.1.1" ], "dependencies": [], - "codeowners": [] + "codeowners": [ + "@MartinHjelmare" + ] } diff --git a/homeassistant/components/xbox_live/sensor.py b/homeassistant/components/xbox_live/sensor.py index d7ca22b2be3..e837cc4bbbc 100644 --- a/homeassistant/components/xbox_live/sensor.py +++ b/homeassistant/components/xbox_live/sensor.py @@ -1,12 +1,16 @@ """Sensor for Xbox Live account status.""" import logging +from datetime import timedelta import voluptuous as vol +from xboxapi import xbox_api -import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import CONF_API_KEY, STATE_UNKNOWN +from homeassistant.const import CONF_API_KEY, CONF_SCAN_INTERVAL +from homeassistant.core import callback +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity +from homeassistant.helpers.event import async_track_time_interval _LOGGER = logging.getLogger(__name__) @@ -24,65 +28,76 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Xbox platform.""" - from xboxapi import xbox_api - - api = xbox_api.XboxApi(config.get(CONF_API_KEY)) - devices = [] + api = xbox_api.XboxApi(config[CONF_API_KEY]) + entities = [] # request personal profile to check api connection profile = api.get_profile() if profile.get("error_code") is not None: _LOGGER.error( "Can't setup XboxAPI connection. Check your account or " - " api key on xboxapi.com. Code: %s Description: %s ", - profile.get("error_code", STATE_UNKNOWN), - profile.get("error_message", STATE_UNKNOWN), + "api key on xboxapi.com. Code: %s Description: %s ", + profile.get("error_code", "unknown"), + profile.get("error_message", "unknown"), ) return - for xuid in config.get(CONF_XUID): - new_device = XboxSensor(hass, api, xuid) - if new_device.success_init: - devices.append(new_device) + users = config[CONF_XUID] - if devices: - add_entities(devices, True) + interval = timedelta(minutes=1 * len(users)) + interval = config.get(CONF_SCAN_INTERVAL, interval) + + for xuid in users: + gamercard = get_user_gamercard(api, xuid) + if gamercard is None: + continue + entities.append(XboxSensor(api, xuid, gamercard, interval)) + + if entities: + add_entities(entities, True) + + +def get_user_gamercard(api, xuid): + """Get profile info.""" + gamercard = api.get_user_gamercard(xuid) + _LOGGER.debug("User gamercard: %s", gamercard) + + if gamercard.get("success", True) and gamercard.get("code") is None: + return gamercard + _LOGGER.error( + "Can't get user profile %s. Error Code: %s Description: %s", + xuid, + gamercard.get("code", "unknown"), + gamercard.get("description", "unknown"), + ) + return None class XboxSensor(Entity): """A class for the Xbox account.""" - def __init__(self, hass, api, xuid): + def __init__(self, api, xuid, gamercard, interval): """Initialize the sensor.""" - self._hass = hass self._state = None - self._presence = {} + self._presence = [] self._xuid = xuid self._api = api - - # get profile info - profile = self._api.get_user_gamercard(self._xuid) - - if profile.get("success", True) and profile.get("code") is None: - self.success_init = True - self._gamertag = profile.get("gamertag") - self._gamerscore = profile.get("gamerscore") - self._picture = profile.get("gamerpicSmallSslImagePath") - self._tier = profile.get("tier") - else: - _LOGGER.error( - "Can't get user profile %s. " "Error Code: %s Description: %s", - self._xuid, - profile.get("code", STATE_UNKNOWN), - profile.get("description", STATE_UNKNOWN), - ) - self.success_init = False + self._gamertag = gamercard.get("gamertag") + self._gamerscore = gamercard.get("gamerscore") + self._interval = interval + self._picture = gamercard.get("gamerpicSmallSslImagePath") + self._tier = gamercard.get("tier") @property def name(self): """Return the name of the sensor.""" return self._gamertag + @property + def should_poll(self): + """Return False as this entity has custom polling.""" + return False + @property def state(self): """Return the state of the sensor.""" @@ -98,7 +113,7 @@ class XboxSensor(Entity): for device in self._presence: for title in device.get("titles"): attributes[ - "{} {}".format(device.get("type"), title.get("placement")) + f'{device.get("type")} {title.get("placement")}' ] = title.get("name") return attributes @@ -113,8 +128,19 @@ class XboxSensor(Entity): """Return the icon to use in the frontend.""" return ICON + async def async_added_to_hass(self): + """Start custom polling.""" + + @callback + def async_update(event_time=None): + """Update the entity.""" + self.async_schedule_update_ha_state(True) + + async_track_time_interval(self.hass, async_update, self._interval) + def update(self): """Update state data from Xbox API.""" presence = self._api.get_user_presence(self._xuid) + _LOGGER.debug("User presence: %s", presence) self._state = presence.get("state") - self._presence = presence.get("devices", {}) + self._presence = presence.get("devices", []) diff --git a/homeassistant/components/xeoma/manifest.json b/homeassistant/components/xeoma/manifest.json index ee8ed2f6de3..7cf061018b9 100644 --- a/homeassistant/components/xeoma/manifest.json +++ b/homeassistant/components/xeoma/manifest.json @@ -1,7 +1,7 @@ { "domain": "xeoma", "name": "Xeoma", - "documentation": "https://www.home-assistant.io/components/xeoma", + "documentation": "https://www.home-assistant.io/integrations/xeoma", "requirements": [ "pyxeoma==1.4.1" ], diff --git a/homeassistant/components/xfinity/manifest.json b/homeassistant/components/xfinity/manifest.json index 71750ccf088..9e800dc2e4a 100644 --- a/homeassistant/components/xfinity/manifest.json +++ b/homeassistant/components/xfinity/manifest.json @@ -1,7 +1,7 @@ { "domain": "xfinity", "name": "Xfinity", - "documentation": "https://www.home-assistant.io/components/xfinity", + "documentation": "https://www.home-assistant.io/integrations/xfinity", "requirements": [ "xfinity-gateway==0.0.4" ], diff --git a/homeassistant/components/xiaomi/manifest.json b/homeassistant/components/xiaomi/manifest.json index d3587100501..a607cda511e 100644 --- a/homeassistant/components/xiaomi/manifest.json +++ b/homeassistant/components/xiaomi/manifest.json @@ -1,7 +1,7 @@ { "domain": "xiaomi", "name": "Xiaomi", - "documentation": "https://www.home-assistant.io/components/xiaomi", + "documentation": "https://www.home-assistant.io/integrations/xiaomi", "requirements": [], "dependencies": [ "ffmpeg" diff --git a/homeassistant/components/xiaomi_aqara/__init__.py b/homeassistant/components/xiaomi_aqara/__init__.py index 6e2298e05b9..7a337dcc497 100644 --- a/homeassistant/components/xiaomi_aqara/__init__.py +++ b/homeassistant/components/xiaomi_aqara/__init__.py @@ -8,6 +8,7 @@ import voluptuous as vol from homeassistant.components.discovery import SERVICE_XIAOMI_GW from homeassistant.const import ( ATTR_BATTERY_LEVEL, + ATTR_VOLTAGE, CONF_HOST, CONF_MAC, CONF_PORT, @@ -323,6 +324,7 @@ class XiaomiDevice(Entity): max_volt = 3300 min_volt = 2800 voltage = data[voltage_key] + self._device_state_attributes[ATTR_VOLTAGE] = round(voltage / 1000.0, 2) voltage = min(voltage, max_volt) voltage = max(voltage, min_volt) percent = ((voltage - min_volt) / (max_volt - min_volt)) * 100 diff --git a/homeassistant/components/xiaomi_aqara/manifest.json b/homeassistant/components/xiaomi_aqara/manifest.json index 36da259f82e..9eeddd357f6 100644 --- a/homeassistant/components/xiaomi_aqara/manifest.json +++ b/homeassistant/components/xiaomi_aqara/manifest.json @@ -1,7 +1,7 @@ { "domain": "xiaomi_aqara", "name": "Xiaomi aqara", - "documentation": "https://www.home-assistant.io/components/xiaomi_aqara", + "documentation": "https://www.home-assistant.io/integrations/xiaomi_aqara", "requirements": [ "PyXiaomiGateway==0.12.4" ], diff --git a/homeassistant/components/xiaomi_aqara/sensor.py b/homeassistant/components/xiaomi_aqara/sensor.py index b3f29e93c63..5ad29af0aaf 100644 --- a/homeassistant/components/xiaomi_aqara/sensor.py +++ b/homeassistant/components/xiaomi_aqara/sensor.py @@ -19,6 +19,7 @@ SENSOR_TYPES = { "illumination": ["lm", None, DEVICE_CLASS_ILLUMINANCE], "lux": ["lx", None, DEVICE_CLASS_ILLUMINANCE], "pressure": ["hPa", None, DEVICE_CLASS_PRESSURE], + "bed_activity": ["μm", None, None], } diff --git a/homeassistant/components/xiaomi_miio/fan.py b/homeassistant/components/xiaomi_miio/fan.py index c6ca6db32fb..67dc12565d8 100644 --- a/homeassistant/components/xiaomi_miio/fan.py +++ b/homeassistant/components/xiaomi_miio/fan.py @@ -12,7 +12,13 @@ from homeassistant.components.fan import ( SUPPORT_SET_SPEED, DOMAIN, ) -from homeassistant.const import CONF_NAME, CONF_HOST, CONF_TOKEN, ATTR_ENTITY_ID +from homeassistant.const import ( + ATTR_MODE, + CONF_NAME, + CONF_HOST, + CONF_TOKEN, + ATTR_ENTITY_ID, +) from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv @@ -75,7 +81,6 @@ ATTR_MODEL = "model" ATTR_TEMPERATURE = "temperature" ATTR_HUMIDITY = "humidity" ATTR_AIR_QUALITY_INDEX = "aqi" -ATTR_MODE = "mode" ATTR_FILTER_HOURS_USED = "filter_hours_used" ATTR_FILTER_LIFE = "filter_life_remaining" ATTR_FAVORITE_LEVEL = "favorite_level" diff --git a/homeassistant/components/xiaomi_miio/manifest.json b/homeassistant/components/xiaomi_miio/manifest.json index d7e0d0d732e..4c01cce2d3c 100644 --- a/homeassistant/components/xiaomi_miio/manifest.json +++ b/homeassistant/components/xiaomi_miio/manifest.json @@ -1,7 +1,7 @@ { "domain": "xiaomi_miio", "name": "Xiaomi miio", - "documentation": "https://www.home-assistant.io/components/xiaomi_miio", + "documentation": "https://www.home-assistant.io/integrations/xiaomi_miio", "requirements": [ "construct==2.9.45", "python-miio==0.4.5" diff --git a/homeassistant/components/xiaomi_miio/switch.py b/homeassistant/components/xiaomi_miio/switch.py index 5f79652621b..7fa1638253c 100644 --- a/homeassistant/components/xiaomi_miio/switch.py +++ b/homeassistant/components/xiaomi_miio/switch.py @@ -6,7 +6,13 @@ import logging import voluptuous as vol from homeassistant.components.switch import DOMAIN, PLATFORM_SCHEMA, SwitchDevice -from homeassistant.const import ATTR_ENTITY_ID, CONF_HOST, CONF_NAME, CONF_TOKEN +from homeassistant.const import ( + ATTR_ENTITY_ID, + ATTR_MODE, + CONF_HOST, + CONF_NAME, + CONF_TOKEN, +) from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv @@ -44,7 +50,6 @@ ATTR_POWER = "power" ATTR_TEMPERATURE = "temperature" ATTR_LOAD_POWER = "load_power" ATTR_MODEL = "model" -ATTR_MODE = "mode" ATTR_POWER_MODE = "power_mode" ATTR_WIFI_LED = "wifi_led" ATTR_POWER_PRICE = "power_price" diff --git a/homeassistant/components/xiaomi_tv/manifest.json b/homeassistant/components/xiaomi_tv/manifest.json index 26940a57c78..740eaf3ea1c 100644 --- a/homeassistant/components/xiaomi_tv/manifest.json +++ b/homeassistant/components/xiaomi_tv/manifest.json @@ -1,7 +1,7 @@ { "domain": "xiaomi_tv", "name": "Xiaomi tv", - "documentation": "https://www.home-assistant.io/components/xiaomi_tv", + "documentation": "https://www.home-assistant.io/integrations/xiaomi_tv", "requirements": [ "pymitv==1.4.3" ], diff --git a/homeassistant/components/xmpp/manifest.json b/homeassistant/components/xmpp/manifest.json index 3d2c3a5e911..21255497503 100644 --- a/homeassistant/components/xmpp/manifest.json +++ b/homeassistant/components/xmpp/manifest.json @@ -1,7 +1,7 @@ { "domain": "xmpp", "name": "Xmpp", - "documentation": "https://www.home-assistant.io/components/xmpp", + "documentation": "https://www.home-assistant.io/integrations/xmpp", "requirements": [ "slixmpp==1.4.2" ], diff --git a/homeassistant/components/xs1/manifest.json b/homeassistant/components/xs1/manifest.json index 4ee13acf647..290c552309b 100644 --- a/homeassistant/components/xs1/manifest.json +++ b/homeassistant/components/xs1/manifest.json @@ -1,7 +1,7 @@ { "domain": "xs1", "name": "Xs1", - "documentation": "https://www.home-assistant.io/components/xs1", + "documentation": "https://www.home-assistant.io/integrations/xs1", "requirements": [ "xs1-api-client==2.3.5" ], diff --git a/homeassistant/components/yale_smart_alarm/manifest.json b/homeassistant/components/yale_smart_alarm/manifest.json index 7b786c7bf7c..05e979ffb0a 100644 --- a/homeassistant/components/yale_smart_alarm/manifest.json +++ b/homeassistant/components/yale_smart_alarm/manifest.json @@ -1,7 +1,7 @@ { "domain": "yale_smart_alarm", "name": "Yale smart alarm", - "documentation": "https://www.home-assistant.io/components/yale_smart_alarm", + "documentation": "https://www.home-assistant.io/integrations/yale_smart_alarm", "requirements": [ "yalesmartalarmclient==0.1.6" ], diff --git a/homeassistant/components/yamaha/manifest.json b/homeassistant/components/yamaha/manifest.json index 5a277fc7ce8..bacb9fc3305 100644 --- a/homeassistant/components/yamaha/manifest.json +++ b/homeassistant/components/yamaha/manifest.json @@ -1,7 +1,7 @@ { "domain": "yamaha", "name": "Yamaha", - "documentation": "https://www.home-assistant.io/components/yamaha", + "documentation": "https://www.home-assistant.io/integrations/yamaha", "requirements": [ "rxv==0.6.0" ], diff --git a/homeassistant/components/yamaha_musiccast/manifest.json b/homeassistant/components/yamaha_musiccast/manifest.json index 7769026e092..ea36c4921c5 100644 --- a/homeassistant/components/yamaha_musiccast/manifest.json +++ b/homeassistant/components/yamaha_musiccast/manifest.json @@ -1,7 +1,7 @@ { "domain": "yamaha_musiccast", "name": "Yamaha musiccast", - "documentation": "https://www.home-assistant.io/components/yamaha_musiccast", + "documentation": "https://www.home-assistant.io/integrations/yamaha_musiccast", "requirements": [ "pymusiccast==0.1.6" ], diff --git a/homeassistant/components/yandex_transport/__init__.py b/homeassistant/components/yandex_transport/__init__.py new file mode 100644 index 00000000000..d007b2d3df8 --- /dev/null +++ b/homeassistant/components/yandex_transport/__init__.py @@ -0,0 +1 @@ +"""Service for obtaining information about closer bus from Transport Yandex Service.""" diff --git a/homeassistant/components/yandex_transport/manifest.json b/homeassistant/components/yandex_transport/manifest.json new file mode 100644 index 00000000000..91267b43480 --- /dev/null +++ b/homeassistant/components/yandex_transport/manifest.json @@ -0,0 +1,12 @@ +{ + "domain": "yandex_transport", + "name": "Yandex Transport", + "documentation": "https://www.home-assistant.io/integrations/yandex_transport", + "requirements": [ + "ya_ma==0.3.7" + ], + "dependencies": [], + "codeowners": [ + "@rishatik92" + ] +} diff --git a/homeassistant/components/yandex_transport/sensor.py b/homeassistant/components/yandex_transport/sensor.py new file mode 100644 index 00000000000..26311a4c72e --- /dev/null +++ b/homeassistant/components/yandex_transport/sensor.py @@ -0,0 +1,127 @@ +"""Service for obtaining information about closer bus from Transport Yandex Service.""" + +import logging +from datetime import timedelta + +import voluptuous as vol +from ya_ma import YandexMapsRequester + +import homeassistant.helpers.config_validation as cv +import homeassistant.util.dt as dt_util +from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import CONF_NAME, ATTR_ATTRIBUTION, DEVICE_CLASS_TIMESTAMP +from homeassistant.helpers.entity import Entity + +_LOGGER = logging.getLogger(__name__) + +STOP_NAME = "stop_name" +USER_AGENT = "Home Assistant" +ATTRIBUTION = "Data provided by maps.yandex.ru" + +CONF_STOP_ID = "stop_id" +CONF_ROUTE = "routes" + +DEFAULT_NAME = "Yandex Transport" +ICON = "mdi:bus" + +SCAN_INTERVAL = timedelta(minutes=1) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_STOP_ID): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_ROUTE, default=[]): vol.All(cv.ensure_list, [cv.string]), + } +) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the Yandex transport sensor.""" + stop_id = config[CONF_STOP_ID] + name = config[CONF_NAME] + routes = config[CONF_ROUTE] + + data = YandexMapsRequester(user_agent=USER_AGENT) + add_entities([DiscoverMoscowYandexTransport(data, stop_id, routes, name)], True) + + +class DiscoverMoscowYandexTransport(Entity): + """Implementation of yandex_transport sensor.""" + + def __init__(self, requester, stop_id, routes, name): + """Initialize sensor.""" + self.requester = requester + self._stop_id = stop_id + self._routes = [] + self._routes = routes + self._state = None + self._name = name + self._attrs = None + + def update(self): + """Get the latest data from maps.yandex.ru and update the states.""" + attrs = {} + closer_time = None + try: + yandex_reply = self.requester.get_stop_info(self._stop_id) + data = yandex_reply["data"] + stop_metadata = data["properties"]["StopMetaData"] + except KeyError as key_error: + _LOGGER.warning( + "Exception KeyError was captured, missing key is %s. Yandex returned: %s", + key_error, + yandex_reply, + ) + self.requester.set_new_session() + data = self.requester.get_stop_info(self._stop_id)["data"] + stop_metadata = data["properties"]["StopMetaData"] + stop_name = data["properties"]["name"] + transport_list = stop_metadata["Transport"] + for transport in transport_list: + route = transport["name"] + if self._routes and route not in self._routes: + # skip unnecessary route info + continue + if "Events" in transport["BriefSchedule"]: + for event in transport["BriefSchedule"]["Events"]: + if "Estimated" in event: + posix_time_next = int(event["Estimated"]["value"]) + if closer_time is None or closer_time > posix_time_next: + closer_time = posix_time_next + if route not in attrs: + attrs[route] = [] + attrs[route].append(event["Estimated"]["text"]) + attrs[STOP_NAME] = stop_name + attrs[ATTR_ATTRIBUTION] = ATTRIBUTION + if closer_time is None: + self._state = None + else: + self._state = dt_util.utc_from_timestamp(closer_time).isoformat( + timespec="seconds" + ) + self._attrs = attrs + + @property + def state(self): + """Return the state of the sensor.""" + return self._state + + @property + def device_class(self): + """Return the device class.""" + return DEVICE_CLASS_TIMESTAMP + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @property + def device_state_attributes(self): + """Return the state attributes.""" + return self._attrs + + @property + def icon(self): + """Icon to use in the frontend, if any.""" + return ICON diff --git a/homeassistant/components/yandextts/manifest.json b/homeassistant/components/yandextts/manifest.json index 7f622a1e25f..66a546abdb4 100644 --- a/homeassistant/components/yandextts/manifest.json +++ b/homeassistant/components/yandextts/manifest.json @@ -1,7 +1,7 @@ { "domain": "yandextts", "name": "Yandextts", - "documentation": "https://www.home-assistant.io/components/yandextts", + "documentation": "https://www.home-assistant.io/integrations/yandextts", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/yeelight/light.py b/homeassistant/components/yeelight/light.py index b47cdb98161..ab63e6fb319 100644 --- a/homeassistant/components/yeelight/light.py +++ b/homeassistant/components/yeelight/light.py @@ -11,7 +11,7 @@ from homeassistant.util.color import ( color_temperature_mired_to_kelvin as mired_to_kelvin, color_temperature_kelvin_to_mired as kelvin_to_mired, ) -from homeassistant.const import CONF_HOST, ATTR_ENTITY_ID, CONF_NAME +from homeassistant.const import CONF_HOST, ATTR_ENTITY_ID, ATTR_MODE, CONF_NAME from homeassistant.core import callback from homeassistant.components.light import ( ATTR_BRIGHTNESS, @@ -64,7 +64,6 @@ SUPPORT_YEELIGHT_WHITE_TEMP = SUPPORT_YEELIGHT | SUPPORT_COLOR_TEMP SUPPORT_YEELIGHT_RGB = SUPPORT_YEELIGHT_WHITE_TEMP | SUPPORT_COLOR -ATTR_MODE = "mode" ATTR_MINUTES = "minutes" SERVICE_SET_MODE = "set_mode" diff --git a/homeassistant/components/yeelight/manifest.json b/homeassistant/components/yeelight/manifest.json index 061d2b065c4..3d27d5bd393 100644 --- a/homeassistant/components/yeelight/manifest.json +++ b/homeassistant/components/yeelight/manifest.json @@ -1,7 +1,7 @@ { "domain": "yeelight", "name": "Yeelight", - "documentation": "https://www.home-assistant.io/components/yeelight", + "documentation": "https://www.home-assistant.io/integrations/yeelight", "requirements": [ "yeelight==0.5.0" ], diff --git a/homeassistant/components/yeelightsunflower/manifest.json b/homeassistant/components/yeelightsunflower/manifest.json index 1a75472b801..390ff742724 100644 --- a/homeassistant/components/yeelightsunflower/manifest.json +++ b/homeassistant/components/yeelightsunflower/manifest.json @@ -1,7 +1,7 @@ { "domain": "yeelightsunflower", "name": "Yeelightsunflower", - "documentation": "https://www.home-assistant.io/components/yeelightsunflower", + "documentation": "https://www.home-assistant.io/integrations/yeelightsunflower", "requirements": [ "yeelightsunflower==0.0.10" ], diff --git a/homeassistant/components/yessssms/const.py b/homeassistant/components/yessssms/const.py new file mode 100644 index 00000000000..473cdfff1e0 --- /dev/null +++ b/homeassistant/components/yessssms/const.py @@ -0,0 +1,3 @@ +"""Const for YesssSMS.""" + +CONF_PROVIDER = "provider" diff --git a/homeassistant/components/yessssms/manifest.json b/homeassistant/components/yessssms/manifest.json index 103a9fce31e..b68649525c2 100644 --- a/homeassistant/components/yessssms/manifest.json +++ b/homeassistant/components/yessssms/manifest.json @@ -1,9 +1,9 @@ { "domain": "yessssms", "name": "Yessssms", - "documentation": "https://www.home-assistant.io/components/yessssms", + "documentation": "https://www.home-assistant.io/integrations/yessssms", "requirements": [ - "YesssSMS==0.2.3" + "YesssSMS==0.4.1" ], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/yessssms/notify.py b/homeassistant/components/yessssms/notify.py index 28c02080f16..1c1eed0e89d 100644 --- a/homeassistant/components/yessssms/notify.py +++ b/homeassistant/components/yessssms/notify.py @@ -3,11 +3,16 @@ import logging import voluptuous as vol +from YesssSMS import YesssSMS + from homeassistant.const import CONF_PASSWORD, CONF_RECIPIENT, CONF_USERNAME import homeassistant.helpers.config_validation as cv from homeassistant.components.notify import PLATFORM_SCHEMA, BaseNotificationService + +from .const import CONF_PROVIDER + _LOGGER = logging.getLogger(__name__) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( @@ -15,27 +20,52 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_PASSWORD): cv.string, vol.Required(CONF_RECIPIENT): cv.string, + vol.Optional(CONF_PROVIDER, default="YESSS"): cv.string, } ) def get_service(hass, config, discovery_info=None): """Get the YesssSMS notification service.""" - return YesssSMSNotificationService( - config[CONF_USERNAME], config[CONF_PASSWORD], config[CONF_RECIPIENT] + + try: + yesss = YesssSMS( + config[CONF_USERNAME], config[CONF_PASSWORD], provider=config[CONF_PROVIDER] + ) + except YesssSMS.UnsupportedProviderError as ex: + _LOGGER.error("Unknown provider: %s", ex) + return None + try: + if not yesss.login_data_valid(): + _LOGGER.error( + "Login data is not valid! Please double check your login data at %s", + yesss.get_login_url(), + ) + return None + + _LOGGER.debug("Login data for '%s' valid", yesss.get_provider()) + except YesssSMS.ConnectionError: + _LOGGER.warning( + "Connection Error, could not verify login data for '%s'", + yesss.get_provider(), + ) + pass + + _LOGGER.debug( + "initialized; library version: %s, with %s", + yesss.version(), + yesss.get_provider(), ) + return YesssSMSNotificationService(yesss, config[CONF_RECIPIENT]) class YesssSMSNotificationService(BaseNotificationService): """Implement a notification service for the YesssSMS service.""" - def __init__(self, username, password, recipient): + def __init__(self, client, recipient): """Initialize the service.""" - from YesssSMS import YesssSMS - - self.yesss = YesssSMS(username, password) + self.yesss = client self._recipient = recipient - _LOGGER.debug("initialized; library version: %s", self.yesss.version()) def send_message(self, message="", **kwargs): """Send a SMS message via Yesss.at's website.""" @@ -56,10 +86,12 @@ class YesssSMSNotificationService(BaseNotificationService): except self.yesss.EmptyMessageError as ex: _LOGGER.error("Cannot send empty SMS message: %s", ex) except self.yesss.SMSSendingError as ex: - _LOGGER.error(str(ex), exc_info=ex) - except ConnectionError as ex: + _LOGGER.error(ex) + except self.yesss.ConnectionError as ex: _LOGGER.error( - "YesssSMS: unable to connect to yesss.at server.", exc_info=ex + "Unable to connect to server of provider (%s): %s", + self.yesss.get_provider(), + ex, ) except self.yesss.AccountSuspendedError as ex: _LOGGER.error( diff --git a/homeassistant/components/yi/manifest.json b/homeassistant/components/yi/manifest.json index bb7fbf55cbc..461f3e24330 100644 --- a/homeassistant/components/yi/manifest.json +++ b/homeassistant/components/yi/manifest.json @@ -1,7 +1,7 @@ { "domain": "yi", "name": "Yi", - "documentation": "https://www.home-assistant.io/components/yi", + "documentation": "https://www.home-assistant.io/integrations/yi", "requirements": [ "aioftp==0.12.0" ], diff --git a/homeassistant/components/yr/manifest.json b/homeassistant/components/yr/manifest.json index 7f06ddddcb5..d49004cc0e3 100644 --- a/homeassistant/components/yr/manifest.json +++ b/homeassistant/components/yr/manifest.json @@ -1,7 +1,7 @@ { "domain": "yr", "name": "Yr", - "documentation": "https://www.home-assistant.io/components/yr", + "documentation": "https://www.home-assistant.io/integrations/yr", "requirements": [ "xmltodict==0.12.0" ], diff --git a/homeassistant/components/yweather/manifest.json b/homeassistant/components/yweather/manifest.json index c3048601595..482951e156f 100644 --- a/homeassistant/components/yweather/manifest.json +++ b/homeassistant/components/yweather/manifest.json @@ -1,7 +1,7 @@ { "domain": "yweather", "name": "Yweather", - "documentation": "https://www.home-assistant.io/components/yweather", + "documentation": "https://www.home-assistant.io/integrations/yweather", "requirements": [ "yahooweather==0.10" ], diff --git a/homeassistant/components/zabbix/manifest.json b/homeassistant/components/zabbix/manifest.json index c0f100fa62f..5959fba5fa2 100644 --- a/homeassistant/components/zabbix/manifest.json +++ b/homeassistant/components/zabbix/manifest.json @@ -1,7 +1,7 @@ { "domain": "zabbix", "name": "Zabbix", - "documentation": "https://www.home-assistant.io/components/zabbix", + "documentation": "https://www.home-assistant.io/integrations/zabbix", "requirements": [ "pyzabbix==0.7.4" ], diff --git a/homeassistant/components/zamg/manifest.json b/homeassistant/components/zamg/manifest.json index ce16e1b523c..2b95a9ec0c7 100644 --- a/homeassistant/components/zamg/manifest.json +++ b/homeassistant/components/zamg/manifest.json @@ -1,7 +1,7 @@ { "domain": "zamg", "name": "Zamg", - "documentation": "https://www.home-assistant.io/components/zamg", + "documentation": "https://www.home-assistant.io/integrations/zamg", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/zengge/manifest.json b/homeassistant/components/zengge/manifest.json index b846c95f5fa..e24e4537837 100644 --- a/homeassistant/components/zengge/manifest.json +++ b/homeassistant/components/zengge/manifest.json @@ -1,7 +1,7 @@ { "domain": "zengge", "name": "Zengge", - "documentation": "https://www.home-assistant.io/components/zengge", + "documentation": "https://www.home-assistant.io/integrations/zengge", "requirements": [ "zengge==0.2" ], diff --git a/homeassistant/components/zeroconf/manifest.json b/homeassistant/components/zeroconf/manifest.json index 1461a54d147..39f016e9d0e 100644 --- a/homeassistant/components/zeroconf/manifest.json +++ b/homeassistant/components/zeroconf/manifest.json @@ -1,7 +1,7 @@ { "domain": "zeroconf", "name": "Zeroconf", - "documentation": "https://www.home-assistant.io/components/zeroconf", + "documentation": "https://www.home-assistant.io/integrations/zeroconf", "requirements": [ "zeroconf==0.23.0" ], diff --git a/homeassistant/components/zestimate/manifest.json b/homeassistant/components/zestimate/manifest.json index 4d1a55eaa09..79c89406fac 100644 --- a/homeassistant/components/zestimate/manifest.json +++ b/homeassistant/components/zestimate/manifest.json @@ -1,7 +1,7 @@ { "domain": "zestimate", "name": "Zestimate", - "documentation": "https://www.home-assistant.io/components/zestimate", + "documentation": "https://www.home-assistant.io/integrations/zestimate", "requirements": [ "xmltodict==0.12.0" ], diff --git a/homeassistant/components/zha/.translations/bg.json b/homeassistant/components/zha/.translations/bg.json index 642a2c0af13..2715ef46dc8 100644 --- a/homeassistant/components/zha/.translations/bg.json +++ b/homeassistant/components/zha/.translations/bg.json @@ -16,5 +16,48 @@ } }, "title": "ZHA" + }, + "device_automation": { + "trigger_subtype": { + "both_buttons": "\u0418 \u0434\u0432\u0430\u0442\u0430 \u0431\u0443\u0442\u043e\u043d\u0430", + "button_1": "\u041f\u044a\u0440\u0432\u0438 \u0431\u0443\u0442\u043e\u043d", + "button_2": "\u0412\u0442\u043e\u0440\u0438 \u0431\u0443\u0442\u043e\u043d", + "button_3": "\u0422\u0440\u0435\u0442\u0438 \u0431\u0443\u0442\u043e\u043d", + "button_4": "\u0427\u0435\u0442\u0432\u044a\u0440\u0442\u0438 \u0431\u0443\u0442\u043e\u043d", + "button_5": "\u041f\u0435\u0442\u0438 \u0431\u0443\u0442\u043e\u043d", + "button_6": "\u0428\u0435\u0441\u0442\u0438 \u0431\u0443\u0442\u043e\u043d", + "close": "\u0417\u0430\u0442\u0432\u043e\u0440\u0438", + "dim_down": "\u0417\u0430\u0442\u044a\u043c\u043d\u044f\u0432\u0430\u043d\u0435", + "dim_up": "\u041e\u0441\u0432\u0435\u0442\u044f\u0432\u0430\u043d\u0435", + "face_1": "\u0441 \u043b\u0438\u0446\u0435 1 \u0435 \u0430\u043a\u0442\u0438\u0432\u0438\u0440\u0430\u043d\u043e", + "face_2": "\u0441 \u043b\u0438\u0446\u0435 2 \u0435 \u0430\u043a\u0442\u0438\u0432\u0438\u0440\u0430\u043d\u043e", + "face_3": "\u0441 \u043b\u0438\u0446\u0435 3 \u0435 \u0430\u043a\u0442\u0438\u0432\u0438\u0440\u0430\u043d\u043e", + "face_4": "\u0441 \u043b\u0438\u0446\u0435 4 \u0435 \u0430\u043a\u0442\u0438\u0432\u0438\u0440\u0430\u043d\u043e", + "face_5": "\u0441 \u043b\u0438\u0446\u0435 5 \u0435 \u0430\u043a\u0442\u0438\u0432\u0438\u0440\u0430\u043d\u043e", + "face_6": "\u0441 \u043b\u0438\u0446\u0435 6 \u0435 \u0430\u043a\u0442\u0438\u0432\u0438\u0440\u0430\u043d\u043e", + "face_any": "\u0421 \u043d\u044f\u043a\u043e\u0438/\u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0435\u043d\u0438 \u043b\u0438\u0446\u0435(\u0430) \u0430\u043a\u0442\u0438\u0432\u0438\u0440\u0430\u043d\u0438", + "left": "\u041b\u044f\u0432\u043e", + "open": "\u041e\u0442\u0432\u043e\u0440\u0435\u043d", + "right": "\u0414\u044f\u0441\u043d\u043e", + "turn_off": "\u0418\u0437\u043a\u043b\u044e\u0447\u0438", + "turn_on": "\u0412\u043a\u043b\u044e\u0447\u0438" + }, + "trigger_type": { + "device_dropped": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0435 \u0438\u0437\u0442\u044a\u0440\u0432\u0430\u043d\u043e", + "device_flipped": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0435 \u043e\u0431\u044a\u0440\u043d\u0430\u0442\u043e \"{subtype}\"", + "device_knocked": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0435 \u043f\u043e\u0447\u0443\u043a\u0430\u043d\u043e \"{subtype}\"", + "device_rotated": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0435 \u0437\u0430\u0432\u044a\u0440\u0442\u044f\u043d\u043e \"{subtype}\"", + "device_shaken": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0435 \u0440\u0430\u0437\u043a\u043b\u0430\u0442\u0435\u043d\u043e", + "device_slid": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0433\u043e \u0435 \u043f\u043b\u044a\u0437\u043d\u0430\u0442\u043e \"{subtype}\"", + "device_tilted": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0435 \u043d\u0430\u043a\u043b\u043e\u043d\u0435\u043d\u043e", + "remote_button_double_press": "\"{subtype}\" \u0431\u0443\u0442\u043e\u043d\u044a\u0442 \u0431\u0435\u0448\u0435 \u043d\u0430\u0442\u0438\u0441\u043d\u0430\u0442 \u0434\u0432\u0443\u043a\u0440\u0430\u0442\u043d\u043e", + "remote_button_long_press": "\"{subtype}\" \u0431\u0443\u0442\u043e\u043d\u044a\u0442 \u0431\u0435\u0448\u0435 \u043d\u0430\u0442\u0438\u0441\u043d\u0430\u0442 \u043f\u0440\u043e\u0434\u044a\u043b\u0436\u0438\u0442\u0435\u043b\u043d\u043e", + "remote_button_long_release": "\"{subtype}\" \u0431\u0443\u0442\u043e\u043d\u044a\u0442 \u0431\u0435\u0448\u0435 \u043e\u0442\u043f\u0443\u0441\u043d\u0430\u0442 \u0441\u043b\u0435\u0434 \u043f\u0440\u043e\u0434\u044a\u043b\u0436\u0438\u0442\u0435\u043b\u043d\u043e \u043d\u0430\u0442\u0438\u0441\u043a\u0430\u043d\u0435", + "remote_button_quadruple_press": "\"{subtype}\" \u0431\u0443\u0442\u043e\u043d\u044a\u0442 \u0431\u0435\u0448\u0435 \u043d\u0430\u0442\u0438\u0441\u043d\u0430\u0442 \u0447\u0435\u0442\u0438\u0440\u0438\u043a\u0440\u0430\u0442\u043d\u043e", + "remote_button_quintuple_press": "\"{subtype}\" \u0431\u0443\u0442\u043e\u043d\u044a\u0442 \u0431\u0435\u0448\u0435 \u043d\u0430\u0442\u0438\u0441\u043d\u0430\u0442 \u043f\u0435\u0442\u043a\u0440\u0430\u0442\u043d\u043e", + "remote_button_short_press": "\"{subtype}\" \u0431\u0443\u0442\u043e\u043d\u044a\u0442 \u0431\u0435\u0448\u0435 \u043d\u0430\u0442\u0438\u0441\u043d\u0430\u0442", + "remote_button_short_release": "\"{subtype}\" \u0431\u0443\u0442\u043e\u043d\u044a\u0442 \u0431\u0435\u0448\u0435 \u043e\u0442\u043f\u0443\u0441\u043d\u0430\u0442", + "remote_button_triple_press": "\"{subtype}\" \u0431\u0443\u0442\u043e\u043d\u044a\u0442 \u0431\u0435\u0448\u0435 \u043d\u0430\u0442\u0438\u0441\u043d\u0430\u0442 \u0442\u0440\u0438\u043a\u0440\u0430\u0442\u043d\u043e" + } } } \ No newline at end of file diff --git a/homeassistant/components/zha/.translations/ca.json b/homeassistant/components/zha/.translations/ca.json index 635d0ecbde2..2b8230ad689 100644 --- a/homeassistant/components/zha/.translations/ca.json +++ b/homeassistant/components/zha/.translations/ca.json @@ -16,5 +16,52 @@ } }, "title": "ZHA" + }, + "device_automation": { + "action_type": { + "squawk": "Squawk", + "warn": "Av\u00eds" + }, + "trigger_subtype": { + "both_buttons": "Ambd\u00f3s botons", + "button_1": "Primer bot\u00f3", + "button_2": "Segon bot\u00f3", + "button_3": "Tercer bot\u00f3", + "button_4": "Quart bot\u00f3", + "button_5": "Cinqu\u00e8 bot\u00f3", + "button_6": "Sis\u00e8 bot\u00f3", + "close": "Tanca", + "dim_down": "Atenua la brillantor", + "dim_up": "Augmenta la brillantor", + "face_1": "amb la cara 1 activada", + "face_2": "amb la cara 2 activada", + "face_3": "amb la cara 3 activada", + "face_4": "amb la cara 4 activada", + "face_5": "amb la cara 5 activada", + "face_6": "amb la cara 6 activada", + "face_any": "Amb qualsevol o alguna de les cares especificades activades.", + "left": "Esquerra", + "open": "Obert", + "right": "Dreta", + "turn_off": "Desactiva", + "turn_on": "Activa" + }, + "trigger_type": { + "device_dropped": "Dispositiu caigut", + "device_flipped": "Dispositiu voltejat a \"{subtype}\"", + "device_knocked": "Dispositiu colpejat a \"{subtype}\"", + "device_rotated": "Dispositiu rotat a \"{subtype}\"", + "device_shaken": "Dispositiu sacsejat", + "device_slid": "Dispositiu lliscat a \"{subtype}\"", + "device_tilted": "Dispositiu inclinat", + "remote_button_double_press": "Bot\u00f3 \"{subtype}\" clicat dues vegades consecutives", + "remote_button_long_press": "Bot\u00f3 \"{subtype}\" premut continuament", + "remote_button_long_release": "Bot\u00f3 \"{subtype}\" alliberat despr\u00e9s d'una estona premut", + "remote_button_quadruple_press": "Bot\u00f3 \"{subtype}\" clicat quatre vegades consecutives", + "remote_button_quintuple_press": "Bot\u00f3 \"{subtype}\" clicat cinc vegades consecutives", + "remote_button_short_press": "Bot\u00f3 \"{subtype}\" premut", + "remote_button_short_release": "Bot\u00f3 \"{subtype}\" alliberat", + "remote_button_triple_press": "Bot\u00f3 \"{subtype}\" clicat tres vegades consecutives" + } } } \ No newline at end of file diff --git a/homeassistant/components/zha/.translations/da.json b/homeassistant/components/zha/.translations/da.json index 3140648f57a..39f254ac9af 100644 --- a/homeassistant/components/zha/.translations/da.json +++ b/homeassistant/components/zha/.translations/da.json @@ -16,5 +16,29 @@ } }, "title": "ZHA" + }, + "device_automation": { + "action_type": { + "squawk": "Squawk", + "warn": "Advare" + }, + "trigger_subtype": { + "both_buttons": "Begge knapper", + "button_1": "F\u00f8rste knap", + "button_2": "Anden knap", + "button_3": "Tredje knap", + "button_4": "Fjerde knap", + "button_5": "Femte knap", + "button_6": "Sjette knap", + "close": "Luk", + "dim_down": "D\u00e6mp ned", + "dim_up": "D\u00e6mp op", + "left": "Venstre", + "open": "\u00c5ben", + "right": "H\u00f8jre" + }, + "trigger_type": { + "device_shaken": "Enhed rystet" + } } } \ No newline at end of file diff --git a/homeassistant/components/zha/.translations/de.json b/homeassistant/components/zha/.translations/de.json index 280c941b427..9ffd5211a1f 100644 --- a/homeassistant/components/zha/.translations/de.json +++ b/homeassistant/components/zha/.translations/de.json @@ -16,5 +16,13 @@ } }, "title": "ZHA" + }, + "device_automation": { + "trigger_subtype": { + "close": "Schlie\u00dfen", + "left": "Links", + "open": "Offen", + "right": "Rechts" + } } } \ No newline at end of file diff --git a/homeassistant/components/zha/.translations/en.json b/homeassistant/components/zha/.translations/en.json index f0da251f5eb..d8e8955a935 100644 --- a/homeassistant/components/zha/.translations/en.json +++ b/homeassistant/components/zha/.translations/en.json @@ -16,5 +16,52 @@ } }, "title": "ZHA" + }, + "device_automation": { + "action_type": { + "squawk": "Squawk", + "warn": "Warn" + }, + "trigger_subtype": { + "both_buttons": "Both buttons", + "button_1": "First button", + "button_2": "Second button", + "button_3": "Third button", + "button_4": "Fourth button", + "button_5": "Fifth button", + "button_6": "Sixth button", + "close": "Close", + "dim_down": "Dim down", + "dim_up": "Dim up", + "face_1": "with face 1 activated", + "face_2": "with face 2 activated", + "face_3": "with face 3 activated", + "face_4": "with face 4 activated", + "face_5": "with face 5 activated", + "face_6": "with face 6 activated", + "face_any": "With any/specified face(s) activated", + "left": "Left", + "open": "Open", + "right": "Right", + "turn_off": "Turn off", + "turn_on": "Turn on" + }, + "trigger_type": { + "device_dropped": "Device dropped", + "device_flipped": "Device flipped \"{subtype}\"", + "device_knocked": "Device knocked \"{subtype}\"", + "device_rotated": "Device rotated \"{subtype}\"", + "device_shaken": "Device shaken", + "device_slid": "Device slid \"{subtype}\"", + "device_tilted": "Device tilted", + "remote_button_double_press": "\"{subtype}\" button double clicked", + "remote_button_long_press": "\"{subtype}\" button continuously pressed", + "remote_button_long_release": "\"{subtype}\" button released after long press", + "remote_button_quadruple_press": "\"{subtype}\" button quadruple clicked", + "remote_button_quintuple_press": "\"{subtype}\" button quintuple clicked", + "remote_button_short_press": "\"{subtype}\" button pressed", + "remote_button_short_release": "\"{subtype}\" button released", + "remote_button_triple_press": "\"{subtype}\" button triple clicked" + } } } \ No newline at end of file diff --git a/homeassistant/components/zha/.translations/es-419.json b/homeassistant/components/zha/.translations/es-419.json index 0047c762a9d..edf38b4fd3b 100644 --- a/homeassistant/components/zha/.translations/es-419.json +++ b/homeassistant/components/zha/.translations/es-419.json @@ -16,5 +16,23 @@ } }, "title": "ZHA" + }, + "device_automation": { + "trigger_subtype": { + "left": "Izquierda", + "open": "Abrir", + "right": "Derecha", + "turn_off": "Apagar", + "turn_on": "Encender" + }, + "trigger_type": { + "device_dropped": "Dispositivo ca\u00eddo", + "device_flipped": "Dispositivo volteado \"{subtype}\"", + "device_knocked": "Dispositivo golpeado \"{subtype}\"", + "device_rotated": "Dispositivo girado \"{subtype}\"", + "device_shaken": "Dispositivo agitado", + "device_slid": "Dispositivo deslizado \"{subtype}\"", + "device_tilted": "Dispositivo inclinado" + } } } \ No newline at end of file diff --git a/homeassistant/components/zha/.translations/es.json b/homeassistant/components/zha/.translations/es.json index 0047c762a9d..b8529ce9047 100644 --- a/homeassistant/components/zha/.translations/es.json +++ b/homeassistant/components/zha/.translations/es.json @@ -16,5 +16,52 @@ } }, "title": "ZHA" + }, + "device_automation": { + "action_type": { + "squawk": "Squawk", + "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": "Subir la 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", + "turn_off": "Apagar", + "turn_on": "Encender" + }, + "trigger_type": { + "device_dropped": "Dispositivo ca\u00eddo", + "device_flipped": "Dispositivo volteado \" {subtype} \"", + "device_knocked": "Dispositivo eliminado \" {subtype} \"", + "device_rotated": "Dispositivo girado \" {subtype} \"", + "device_shaken": "Dispositivo agitado", + "device_slid": "Dispositivo deslizado \" {subtype} \"", + "device_tilted": "Dispositivo inclinado", + "remote_button_double_press": "\"{subtipo}\" bot\u00f3n de doble clic", + "remote_button_long_press": "Bot\u00f3n \"{subtipo}\" pulsado continuamente", + "remote_button_long_release": "Bot\u00f3n \"{subtipo}\" liberado despu\u00e9s de una pulsaci\u00f3n prolongada", + "remote_button_quadruple_press": "\"{subtipo}\" bot\u00f3n cu\u00e1druple pulsado", + "remote_button_quintuple_press": "\"{subtipo}\" bot\u00f3n qu\u00edntuple pulsado", + "remote_button_short_press": "Bot\u00f3n \"{subtipo}\" pulsado", + "remote_button_short_release": "Bot\u00f3n \"{subtipo}\" liberado", + "remote_button_triple_press": "\"{subtipo}\" bot\u00f3n de triple clic" + } } } \ No newline at end of file diff --git a/homeassistant/components/zha/.translations/fr.json b/homeassistant/components/zha/.translations/fr.json index 48328aed878..f8b78af5721 100644 --- a/homeassistant/components/zha/.translations/fr.json +++ b/homeassistant/components/zha/.translations/fr.json @@ -16,5 +16,32 @@ } }, "title": "ZHA" + }, + "device_automation": { + "action_type": { + "squawk": "Hurlement", + "warn": "Pr\u00e9venir" + }, + "trigger_subtype": { + "both_buttons": "Les deux boutons", + "button_1": "Premier bouton", + "button_2": "Deuxi\u00e8me bouton", + "button_3": "Troisi\u00e8me bouton", + "button_4": "Quatri\u00e8me bouton", + "button_5": "Cinqui\u00e8me bouton", + "button_6": "Sixi\u00e8me bouton", + "close": "Fermer", + "left": "Gauche", + "open": "Ouvert", + "right": "Droite", + "turn_off": "\u00c9teindre", + "turn_on": "Allumer" + }, + "trigger_type": { + "device_shaken": "Appareil secou\u00e9", + "device_tilted": "Dispositif inclin\u00e9", + "remote_button_short_release": "Bouton \" {subtype} \" est rel\u00e2ch\u00e9", + "remote_button_triple_press": "Bouton\"{sous-type}\" \u00e0 trois clics" + } } } \ No newline at end of file diff --git a/homeassistant/components/zha/.translations/it.json b/homeassistant/components/zha/.translations/it.json index e4b87c9d7b6..bb05977fd09 100644 --- a/homeassistant/components/zha/.translations/it.json +++ b/homeassistant/components/zha/.translations/it.json @@ -16,5 +16,52 @@ } }, "title": "ZHA" + }, + "device_automation": { + "action_type": { + "squawk": "Strillare", + "warn": "Avvertire" + }, + "trigger_subtype": { + "both_buttons": "Entrambi i pulsanti", + "button_1": "Primo pulsante", + "button_2": "Secondo pulsante", + "button_3": "Terzo pulsante", + "button_4": "Quarto pulsante", + "button_5": "Quinto pulsante", + "button_6": "Sesto pulsante", + "close": "Chiudere", + "dim_down": "Diminuire luminosit\u00e0", + "dim_up": "Aumentare luminosit\u00e0", + "face_1": "con faccia 1 attivata", + "face_2": "con faccia 2 attivata", + "face_3": "con faccia 3 attivata", + "face_4": "con faccia 4 attivata", + "face_5": "con faccia 5 attivata", + "face_6": "con faccia 6 attivata", + "face_any": "Con una o pi\u00f9 facce specificate attivate", + "left": "Sinistra", + "open": "Aperto", + "right": "Destra", + "turn_off": "Spento", + "turn_on": "Acceso" + }, + "trigger_type": { + "device_dropped": "Dispositivo caduto", + "device_flipped": "Dispositivo capovolto \" {subtype} \"", + "device_knocked": "Dispositivo bussato \" {subtype} \"", + "device_rotated": "Dispositivo ruotato \" {subtype} \"", + "device_shaken": "Dispositivo in vibrazione", + "device_slid": "Dispositivo scivolato \"{sottotipo}\"", + "device_tilted": "Dispositivo inclinato", + "remote_button_double_press": "Pulsante \"{subtype}\" cliccato due volte", + "remote_button_long_press": "Pulsante \"{subtype}\" premuto continuamente", + "remote_button_long_release": "Pulsante \"{subtype}\" rilasciato dopo una lunga pressione", + "remote_button_quadruple_press": "Pulsante \"{subtype}\" cliccato quattro volte", + "remote_button_quintuple_press": "Pulsante \"{subtype}\" cliccato cinque volte", + "remote_button_short_press": "Pulsante \"{subtype}\" premuto", + "remote_button_short_release": "Pulsante \"{subtype}\" rilasciato", + "remote_button_triple_press": "Pulsante \"{subtype}\" cliccato tre volte" + } } } \ No newline at end of file diff --git a/homeassistant/components/zha/.translations/lb.json b/homeassistant/components/zha/.translations/lb.json index f3e7053ca11..a289e05e667 100644 --- a/homeassistant/components/zha/.translations/lb.json +++ b/homeassistant/components/zha/.translations/lb.json @@ -16,5 +16,52 @@ } }, "title": "ZHA" + }, + "device_automation": { + "action_type": { + "squawk": "Mellen", + "warn": "Warnen" + }, + "trigger_subtype": { + "both_buttons": "B\u00e9id Kn\u00e4ppchen", + "button_1": "\u00c9ischte Kn\u00e4ppchen", + "button_2": "Zweete Kn\u00e4ppchen", + "button_3": "Dr\u00ebtte Kn\u00e4ppchen", + "button_4": "V\u00e9ierte Kn\u00e4ppchen", + "button_5": "F\u00ebnnefte Kn\u00e4ppchen", + "button_6": "Sechste Kn\u00e4ppchen", + "close": "Zoumaachen", + "dim_down": "Verd\u00e4ischteren", + "dim_up": "Erhellen", + "face_1": "mat S\u00e4it 1 aktiv\u00e9iert", + "face_2": "mat S\u00e4it 2 aktiv\u00e9iert", + "face_3": "mat S\u00e4it 3 aktiv\u00e9iert", + "face_4": "mat S\u00e4it 4 aktiv\u00e9iert", + "face_5": "mat S\u00e4it 5 aktiv\u00e9iert", + "face_6": "mat S\u00e4it 6 aktiv\u00e9iert", + "face_any": "Mat iergendenger/spezifiz\u00e9ierter S\u00e4it(en) aktiv\u00e9iert", + "left": "L\u00e9nks", + "open": "Op", + "right": "Riets", + "turn_off": "Ausschalten", + "turn_on": "Uschalten" + }, + "trigger_type": { + "device_dropped": "Apparat gefall", + "device_flipped": "Apparat \u00ebmgedr\u00e9int \"{subtype}\"", + "device_knocked": "Apparat geklappt \"{subtype}\"", + "device_rotated": "Apparat gedr\u00e9int \"{subtype}\"", + "device_shaken": "Apparat ger\u00ebselt", + "device_slid": "Apparat gerutscht \"{subtype}\"", + "device_tilted": "Apparat ass gekippt", + "remote_button_double_press": "\"{subtype}\" Kn\u00e4ppche zwee mol gedr\u00e9ckt", + "remote_button_long_press": "\"{subtype}\" Kn\u00e4ppche permanent gedr\u00e9ckt", + "remote_button_long_release": "\"{subtype}\" Kn\u00e4ppche no laangem unhalen lassgelooss", + "remote_button_quadruple_press": "\"{subtype}\" Kn\u00e4ppche v\u00e9ier mol gedr\u00e9ckt", + "remote_button_quintuple_press": "\"{subtype}\" Kn\u00e4ppche f\u00ebnnef mol gedr\u00e9ckt", + "remote_button_short_press": "\"{subtype}\" Kn\u00e4ppche gedr\u00e9ckt", + "remote_button_short_release": "\"{subtype}\" Kn\u00e4ppche lassgelooss", + "remote_button_triple_press": "\"{subtype}\" Kn\u00e4ppche dr\u00e4imol gedr\u00e9ckt" + } } } \ No newline at end of file diff --git a/homeassistant/components/zha/.translations/nl.json b/homeassistant/components/zha/.translations/nl.json index 56c2518f11e..5e5c666b1a4 100644 --- a/homeassistant/components/zha/.translations/nl.json +++ b/homeassistant/components/zha/.translations/nl.json @@ -16,5 +16,31 @@ } }, "title": "ZHA" + }, + "device_automation": { + "trigger_subtype": { + "left": "Links", + "open": "Open", + "right": "Rechts", + "turn_off": "Uitschakelen", + "turn_on": "Inschakelen" + }, + "trigger_type": { + "device_dropped": "Apparaat gevallen", + "device_flipped": "Apparaat omgedraaid \"{subtype}\"", + "device_knocked": "Apparaat klopte \"{subtype}\"", + "device_rotated": "Apparaat gedraaid \" {subtype} \"", + "device_shaken": "Apparaat geschud", + "device_slid": "Apparaat geschoven \"{subtype}\"\".", + "device_tilted": "Apparaat gekanteld", + "remote_button_double_press": "\"{subtype}\" knop dubbel geklikt", + "remote_button_long_press": "\" {subtype} \" knop continu ingedrukt", + "remote_button_long_release": "\"{subtype}\" knop losgelaten na lang indrukken van de knop", + "remote_button_quadruple_press": "\" {subtype} \" knop viervoudig aangeklikt", + "remote_button_quintuple_press": "\" {subtype} \" knop vijf keer aangeklikt", + "remote_button_short_press": "\" {subtype} \" knop ingedrukt", + "remote_button_short_release": "\"{subtype}\" knop losgelaten", + "remote_button_triple_press": "\" {subtype} \" knop driemaal geklikt" + } } } \ No newline at end of file diff --git a/homeassistant/components/zha/.translations/nn.json b/homeassistant/components/zha/.translations/nn.json new file mode 100644 index 00000000000..ad2c240baf1 --- /dev/null +++ b/homeassistant/components/zha/.translations/nn.json @@ -0,0 +1,10 @@ +{ + "config": { + "step": { + "user": { + "title": "ZHA" + } + }, + "title": "ZHA" + } +} \ No newline at end of file diff --git a/homeassistant/components/zha/.translations/no.json b/homeassistant/components/zha/.translations/no.json index 9db55494ba4..18c4c3c9ff2 100644 --- a/homeassistant/components/zha/.translations/no.json +++ b/homeassistant/components/zha/.translations/no.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "Kun \u00e9n enkelt konfigurasjon av ZHA er tillatt." + "single_instance_allowed": "Kun en konfigurasjon av ZHA er tillatt." }, "error": { "cannot_connect": "Kan ikke koble til ZHA-enhet." @@ -16,5 +16,52 @@ } }, "title": "ZHA" + }, + "device_automation": { + "action_type": { + "squawk": "Squawk", + "warn": "Advarer" + }, + "trigger_subtype": { + "both_buttons": "Begge knapper", + "button_1": "F\u00f8rste knapp", + "button_2": "Andre knapp", + "button_3": "Tredje knapp", + "button_4": "Fjerde knapp", + "button_5": "Femte knapp", + "button_6": "Sjette knapp", + "close": "Lukk", + "dim_down": "Dimm ned", + "dim_up": "Dimm opp", + "face_1": "med ansikt 1 aktivert", + "face_2": "med ansikt 2 aktivert", + "face_3": "med ansikt 3 aktivert", + "face_4": "med ansikt 4 aktivert", + "face_5": "med ansikt 5 aktivert", + "face_6": "med ansikt 6 aktivert", + "face_any": "Med alle/angitte ansikt (er) aktivert", + "left": "Venstre", + "open": "\u00c5pen", + "right": "H\u00f8yre", + "turn_off": "Skru av", + "turn_on": "Sl\u00e5 p\u00e5" + }, + "trigger_type": { + "device_dropped": "Enheten ble brutt", + "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_double_press": "\"{subtype}\"-knappen ble dobbeltklikket", + "remote_button_long_press": "\"{subtype}\"-knappen ble kontinuerlig trykket", + "remote_button_long_release": "\"{subtype}\"-knappen sluppet etter langt trykk", + "remote_button_quadruple_press": "\"{subtype}\"-knappen ble firedoblet klikket", + "remote_button_quintuple_press": "\"{subtype}\"-knappen ble femdobbelt klikket", + "remote_button_short_press": "\"{subtype}\"-knappen ble trykket", + "remote_button_short_release": "\"{subtype}\"-knappen sluppet", + "remote_button_triple_press": "\"{subtype}\"-knappen ble trippel klikket" + } } } \ No newline at end of file diff --git a/homeassistant/components/zha/.translations/pl.json b/homeassistant/components/zha/.translations/pl.json index 93867c0c84f..0e1b7028dbb 100644 --- a/homeassistant/components/zha/.translations/pl.json +++ b/homeassistant/components/zha/.translations/pl.json @@ -16,5 +16,52 @@ } }, "title": "ZHA" + }, + "device_automation": { + "action_type": { + "squawk": "squawk", + "warn": "ostrze\u017cenie" + }, + "trigger_subtype": { + "both_buttons": "oba przyciski", + "button_1": "pierwszy przycisk", + "button_2": "drugi przycisk", + "button_3": "trzeci przycisk", + "button_4": "czwarty przycisk", + "button_5": "pi\u0105ty przycisk", + "button_6": "sz\u00f3sty przycisk", + "close": "zamkni\u0119cie", + "dim_down": "zmniejszenie jasno\u015bci", + "dim_up": "zwi\u0119kszenie jasno\u015bci", + "face_1": "z aktywowan\u0105 twarz\u0105 1", + "face_2": "z aktywowan\u0105 twarz\u0105 2", + "face_3": "z aktywowan\u0105 twarz\u0105 3", + "face_4": "z aktywowan\u0105 twarz\u0105 4", + "face_5": "z aktywowan\u0105 twarz\u0105 5", + "face_6": "z aktywowan\u0105 twarz\u0105 6", + "face_any": "z dowoln\u0105 twarz\u0105 aktywowan\u0105", + "left": "w lewo", + "open": "otwarcie", + "right": "w prawo", + "turn_off": "wy\u0142\u0105czenie", + "turn_on": "w\u0142\u0105czenie" + }, + "trigger_type": { + "device_dropped": "upadek urz\u0105dzenia", + "device_flipped": "odwr\u00f3cenie urz\u0105dzenia \"{subtype}\"", + "device_knocked": "pukni\u0119cie urz\u0105dzenia \"{subtype}\"", + "device_rotated": "obr\u00f3cenie urz\u0105dzenia \"{subtype}\"", + "device_shaken": "potrz\u0105\u015bni\u0119cie urz\u0105dzeniem", + "device_slid": "przesuni\u0119cie urz\u0105dzenia \"{subtype}\"", + "device_tilted": "przechylenie urz\u0105dzenia", + "remote_button_double_press": "przycisk \"{subtype}\" podw\u00f3jnie naci\u015bni\u0119ty", + "remote_button_long_press": "przycisk \"{subtype}\" naci\u015bni\u0119ty w spos\u00f3b ci\u0105g\u0142y", + "remote_button_long_release": "przycisk \"{subtype}\" zwolniony po d\u0142ugim naci\u015bni\u0119ciu", + "remote_button_quadruple_press": "przycisk \"{subtype}\" czterokrotnie naci\u015bni\u0119ty", + "remote_button_quintuple_press": "przycisk \"{subtype}\" pi\u0119ciokrotnie naci\u015bni\u0119ty", + "remote_button_short_press": "przycisk \"{subtype}\" naci\u015bni\u0119ty", + "remote_button_short_release": "przycisk \"{subtype}\" zwolniony", + "remote_button_triple_press": "przycisk \"{subtype}\" trzykrotnie naci\u015bni\u0119ty" + } } } \ No newline at end of file diff --git a/homeassistant/components/zha/.translations/pt-BR.json b/homeassistant/components/zha/.translations/pt-BR.json index 8606a04e197..7ccc661dd28 100644 --- a/homeassistant/components/zha/.translations/pt-BR.json +++ b/homeassistant/components/zha/.translations/pt-BR.json @@ -16,5 +16,11 @@ } }, "title": "ZHA" + }, + "device_automation": { + "action_type": { + "squawk": "Squawk", + "warn": "Aviso" + } } } \ No newline at end of file diff --git a/homeassistant/components/zha/.translations/ru.json b/homeassistant/components/zha/.translations/ru.json index cd618072592..2f6f42311c3 100644 --- a/homeassistant/components/zha/.translations/ru.json +++ b/homeassistant/components/zha/.translations/ru.json @@ -16,5 +16,10 @@ } }, "title": "Zigbee Home Automation" + }, + "device_automation": { + "action_type": { + "warn": "\u041f\u0440\u0435\u0434\u0443\u043f\u0440\u0435\u0436\u0434\u0435\u043d\u0438\u0435" + } } } \ No newline at end of file diff --git a/homeassistant/components/zha/.translations/sl.json b/homeassistant/components/zha/.translations/sl.json index 30df6716f97..226bd37200e 100644 --- a/homeassistant/components/zha/.translations/sl.json +++ b/homeassistant/components/zha/.translations/sl.json @@ -16,5 +16,52 @@ } }, "title": "ZHA" + }, + "device_automation": { + "action_type": { + "squawk": "Squawk", + "warn": "Opozori" + }, + "trigger_subtype": { + "both_buttons": "Oba gumba", + "button_1": "Prvi gumb", + "button_2": "Drugi gumb", + "button_3": "Tretji gumb", + "button_4": "\u010cetrti gumb", + "button_5": "Peti gumb", + "button_6": "\u0160esti gumb", + "close": "Zapri", + "dim_down": "Zatemnite", + "dim_up": "pove\u010dajte mo\u010d", + "face_1": "aktivirano z obrazom 1", + "face_2": "aktivirano z obrazom 2", + "face_3": "aktivirano z obrazom 3", + "face_4": "aktivirano z obrazom 4", + "face_5": "aktivirano z obrazom 5", + "face_6": "aktivirano z obrazom 6", + "face_any": "Z vsemi/dolo\u010denimi obrazi vklju\u010deno", + "left": "Levo", + "open": "Odprto", + "right": "Desno", + "turn_off": "Ugasni", + "turn_on": "Pri\u017egi" + }, + "trigger_type": { + "device_dropped": "Naprava padla", + "device_flipped": "Naprava obrnjena \"{subtype}\"", + "device_knocked": "Naprava prevrnjena \"{subtype}\"", + "device_rotated": "Naprava je zasukana \"{subtype}\"", + "device_shaken": "Naprava se je pretresla", + "device_slid": "Naprava zdrsnila \"{subtype}\"", + "device_tilted": "Naprava je nagnjena", + "remote_button_double_press": "Dvakrat kliknete gumb \"{subtype}\"", + "remote_button_long_press": "\"{subtype}\" gumb neprekinjeno pritisnjen", + "remote_button_long_release": "\"{subtype}\" gumb spro\u0161\u010den po dolgem pritisku", + "remote_button_quadruple_press": "\"{subtype}\" gumb \u0161tirikrat kliknjen", + "remote_button_quintuple_press": "\"{subtype}\" gumb petkrat kliknjen", + "remote_button_short_press": "Pritisnjen \"{subtype}\" gumb", + "remote_button_short_release": "Gumb \"{subtype}\" spro\u0161\u010den", + "remote_button_triple_press": "Gumb \"{subtype}\" trikrat kliknjen" + } } } \ No newline at end of file diff --git a/homeassistant/components/zha/.translations/sv.json b/homeassistant/components/zha/.translations/sv.json index 818d041b4f1..2762adc0fba 100644 --- a/homeassistant/components/zha/.translations/sv.json +++ b/homeassistant/components/zha/.translations/sv.json @@ -16,5 +16,17 @@ } }, "title": "ZHA" + }, + "device_automation": { + "trigger_subtype": { + "close": "St\u00e4ng", + "dim_down": "Dimma ned", + "dim_up": "Dimma upp", + "left": "V\u00e4nster", + "open": "\u00d6ppen", + "right": "H\u00f6ger", + "turn_off": "St\u00e4ng av", + "turn_on": "Starta" + } } } \ No newline at end of file diff --git a/homeassistant/components/zha/.translations/zh-Hant.json b/homeassistant/components/zha/.translations/zh-Hant.json index e31e42bfbcf..d7f421c7e84 100644 --- a/homeassistant/components/zha/.translations/zh-Hant.json +++ b/homeassistant/components/zha/.translations/zh-Hant.json @@ -4,17 +4,64 @@ "single_instance_allowed": "\u50c5\u5141\u8a31\u8a2d\u5b9a\u4e00\u7d44 ZHA\u3002" }, "error": { - "cannot_connect": "\u7121\u6cd5\u9023\u7dda\u81f3 ZHA \u88dd\u7f6e\u3002" + "cannot_connect": "\u7121\u6cd5\u9023\u7dda\u81f3 ZHA \u8a2d\u5099\u3002" }, "step": { "user": { "data": { "radio_type": "\u7121\u7dda\u96fb\u985e\u578b", - "usb_path": "USB \u88dd\u7f6e\u8def\u5f91" + "usb_path": "USB \u8a2d\u5099\u8def\u5f91" }, "title": "ZHA" } }, "title": "ZHA" + }, + "device_automation": { + "action_type": { + "squawk": "\u61c9\u7b54", + "warn": "\u8b66\u544a" + }, + "trigger_subtype": { + "both_buttons": "\u5169\u500b\u6309\u9215", + "button_1": "\u7b2c\u4e00\u500b\u6309\u9215", + "button_2": "\u7b2c\u4e8c\u500b\u6309\u9215", + "button_3": "\u7b2c\u4e09\u500b\u6309\u9215", + "button_4": "\u7b2c\u56db\u500b\u6309\u9215", + "button_5": "\u7b2c\u4e94\u500b\u6309\u9215", + "button_6": "\u7b2c\u516d\u500b\u6309\u9215", + "close": "\u95dc\u9589", + "dim_down": "\u8abf\u6697", + "dim_up": "\u8abf\u4eae", + "face_1": "\u5df2\u7531\u9762\u5bb9 1 \u958b\u555f", + "face_2": "\u5df2\u7531\u9762\u5bb9 2 \u958b\u555f", + "face_3": "\u5df2\u7531\u9762\u5bb9 3 \u958b\u555f", + "face_4": "\u5df2\u7531\u9762\u5bb9 4 \u958b\u555f", + "face_5": "\u5df2\u7531\u9762\u5bb9 5 \u958b\u555f", + "face_6": "\u5df2\u7531\u9762\u5bb9 6 \u958b\u555f", + "face_any": "\u5df2\u7531\u4efb\u4f55/\u7279\u5b9a\u9762\u5bb9\u958b\u555f", + "left": "\u5de6", + "open": "\u958b\u555f", + "right": "\u53f3", + "turn_off": "\u95dc\u9589", + "turn_on": "\u958b\u555f" + }, + "trigger_type": { + "device_dropped": "\u8a2d\u5099\u6389\u843d", + "device_flipped": "\u7ffb\u52d5 \"{subtype}\" \u8a2d\u5099", + "device_knocked": "\u6572\u64ca \"{subtype}\" \u8a2d\u5099", + "device_rotated": "\u65cb\u8f49 \"{subtype}\" \u8a2d\u5099", + "device_shaken": "\u8a2d\u5099\u6416\u6643", + "device_slid": "\u63a8\u52d5 \"{subtype}\" \u8a2d\u5099", + "device_tilted": "\u8a2d\u5099\u540d\u7a31", + "remote_button_double_press": "\"{subtype}\" \u6309\u9215\u96d9\u64ca", + "remote_button_long_press": "\"{subtype}\" \u6309\u9215\u6301\u7e8c\u6309\u4e0b", + "remote_button_long_release": "\"{subtype}\" \u6309\u9215\u9577\u6309\u5f8c\u91cb\u653e", + "remote_button_quadruple_press": "\"{subtype}\" \u6309\u9215\u56db\u9023\u64ca", + "remote_button_quintuple_press": "\"{subtype}\" \u6309\u9215\u4e94\u9023\u64ca", + "remote_button_short_press": "\"{subtype}\" \u6309\u9215\u5df2\u6309\u4e0b", + "remote_button_short_release": "\"{subtype}\" \u6309\u9215\u5df2\u91cb\u653e", + "remote_button_triple_press": "\"{subtype}\" \u6309\u9215\u4e09\u9023\u64ca" + } } } \ No newline at end of file diff --git a/homeassistant/components/zha/api.py b/homeassistant/components/zha/api.py index be079e83fa6..ff9f27d4843 100644 --- a/homeassistant/components/zha/api.py +++ b/homeassistant/components/zha/api.py @@ -19,9 +19,16 @@ from .core.const import ( ATTR_COMMAND, ATTR_COMMAND_TYPE, ATTR_ENDPOINT_ID, + ATTR_LEVEL, ATTR_MANUFACTURER, ATTR_NAME, ATTR_VALUE, + ATTR_WARNING_DEVICE_DURATION, + ATTR_WARNING_DEVICE_MODE, + ATTR_WARNING_DEVICE_STROBE, + ATTR_WARNING_DEVICE_STROBE_DUTY_CYCLE, + ATTR_WARNING_DEVICE_STROBE_INTENSITY, + CHANNEL_IAS_WD, CLUSTER_COMMAND_SERVER, CLUSTER_COMMANDS_CLIENT, CLUSTER_COMMANDS_SERVER, @@ -31,6 +38,11 @@ from .core.const import ( DATA_ZHA_GATEWAY, DOMAIN, MFG_CLUSTER_ID_START, + WARNING_DEVICE_MODE_EMERGENCY, + WARNING_DEVICE_SOUND_HIGH, + WARNING_DEVICE_SQUAWK_MODE_ARMED, + WARNING_DEVICE_STROBE_HIGH, + WARNING_DEVICE_STROBE_YES, ) from .core.helpers import async_is_bindable_target, convert_ieee, get_matched_clusters @@ -56,6 +68,8 @@ SERVICE_SET_ZIGBEE_CLUSTER_ATTRIBUTE = "set_zigbee_cluster_attribute" SERVICE_ISSUE_ZIGBEE_CLUSTER_COMMAND = "issue_zigbee_cluster_command" SERVICE_DIRECT_ZIGBEE_BIND = "issue_direct_zigbee_bind" SERVICE_DIRECT_ZIGBEE_UNBIND = "issue_direct_zigbee_unbind" +SERVICE_WARNING_DEVICE_SQUAWK = "warning_device_squawk" +SERVICE_WARNING_DEVICE_WARN = "warning_device_warn" SERVICE_ZIGBEE_BIND = "service_zigbee_bind" IEEE_SERVICE = "ieee_based_service" @@ -80,6 +94,41 @@ SERVICE_SCHEMAS = { vol.Optional(ATTR_MANUFACTURER): cv.positive_int, } ), + SERVICE_WARNING_DEVICE_SQUAWK: vol.Schema( + { + vol.Required(ATTR_IEEE): convert_ieee, + vol.Optional( + ATTR_WARNING_DEVICE_MODE, default=WARNING_DEVICE_SQUAWK_MODE_ARMED + ): cv.positive_int, + vol.Optional( + ATTR_WARNING_DEVICE_STROBE, default=WARNING_DEVICE_STROBE_YES + ): cv.positive_int, + vol.Optional( + ATTR_LEVEL, default=WARNING_DEVICE_SOUND_HIGH + ): cv.positive_int, + } + ), + SERVICE_WARNING_DEVICE_WARN: vol.Schema( + { + vol.Required(ATTR_IEEE): convert_ieee, + vol.Optional( + ATTR_WARNING_DEVICE_MODE, default=WARNING_DEVICE_MODE_EMERGENCY + ): cv.positive_int, + vol.Optional( + ATTR_WARNING_DEVICE_STROBE, default=WARNING_DEVICE_STROBE_YES + ): cv.positive_int, + vol.Optional( + ATTR_LEVEL, default=WARNING_DEVICE_SOUND_HIGH + ): cv.positive_int, + vol.Optional(ATTR_WARNING_DEVICE_DURATION, default=5): cv.positive_int, + vol.Optional( + ATTR_WARNING_DEVICE_STROBE_DUTY_CYCLE, default=0x00 + ): cv.positive_int, + vol.Optional( + ATTR_WARNING_DEVICE_STROBE_INTENSITY, default=WARNING_DEVICE_STROBE_HIGH + ): cv.positive_int, + } + ), SERVICE_ISSUE_ZIGBEE_CLUSTER_COMMAND: vol.Schema( { vol.Required(ATTR_IEEE): convert_ieee, @@ -610,6 +659,85 @@ def async_load_api(hass): schema=SERVICE_SCHEMAS[SERVICE_ISSUE_ZIGBEE_CLUSTER_COMMAND], ) + async def warning_device_squawk(service): + """Issue the squawk command for an IAS warning device.""" + ieee = service.data[ATTR_IEEE] + mode = service.data.get(ATTR_WARNING_DEVICE_MODE) + strobe = service.data.get(ATTR_WARNING_DEVICE_STROBE) + level = service.data.get(ATTR_LEVEL) + + zha_device = zha_gateway.get_device(ieee) + if zha_device is not None: + channel = zha_device.cluster_channels.get(CHANNEL_IAS_WD) + if channel: + await channel.squawk(mode, strobe, level) + else: + _LOGGER.error( + "Squawking IASWD: %s is missing the required IASWD channel!", + "{}: [{}]".format(ATTR_IEEE, str(ieee)), + ) + else: + _LOGGER.error( + "Squawking IASWD: %s could not be found!", + "{}: [{}]".format(ATTR_IEEE, str(ieee)), + ) + _LOGGER.debug( + "Squawking IASWD: %s %s %s %s", + "{}: [{}]".format(ATTR_IEEE, str(ieee)), + "{}: [{}]".format(ATTR_WARNING_DEVICE_MODE, mode), + "{}: [{}]".format(ATTR_WARNING_DEVICE_STROBE, strobe), + "{}: [{}]".format(ATTR_LEVEL, level), + ) + + hass.helpers.service.async_register_admin_service( + DOMAIN, + SERVICE_WARNING_DEVICE_SQUAWK, + warning_device_squawk, + schema=SERVICE_SCHEMAS[SERVICE_WARNING_DEVICE_SQUAWK], + ) + + async def warning_device_warn(service): + """Issue the warning command for an IAS warning device.""" + ieee = service.data[ATTR_IEEE] + mode = service.data.get(ATTR_WARNING_DEVICE_MODE) + strobe = service.data.get(ATTR_WARNING_DEVICE_STROBE) + level = service.data.get(ATTR_LEVEL) + duration = service.data.get(ATTR_WARNING_DEVICE_DURATION) + duty_mode = service.data.get(ATTR_WARNING_DEVICE_STROBE_DUTY_CYCLE) + intensity = service.data.get(ATTR_WARNING_DEVICE_STROBE_INTENSITY) + + zha_device = zha_gateway.get_device(ieee) + if zha_device is not None: + channel = zha_device.cluster_channels.get(CHANNEL_IAS_WD) + if channel: + await channel.start_warning( + mode, strobe, level, duration, duty_mode, intensity + ) + else: + _LOGGER.error( + "Warning IASWD: %s is missing the required IASWD channel!", + "{}: [{}]".format(ATTR_IEEE, str(ieee)), + ) + else: + _LOGGER.error( + "Warning IASWD: %s could not be found!", + "{}: [{}]".format(ATTR_IEEE, str(ieee)), + ) + _LOGGER.debug( + "Warning IASWD: %s %s %s %s", + "{}: [{}]".format(ATTR_IEEE, str(ieee)), + "{}: [{}]".format(ATTR_WARNING_DEVICE_MODE, mode), + "{}: [{}]".format(ATTR_WARNING_DEVICE_STROBE, strobe), + "{}: [{}]".format(ATTR_LEVEL, level), + ) + + hass.helpers.service.async_register_admin_service( + DOMAIN, + SERVICE_WARNING_DEVICE_WARN, + warning_device_warn, + schema=SERVICE_SCHEMAS[SERVICE_WARNING_DEVICE_WARN], + ) + websocket_api.async_register_command(hass, websocket_permit_devices) websocket_api.async_register_command(hass, websocket_get_devices) websocket_api.async_register_command(hass, websocket_get_device) @@ -629,3 +757,5 @@ def async_unload_api(hass): hass.services.async_remove(DOMAIN, SERVICE_REMOVE) hass.services.async_remove(DOMAIN, SERVICE_SET_ZIGBEE_CLUSTER_ATTRIBUTE) hass.services.async_remove(DOMAIN, SERVICE_ISSUE_ZIGBEE_CLUSTER_COMMAND) + hass.services.async_remove(DOMAIN, SERVICE_WARNING_DEVICE_SQUAWK) + hass.services.async_remove(DOMAIN, SERVICE_WARNING_DEVICE_WARN) diff --git a/homeassistant/components/zha/core/__init__.py b/homeassistant/components/zha/core/__init__.py index 145b725fc79..1873cd7dc55 100644 --- a/homeassistant/components/zha/core/__init__.py +++ b/homeassistant/components/zha/core/__init__.py @@ -2,7 +2,7 @@ Core module for Zigbee Home Automation. For more details about this component, please refer to the documentation at -https://home-assistant.io/components/zha/ +https://home-assistant.io/integrations/zha/ """ # flake8: noqa diff --git a/homeassistant/components/zha/core/channels/__init__.py b/homeassistant/components/zha/core/channels/__init__.py index aed12bc65a5..37b0bec207b 100644 --- a/homeassistant/components/zha/core/channels/__init__.py +++ b/homeassistant/components/zha/core/channels/__init__.py @@ -2,7 +2,7 @@ Channels module for Zigbee Home Automation. For more details about this component, please refer to the documentation at -https://home-assistant.io/components/zha/ +https://home-assistant.io/integrations/zha/ """ import asyncio from concurrent.futures import TimeoutError as Timeout @@ -240,6 +240,8 @@ class ZigbeeChannel(LogMixin): { "unique_id": self._unique_id, "device_ieee": str(self._zha_device.ieee), + "endpoint_id": cluster.endpoint.endpoint_id, + "cluster_id": cluster.cluster_id, "command": command, "args": args, }, diff --git a/homeassistant/components/zha/core/channels/closures.py b/homeassistant/components/zha/core/channels/closures.py index 378be778e6f..16592c9a8df 100644 --- a/homeassistant/components/zha/core/channels/closures.py +++ b/homeassistant/components/zha/core/channels/closures.py @@ -2,7 +2,7 @@ Closures channels module for Zigbee Home Automation. For more details about this component, please refer to the documentation at -https://home-assistant.io/components/zha/ +https://home-assistant.io/integrations/zha/ """ import logging diff --git a/homeassistant/components/zha/core/channels/general.py b/homeassistant/components/zha/core/channels/general.py index f67ee2fb75a..7afde3e5f78 100644 --- a/homeassistant/components/zha/core/channels/general.py +++ b/homeassistant/components/zha/core/channels/general.py @@ -2,7 +2,7 @@ General channels module for Zigbee Home Automation. For more details about this component, please refer to the documentation at -https://home-assistant.io/components/zha/ +https://home-assistant.io/integrations/zha/ """ import logging diff --git a/homeassistant/components/zha/core/channels/homeautomation.py b/homeassistant/components/zha/core/channels/homeautomation.py index 7a5f0161fb4..dda6c1f4c13 100644 --- a/homeassistant/components/zha/core/channels/homeautomation.py +++ b/homeassistant/components/zha/core/channels/homeautomation.py @@ -2,7 +2,7 @@ Home automation channels module for Zigbee Home Automation. For more details about this component, please refer to the documentation at -https://home-assistant.io/components/zha/ +https://home-assistant.io/integrations/zha/ """ import logging diff --git a/homeassistant/components/zha/core/channels/hvac.py b/homeassistant/components/zha/core/channels/hvac.py index 2f6e6c1b3e8..14d982ab1e8 100644 --- a/homeassistant/components/zha/core/channels/hvac.py +++ b/homeassistant/components/zha/core/channels/hvac.py @@ -2,7 +2,7 @@ HVAC channels module for Zigbee Home Automation. For more details about this component, please refer to the documentation at -https://home-assistant.io/components/zha/ +https://home-assistant.io/integrations/zha/ """ import logging diff --git a/homeassistant/components/zha/core/channels/lighting.py b/homeassistant/components/zha/core/channels/lighting.py index d8f769a3e24..272fa28905c 100644 --- a/homeassistant/components/zha/core/channels/lighting.py +++ b/homeassistant/components/zha/core/channels/lighting.py @@ -2,7 +2,7 @@ Lighting channels module for Zigbee Home Automation. For more details about this component, please refer to the documentation at -https://home-assistant.io/components/zha/ +https://home-assistant.io/integrations/zha/ """ import logging diff --git a/homeassistant/components/zha/core/channels/lightlink.py b/homeassistant/components/zha/core/channels/lightlink.py index 99fed7d5d68..7cd2134988d 100644 --- a/homeassistant/components/zha/core/channels/lightlink.py +++ b/homeassistant/components/zha/core/channels/lightlink.py @@ -2,7 +2,7 @@ Lightlink channels module for Zigbee Home Automation. For more details about this component, please refer to the documentation at -https://home-assistant.io/components/zha/ +https://home-assistant.io/integrations/zha/ """ import logging diff --git a/homeassistant/components/zha/core/channels/manufacturerspecific.py b/homeassistant/components/zha/core/channels/manufacturerspecific.py index e15acdaf5e3..31dd5cd63d1 100644 --- a/homeassistant/components/zha/core/channels/manufacturerspecific.py +++ b/homeassistant/components/zha/core/channels/manufacturerspecific.py @@ -2,7 +2,7 @@ Manufacturer specific channels module for Zigbee Home Automation. For more details about this component, please refer to the documentation at -https://home-assistant.io/components/zha/ +https://home-assistant.io/integrations/zha/ """ import logging diff --git a/homeassistant/components/zha/core/channels/measurement.py b/homeassistant/components/zha/core/channels/measurement.py index 94d885592eb..369ecb69aa1 100644 --- a/homeassistant/components/zha/core/channels/measurement.py +++ b/homeassistant/components/zha/core/channels/measurement.py @@ -2,7 +2,7 @@ Measurement channels module for Zigbee Home Automation. For more details about this component, please refer to the documentation at -https://home-assistant.io/components/zha/ +https://home-assistant.io/integrations/zha/ """ import logging diff --git a/homeassistant/components/zha/core/channels/protocol.py b/homeassistant/components/zha/core/channels/protocol.py index b9785068f21..aa463392e55 100644 --- a/homeassistant/components/zha/core/channels/protocol.py +++ b/homeassistant/components/zha/core/channels/protocol.py @@ -2,7 +2,7 @@ Protocol channels module for Zigbee Home Automation. For more details about this component, please refer to the documentation at -https://home-assistant.io/components/zha/ +https://home-assistant.io/integrations/zha/ """ import logging diff --git a/homeassistant/components/zha/core/channels/security.py b/homeassistant/components/zha/core/channels/security.py index cd407cfc416..e4840dae86d 100644 --- a/homeassistant/components/zha/core/channels/security.py +++ b/homeassistant/components/zha/core/channels/security.py @@ -2,7 +2,7 @@ Security channels module for Zigbee Home Automation. For more details about this component, please refer to the documentation at -https://home-assistant.io/components/zha/ +https://home-assistant.io/integrations/zha/ """ import logging @@ -13,7 +13,15 @@ from homeassistant.helpers.dispatcher import async_dispatcher_send from . import ZigbeeChannel from .. import registries -from ..const import SIGNAL_ATTR_UPDATED +from ..const import ( + CLUSTER_COMMAND_SERVER, + SIGNAL_ATTR_UPDATED, + WARNING_DEVICE_MODE_EMERGENCY, + WARNING_DEVICE_SOUND_HIGH, + WARNING_DEVICE_SQUAWK_MODE_ARMED, + WARNING_DEVICE_STROBE_HIGH, + WARNING_DEVICE_STROBE_YES, +) _LOGGER = logging.getLogger(__name__) @@ -25,11 +33,95 @@ class IasAce(ZigbeeChannel): pass +@registries.CHANNEL_ONLY_CLUSTERS.register(security.IasWd.cluster_id) @registries.ZIGBEE_CHANNEL_REGISTRY.register(security.IasWd.cluster_id) class IasWd(ZigbeeChannel): """IAS Warning Device channel.""" - pass + @staticmethod + def set_bit(destination_value, destination_bit, source_value, source_bit): + """Set the specified bit in the value.""" + + if IasWd.get_bit(source_value, source_bit): + return destination_value | (1 << destination_bit) + return destination_value + + @staticmethod + def get_bit(value, bit): + """Get the specified bit from the value.""" + return (value & (1 << bit)) != 0 + + async def squawk( + self, + mode=WARNING_DEVICE_SQUAWK_MODE_ARMED, + strobe=WARNING_DEVICE_STROBE_YES, + squawk_level=WARNING_DEVICE_SOUND_HIGH, + ): + """Issue a squawk command. + + This command uses the WD capabilities to emit a quick audible/visible pulse called a + "squawk". The squawk command has no effect if the WD is currently active + (warning in progress). + """ + value = 0 + value = IasWd.set_bit(value, 0, squawk_level, 0) + value = IasWd.set_bit(value, 1, squawk_level, 1) + + value = IasWd.set_bit(value, 3, strobe, 0) + + value = IasWd.set_bit(value, 4, mode, 0) + value = IasWd.set_bit(value, 5, mode, 1) + value = IasWd.set_bit(value, 6, mode, 2) + value = IasWd.set_bit(value, 7, mode, 3) + + await self.device.issue_cluster_command( + self.cluster.endpoint.endpoint_id, + self.cluster.cluster_id, + 0x0001, + CLUSTER_COMMAND_SERVER, + [value], + ) + + async def start_warning( + self, + mode=WARNING_DEVICE_MODE_EMERGENCY, + strobe=WARNING_DEVICE_STROBE_YES, + siren_level=WARNING_DEVICE_SOUND_HIGH, + warning_duration=5, # seconds + strobe_duty_cycle=0x00, + strobe_intensity=WARNING_DEVICE_STROBE_HIGH, + ): + """Issue a start warning command. + + This command starts the WD operation. The WD alerts the surrounding area by audible + (siren) and visual (strobe) signals. + + strobe_duty_cycle indicates the length of the flash cycle. This provides a means + of varying the flash duration for different alarm types (e.g., fire, police, burglar). + Valid range is 0-100 in increments of 10. All other values SHALL be rounded to the + nearest valid value. Strobe SHALL calculate duty cycle over a duration of one second. + The ON state SHALL precede the OFF state. For example, if Strobe Duty Cycle Field specifies + “40,” then the strobe SHALL flash ON for 4/10ths of a second and then turn OFF for + 6/10ths of a second. + """ + value = 0 + value = IasWd.set_bit(value, 0, siren_level, 0) + value = IasWd.set_bit(value, 1, siren_level, 1) + + value = IasWd.set_bit(value, 2, strobe, 0) + + value = IasWd.set_bit(value, 4, mode, 0) + value = IasWd.set_bit(value, 5, mode, 1) + value = IasWd.set_bit(value, 6, mode, 2) + value = IasWd.set_bit(value, 7, mode, 3) + + await self.device.issue_cluster_command( + self.cluster.endpoint.endpoint_id, + self.cluster.cluster_id, + 0x0000, + CLUSTER_COMMAND_SERVER, + [value, warning_duration, strobe_duty_cycle, strobe_intensity], + ) @registries.BINARY_SENSOR_CLUSTERS.register(security.IasZone.cluster_id) diff --git a/homeassistant/components/zha/core/channels/smartenergy.py b/homeassistant/components/zha/core/channels/smartenergy.py index 8e2fa7e3d5a..c7de2943691 100644 --- a/homeassistant/components/zha/core/channels/smartenergy.py +++ b/homeassistant/components/zha/core/channels/smartenergy.py @@ -2,7 +2,7 @@ Smart energy channels module for Zigbee Home Automation. For more details about this component, please refer to the documentation at -https://home-assistant.io/components/zha/ +https://home-assistant.io/integrations/zha/ """ import logging diff --git a/homeassistant/components/zha/core/const.py b/homeassistant/components/zha/core/const.py index c35cb168fdf..ac83c2cdcd8 100644 --- a/homeassistant/components/zha/core/const.py +++ b/homeassistant/components/zha/core/const.py @@ -34,6 +34,11 @@ ATTR_RSSI = "rssi" ATTR_SIGNATURE = "signature" ATTR_TYPE = "type" ATTR_VALUE = "value" +ATTR_WARNING_DEVICE_DURATION = "duration" +ATTR_WARNING_DEVICE_MODE = "mode" +ATTR_WARNING_DEVICE_STROBE = "strobe" +ATTR_WARNING_DEVICE_STROBE_DUTY_CYCLE = "duty_cycle" +ATTR_WARNING_DEVICE_STROBE_INTENSITY = "intensity" BAUD_RATES = [2400, 4800, 9600, 14400, 19200, 38400, 57600, 115200, 128000, 256000] @@ -44,6 +49,7 @@ CHANNEL_DOORLOCK = "door_lock" CHANNEL_ELECTRICAL_MEASUREMENT = "electrical_measurement" CHANNEL_EVENT_RELAY = "event_relay" CHANNEL_FAN = "fan" +CHANNEL_IAS_WD = "ias_wd" CHANNEL_LEVEL = ATTR_LEVEL CHANNEL_ON_OFF = "on_off" CHANNEL_POWER_CONFIGURATION = "power" @@ -177,6 +183,30 @@ UNKNOWN = "unknown" UNKNOWN_MANUFACTURER = "unk_manufacturer" UNKNOWN_MODEL = "unk_model" +WARNING_DEVICE_MODE_STOP = 0 +WARNING_DEVICE_MODE_BURGLAR = 1 +WARNING_DEVICE_MODE_FIRE = 2 +WARNING_DEVICE_MODE_EMERGENCY = 3 +WARNING_DEVICE_MODE_POLICE_PANIC = 4 +WARNING_DEVICE_MODE_FIRE_PANIC = 5 +WARNING_DEVICE_MODE_EMERGENCY_PANIC = 6 + +WARNING_DEVICE_STROBE_NO = 0 +WARNING_DEVICE_STROBE_YES = 1 + +WARNING_DEVICE_SOUND_LOW = 0 +WARNING_DEVICE_SOUND_MEDIUM = 1 +WARNING_DEVICE_SOUND_HIGH = 2 +WARNING_DEVICE_SOUND_VERY_HIGH = 3 + +WARNING_DEVICE_STROBE_LOW = 0x00 +WARNING_DEVICE_STROBE_MEDIUM = 0x01 +WARNING_DEVICE_STROBE_HIGH = 0x02 +WARNING_DEVICE_STROBE_VERY_HIGH = 0x03 + +WARNING_DEVICE_SQUAWK_MODE_ARMED = 0 +WARNING_DEVICE_SQUAWK_MODE_DISARMED = 1 + ZHA_DISCOVERY_NEW = "zha_discovery_new_{}" ZHA_GW_MSG_RAW_INIT = "raw_device_initialized" ZHA_GW_MSG = "zha_gateway_message" diff --git a/homeassistant/components/zha/core/device.py b/homeassistant/components/zha/core/device.py index 1db4aafeeb9..e9e2c3b7ea6 100644 --- a/homeassistant/components/zha/core/device.py +++ b/homeassistant/components/zha/core/device.py @@ -2,7 +2,7 @@ Device for Zigbee Home Automation. For more details about this component, please refer to the documentation at -https://home-assistant.io/components/zha/ +https://home-assistant.io/integrations/zha/ """ import asyncio from datetime import timedelta @@ -187,6 +187,13 @@ class ZHADevice(LogMixin): """Return cluster channels and relay channels for device.""" return self._all_channels + @property + def device_automation_triggers(self): + """Return the device automation triggers for this device.""" + if hasattr(self._zigpy_device, "device_automation_triggers"): + return self._zigpy_device.device_automation_triggers + return None + @property def available_signal(self): """Signal to use to subscribe to device availability changes.""" diff --git a/homeassistant/components/zha/core/discovery.py b/homeassistant/components/zha/core/discovery.py index 5a5ffb34ab1..622adead803 100644 --- a/homeassistant/components/zha/core/discovery.py +++ b/homeassistant/components/zha/core/discovery.py @@ -2,7 +2,7 @@ Device discovery functions for Zigbee Home Automation. For more details about this component, please refer to the documentation at -https://home-assistant.io/components/zha/ +https://home-assistant.io/integrations/zha/ """ import logging @@ -199,11 +199,13 @@ def _async_handle_single_cluster_matches( zha_device.is_mains_powered or matched_power_configuration ): continue - elif ( + + if ( cluster.cluster_id == PowerConfiguration.cluster_id and not zha_device.is_mains_powered ): matched_power_configuration = True + cluster_match_results.append( _async_handle_single_cluster_match( hass, diff --git a/homeassistant/components/zha/core/gateway.py b/homeassistant/components/zha/core/gateway.py index be09312f693..a64e8cf7fd9 100644 --- a/homeassistant/components/zha/core/gateway.py +++ b/homeassistant/components/zha/core/gateway.py @@ -2,7 +2,7 @@ Virtual gateway for Zigbee Home Automation. For more details about this component, please refer to the documentation at -https://home-assistant.io/components/zha/ +https://home-assistant.io/integrations/zha/ """ import asyncio @@ -310,7 +310,7 @@ class ZHAGateway: @callback def async_device_became_available( - self, sender, is_reply, profile, cluster, src_ep, dst_ep, tsn, command_id, args + self, sender, profile, cluster, src_ep, dst_ep, message ): """Handle tasks when a device becomes available.""" self.async_update_device(sender) diff --git a/homeassistant/components/zha/core/helpers.py b/homeassistant/components/zha/core/helpers.py index 37bc6c7a2c1..88a472716cc 100644 --- a/homeassistant/components/zha/core/helpers.py +++ b/homeassistant/components/zha/core/helpers.py @@ -2,7 +2,7 @@ Helpers for Zigbee Home Automation. For more details about this component, please refer to the documentation at -https://home-assistant.io/components/zha/ +https://home-assistant.io/integrations/zha/ """ import asyncio import collections @@ -10,7 +10,14 @@ import logging from homeassistant.core import callback -from .const import CLUSTER_TYPE_IN, CLUSTER_TYPE_OUT, DEFAULT_BAUDRATE, RadioType +from .const import ( + CLUSTER_TYPE_IN, + CLUSTER_TYPE_OUT, + DATA_ZHA, + DATA_ZHA_GATEWAY, + DEFAULT_BAUDRATE, + RadioType, +) from .registries import BINDABLE_CLUSTERS _LOGGER = logging.getLogger(__name__) @@ -132,6 +139,16 @@ def async_is_bindable_target(source_zha_device, target_zha_device): return False +async def async_get_zha_device(hass, device_id): + """Get a ZHA device for the given device registry id.""" + device_registry = await hass.helpers.device_registry.async_get_registry() + registry_device = device_registry.async_get(device_id) + zha_gateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] + ieee_address = list(list(registry_device.identifiers)[0])[1] + ieee = convert_ieee(ieee_address) + return zha_gateway.devices[ieee] + + class LogMixin: """Log helper.""" diff --git a/homeassistant/components/zha/core/patches.py b/homeassistant/components/zha/core/patches.py index 75ef8cce19d..a4e84e83105 100644 --- a/homeassistant/components/zha/core/patches.py +++ b/homeassistant/components/zha/core/patches.py @@ -2,16 +2,14 @@ Patch functions for Zigbee Home Automation. For more details about this component, please refer to the documentation at -https://home-assistant.io/components/zha/ +https://home-assistant.io/integrations/zha/ """ def apply_application_controller_patch(zha_gateway): """Apply patches to ZHA objects.""" # Patch handle_message until zigpy can provide an event here - def handle_message( - sender, is_reply, profile, cluster, src_ep, dst_ep, tsn, command_id, args - ): + def handle_message(sender, profile, cluster, src_ep, dst_ep, message): """Handle message from a device.""" if ( not sender.initializing @@ -19,18 +17,8 @@ def apply_application_controller_patch(zha_gateway): and not zha_gateway.devices[sender.ieee].available ): zha_gateway.async_device_became_available( - sender, - is_reply, - profile, - cluster, - src_ep, - dst_ep, - tsn, - command_id, - args, + sender, profile, cluster, src_ep, dst_ep, message ) - return sender.handle_message( - is_reply, profile, cluster, src_ep, dst_ep, tsn, command_id, args - ) + return sender.handle_message(profile, cluster, src_ep, dst_ep, message) zha_gateway.application_controller.handle_message = handle_message diff --git a/homeassistant/components/zha/core/registries.py b/homeassistant/components/zha/core/registries.py index db7e89dce82..43ddc888d2f 100644 --- a/homeassistant/components/zha/core/registries.py +++ b/homeassistant/components/zha/core/registries.py @@ -2,7 +2,7 @@ Mapping registries for Zigbee Home Automation. For more details about this component, please refer to the documentation at -https://home-assistant.io/components/zha/ +https://home-assistant.io/integrations/zha/ """ import collections diff --git a/homeassistant/components/zha/device_action.py b/homeassistant/components/zha/device_action.py new file mode 100644 index 00000000000..460676a75a0 --- /dev/null +++ b/homeassistant/components/zha/device_action.py @@ -0,0 +1,91 @@ +"""Provides device actions for ZHA devices.""" +from typing import List + +import voluptuous as vol + +from homeassistant.const import CONF_DEVICE_ID, CONF_DOMAIN, CONF_TYPE +from homeassistant.core import Context, HomeAssistant +from homeassistant.helpers import config_validation as cv, service +from homeassistant.helpers.typing import ConfigType, TemplateVarsType + +from . import DOMAIN +from .api import SERVICE_WARNING_DEVICE_SQUAWK, SERVICE_WARNING_DEVICE_WARN +from .core.const import CHANNEL_IAS_WD +from .core.helpers import async_get_zha_device + +ACTION_SQUAWK = "squawk" +ACTION_WARN = "warn" +ATTR_DATA = "data" +ATTR_IEEE = "ieee" +CONF_ZHA_ACTION_TYPE = "zha_action_type" +ZHA_ACTION_TYPE_SERVICE_CALL = "service_call" + +ACTION_SCHEMA = cv.DEVICE_ACTION_BASE_SCHEMA.extend( + {vol.Required(CONF_DOMAIN): DOMAIN, vol.Required(CONF_TYPE): str} +) + +DEVICE_ACTIONS = { + CHANNEL_IAS_WD: [ + {CONF_TYPE: ACTION_SQUAWK, CONF_DOMAIN: DOMAIN}, + {CONF_TYPE: ACTION_WARN, CONF_DOMAIN: DOMAIN}, + ] +} + +DEVICE_ACTION_TYPES = { + ACTION_SQUAWK: ZHA_ACTION_TYPE_SERVICE_CALL, + ACTION_WARN: ZHA_ACTION_TYPE_SERVICE_CALL, +} + +SERVICE_NAMES = { + ACTION_SQUAWK: SERVICE_WARNING_DEVICE_SQUAWK, + ACTION_WARN: SERVICE_WARNING_DEVICE_WARN, +} + + +async def async_call_action_from_config( + hass: HomeAssistant, + config: ConfigType, + variables: TemplateVarsType, + context: Context, +) -> None: + """Perform an action based on configuration.""" + await ZHA_ACTION_TYPES[DEVICE_ACTION_TYPES[config[CONF_TYPE]]]( + hass, config, variables, context + ) + + +async def async_get_actions(hass: HomeAssistant, device_id: str) -> List[dict]: + """List device actions.""" + zha_device = await async_get_zha_device(hass, device_id) + actions = [ + action + for channel in DEVICE_ACTIONS + for action in DEVICE_ACTIONS[channel] + if channel in zha_device.cluster_channels + ] + for action in actions: + action[CONF_DEVICE_ID] = device_id + return actions + + +async def _execute_service_based_action( + hass: HomeAssistant, + config: ACTION_SCHEMA, + variables: TemplateVarsType, + context: Context, +) -> None: + action_type = config[CONF_TYPE] + service_name = SERVICE_NAMES[action_type] + zha_device = await async_get_zha_device(hass, config[CONF_DEVICE_ID]) + + service_action = { + service.CONF_SERVICE: "{}.{}".format(DOMAIN, service_name), + ATTR_DATA: {ATTR_IEEE: str(zha_device.ieee)}, + } + + await service.async_call_from_config( + hass, service_action, blocking=True, variables=variables, context=context + ) + + +ZHA_ACTION_TYPES = {ZHA_ACTION_TYPE_SERVICE_CALL: _execute_service_based_action} diff --git a/homeassistant/components/zha/device_trigger.py b/homeassistant/components/zha/device_trigger.py new file mode 100644 index 00000000000..c1ea3c2b761 --- /dev/null +++ b/homeassistant/components/zha/device_trigger.py @@ -0,0 +1,70 @@ +"""Provides device automations for ZHA devices that emit events.""" +import voluptuous as vol + +import homeassistant.components.automation.event as event +from homeassistant.components.device_automation.exceptions import ( + InvalidDeviceAutomationConfig, +) +from homeassistant.const import CONF_DEVICE_ID, CONF_DOMAIN, CONF_PLATFORM, CONF_TYPE +from homeassistant.components.device_automation import TRIGGER_BASE_SCHEMA + +from . import DOMAIN +from .core.helpers import async_get_zha_device + +CONF_SUBTYPE = "subtype" +DEVICE = "device" +DEVICE_IEEE = "device_ieee" +ZHA_EVENT = "zha_event" + +TRIGGER_SCHEMA = TRIGGER_BASE_SCHEMA.extend( + {vol.Required(CONF_TYPE): str, vol.Required(CONF_SUBTYPE): str} +) + + +async def async_attach_trigger(hass, config, action, automation_info): + """Listen for state changes based on configuration.""" + trigger = (config[CONF_TYPE], config[CONF_SUBTYPE]) + zha_device = await async_get_zha_device(hass, config[CONF_DEVICE_ID]) + + if ( + zha_device.device_automation_triggers is None + or trigger not in zha_device.device_automation_triggers + ): + raise InvalidDeviceAutomationConfig + + trigger = zha_device.device_automation_triggers[trigger] + + event_config = { + event.CONF_EVENT_TYPE: ZHA_EVENT, + event.CONF_EVENT_DATA: {DEVICE_IEEE: str(zha_device.ieee), **trigger}, + } + + return await event.async_attach_trigger( + hass, event_config, action, automation_info, platform_type="device" + ) + + +async def async_get_triggers(hass, device_id): + """List device triggers. + + Make sure the device supports device automations and + if it does return the trigger list. + """ + zha_device = await async_get_zha_device(hass, device_id) + + if not zha_device.device_automation_triggers: + return + + triggers = [] + for trigger, subtype in zha_device.device_automation_triggers.keys(): + triggers.append( + { + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_PLATFORM: DEVICE, + CONF_TYPE: trigger, + CONF_SUBTYPE: subtype, + } + ) + + return triggers diff --git a/homeassistant/components/zha/light.py b/homeassistant/components/zha/light.py index 379f69febbb..fb388afac0f 100644 --- a/homeassistant/components/zha/light.py +++ b/homeassistant/components/zha/light.py @@ -25,11 +25,15 @@ from .entity import ZhaEntity _LOGGER = logging.getLogger(__name__) -DEFAULT_DURATION = 5 - +CAPABILITIES_COLOR_LOOP = 0x4 CAPABILITIES_COLOR_XY = 0x08 CAPABILITIES_COLOR_TEMP = 0x10 +UPDATE_COLORLOOP_ACTION = 0x1 +UPDATE_COLORLOOP_DIRECTION = 0x2 +UPDATE_COLORLOOP_TIME = 0x4 +UPDATE_COLORLOOP_HUE = 0x8 + UNSUPPORTED_ATTRIBUTE = 0x86 SCAN_INTERVAL = timedelta(minutes=60) PARALLEL_UPDATES = 5 @@ -85,6 +89,9 @@ class Light(ZhaEntity, light.Light): self._color_temp = None self._hs_color = None self._brightness = None + self._off_brightness = None + self._effect_list = [] + self._effect = None self._on_off_channel = self.cluster_channels.get(CHANNEL_ON_OFF) self._level_channel = self.cluster_channels.get(CHANNEL_LEVEL) self._color_channel = self.cluster_channels.get(CHANNEL_COLOR) @@ -103,6 +110,10 @@ class Light(ZhaEntity, light.Light): self._supported_features |= light.SUPPORT_COLOR self._hs_color = (0, 0) + if color_capabilities & CAPABILITIES_COLOR_LOOP: + self._supported_features |= light.SUPPORT_EFFECT + self._effect_list.append(light.EFFECT_COLORLOOP) + @property def is_on(self) -> bool: """Return true if entity is on.""" @@ -118,7 +129,8 @@ class Light(ZhaEntity, light.Light): @property def device_state_attributes(self): """Return state attributes.""" - return self.state_attributes + attributes = {"off_brightness": self._off_brightness} + return attributes def set_level(self, value): """Set the brightness of this light between 0..254. @@ -141,6 +153,16 @@ class Light(ZhaEntity, light.Light): """Return the CT color value in mireds.""" return self._color_temp + @property + def effect_list(self): + """Return the list of supported effects.""" + return self._effect_list + + @property + def effect(self): + """Return the current effect.""" + return self._effect + @property def supported_features(self): """Flag supported features.""" @@ -149,6 +171,8 @@ class Light(ZhaEntity, light.Light): def async_set_state(self, state): """Set the state.""" self._state = bool(state) + if state: + self._off_brightness = None self.async_schedule_update_ha_state() async def async_added_to_hass(self): @@ -169,16 +193,24 @@ class Light(ZhaEntity, light.Light): self._state = last_state.state == STATE_ON if "brightness" in last_state.attributes: self._brightness = last_state.attributes["brightness"] + if "off_brightness" in last_state.attributes: + self._off_brightness = last_state.attributes["off_brightness"] if "color_temp" in last_state.attributes: self._color_temp = last_state.attributes["color_temp"] if "hs_color" in last_state.attributes: self._hs_color = last_state.attributes["hs_color"] + if "effect" in last_state.attributes: + self._effect = last_state.attributes["effect"] async def async_turn_on(self, **kwargs): """Turn the entity on.""" transition = kwargs.get(light.ATTR_TRANSITION) - duration = transition * 10 if transition else DEFAULT_DURATION + duration = transition * 10 if transition else 0 brightness = kwargs.get(light.ATTR_BRIGHTNESS) + effect = kwargs.get(light.ATTR_EFFECT) + + if brightness is None and self._off_brightness is not None: + brightness = self._off_brightness t_log = {} if ( @@ -200,13 +232,14 @@ class Light(ZhaEntity, light.Light): self._brightness = level if brightness is None or brightness: + # since some lights don't always turn on with move_to_level_with_on_off, + # we should call the on command on the on_off cluster if brightness is not 0. result = await self._on_off_channel.on() t_log["on_off"] = result if not isinstance(result, list) or result[1] is not Status.SUCCESS: self.debug("turned on: %s", t_log) return self._state = True - if ( light.ATTR_COLOR_TEMP in kwargs and self.supported_features & light.SUPPORT_COLOR_TEMP @@ -234,6 +267,37 @@ class Light(ZhaEntity, light.Light): return self._hs_color = hs_color + if ( + effect == light.EFFECT_COLORLOOP + and self.supported_features & light.SUPPORT_EFFECT + ): + result = await self._color_channel.color_loop_set( + UPDATE_COLORLOOP_ACTION + | UPDATE_COLORLOOP_DIRECTION + | UPDATE_COLORLOOP_TIME, + 0x2, # start from current hue + 0x1, # only support up + transition if transition else 7, # transition + 0, # no hue + ) + t_log["color_loop_set"] = result + self._effect = light.EFFECT_COLORLOOP + elif ( + self._effect == light.EFFECT_COLORLOOP + and effect != light.EFFECT_COLORLOOP + and self.supported_features & light.SUPPORT_EFFECT + ): + result = await self._color_channel.color_loop_set( + UPDATE_COLORLOOP_ACTION, + 0x0, + 0x0, + 0x0, + 0x0, # update action only, action off, no dir,time,hue + ) + t_log["color_loop_set"] = result + self._effect = None + + self._off_brightness = None self.debug("turned on: %s", t_log) self.async_schedule_update_ha_state() @@ -241,6 +305,7 @@ class Light(ZhaEntity, light.Light): """Turn the entity off.""" duration = kwargs.get(light.ATTR_TRANSITION) supports_level = self.supported_features & light.SUPPORT_BRIGHTNESS + if duration and supports_level: result = await self._level_channel.move_to_level_with_on_off( 0, duration * 10 @@ -251,6 +316,11 @@ class Light(ZhaEntity, light.Light): if not isinstance(result, list) or result[1] is not Status.SUCCESS: return self._state = False + + if duration and supports_level: + # store current brightness so that the next turn_on uses it. + self._off_brightness = self._brightness + self.async_schedule_update_ha_state() async def async_update(self): @@ -292,6 +362,15 @@ class Light(ZhaEntity, light.Light): self._hs_color = color_util.color_xy_to_hs( float(color_x / 65535), float(color_y / 65535) ) + if ( + color_capabilities is not None + and color_capabilities & CAPABILITIES_COLOR_LOOP + ): + color_loop_active = await self._color_channel.get_attribute_value( + "color_loop_active", from_cache=from_cache + ) + if color_loop_active is not None and color_loop_active == 1: + self._effect = light.EFFECT_COLORLOOP async def refresh(self, time): """Call async_get_state at an interval.""" diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index e78661a04e5..9790fbffd06 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -2,14 +2,14 @@ "domain": "zha", "name": "Zigbee Home Automation", "config_flow": true, - "documentation": "https://www.home-assistant.io/components/zha", + "documentation": "https://www.home-assistant.io/integrations/zha", "requirements": [ - "bellows-homeassistant==0.9.1", - "zha-quirks==0.0.23", - "zigpy-deconz==0.3.0", - "zigpy-homeassistant==0.8.0", - "zigpy-xbee-homeassistant==0.4.0", - "zigpy-zigate==0.3.1" + "bellows-homeassistant==0.10.0", + "zha-quirks==0.0.26", + "zigpy-deconz==0.5.0", + "zigpy-homeassistant==0.9.0", + "zigpy-xbee-homeassistant==0.5.0", + "zigpy-zigate==0.4.1" ], "dependencies": [], "codeowners": ["@dmulcahey", "@adminiuga"] diff --git a/homeassistant/components/zha/services.yaml b/homeassistant/components/zha/services.yaml index ffd5aa21472..d279af46335 100644 --- a/homeassistant/components/zha/services.yaml +++ b/homeassistant/components/zha/services.yaml @@ -82,3 +82,55 @@ issue_zigbee_cluster_command: manufacturer: description: manufacturer code example: 0x00FC + +warning_device_squawk: + description: >- + This service uses the WD capabilities to emit a quick audible/visible pulse called a "squawk". The squawk command has no effect if the WD is currently active (warning in progress). + fields: + ieee: + description: IEEE address for the device + example: "00:0d:6f:00:05:7d:2d:34" + mode: + description: >- + The Squawk Mode field is used as a 4-bit enumeration, and can have one of the values shown in Table 8-24 of the ZCL spec - Squawk Mode Field. The exact operation of each mode (how the WD “squawks”) is implementation specific. + example: 1 + strobe: + description: >- + The strobe field is used as a Boolean, and determines if the visual indication is also required in addition to the audible squawk, as shown in Table 8-25 of the ZCL spec - Strobe Bit. + example: 1 + level: + description: >- + The squawk level field is used as a 2-bit enumeration, and determines the intensity of audible squawk sound as shown in Table 8-26 of the ZCL spec - Squawk Level Field Values. + example: 2 + +warning_device_warn: + description: >- + This service starts the WD operation. The WD alerts the surrounding area by audible (siren) and visual (strobe) signals. + fields: + ieee: + description: IEEE address for the device + example: "00:0d:6f:00:05:7d:2d:34" + mode: + description: >- + The Warning Mode field is used as an 4-bit enumeration, can have one of the values defined below in table 8-20 of the ZCL spec. The exact behavior of the WD device in each mode is according to the relevant security standards. + example: 1 + strobe: + description: >- + The Strobe field is used as a 2-bit enumeration, and determines if the visual indication is required in addition to the audible siren, as indicated in Table 8-21 of the ZCL spec. If the strobe field is “1” and the Warning Mode is “0” (“Stop”) then only the strobe is activated. + example: 1 + level: + description: >- + The Siren Level field is used as a 2-bit enumeration, and indicates the intensity of audible squawk sound as shown in Table 8-22 of the ZCL spec. + example: 2 + duration: + description: >- + Requested duration of warning, in seconds. If both Strobe and Warning Mode are "0" this field SHALL be ignored. + example: 2 + duty_cycle: + description: >- + Indicates the length of the flash cycle. This provides a means of varying the flash duration for different alarm types (e.g., fire, police, burglar). Valid range is 0-100 in increments of 10. All other values SHALL be rounded to the nearest valid value. Strobe SHALL calculate duty cycle over a duration of one second. The ON state SHALL precede the OFF state. For example, if Strobe Duty Cycle Field specifies “40,” then the strobe SHALL flash ON for 4/10ths of a second and then turn OFF for 6/10ths of a second. + example: 2 + intensity: + description: >- + Indicates the intensity of the strobe as shown in Table 8-23 of the ZCL spec. This attribute is designed to vary the output of the strobe (i.e., brightness) and not its frequency, which is detailed in section 8.4.2.3.1.6 of the ZCL spec. + example: 2 diff --git a/homeassistant/components/zha/strings.json b/homeassistant/components/zha/strings.json index e1ed6a678e3..a41f6de24be 100644 --- a/homeassistant/components/zha/strings.json +++ b/homeassistant/components/zha/strings.json @@ -1,20 +1,67 @@ { - "config": { + "config": { + "title": "ZHA", + "step": { + "user": { "title": "ZHA", - "step": { - "user": { - "title": "ZHA", - "data": { - "radio_type": "Radio Type", - "usb_path": "USB Device Path" - } - } - }, - "error": { - "cannot_connect": "Unable to connect to ZHA device." - }, - "abort": { - "single_instance_allowed": "Only a single configuration of ZHA is allowed." + "data": { + "radio_type": "Radio Type", + "usb_path": "USB Device Path" } + } + }, + "error": { + "cannot_connect": "Unable to connect to ZHA device." + }, + "abort": { + "single_instance_allowed": "Only a single configuration of ZHA is allowed." } -} \ No newline at end of file + }, + "device_automation": { + "action_type": { + "squawk": "Squawk", + "warn": "Warn" + }, + "trigger_type": { + "remote_button_short_press": "\"{subtype}\" button pressed", + "remote_button_short_release": "\"{subtype}\" button released", + "remote_button_long_press": "\"{subtype}\" button continuously pressed", + "remote_button_long_release": "\"{subtype}\" button released after long press", + "remote_button_double_press": "\"{subtype}\" button double clicked", + "remote_button_triple_press": "\"{subtype}\" button triple clicked", + "remote_button_quadruple_press": "\"{subtype}\" button quadruple clicked", + "remote_button_quintuple_press": "\"{subtype}\" button quintuple clicked", + "device_rotated": "Device rotated \"{subtype}\"", + "device_shaken": "Device shaken", + "device_slid": "Device slid \"{subtype}\"", + "device_tilted": "Device tilted", + "device_knocked": "Device knocked \"{subtype}\"", + "device_dropped": "Device dropped", + "device_flipped": "Device flipped \"{subtype}\"" + }, + "trigger_subtype": { + "turn_on": "Turn on", + "turn_off": "Turn off", + "dim_up": "Dim up", + "dim_down": "Dim down", + "left": "Left", + "right": "Right", + "open": "Open", + "close": "Close", + "both_buttons": "Both buttons", + "button_1": "First button", + "button_2": "Second button", + "button_3": "Third button", + "button_4": "Fourth button", + "button_5": "Fifth button", + "button_6": "Sixth button", + "face_any": "With any/specified face(s) activated", + "face_1": "with face 1 activated", + "face_2": "with face 2 activated", + "face_3": "with face 3 activated", + "face_4": "with face 4 activated", + "face_5": "with face 5 activated", + "face_6": "with face 6 activated" + } + } +} diff --git a/homeassistant/components/zhong_hong/manifest.json b/homeassistant/components/zhong_hong/manifest.json index 6382a830dcf..32ee6964321 100644 --- a/homeassistant/components/zhong_hong/manifest.json +++ b/homeassistant/components/zhong_hong/manifest.json @@ -1,7 +1,7 @@ { "domain": "zhong_hong", "name": "Zhong hong", - "documentation": "https://www.home-assistant.io/components/zhong_hong", + "documentation": "https://www.home-assistant.io/integrations/zhong_hong", "requirements": [ "zhong_hong_hvac==1.0.9" ], diff --git a/homeassistant/components/zigbee/__init__.py b/homeassistant/components/zigbee/__init__.py index 7c1979f3ab9..31cbc0c65b6 100644 --- a/homeassistant/components/zigbee/__init__.py +++ b/homeassistant/components/zigbee/__init__.py @@ -172,7 +172,7 @@ class ZigBeeDigitalInConfig(ZigBeePinConfig): def __init__(self, config): """Initialise the Zigbee Digital input config.""" - super(ZigBeeDigitalInConfig, self).__init__(config) + super().__init__(config) self._bool2state, self._state2bool = self.boolean_maps @property @@ -216,7 +216,7 @@ class ZigBeeDigitalOutConfig(ZigBeePinConfig): def __init__(self, config): """Initialize the Zigbee Digital out.""" - super(ZigBeeDigitalOutConfig, self).__init__(config) + super().__init__(config) self._bool2state, self._state2bool = self.boolean_maps self._should_poll = config.get("poll", False) diff --git a/homeassistant/components/zigbee/manifest.json b/homeassistant/components/zigbee/manifest.json index 1e4076b8439..3968b0e294f 100644 --- a/homeassistant/components/zigbee/manifest.json +++ b/homeassistant/components/zigbee/manifest.json @@ -1,7 +1,7 @@ { "domain": "zigbee", "name": "Zigbee", - "documentation": "https://www.home-assistant.io/components/zigbee", + "documentation": "https://www.home-assistant.io/integrations/zigbee", "requirements": [ "xbee-helper==0.0.7" ], diff --git a/homeassistant/components/ziggo_mediabox_xl/manifest.json b/homeassistant/components/ziggo_mediabox_xl/manifest.json index 9e587137922..ff9e64ae78e 100644 --- a/homeassistant/components/ziggo_mediabox_xl/manifest.json +++ b/homeassistant/components/ziggo_mediabox_xl/manifest.json @@ -1,7 +1,7 @@ { "domain": "ziggo_mediabox_xl", "name": "Ziggo mediabox xl", - "documentation": "https://www.home-assistant.io/components/ziggo_mediabox_xl", + "documentation": "https://www.home-assistant.io/integrations/ziggo_mediabox_xl", "requirements": [ "ziggo-mediabox-xl==1.1.0" ], diff --git a/homeassistant/components/zone/.translations/ru.json b/homeassistant/components/zone/.translations/ru.json index dc408035d0f..6a017e9e1c3 100644 --- a/homeassistant/components/zone/.translations/ru.json +++ b/homeassistant/components/zone/.translations/ru.json @@ -1,7 +1,7 @@ { "config": { "error": { - "name_exists": "\u0418\u043c\u044f \u0443\u0436\u0435 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u0435\u0442" + "name_exists": "\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." }, "step": { "init": { diff --git a/homeassistant/components/zone/__init__.py b/homeassistant/components/zone/__init__.py index 2ee03c08189..6ae62be3eb9 100644 --- a/homeassistant/components/zone/__init__.py +++ b/homeassistant/components/zone/__init__.py @@ -1,9 +1,10 @@ """Support for the definition of zones.""" import logging +from typing import Set, cast import voluptuous as vol -from homeassistant.core import callback +from homeassistant.core import callback, State from homeassistant.loader import bind_hass import homeassistant.helpers.config_validation as cv from homeassistant.const import ( @@ -25,6 +26,9 @@ from .config_flow import configured_zones from .const import CONF_PASSIVE, DOMAIN, HOME_ZONE, ATTR_PASSIVE, ATTR_RADIUS from .zone import Zone + +# mypy: allow-untyped-calls, allow-untyped-defs + _LOGGER = logging.getLogger(__name__) DEFAULT_NAME = "Unnamed zone" @@ -78,10 +82,11 @@ def async_active_zone(hass, latitude, longitude, radius=0): ) within_zone = zone_dist - radius < zone.attributes[ATTR_RADIUS] - closer_zone = closest is None or zone_dist < min_dist + closer_zone = closest is None or zone_dist < min_dist # type: ignore smaller_zone = ( zone_dist == min_dist - and zone.attributes[ATTR_RADIUS] < closest.attributes[ATTR_RADIUS] + and zone.attributes[ATTR_RADIUS] + < cast(State, closest).attributes[ATTR_RADIUS] ) if within_zone and (closer_zone or smaller_zone): @@ -94,7 +99,7 @@ def async_active_zone(hass, latitude, longitude, radius=0): async def async_setup(hass, config): """Set up configured zones as well as home assistant zone if necessary.""" hass.data[DOMAIN] = {} - entities = set() + entities: Set[str] = set() zone_entries = configured_zones(hass) for _, entry in config_per_platform(config, DOMAIN): if slugify(entry[CONF_NAME]) not in zone_entries: diff --git a/homeassistant/components/zone/config_flow.py b/homeassistant/components/zone/config_flow.py index 05ba28e4ca7..d23fb5a4757 100644 --- a/homeassistant/components/zone/config_flow.py +++ b/homeassistant/components/zone/config_flow.py @@ -1,5 +1,7 @@ """Config flow to configure zone component.""" +from typing import Set + import voluptuous as vol import homeassistant.helpers.config_validation as cv @@ -12,17 +14,23 @@ from homeassistant.const import ( CONF_RADIUS, ) from homeassistant.core import callback +from homeassistant.helpers.typing import HomeAssistantType from homeassistant.util import slugify from .const import CONF_PASSIVE, DOMAIN, HOME_ZONE +# mypy: allow-untyped-defs + + @callback -def configured_zones(hass): +def configured_zones(hass: HomeAssistantType) -> Set[str]: """Return a set of the configured zones.""" return set( (slugify(entry.data[CONF_NAME])) - for entry in hass.config_entries.async_entries(DOMAIN) + for entry in ( + hass.config_entries.async_entries(DOMAIN) if hass.config_entries else [] + ) ) diff --git a/homeassistant/components/zone/manifest.json b/homeassistant/components/zone/manifest.json index e9281fec3f7..7a8cdcf6c6c 100644 --- a/homeassistant/components/zone/manifest.json +++ b/homeassistant/components/zone/manifest.json @@ -2,7 +2,7 @@ "domain": "zone", "name": "Zone", "config_flow": true, - "documentation": "https://www.home-assistant.io/components/zone", + "documentation": "https://www.home-assistant.io/integrations/zone", "requirements": [], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/zone/zone.py b/homeassistant/components/zone/zone.py index ccd8e55a4ce..f084492bd34 100644 --- a/homeassistant/components/zone/zone.py +++ b/homeassistant/components/zone/zone.py @@ -1,5 +1,9 @@ """Zone entity and functionality.""" + +from typing import cast + from homeassistant.const import ATTR_HIDDEN, ATTR_LATITUDE, ATTR_LONGITUDE +from homeassistant.core import State from homeassistant.helpers.entity import Entity from homeassistant.util.location import distance @@ -8,7 +12,10 @@ from .const import ATTR_PASSIVE, ATTR_RADIUS STATE = "zoning" -def in_zone(zone, latitude, longitude, radius=0) -> bool: +# mypy: allow-untyped-defs + + +def in_zone(zone: State, latitude: float, longitude: float, radius: float = 0) -> bool: """Test if given latitude, longitude is in given zone. Async friendly. @@ -20,7 +27,9 @@ def in_zone(zone, latitude, longitude, radius=0) -> bool: zone.attributes[ATTR_LONGITUDE], ) - return zone_dist - radius < zone.attributes[ATTR_RADIUS] + if zone_dist is None or zone.attributes[ATTR_RADIUS] is None: + return False + return zone_dist - radius < cast(float, zone.attributes[ATTR_RADIUS]) class Zone(Entity): diff --git a/homeassistant/components/zoneminder/manifest.json b/homeassistant/components/zoneminder/manifest.json index 9d371fbabf7..c29a97e857e 100644 --- a/homeassistant/components/zoneminder/manifest.json +++ b/homeassistant/components/zoneminder/manifest.json @@ -1,7 +1,7 @@ { "domain": "zoneminder", "name": "Zoneminder", - "documentation": "https://www.home-assistant.io/components/zoneminder", + "documentation": "https://www.home-assistant.io/integrations/zoneminder", "requirements": [ "zm-py==0.3.3" ], diff --git a/homeassistant/components/zwave/.translations/nn.json b/homeassistant/components/zwave/.translations/nn.json index ebd9d44796c..8d1c737170f 100644 --- a/homeassistant/components/zwave/.translations/nn.json +++ b/homeassistant/components/zwave/.translations/nn.json @@ -4,6 +4,7 @@ "user": { "description": "Sj\u00e5 [www.home-assistant.io/docs/z-wave/installation/](https://www.home-assistant.io/docs/z-wave/installation/) for informasjon om konfigurasjonsvariablene." } - } + }, + "title": "Z-Wave" } } \ No newline at end of file diff --git a/homeassistant/components/zwave/.translations/no.json b/homeassistant/components/zwave/.translations/no.json index f70eaa48260..1d5584a82a0 100644 --- a/homeassistant/components/zwave/.translations/no.json +++ b/homeassistant/components/zwave/.translations/no.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Z-Wave er allerede konfigurert", - "one_instance_only": "Komponenten st\u00f8tter kun en enkelt Z-Wave-forekomst" + "one_instance_only": "Komponenten st\u00f8tter kun en Z-Wave-forekomst" }, "error": { "option_error": "Z-Wave-validering mislyktes. Er banen til USB dongel riktig?" diff --git a/homeassistant/components/zwave/.translations/ru.json b/homeassistant/components/zwave/.translations/ru.json index a64b4db185d..ed2e20f3527 100644 --- a/homeassistant/components/zwave/.translations/ru.json +++ b/homeassistant/components/zwave/.translations/ru.json @@ -1,7 +1,7 @@ { "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.", "one_instance_only": "\u041a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442 \u043c\u043e\u0436\u0435\u0442 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c\u0441\u044f \u0442\u043e\u043b\u044c\u043a\u043e \u0441 \u043e\u0434\u043d\u0438\u043c \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u043b\u0435\u0440\u043e\u043c Z-Wave" }, "error": { diff --git a/homeassistant/components/zwave/__init__.py b/homeassistant/components/zwave/__init__.py index 223ce810d7c..841b283a98d 100644 --- a/homeassistant/components/zwave/__init__.py +++ b/homeassistant/components/zwave/__init__.py @@ -851,7 +851,8 @@ async def async_setup_entry(hass, config_entry): # Need to be in STATE_AWAKED before talking to nodes. _LOGGER.info("Z-Wave ready after %d seconds", waited) break - elif waited >= const.NETWORK_READY_WAIT_SECS: + + if waited >= const.NETWORK_READY_WAIT_SECS: # Wait up to NETWORK_READY_WAIT_SECS seconds for the Z-Wave # network to be ready. _LOGGER.warning( @@ -861,8 +862,8 @@ async def async_setup_entry(hass, config_entry): "final network state: %d %s", network.state, network.state_str ) break - else: - await asyncio.sleep(1) + + await asyncio.sleep(1) hass.async_add_job(_finalize_start) diff --git a/homeassistant/components/zwave/binary_sensor.py b/homeassistant/components/zwave/binary_sensor.py index fd18772edbf..a28f53f93d4 100644 --- a/homeassistant/components/zwave/binary_sensor.py +++ b/homeassistant/components/zwave/binary_sensor.py @@ -71,7 +71,7 @@ class ZWaveTriggerSensor(ZWaveBinarySensor): def __init__(self, values, device_class): """Initialize the sensor.""" - super(ZWaveTriggerSensor, self).__init__(values, device_class) + super().__init__(values, device_class) # Set default off delay to 60 sec self.re_arm_sec = 60 self.invalidate_after = None diff --git a/homeassistant/components/zwave/discovery_schemas.py b/homeassistant/components/zwave/discovery_schemas.py index dbec1484508..e2254073290 100644 --- a/homeassistant/components/zwave/discovery_schemas.py +++ b/homeassistant/components/zwave/discovery_schemas.py @@ -277,6 +277,7 @@ DISCOVERY_SCHEMAS = [ const.COMMAND_CLASS_ALARM, const.COMMAND_CLASS_SENSOR_ALARM, const.COMMAND_CLASS_INDICATOR, + const.COMMAND_CLASS_BATTERY, ], const.DISC_GENRE: const.GENRE_USER, } diff --git a/homeassistant/components/zwave/manifest.json b/homeassistant/components/zwave/manifest.json index f88945fa281..9268a50a14d 100644 --- a/homeassistant/components/zwave/manifest.json +++ b/homeassistant/components/zwave/manifest.json @@ -2,7 +2,7 @@ "domain": "zwave", "name": "Z-Wave", "config_flow": true, - "documentation": "https://www.home-assistant.io/components/zwave", + "documentation": "https://www.home-assistant.io/integrations/zwave", "requirements": [ "homeassistant-pyozw==0.1.4", "pydispatcher==2.0.5" diff --git a/homeassistant/components/zwave/sensor.py b/homeassistant/components/zwave/sensor.py index 240809548ee..0820feb8d0f 100644 --- a/homeassistant/components/zwave/sensor.py +++ b/homeassistant/components/zwave/sensor.py @@ -1,7 +1,7 @@ """Support for Z-Wave sensors.""" import logging from homeassistant.core import callback -from homeassistant.components.sensor import DOMAIN +from homeassistant.components.sensor import DOMAIN, DEVICE_CLASS_BATTERY from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT from homeassistant.helpers.dispatcher import async_dispatcher_connect from . import const, ZWaveDeviceEntity @@ -28,6 +28,8 @@ async def async_setup_entry(hass, config_entry, async_add_entities): def get_device(node, values, **kwargs): """Create Z-Wave entity device.""" # Generic Device mappings + if values.primary.command_class == const.COMMAND_CLASS_BATTERY: + return ZWaveBatterySensor(values) if node.has_command_class(const.COMMAND_CLASS_SENSOR_MULTILEVEL): return ZWaveMultilevelSensor(values) if ( @@ -107,3 +109,12 @@ class ZWaveAlarmSensor(ZWaveSensor): """ pass + + +class ZWaveBatterySensor(ZWaveSensor): + """Representation of Z-Wave device battery level.""" + + @property + def device_class(self): + """Return the class of this device.""" + return DEVICE_CLASS_BATTERY diff --git a/homeassistant/config.py b/homeassistant/config.py index d3bd97dad8f..97c996d9e59 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -60,7 +60,7 @@ _LOGGER = logging.getLogger(__name__) DATA_PERSISTENT_ERRORS = "bootstrap_persistent_errors" RE_YAML_ERROR = re.compile(r"homeassistant\.util\.yaml") RE_ASCII = re.compile(r"\033\[[^m]*m") -HA_COMPONENT_URL = "[{}](https://home-assistant.io/components/{}/)" +HA_COMPONENT_URL = "[{}](https://home-assistant.io/integrations/{}/)" YAML_CONFIG_FILE = "configuration.yaml" VERSION_FILE = ".HA_VERSION" CONFIG_DIR_NAME = ".homeassistant" @@ -416,7 +416,7 @@ def process_ha_config_upgrade(hass: HomeAssistant) -> None: @callback def async_log_exception( - ex: vol.Invalid, domain: str, config: Dict, hass: HomeAssistant + ex: Exception, domain: str, config: Dict, hass: HomeAssistant ) -> None: """Log an error for configuration validation. @@ -428,23 +428,26 @@ def async_log_exception( @callback -def _format_config_error(ex: vol.Invalid, domain: str, config: Dict) -> str: +def _format_config_error(ex: Exception, domain: str, config: Dict) -> str: """Generate log exception for configuration validation. This method must be run in the event loop. """ message = f"Invalid config for [{domain}]: " - if "extra keys not allowed" in ex.error_message: - message += ( - "[{option}] is an invalid option for [{domain}]. " - "Check: {domain}->{path}.".format( - option=ex.path[-1], - domain=domain, - path="->".join(str(m) for m in ex.path), + if isinstance(ex, vol.Invalid): + if "extra keys not allowed" in ex.error_message: + message += ( + "[{option}] is an invalid option for [{domain}]. " + "Check: {domain}->{path}.".format( + option=ex.path[-1], + domain=domain, + path="->".join(str(m) for m in ex.path), + ) ) - ) + else: + message += "{}.".format(humanize_error(config, ex)) else: - message += "{}.".format(humanize_error(config, ex)) + message += str(ex) try: domain_config = config.get(domain, config) @@ -459,7 +462,7 @@ def _format_config_error(ex: vol.Invalid, domain: str, config: Dict) -> str: if domain != CONF_CORE: message += ( "Please check the docs at " - "https://home-assistant.io/components/{}/".format(domain) + "https://home-assistant.io/integrations/{}/".format(domain) ) return message @@ -717,6 +720,24 @@ async def async_process_component_config( _LOGGER.error("Unable to import %s: %s", domain, ex) return None + # Check if the integration has a custom config validator + config_validator = None + try: + config_validator = integration.get_platform("config") + except ImportError: + pass + if config_validator is not None and hasattr( + config_validator, "async_validate_config" + ): + try: + return await config_validator.async_validate_config( # type: ignore + hass, config + ) + except (vol.Invalid, HomeAssistantError) as ex: + async_log_exception(ex, domain, config, hass) + return None + + # No custom config validator, proceed with schema validation if hasattr(component, "CONFIG_SCHEMA"): try: return component.CONFIG_SCHEMA(config) # type: ignore diff --git a/homeassistant/const.py b/homeassistant/const.py index 3e4bf1154ba..c5b566e5084 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,8 +1,7 @@ -# coding: utf-8 """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 -MINOR_VERSION = 99 -PATCH_VERSION = "3" +MINOR_VERSION = 100 +PATCH_VERSION = "0" __short_version__ = "{}.{}".format(MAJOR_VERSION, MINOR_VERSION) __version__ = "{}.{}".format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 6, 0) @@ -272,6 +271,8 @@ ATTR_DISCOVERED = "discovered" # Location of the device/sensor ATTR_LOCATION = "location" +ATTR_MODE = "mode" + ATTR_BATTERY_CHARGING = "battery_charging" ATTR_BATTERY_LEVEL = "battery_level" ATTR_WAKEUP = "wake_up_interval" diff --git a/homeassistant/core.py b/homeassistant/core.py index c29d41ace9a..feb4445d36d 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -28,6 +28,7 @@ from typing import ( Set, TYPE_CHECKING, Awaitable, + Mapping, ) from async_timeout import timeout @@ -63,11 +64,7 @@ from homeassistant.exceptions import ( Unauthorized, ServiceNotFound, ) -from homeassistant.util.async_ import ( - run_coroutine_threadsafe, - run_callback_threadsafe, - fire_coroutine_threadsafe, -) +from homeassistant.util.async_ import run_callback_threadsafe, fire_coroutine_threadsafe from homeassistant import util import homeassistant.util.dt as dt_util from homeassistant.util import location, slugify @@ -144,8 +141,8 @@ def async_loop_exception_handler(_: Any, context: Dict) -> None: if exception: kwargs["exc_info"] = (type(exception), exception, exception.__traceback__) - _LOGGER.error( # type: ignore - "Error doing job: %s", context["message"], **kwargs + _LOGGER.error( + "Error doing job: %s", context["message"], **kwargs # type: ignore ) @@ -374,7 +371,9 @@ class HomeAssistant: def block_till_done(self) -> None: """Block till all pending work is done.""" - run_coroutine_threadsafe(self.async_block_till_done(), self.loop).result() + asyncio.run_coroutine_threadsafe( + self.async_block_till_done(), self.loop + ).result() async def async_block_till_done(self) -> None: """Block till all pending work is done.""" @@ -703,8 +702,8 @@ class State: def __init__( self, entity_id: str, - state: Any, - attributes: Optional[Dict] = None, + state: str, + attributes: Optional[Mapping] = None, last_changed: Optional[datetime.datetime] = None, last_updated: Optional[datetime.datetime] = None, context: Optional[Context] = None, @@ -732,7 +731,7 @@ class State: ) self.entity_id = entity_id.lower() - self.state: str = state + self.state = state self.attributes = MappingProxyType(attributes or {}) self.last_updated = last_updated or dt_util.utcnow() self.last_changed = last_changed or self.last_updated @@ -924,7 +923,7 @@ class StateMachine: def set( self, entity_id: str, - new_state: Any, + new_state: str, attributes: Optional[Dict] = None, force_update: bool = False, context: Optional[Context] = None, @@ -950,7 +949,7 @@ class StateMachine: def async_set( self, entity_id: str, - new_state: Any, + new_state: str, attributes: Optional[Dict] = None, force_update: bool = False, context: Optional[Context] = None, @@ -1167,7 +1166,7 @@ class ServiceRegistry: Because the service is sent as an event you are not allowed to use the keys ATTR_DOMAIN and ATTR_SERVICE in your service_data. """ - return run_coroutine_threadsafe( # type: ignore + return asyncio.run_coroutine_threadsafe( self.async_call(domain, service, service_data, blocking, context), self._hass.loop, ).result() diff --git a/homeassistant/data_entry_flow.py b/homeassistant/data_entry_flow.py index 3b128646219..0bc27498f76 100644 --- a/homeassistant/data_entry_flow.py +++ b/homeassistant/data_entry_flow.py @@ -170,7 +170,7 @@ class FlowHandler: # Set by flow manager flow_id: Optional[str] = None hass: Optional[HomeAssistant] = None - handler = None + handler: Optional[Hashable] = None cur_step: Optional[Dict[str, str]] = None context: Dict @@ -188,7 +188,7 @@ class FlowHandler: data_schema: vol.Schema = None, errors: Optional[Dict] = None, description_placeholders: Optional[Dict] = None, - ) -> Dict: + ) -> Dict[str, Any]: """Return the definition of a form to gather user input.""" return { "type": RESULT_TYPE_FORM, @@ -208,7 +208,7 @@ class FlowHandler: data: Dict, description: Optional[str] = None, description_placeholders: Optional[Dict] = None, - ) -> Dict: + ) -> Dict[str, Any]: """Finish config flow and create a config entry.""" return { "version": self.VERSION, @@ -224,7 +224,7 @@ class FlowHandler: @callback def async_abort( self, *, reason: str, description_placeholders: Optional[Dict] = None - ) -> Dict: + ) -> Dict[str, Any]: """Abort the config flow.""" return { "type": RESULT_TYPE_ABORT, @@ -237,7 +237,7 @@ class FlowHandler: @callback def async_external_step( self, *, step_id: str, url: str, description_placeholders: Optional[Dict] = None - ) -> Dict: + ) -> Dict[str, Any]: """Return the definition of an external step for the user to take.""" return { "type": RESULT_TYPE_EXTERNAL_STEP, @@ -249,7 +249,7 @@ class FlowHandler: } @callback - def async_external_step_done(self, *, next_step_id: str) -> Dict: + def async_external_step_done(self, *, next_step_id: str) -> Dict[str, Any]: """Return the definition of an external step for the user to take.""" return { "type": RESULT_TYPE_EXTERNAL_STEP_DONE, diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 7f3f5c1f20d..21f57934e95 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -15,6 +15,7 @@ FLOWS = [ "daikin", "deconz", "dialogflow", + "ecobee", "emulated_roku", "esphome", "geofency", @@ -30,6 +31,7 @@ FLOWS = [ "ios", "ipma", "iqvia", + "izone", "life360", "lifx", "linky", @@ -45,6 +47,7 @@ FLOWS = [ "openuv", "owntracks", "plaato", + "plex", "point", "ps4", "rainmachine", @@ -52,6 +55,7 @@ FLOWS = [ "smartthings", "smhi", "solaredge", + "soma", "somfy", "sonos", "tellduslive", @@ -59,6 +63,7 @@ FLOWS = [ "tplink", "traccar", "tradfri", + "transmission", "twentemilieu", "twilio", "unifi", diff --git a/homeassistant/helpers/__init__.py b/homeassistant/helpers/__init__.py index dbfb9ae1864..4c1a9803d75 100644 --- a/homeassistant/helpers/__init__.py +++ b/homeassistant/helpers/__init__.py @@ -19,7 +19,8 @@ def config_per_platform(config: ConfigType, domain: str) -> Iterable[Tuple[Any, if not platform_config: continue - elif not isinstance(platform_config, list): + + if not isinstance(platform_config, list): platform_config = [platform_config] for item in platform_config: diff --git a/homeassistant/helpers/condition.py b/homeassistant/helpers/condition.py index 133251e779d..df82ba6076f 100644 --- a/homeassistant/helpers/condition.py +++ b/homeassistant/helpers/condition.py @@ -8,31 +8,31 @@ from typing import Callable, Container, Optional, Union, cast from homeassistant.helpers.template import Template from homeassistant.helpers.typing import ConfigType, TemplateVarsType - from homeassistant.core import HomeAssistant, State from homeassistant.components import zone as zone_cmp -from homeassistant.components.device_automation import ( # noqa: F401 pylint: disable=unused-import - async_device_condition_from_config as async_device_from_config, +from homeassistant.components.device_automation import ( + async_get_device_automation_platform, ) from homeassistant.const import ( ATTR_GPS_ACCURACY, ATTR_LATITUDE, ATTR_LONGITUDE, - CONF_ENTITY_ID, - CONF_VALUE_TEMPLATE, - CONF_CONDITION, - WEEKDAYS, - CONF_STATE, - CONF_ZONE, - CONF_BEFORE, - CONF_AFTER, - CONF_WEEKDAY, - SUN_EVENT_SUNRISE, - SUN_EVENT_SUNSET, - CONF_BELOW, CONF_ABOVE, + CONF_AFTER, + CONF_BEFORE, + CONF_BELOW, + CONF_CONDITION, + CONF_DOMAIN, + CONF_ENTITY_ID, + CONF_STATE, + CONF_VALUE_TEMPLATE, + CONF_WEEKDAY, + CONF_ZONE, STATE_UNAVAILABLE, STATE_UNKNOWN, + SUN_EVENT_SUNRISE, + SUN_EVENT_SUNSET, + WEEKDAYS, ) from homeassistant.exceptions import TemplateError, HomeAssistantError import homeassistant.helpers.config_validation as cv @@ -45,10 +45,12 @@ ASYNC_FROM_CONFIG_FORMAT = "async_{}_from_config" _LOGGER = logging.getLogger(__name__) +ConditionCheckerType = Callable[[HomeAssistant, TemplateVarsType], bool] + async def async_from_config( hass: HomeAssistant, config: ConfigType, config_validation: bool = True -) -> Callable[..., bool]: +) -> ConditionCheckerType: """Turn a condition configuration into a method. Should be run on the event loop. @@ -74,13 +76,15 @@ async def async_from_config( check_factory = check_factory.func if asyncio.iscoroutinefunction(check_factory): - return cast(Callable[..., bool], await factory(hass, config, config_validation)) - return cast(Callable[..., bool], factory(config, config_validation)) + return cast( + ConditionCheckerType, await factory(hass, config, config_validation) + ) + return cast(ConditionCheckerType, factory(config, config_validation)) async def async_and_from_config( hass: HomeAssistant, config: ConfigType, config_validation: bool = True -) -> Callable[..., bool]: +) -> ConditionCheckerType: """Create multi condition matcher using 'AND'.""" if config_validation: config = cv.AND_CONDITION_SCHEMA(config) @@ -107,7 +111,7 @@ async def async_and_from_config( async def async_or_from_config( hass: HomeAssistant, config: ConfigType, config_validation: bool = True -) -> Callable[..., bool]: +) -> ConditionCheckerType: """Create multi condition matcher using 'OR'.""" if config_validation: config = cv.OR_CONDITION_SCHEMA(config) @@ -205,7 +209,7 @@ def async_numeric_state( def async_numeric_state_from_config( config: ConfigType, config_validation: bool = True -) -> Callable[..., bool]: +) -> ConditionCheckerType: """Wrap action method with state based condition.""" if config_validation: config = cv.NUMERIC_STATE_CONDITION_SCHEMA(config) @@ -255,7 +259,7 @@ def state( def state_from_config( config: ConfigType, config_validation: bool = True -) -> Callable[..., bool]: +) -> ConditionCheckerType: """Wrap action method with state based condition.""" if config_validation: config = cv.STATE_CONDITION_SCHEMA(config) @@ -327,7 +331,7 @@ def sun( def sun_from_config( config: ConfigType, config_validation: bool = True -) -> Callable[..., bool]: +) -> ConditionCheckerType: """Wrap action method with sun based condition.""" if config_validation: config = cv.SUN_CONDITION_SCHEMA(config) @@ -370,7 +374,7 @@ def async_template( def async_template_from_config( config: ConfigType, config_validation: bool = True -) -> Callable[..., bool]: +) -> ConditionCheckerType: """Wrap action method with state based condition.""" if config_validation: config = cv.TEMPLATE_CONDITION_SCHEMA(config) @@ -427,7 +431,7 @@ def time( def time_from_config( config: ConfigType, config_validation: bool = True -) -> Callable[..., bool]: +) -> ConditionCheckerType: """Wrap action method with time based condition.""" if config_validation: config = cv.TIME_CONDITION_SCHEMA(config) @@ -476,7 +480,7 @@ def zone( def zone_from_config( config: ConfigType, config_validation: bool = True -) -> Callable[..., bool]: +) -> ConditionCheckerType: """Wrap action method with zone based condition.""" if config_validation: config = cv.ZONE_CONDITION_SCHEMA(config) @@ -488,3 +492,40 @@ def zone_from_config( return zone(hass, zone_entity_id, entity_id) return if_in_zone + + +async def async_device_from_config( + hass: HomeAssistant, config: ConfigType, config_validation: bool = True +) -> ConditionCheckerType: + """Test a device condition.""" + if config_validation: + config = cv.DEVICE_CONDITION_SCHEMA(config) + platform = await async_get_device_automation_platform( + hass, config[CONF_DOMAIN], "condition" + ) + return cast( + ConditionCheckerType, + platform.async_condition_from_config(config, config_validation), # type: ignore + ) + + +async def async_validate_condition_config( + hass: HomeAssistant, config: ConfigType +) -> ConfigType: + """Validate config.""" + condition = config[CONF_CONDITION] + if condition in ("and", "or"): + conditions = [] + for sub_cond in config["conditions"]: + sub_cond = await async_validate_condition_config(hass, sub_cond) + conditions.append(sub_cond) + config["conditions"] = conditions + + if condition == "device": + config = cv.DEVICE_CONDITION_SCHEMA(config) + platform = await async_get_device_automation_platform( + hass, config[CONF_DOMAIN], "condition" + ) + return cast(ConfigType, platform.CONDITION_SCHEMA(config)) # type: ignore + + return config diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index e53954a65dd..2d1bb89d23a 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -15,8 +15,9 @@ from typing import Any, Union, TypeVar, Callable, List, Dict, Optional from urllib.parse import urlparse from uuid import UUID -import voluptuous as vol from pkg_resources import parse_version +import voluptuous as vol +import voluptuous_serialize import homeassistant.util.dt as dt_util from homeassistant.const import ( @@ -52,7 +53,7 @@ from homeassistant.helpers.logging import KeywordStyleAdapter from homeassistant.util import slugify as util_slugify -# mypy: allow-incomplete-defs, allow-untyped-calls, allow-untyped-defs +# mypy: allow-untyped-calls, allow-untyped-defs # mypy: no-check-untyped-defs, no-warn-return-any # pylint: disable=invalid-name @@ -90,12 +91,12 @@ def has_at_least_one_key(*keys: str) -> Callable: for k in obj.keys(): if k in keys: return obj - raise vol.Invalid("must contain one of {}.".format(", ".join(keys))) + raise vol.Invalid("must contain at least one of {}.".format(", ".join(keys))) return validate -def has_at_most_one_key(*keys: str) -> Callable: +def has_at_most_one_key(*keys: str) -> Callable[[Dict], Dict]: """Validate that zero keys exist or one key exists.""" def validate(obj: Dict) -> Dict: @@ -224,7 +225,7 @@ def entity_ids(value: Union[str, List]) -> List[str]: comp_entity_ids = vol.Any(vol.All(vol.Lower, ENTITY_MATCH_ALL), entity_ids) -def entity_domain(domain: str): +def entity_domain(domain: str) -> Callable[[Any], str]: """Validate that entity belong to domain.""" def validate(value: Any) -> str: @@ -235,7 +236,7 @@ def entity_domain(domain: str): return validate -def entities_domain(domain: str): +def entities_domain(domain: str) -> Callable[[Union[str, List]], List[str]]: """Validate that entities belong to domain.""" def validate(values: Union[str, List]) -> List[str]: @@ -284,7 +285,7 @@ time_period_dict = vol.All( ) -def time(value) -> time_sys: +def time(value: Any) -> time_sys: """Validate and transform a time.""" if isinstance(value, time_sys): return value @@ -300,7 +301,7 @@ def time(value) -> time_sys: return time_val -def date(value) -> date_sys: +def date(value: Any) -> date_sys: """Validate and transform a date.""" if isinstance(value, date_sys): return value @@ -374,6 +375,9 @@ def positive_timedelta(value: timedelta) -> timedelta: return value +positive_time_period_dict = vol.All(time_period_dict, positive_timedelta) + + def remove_falsy(value: List[T]) -> List[T]: """Remove falsy values from a list.""" return [v for v in value if v] @@ -439,7 +443,7 @@ def string(value: Any) -> str: return str(value) -def temperature_unit(value) -> str: +def temperature_unit(value: Any) -> str: """Validate and transform temperature unit.""" value = str(value).upper() if value == "C": @@ -578,7 +582,7 @@ def deprecated( replacement_key: Optional[str] = None, invalidation_version: Optional[str] = None, default: Optional[Any] = None, -): +) -> Callable[[Dict], Dict]: """ Log key as deprecated and provide a replacement (if exists). @@ -598,8 +602,7 @@ def deprecated( else: # Unclear when it is None, but it happens, so let's guard. # https://github.com/home-assistant/home-assistant/issues/24982 - # type ignore/unreachable: https://github.com/python/typeshed/pull/3137 - module_name = __name__ # type: ignore + module_name = __name__ if replacement_key and invalidation_version: warning = ( @@ -626,7 +629,7 @@ def deprecated( " deprecated, please remove it from your configuration" ) - def check_for_invalid_version(value: Optional[Any]): + def check_for_invalid_version(value: Optional[Any]) -> None: """Raise error if current version has reached invalidation.""" if not invalidation_version: return @@ -641,7 +644,7 @@ def deprecated( ) ) - def validator(config: Dict): + def validator(config: Dict) -> Dict: """Check if key is in config and log warning.""" if key in config: value = config[key] @@ -691,6 +694,14 @@ def key_dependency(key, dependency): return validator +def custom_serializer(schema): + """Serialize additional types for voluptuous_serialize.""" + if schema is positive_time_period_dict: + return {"type": "positive_time_period_dict"} + + return voluptuous_serialize.UNSUPPORTED + + # Schemas PLATFORM_SCHEMA = vol.Schema( { @@ -827,11 +838,16 @@ OR_CONDITION_SCHEMA = vol.Schema( } ) -DEVICE_CONDITION_SCHEMA = vol.Schema( - {vol.Required(CONF_CONDITION): "device", vol.Required(CONF_DOMAIN): str}, - extra=vol.ALLOW_EXTRA, +DEVICE_CONDITION_BASE_SCHEMA = vol.Schema( + { + vol.Required(CONF_CONDITION): "device", + vol.Required(CONF_DEVICE_ID): str, + vol.Required(CONF_DOMAIN): str, + } ) +DEVICE_CONDITION_SCHEMA = DEVICE_CONDITION_BASE_SCHEMA.extend({}, extra=vol.ALLOW_EXTRA) + CONDITION_SCHEMA: vol.Schema = vol.Any( NUMERIC_STATE_CONDITION_SCHEMA, STATE_CONDITION_SCHEMA, @@ -862,11 +878,12 @@ _SCRIPT_WAIT_TEMPLATE_SCHEMA = vol.Schema( } ) -DEVICE_ACTION_SCHEMA = vol.Schema( - {vol.Required(CONF_DEVICE_ID): string, vol.Required(CONF_DOMAIN): str}, - extra=vol.ALLOW_EXTRA, +DEVICE_ACTION_BASE_SCHEMA = vol.Schema( + {vol.Required(CONF_DEVICE_ID): string, vol.Required(CONF_DOMAIN): str} ) +DEVICE_ACTION_SCHEMA = DEVICE_ACTION_BASE_SCHEMA.extend({}, extra=vol.ALLOW_EXTRA) + SCRIPT_SCHEMA = vol.All( ensure_list, [ diff --git a/homeassistant/helpers/deprecation.py b/homeassistant/helpers/deprecation.py index db1c34d8fd4..881534b5bed 100644 --- a/homeassistant/helpers/deprecation.py +++ b/homeassistant/helpers/deprecation.py @@ -54,7 +54,7 @@ def get_deprecated( and a warning is issued to the user. """ if old_name in config: - module_name = inspect.getmodule(inspect.stack()[1][0]).__name__ + module_name = inspect.getmodule(inspect.stack()[1][0]).__name__ # type: ignore logger = logging.getLogger(module_name) logger.warning( "'%s' is deprecated. Please rename '%s' to '%s' in your " diff --git a/homeassistant/helpers/device_registry.py b/homeassistant/helpers/device_registry.py index 19b4a1333b6..456678edac7 100644 --- a/homeassistant/helpers/device_registry.py +++ b/homeassistant/helpers/device_registry.py @@ -157,6 +157,7 @@ class DeviceRegistry: name_by_user=_UNDEF, new_identifiers=_UNDEF, via_device_id=_UNDEF, + remove_config_entry_id=_UNDEF, ): """Update properties of a device.""" return self._async_update_device( @@ -166,6 +167,7 @@ class DeviceRegistry: name_by_user=name_by_user, new_identifiers=new_identifiers, via_device_id=via_device_id, + remove_config_entry_id=remove_config_entry_id, ) @callback @@ -203,6 +205,10 @@ class DeviceRegistry: remove_config_entry_id is not _UNDEF and remove_config_entry_id in config_entries ): + if config_entries == {remove_config_entry_id}: + self.async_remove_device(device_id) + return + config_entries = config_entries - {remove_config_entry_id} if config_entries is not old.config_entries: diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index af8d5589c8a..836ad954ae0 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -1,9 +1,11 @@ """An abstract class for entities.""" -from datetime import timedelta + +import asyncio +from datetime import datetime, timedelta import logging import functools as ft from timeit import default_timer as timer -from typing import Any, Optional, List, Iterable +from typing import Any, Dict, Iterable, List, Optional, Union from homeassistant.const import ( ATTR_ASSUMED_STATE, @@ -22,11 +24,12 @@ from homeassistant.const import ( ATTR_SUPPORTED_FEATURES, ATTR_DEVICE_CLASS, ) +from homeassistant.helpers.entity_platform import EntityPlatform from homeassistant.helpers.entity_registry import ( EVENT_ENTITY_REGISTRY_UPDATED, RegistryEntry, ) -from homeassistant.core import HomeAssistant, callback, CALLBACK_TYPE +from homeassistant.core import HomeAssistant, callback, CALLBACK_TYPE, Context from homeassistant.config import DATA_CUSTOMIZE from homeassistant.exceptions import NoEntitySpecifiedError from homeassistant.util import ensure_unique_string, slugify @@ -94,7 +97,7 @@ class Entity: hass: Optional[HomeAssistant] = None # Owning platform instance. Will be set by EntityPlatform - platform = None + platform: Optional[EntityPlatform] = None # If we reported if this entity was slow _slow_reported = False @@ -106,7 +109,7 @@ class Entity: _update_staged = False # Process updates in parallel - parallel_updates = None + parallel_updates: Optional[asyncio.Semaphore] = None # Entry in the entity registry registry_entry: Optional[RegistryEntry] = None @@ -115,8 +118,8 @@ class Entity: _on_remove: Optional[List[CALLBACK_TYPE]] = None # Context - _context = None - _context_set = None + _context: Optional[Context] = None + _context_set: Optional[datetime] = None @property def should_poll(self) -> bool: @@ -137,12 +140,12 @@ class Entity: return None @property - def state(self) -> str: + def state(self) -> Union[None, str, int, float]: """Return the state of the entity.""" return STATE_UNKNOWN @property - def state_attributes(self): + def state_attributes(self) -> Optional[Dict[str, Any]]: """Return the state attributes. Implemented by component base class. @@ -150,7 +153,7 @@ class Entity: return None @property - def device_state_attributes(self): + def device_state_attributes(self) -> Optional[Dict[str, Any]]: """Return device specific state attributes. Implemented by platform classes. @@ -158,7 +161,7 @@ class Entity: return None @property - def device_info(self): + def device_info(self) -> Optional[Dict[str, Any]]: """Return device specific attributes. Implemented by platform classes. @@ -171,17 +174,17 @@ class Entity: return None @property - def unit_of_measurement(self): + def unit_of_measurement(self) -> Optional[str]: """Return the unit of measurement of this entity, if any.""" return None @property - def icon(self): + def icon(self) -> Optional[str]: """Return the icon to use in the frontend, if any.""" return None @property - def entity_picture(self): + def entity_picture(self) -> Optional[str]: """Return the entity picture to use in the frontend, if any.""" return None @@ -215,12 +218,12 @@ class Entity: return None @property - def context_recent_time(self): + def context_recent_time(self) -> timedelta: """Time that a context is considered recent.""" return timedelta(seconds=5) @property - def entity_registry_enabled_default(self): + def entity_registry_enabled_default(self) -> bool: """Return if the entity should be enabled when first added to the entity registry.""" return True @@ -230,12 +233,16 @@ class Entity: # produce undesirable effects in the entity's operation. @property - def enabled(self): - """Return if the entity is enabled in the entity registry.""" + def enabled(self) -> bool: + """Return if the entity is enabled in the entity registry. + + If an entity is not part of the registry, it cannot be disabled + and will therefore always be enabled. + """ return self.registry_entry is None or not self.registry_entry.disabled @callback - def async_set_context(self, context): + def async_set_context(self, context: Context) -> None: """Set the context the entity currently operates under.""" self._context = context self._context_set = dt_util.utcnow() @@ -540,7 +547,7 @@ class Entity: return self.unique_id == other.unique_id - def __repr__(self): + def __repr__(self) -> str: """Return the representation.""" return "".format(self.name, self.state) diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py index 7d5debd484d..5c59dc6c13e 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -6,7 +6,7 @@ from typing import Optional from homeassistant.const import DEVICE_DEFAULT_NAME from homeassistant.core import callback, valid_entity_id, split_entity_id from homeassistant.exceptions import HomeAssistantError, PlatformNotReady -from homeassistant.util.async_ import run_callback_threadsafe, run_coroutine_threadsafe +from homeassistant.util.async_ import run_callback_threadsafe from .entity_registry import DISABLED_INTEGRATION from .event import async_track_time_interval, async_call_later @@ -220,7 +220,7 @@ class EntityPlatform: "only inside tests or you can run into a deadlock!" ) - run_coroutine_threadsafe( + asyncio.run_coroutine_threadsafe( self.async_add_entities(list(new_entities), update_before_add), self.hass.loop, ).result() diff --git a/homeassistant/helpers/intent.py b/homeassistant/helpers/intent.py index 1fa0ec76a67..dc48d825348 100644 --- a/homeassistant/helpers/intent.py +++ b/homeassistant/helpers/intent.py @@ -124,7 +124,7 @@ class IntentHandler: intent_type: Optional[str] = None slot_schema: Optional[vol.Schema] = None - _slot_schema = None + _slot_schema: Optional[vol.Schema] = None platforms: Optional[Iterable[str]] = [] @callback diff --git a/homeassistant/helpers/logging.py b/homeassistant/helpers/logging.py index e69564453fa..dd9e3833801 100644 --- a/homeassistant/helpers/logging.py +++ b/homeassistant/helpers/logging.py @@ -29,7 +29,7 @@ class KeywordStyleAdapter(logging.LoggerAdapter): def __init__(self, logger, extra=None): """Initialize a new StyleAdapter for the provided logger.""" - super(KeywordStyleAdapter, self).__init__(logger, extra or {}) + super().__init__(logger, extra or {}) def log(self, level, msg, *args, **kwargs): """Log the message provided at the appropriate level.""" diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py index 0b569e2d4ad..05b28102726 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -1,13 +1,14 @@ """Helpers to execute scripts.""" - +import asyncio import logging from contextlib import suppress from datetime import datetime from itertools import islice -from typing import Optional, Sequence, Callable, Dict, List, Set, Tuple +from typing import Optional, Sequence, Callable, Dict, List, Set, Tuple, Any import voluptuous as vol +import homeassistant.components.device_automation as device_automation from homeassistant.core import HomeAssistant, Context, callback, CALLBACK_TYPE from homeassistant.const import ( CONF_CONDITION, @@ -27,13 +28,11 @@ from homeassistant.helpers.event import ( async_track_template, ) from homeassistant.helpers.typing import ConfigType -from homeassistant.loader import async_get_integration import homeassistant.util.dt as date_util -from homeassistant.util.async_ import run_coroutine_threadsafe, run_callback_threadsafe +from homeassistant.util.async_ import run_callback_threadsafe -# mypy: allow-incomplete-defs, allow-untyped-calls, allow-untyped-defs -# mypy: no-check-untyped-defs +# mypy: allow-untyped-calls, allow-untyped-defs, no-check-untyped-defs _LOGGER = logging.getLogger(__name__) @@ -87,6 +86,26 @@ def call_from_config( Script(hass, cv.SCRIPT_SCHEMA(config)).run(variables, context) +async def async_validate_action_config( + hass: HomeAssistant, config: ConfigType +) -> ConfigType: + """Validate config.""" + action_type = _determine_action(config) + + if action_type == ACTION_DEVICE_AUTOMATION: + platform = await device_automation.async_get_device_automation_platform( + hass, config[CONF_DOMAIN], "action" + ) + config = platform.ACTION_SCHEMA(config) # type: ignore + if action_type == ACTION_CHECK_CONDITION and config[CONF_CONDITION] == "device": + platform = await device_automation.async_get_device_automation_platform( + hass, config[CONF_DOMAIN], "condition" + ) + config = platform.CONDITION_SCHEMA(config) # type: ignore + + return config + + class _StopScript(Exception): """Throw if script needs to stop.""" @@ -101,9 +120,9 @@ class Script: def __init__( self, hass: HomeAssistant, - sequence, + sequence: Sequence[Dict[str, Any]], name: Optional[str] = None, - change_listener=None, + change_listener: Optional[Callable[..., Any]] = None, ) -> None: """Initialize the script.""" self.hass = hass @@ -137,7 +156,7 @@ class Script: def run(self, variables=None, context=None): """Run script.""" - run_coroutine_threadsafe( + asyncio.run_coroutine_threadsafe( self.async_run(variables, context), self.hass.loop ).result() @@ -336,8 +355,9 @@ class Script: """ self.last_action = action.get(CONF_ALIAS, "device automation") self._log("Executing step %s" % self.last_action) - integration = await async_get_integration(self.hass, action[CONF_DOMAIN]) - platform = integration.get_platform("device_automation") + platform = await device_automation.async_get_device_automation_platform( + self.hass, action[CONF_DOMAIN], "action" + ) await platform.async_call_action_from_config( self.hass, action, variables, context ) diff --git a/homeassistant/helpers/service.py b/homeassistant/helpers/service.py index f29d1885d1e..e177c86c65c 100644 --- a/homeassistant/helpers/service.py +++ b/homeassistant/helpers/service.py @@ -20,7 +20,6 @@ from homeassistant.loader import async_get_integration, bind_hass from homeassistant.util.yaml import load_yaml from homeassistant.util.yaml.loader import JSON_TYPE import homeassistant.helpers.config_validation as cv -from homeassistant.util.async_ import run_coroutine_threadsafe from homeassistant.helpers.typing import HomeAssistantType @@ -42,7 +41,7 @@ def call_from_config( hass, config, blocking=False, variables=None, validate_config=True ): """Call a service based on a config hash.""" - run_coroutine_threadsafe( + asyncio.run_coroutine_threadsafe( async_call_from_config(hass, config, blocking, variables, validate_config), hass.loop, ).result() @@ -105,7 +104,7 @@ def extract_entity_ids(hass, service_call, expand_group=True): Will convert group entity ids to the entity ids it represents. """ - return run_coroutine_threadsafe( + return asyncio.run_coroutine_threadsafe( async_extract_entity_ids(hass, service_call, expand_group), hass.loop ).result() diff --git a/homeassistant/helpers/state.py b/homeassistant/helpers/state.py index 7f9692b3380..2f49a566a32 100644 --- a/homeassistant/helpers/state.py +++ b/homeassistant/helpers/state.py @@ -44,7 +44,6 @@ from homeassistant.const import ( SERVICE_SELECT_OPTION, ) from homeassistant.core import Context, State, DOMAIN as HASS_DOMAIN -from homeassistant.util.async_ import run_coroutine_threadsafe from .typing import HomeAssistantType _LOGGER = logging.getLogger(__name__) @@ -122,7 +121,7 @@ def reproduce_state( blocking: bool = False, ) -> None: """Reproduce given state.""" - return run_coroutine_threadsafe( # type: ignore + return asyncio.run_coroutine_threadsafe( async_reproduce_state(hass, states, blocking), hass.loop ).result() diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index 98e3849bfb6..9af1998e894 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -7,7 +7,7 @@ import random import re from datetime import datetime from functools import wraps -from typing import Iterable +from typing import Any, Iterable import jinja2 from jinja2 import contextfilter, contextfunction @@ -25,13 +25,13 @@ from homeassistant.const import ( from homeassistant.core import State, callback, split_entity_id, valid_entity_id from homeassistant.exceptions import TemplateError from homeassistant.helpers import location as loc_helper -from homeassistant.helpers.typing import TemplateVarsType +from homeassistant.helpers.typing import HomeAssistantType, TemplateVarsType from homeassistant.loader import bind_hass from homeassistant.util import convert, dt as dt_util, location as loc_util from homeassistant.util.async_ import run_callback_threadsafe -# mypy: allow-incomplete-defs, allow-untyped-calls, allow-untyped-defs +# mypy: allow-untyped-calls, allow-untyped-defs # mypy: no-check-untyped-defs, no-warn-return-any _LOGGER = logging.getLogger(__name__) @@ -106,7 +106,7 @@ def extract_entities(template, variables=None): return MATCH_ALL -def _true(arg) -> bool: +def _true(arg: Any) -> bool: return True @@ -191,7 +191,7 @@ class Template: """Extract all entities for state_changed listener.""" return extract_entities(self.template, variables) - def render(self, variables: TemplateVarsType = None, **kwargs): + def render(self, variables: TemplateVarsType = None, **kwargs: Any) -> str: """Render given template.""" if variables is not None: kwargs.update(variables) @@ -201,7 +201,7 @@ class Template: ).result() @callback - def async_render(self, variables: TemplateVarsType = None, **kwargs) -> str: + def async_render(self, variables: TemplateVarsType = None, **kwargs: Any) -> str: """Render given template. This method must be run in the event loop. @@ -218,7 +218,7 @@ class Template: @callback def async_render_to_info( - self, variables: TemplateVarsType = None, **kwargs + self, variables: TemplateVarsType = None, **kwargs: Any ) -> RenderInfo: """Render the template and collect an entity filter.""" assert self.hass and _RENDER_INFO not in self.hass.data @@ -479,7 +479,7 @@ def _resolve_state(hass, entity_id_or_state): return None -def expand(hass, *args) -> Iterable[State]: +def expand(hass: HomeAssistantType, *args: Any) -> Iterable[State]: """Expand out any groups into entity states.""" search = list(args) found = {} @@ -635,7 +635,7 @@ def distance(hass, *args): ) -def is_state(hass, entity_id: str, state: State) -> bool: +def is_state(hass: HomeAssistantType, entity_id: str, state: State) -> bool: """Test if a state is a specific value.""" state_obj = _get_state(hass, entity_id) return state_obj is not None and state_obj.state == state diff --git a/homeassistant/loader.py b/homeassistant/loader.py index 1a9a3d256ac..272271eefb3 100644 --- a/homeassistant/loader.py +++ b/homeassistant/loader.py @@ -307,7 +307,7 @@ class IntegrationNotFound(LoaderError): def __init__(self, domain: str) -> None: """Initialize a component not found error.""" - super().__init__(f"Integration {domain} not found.") + super().__init__(f"Integration '{domain}' not found.") self.domain = domain diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index c0a79b33ad3..19acae64b16 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -1,28 +1,28 @@ PyJWT==1.7.1 PyNaCl==1.3.0 -aiohttp==3.5.4 +aiohttp==3.6.1 aiohttp_cors==0.7.0 astral==1.10.1 async_timeout==3.0.1 -attrs==19.1.0 +attrs==19.2.0 bcrypt==3.1.7 certifi>=2019.6.16 contextvars==2.4;python_version<"3.7" cryptography==2.7 distro==1.4.0 -hass-nabucasa==0.17 -home-assistant-frontend==20190919.1 -importlib-metadata==0.19 +hass-nabucasa==0.22 +home-assistant-frontend==20191002.2 +importlib-metadata==0.23 jinja2>=2.10.1 netdisco==2.6.0 pip>=8.0.3 -python-slugify==3.0.3 +python-slugify==3.0.4 pytz>=2019.02 pyyaml==5.1.2 requests==2.22.0 ruamel.yaml==0.15.100 sqlalchemy==1.3.8 -voluptuous-serialize==2.2.0 +voluptuous-serialize==2.3.0 voluptuous==0.11.7 zeroconf==0.23.0 diff --git a/homeassistant/scripts/__init__.py b/homeassistant/scripts/__init__.py index 0a9bac30188..ecac61895c5 100644 --- a/homeassistant/scripts/__init__.py +++ b/homeassistant/scripts/__init__.py @@ -5,7 +5,7 @@ import importlib import logging import os import sys -from typing import List +from typing import List, Optional, Sequence, Text from homeassistant.bootstrap import async_mount_local_lib_path from homeassistant.config import get_default_config_dir @@ -13,7 +13,7 @@ from homeassistant.requirements import pip_kwargs from homeassistant.util.package import install_package, is_virtual_env, is_installed -# mypy: allow-untyped-defs, allow-incomplete-defs, no-warn-return-any +# mypy: allow-untyped-defs, no-warn-return-any def run(args: List) -> int: @@ -23,7 +23,8 @@ def run(args: List) -> int: for fil in os.listdir(path): if fil == "__pycache__": continue - elif os.path.isdir(os.path.join(path, fil)): + + if os.path.isdir(os.path.join(path, fil)): scripts.append(fil) elif fil != "__init__.py" and fil.endswith(".py"): scripts.append(fil[:-3]) @@ -62,13 +63,13 @@ def run(args: List) -> int: return script.run(args[1:]) # type: ignore -def extract_config_dir(args=None) -> str: +def extract_config_dir(args: Optional[Sequence[Text]] = None) -> str: """Extract the config dir from the arguments or get the default.""" parser = argparse.ArgumentParser(add_help=False) parser.add_argument("-c", "--config", default=None) - args = parser.parse_known_args(args)[0] + parsed_args = parser.parse_known_args(args)[0] return ( - os.path.join(os.getcwd(), args.config) - if args.config + os.path.join(os.getcwd(), parsed_args.config) + if parsed_args.config else get_default_config_dir() ) diff --git a/homeassistant/setup.py b/homeassistant/setup.py index 58e4fc19eb0..07de3b2942d 100644 --- a/homeassistant/setup.py +++ b/homeassistant/setup.py @@ -10,7 +10,6 @@ from homeassistant import requirements, core, loader, config as conf_util from homeassistant.config import async_notify_setup_error from homeassistant.const import EVENT_COMPONENT_LOADED, PLATFORM_FORMAT from homeassistant.exceptions import HomeAssistantError -from homeassistant.util.async_ import run_coroutine_threadsafe _LOGGER = logging.getLogger(__name__) @@ -25,7 +24,7 @@ SLOW_SETUP_WARNING = 10 def setup_component(hass: core.HomeAssistant, domain: str, config: Dict) -> bool: """Set up a component and all its dependencies.""" - return run_coroutine_threadsafe( # type: ignore + return asyncio.run_coroutine_threadsafe( async_setup_component(hass, domain, config), hass.loop ).result() diff --git a/homeassistant/util/async_.py b/homeassistant/util/async_.py index 271b9caa62a..64bedfe2501 100644 --- a/homeassistant/util/async_.py +++ b/homeassistant/util/async_.py @@ -1,30 +1,20 @@ -"""Asyncio backports for Python 3.4.3 compatibility.""" +"""Asyncio backports for Python 3.6 compatibility.""" import concurrent.futures import threading import logging from asyncio import coroutines from asyncio.events import AbstractEventLoop -from asyncio.futures import Future import asyncio from asyncio import ensure_future -from typing import ( - Any, - Union, - Coroutine, - Callable, - Generator, - TypeVar, - Awaitable, - Optional, -) +from typing import Any, Coroutine, Callable, TypeVar, Awaitable _LOGGER = logging.getLogger(__name__) try: # pylint: disable=invalid-name - asyncio_run = asyncio.run + asyncio_run = asyncio.run # type: ignore except AttributeError: _T = TypeVar("_T") @@ -40,134 +30,6 @@ except AttributeError: loop.close() -def _set_result_unless_cancelled(fut: Future, result: Any) -> None: - """Set the result only if the Future was not cancelled.""" - if fut.cancelled(): - return - fut.set_result(result) - - -def _set_concurrent_future_state( - concurr: concurrent.futures.Future, source: Union[concurrent.futures.Future, Future] -) -> None: - """Copy state from a future to a concurrent.futures.Future.""" - assert source.done() - if source.cancelled(): - concurr.cancel() - if not concurr.set_running_or_notify_cancel(): - return - exception = source.exception() - if exception is not None: - concurr.set_exception(exception) - else: - result = source.result() - concurr.set_result(result) - - -def _copy_future_state( - source: Union[concurrent.futures.Future, Future], - dest: Union[concurrent.futures.Future, Future], -) -> None: - """Copy state from another Future. - - The other Future may be a concurrent.futures.Future. - """ - assert source.done() - if dest.cancelled(): - return - assert not dest.done() - if source.cancelled(): - dest.cancel() - else: - exception = source.exception() - if exception is not None: - dest.set_exception(exception) - else: - result = source.result() - dest.set_result(result) - - -def _chain_future( - source: Union[concurrent.futures.Future, Future], - destination: Union[concurrent.futures.Future, Future], -) -> None: - """Chain two futures so that when one completes, so does the other. - - The result (or exception) of source will be copied to destination. - If destination is cancelled, source gets cancelled too. - Compatible with both asyncio.Future and concurrent.futures.Future. - """ - if not isinstance(source, (Future, concurrent.futures.Future)): - raise TypeError("A future is required for source argument") - if not isinstance(destination, (Future, concurrent.futures.Future)): - raise TypeError("A future is required for destination argument") - # pylint: disable=protected-access - if isinstance(source, Future): - source_loop: Optional[AbstractEventLoop] = source._loop - else: - source_loop = None - if isinstance(destination, Future): - dest_loop: Optional[AbstractEventLoop] = destination._loop - else: - dest_loop = None - - def _set_state( - future: Union[concurrent.futures.Future, Future], - other: Union[concurrent.futures.Future, Future], - ) -> None: - if isinstance(future, Future): - _copy_future_state(other, future) - else: - _set_concurrent_future_state(future, other) - - def _call_check_cancel( - destination: Union[concurrent.futures.Future, Future] - ) -> None: - if destination.cancelled(): - if source_loop is None or source_loop is dest_loop: - source.cancel() - else: - source_loop.call_soon_threadsafe(source.cancel) - - def _call_set_state(source: Union[concurrent.futures.Future, Future]) -> None: - if dest_loop is None or dest_loop is source_loop: - _set_state(destination, source) - else: - dest_loop.call_soon_threadsafe(_set_state, destination, source) - - destination.add_done_callback(_call_check_cancel) - source.add_done_callback(_call_set_state) - - -def run_coroutine_threadsafe( - coro: Union[Coroutine, Generator], loop: AbstractEventLoop -) -> concurrent.futures.Future: - """Submit a coroutine object to a given event loop. - - Return a concurrent.futures.Future to access the result. - """ - ident = loop.__dict__.get("_thread_ident") - if ident is not None and ident == threading.get_ident(): - raise RuntimeError("Cannot be called from within the event loop") - - if not coroutines.iscoroutine(coro): - raise TypeError("A coroutine object is required") - future: concurrent.futures.Future = concurrent.futures.Future() - - def callback() -> None: - """Handle the call to the coroutine.""" - try: - _chain_future(ensure_future(coro, loop=loop), future) - except Exception as exc: # pylint: disable=broad-except - if future.set_running_or_notify_cancel(): - future.set_exception(exc) - else: - _LOGGER.warning("Exception on lost future: ", exc_info=True) - - loop.call_soon_threadsafe(callback) - return future - - def fire_coroutine_threadsafe(coro: Coroutine, loop: AbstractEventLoop) -> None: """Submit a coroutine object to a given event loop. diff --git a/homeassistant/util/location.py b/homeassistant/util/location.py index 7c61a8ab1e9..f81c40a52bb 100644 --- a/homeassistant/util/location.py +++ b/homeassistant/util/location.py @@ -6,7 +6,7 @@ detect_location_info and elevation are mocked by default during tests. import asyncio import collections import math -from typing import Any, Optional, Tuple, Dict +from typing import Any, Optional, Tuple, Dict, cast import aiohttp @@ -159,7 +159,7 @@ def vincenty( if miles: s *= MILES_PER_KILOMETER # kilometers to miles - return round(s, 6) + return round(cast(float, s), 6) async def _get_ipapi(session: aiohttp.ClientSession) -> Optional[Dict[str, Any]]: diff --git a/homeassistant/util/logging.py b/homeassistant/util/logging.py index 236e2fc1aa2..99e606d2866 100644 --- a/homeassistant/util/logging.py +++ b/homeassistant/util/logging.py @@ -8,8 +8,6 @@ import threading import traceback from typing import Any, Callable, Coroutine, Optional -from .async_ import run_coroutine_threadsafe - class HideSensitiveDataFilter(logging.Filter): """Filter API password calls.""" @@ -83,7 +81,9 @@ class AsyncHandler: def _process(self) -> None: """Process log in a thread.""" while True: - record = run_coroutine_threadsafe(self._queue.get(), self.loop).result() + record = asyncio.run_coroutine_threadsafe( + self._queue.get(), self.loop + ).result() if record is None: self.handler.close() @@ -130,7 +130,7 @@ def catch_log_exception( """Decorate a callback to catch and log exceptions.""" def log_exception(*args: Any) -> None: - module_name = inspect.getmodule(inspect.trace()[1][0]).__name__ + module_name = inspect.getmodule(inspect.trace()[1][0]).__name__ # type: ignore # Do not print the wrapper in the traceback frames = len(inspect.trace()) - 1 exc_msg = traceback.format_exc(-frames) @@ -178,7 +178,9 @@ def catch_log_coro_exception( try: return await target except Exception: # pylint: disable=broad-except - module_name = inspect.getmodule(inspect.trace()[1][0]).__name__ + module_name = inspect.getmodule( # type: ignore + inspect.trace()[1][0] + ).__name__ # Do not print the wrapper in the traceback frames = len(inspect.trace()) - 1 exc_msg = traceback.format_exc(-frames) diff --git a/homeassistant/util/yaml/loader.py b/homeassistant/util/yaml/loader.py index ccc55691ee1..9822b7c63c2 100644 --- a/homeassistant/util/yaml/loader.py +++ b/homeassistant/util/yaml/loader.py @@ -48,7 +48,7 @@ class SafeLineLoader(yaml.SafeLoader): def compose_node(self, parent: yaml.nodes.Node, index: int) -> yaml.nodes.Node: """Annotate a node with the first line it was seen.""" last_line: int = self.line - node: yaml.nodes.Node = super(SafeLineLoader, self).compose_node(parent, index) + node: yaml.nodes.Node = super().compose_node(parent, index) node.__line__ = last_line + 1 # type: ignore return node diff --git a/mypyrc b/mypyrc index f3866f40e57..08413ecd23c 100644 --- a/mypyrc +++ b/mypyrc @@ -6,8 +6,10 @@ homeassistant/components/binary_sensor/ homeassistant/components/calendar/ homeassistant/components/camera/ homeassistant/components/cover/ +homeassistant/components/device_automation/ homeassistant/components/frontend/ homeassistant/components/geo_location/ +homeassistant/components/group/ homeassistant/components/history/ homeassistant/components/http/ homeassistant/components/image_processing/ @@ -17,16 +19,20 @@ homeassistant/components/lock/ homeassistant/components/mailbox/ homeassistant/components/media_player/ homeassistant/components/notify/ +homeassistant/components/persistent_notification/ homeassistant/components/proximity/ homeassistant/components/remote/ homeassistant/components/scene/ homeassistant/components/sensor/ +homeassistant/components/sun/ homeassistant/components/switch/ homeassistant/components/systemmonitor/ homeassistant/components/tts/ homeassistant/components/vacuum/ homeassistant/components/water_heater/ homeassistant/components/weather/ +homeassistant/components/websocket_api/ +homeassistant/components/zone/ homeassistant/helpers/ homeassistant/scripts/ homeassistant/util/ diff --git a/requirements_all.txt b/requirements_all.txt index 07b6599cd69..44c10f8ff29 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1,23 +1,23 @@ # Home Assistant core -aiohttp==3.5.4 +aiohttp==3.6.1 astral==1.10.1 async_timeout==3.0.1 -attrs==19.1.0 +attrs==19.2.0 bcrypt==3.1.7 certifi>=2019.6.16 contextvars==2.4;python_version<"3.7" -importlib-metadata==0.19 +importlib-metadata==0.23 jinja2>=2.10.1 PyJWT==1.7.1 cryptography==2.7 pip>=8.0.3 -python-slugify==3.0.3 +python-slugify==3.0.4 pytz>=2019.02 pyyaml==5.1.2 requests==2.22.0 ruamel.yaml==0.15.100 voluptuous==0.11.7 -voluptuous-serialize==2.2.0 +voluptuous-serialize==2.3.0 # homeassistant.components.nuimo_controller --only-binary=all nuimo==0.1.0 @@ -35,7 +35,7 @@ Adafruit-SHT31==1.0.2 # Adafruit_BBIO==1.0.0 # homeassistant.components.homekit -HAP-python==2.5.0 +HAP-python==2.6.0 # homeassistant.components.mastodon Mastodon.py==1.4.6 @@ -100,7 +100,7 @@ TwitterAPI==2.5.9 WazeRouteCalculator==0.10 # homeassistant.components.yessssms -YesssSMS==0.2.3 +YesssSMS==0.4.1 # homeassistant.components.abode abodepy==0.15.0 @@ -111,6 +111,9 @@ adafruit-blinka==1.2.1 # homeassistant.components.mcp23017 adafruit-circuitpython-mcp230xx==1.1.2 +# homeassistant.components.androidtv +adb-shell==0.0.4 + # homeassistant.components.adguard adguardhome==0.2.1 @@ -118,7 +121,7 @@ adguardhome==0.2.1 afsapi==0.0.4 # homeassistant.components.geonetnz_quakes -aio_geojson_geonetnz_quakes==0.9 +aio_geojson_geonetnz_quakes==0.10 # homeassistant.components.ambient_station aioambient==0.3.2 @@ -152,7 +155,7 @@ aioharmony==0.1.13 aiohttp_cors==0.7.0 # homeassistant.components.hue -aiohue==1.9.1 +aiohue==1.9.2 # homeassistant.components.imap aioimaplib==0.7.15 @@ -179,7 +182,7 @@ aioswitcher==2019.4.26 aiounifi==11 # homeassistant.components.wwlln -aiowwlln==2.0.1 +aiowwlln==2.0.2 # homeassistant.components.aladdin_connect aladdin_connect==0.3 @@ -197,7 +200,7 @@ ambiclimate==0.2.1 amcrest==1.5.3 # homeassistant.components.androidtv -androidtv==0.0.27 +androidtv==0.0.30 # homeassistant.components.anel_pwrctrl anel_pwrctrl-homeassistant==0.0.1.dev2 @@ -260,16 +263,14 @@ batinfo==0.4.2 # homeassistant.components.eddystone_temperature # beacontools[scan]==1.2.3 -# homeassistant.components.linksys_ap # homeassistant.components.scrape -# homeassistant.components.sytadin beautifulsoup4==4.8.0 # homeassistant.components.beewi_smartclim beewi_smartclim==0.0.7 # homeassistant.components.zha -bellows-homeassistant==0.9.1 +bellows-homeassistant==0.10.0 # homeassistant.components.bmw_connected_drive bimmer_connected==0.6.0 @@ -301,7 +302,7 @@ bomradarloop==0.1.3 # homeassistant.components.amazon_polly # homeassistant.components.route53 -boto3==1.9.16 +boto3==1.9.233 # homeassistant.components.braviatv braviarc-homeassistant==0.3.7.dev0 @@ -355,7 +356,7 @@ colorlog==4.0.2 concord232==0.15 # homeassistant.components.upc_connect -connect-box==0.2.4 +connect-box==0.2.5 # homeassistant.components.eddystone_temperature # homeassistant.components.eq3btsmart @@ -446,7 +447,7 @@ enocean==0.50 enturclient==0.2.0 # homeassistant.components.environment_canada -env_canada==0.0.24 +env_canada==0.0.25 # homeassistant.components.envirophat # envirophat==0.0.6 @@ -479,9 +480,6 @@ evohome-async==0.3.3b4 # homeassistant.components.fastdotcom fastdotcom==0.0.3 -# homeassistant.components.fedex -fedexdeliverymanager==1.0.6 - # homeassistant.components.feedreader feedparser-homeassistant==5.2.2.dev1 @@ -527,7 +525,7 @@ gearbest_parser==1.0.7 geizhals==0.0.9 # homeassistant.components.geniushub -geniushub-client==0.6.13 +geniushub-client==0.6.26 # homeassistant.components.geo_json_events # homeassistant.components.nsw_rural_fire_service_feed @@ -576,7 +574,7 @@ googlemaps==2.5.1 goslide-api==0.5.1 # homeassistant.components.remote_rpi_gpio -gpiozero==1.4.1 +gpiozero==1.5.1 # homeassistant.components.gpsd gps3==0.33.3 @@ -609,10 +607,10 @@ habitipy==0.2.0 hangups==0.4.9 # homeassistant.components.cloud -hass-nabucasa==0.17 +hass-nabucasa==0.22 # homeassistant.components.mqtt -hbmqtt==0.9.4 +hbmqtt==0.9.5 # homeassistant.components.jewish_calendar hdate==0.9.0 @@ -620,6 +618,9 @@ hdate==0.9.0 # homeassistant.components.heatmiser heatmiserV3==0.9.1 +# homeassistant.components.here_travel_time +herepy==0.6.3.1 + # homeassistant.components.hikvisioncam hikvision==0.4 @@ -639,7 +640,7 @@ hole==0.5.0 holidays==0.9.11 # homeassistant.components.frontend -home-assistant-frontend==20190919.1 +home-assistant-frontend==20191002.2 # homeassistant.components.zwave homeassistant-pyozw==0.1.4 @@ -648,7 +649,7 @@ homeassistant-pyozw==0.1.4 homekit[IP]==0.15.0 # homeassistant.components.homematicip_cloud -homematicip==0.10.10 +homematicip==0.10.12 # homeassistant.components.horizon horimote==0.4.1 @@ -684,16 +685,16 @@ iglo==1.2.7 ihcsdk==2.3.0 # homeassistant.components.incomfort -incomfort-client==0.3.1 +incomfort-client==0.3.5 # homeassistant.components.influxdb -influxdb==5.2.0 +influxdb==5.2.3 # homeassistant.components.insteon insteonplm==0.16.5 # homeassistant.components.iperf3 -iperf3==0.1.10 +iperf3==0.1.11 # homeassistant.components.route53 ipify==1.0.0 @@ -707,6 +708,9 @@ jsonrpc-async==0.6 # homeassistant.components.kodi jsonrpc-websocket==0.6 +# homeassistant.components.kaiterra +kaiterra-async-client==0.0.2 + # homeassistant.components.keba keba-kecontact==0.2.0 @@ -732,7 +736,7 @@ libpurecool==0.5.0 libpyfoscam==1.0 # homeassistant.components.vivotek -libpyvivotek==0.2.1 +libpyvivotek==0.2.2 # homeassistant.components.mikrotik librouteros==2.3.0 @@ -801,7 +805,7 @@ mbddns==0.1.2 messagebird==1.2.0 # homeassistant.components.meteoalarm -meteoalertapi==0.1.5 +meteoalertapi==0.1.6 # homeassistant.components.meteo_france meteofrance==0.3.7 @@ -833,9 +837,6 @@ mychevy==1.2.0 # homeassistant.components.mycroft mycroftapi==2.0 -# homeassistant.components.usps -myusps==1.3.2 - # homeassistant.components.n26 n26==0.2.7 @@ -843,7 +844,7 @@ n26==0.2.7 nad_receiver==0.0.11 # homeassistant.components.keenetic_ndms2 -ndms2_client==0.0.8 +ndms2_client==0.0.9 # homeassistant.components.ness_alarm nessclient==0.9.15 @@ -864,9 +865,6 @@ niko-home-control==0.2.1 # homeassistant.components.nilu niluclient==0.1.2 -# homeassistant.components.withings -nokia==1.2.0 - # homeassistant.components.nederlandse_spoorwegen nsapi==2.7.4 @@ -913,14 +911,11 @@ opensensemap-api==0.1.5 openwebifpy==3.1.1 # homeassistant.components.luci -openwrt-luci-rpc==1.1.0 +openwrt-luci-rpc==1.1.1 # homeassistant.components.orvibo orvibo==1.1.1 -# homeassistant.components.luci -packaging==19.1 - # homeassistant.components.mqtt # homeassistant.components.shiftr paho-mqtt==1.4.0 @@ -955,9 +950,9 @@ piglow==1.2.4 # homeassistant.components.pilight pilight==0.1.1 +# homeassistant.components.image_processing # homeassistant.components.proxy # homeassistant.components.qrcode -# homeassistant.components.tensorflow pillow==6.1.0 # homeassistant.components.dominos @@ -966,6 +961,9 @@ pizzapi==0.0.3 # homeassistant.components.plex plexapi==3.0.6 +# homeassistant.components.plex +plexauth==0.0.4 + # homeassistant.components.plum_lightpad plumlightpad==0.0.11 @@ -1059,7 +1057,7 @@ pyW215==0.6.0 pyW800rf32==0.1 # homeassistant.components.nextbus -py_nextbus==0.1.2 +py_nextbusnext==0.1.4 # homeassistant.components.noaa_tides # py_noaa==0.3.0 @@ -1134,7 +1132,7 @@ pydaikin==1.6.1 pydanfossair==0.1.0 # homeassistant.components.deconz -pydeconz==62 +pydeconz==63 # homeassistant.components.delijn pydelijn==0.5.1 @@ -1142,6 +1140,9 @@ pydelijn==0.5.1 # homeassistant.components.zwave pydispatcher==2.0.5 +# homeassistant.components.doods +pydoods==1.0.2 + # homeassistant.components.android_ip_webcam pydroid-ipcam==0.8 @@ -1222,7 +1223,7 @@ pyheos==0.6.0 pyhik==0.2.3 # homeassistant.components.hive -pyhiveapi==0.2.18.1 +pyhiveapi==0.2.19.2 # homeassistant.components.homematic pyhomematic==0.1.60 @@ -1288,7 +1289,7 @@ pyloopenergy==0.1.3 pylutron-caseta==0.5.0 # homeassistant.components.lutron -pylutron==0.2.2 +pylutron==0.2.5 # homeassistant.components.mailgun pymailgunner==1.4 @@ -1339,13 +1340,19 @@ pynuki==1.3.3 pynut2==2.1.2 # homeassistant.components.nws -pynws==0.7.4 +pynws==0.8.1 # homeassistant.components.nx584 pynx584==0.4 +# homeassistant.components.nzbget +pynzbgetapi==0.2.0 + # homeassistant.components.obihai -pyobihai==1.1.0 +pyobihai==1.2.0 + +# homeassistant.components.ombi +pyombi==0.1.5 # homeassistant.components.openuv pyopenuv==1.0.9 @@ -1362,16 +1369,16 @@ pyotgw==0.4b4 # homeassistant.auth.mfa_modules.notify # homeassistant.auth.mfa_modules.totp # homeassistant.components.otp -pyotp==2.2.7 +pyotp==2.3.0 # homeassistant.components.owlet -pyowlet==1.0.2 +pyowlet==1.0.3 # homeassistant.components.openweathermap pyowm==2.10.0 # homeassistant.components.elv -pypca==0.0.4 +pypca==0.0.5 # homeassistant.components.lcn pypck==0.6.3 @@ -1392,7 +1399,7 @@ pyqwikswitch==0.93 pyrail==0.0.3 # homeassistant.components.rainbird -pyrainbird==0.2.1 +pyrainbird==0.4.1 # homeassistant.components.recswitch pyrecswitch==1.0.2 @@ -1403,6 +1410,9 @@ pyrepetier==3.0.5 # homeassistant.components.sabnzbd pysabnzbd==1.1.0 +# homeassistant.components.saj +pysaj==0.0.9 + # homeassistant.components.sony_projector pysdcp==1 @@ -1436,6 +1446,9 @@ pysmarty==0.8 # homeassistant.components.snmp pysnmp==4.4.11 +# homeassistant.components.soma +pysoma==0.0.10 + # homeassistant.components.sonos pysonos==0.0.23 @@ -1464,7 +1477,7 @@ pytautulli==0.5.0 pyteleloisirs==3.5 # homeassistant.components.tfiac -pytfiac==0.3 +pytfiac==0.4 # homeassistant.components.thinkingcleaner pythinkingcleaner==0.0.3 @@ -1479,7 +1492,7 @@ python-clementine-remote==1.0.1 python-digitalocean==1.13.2 # homeassistant.components.ecobee -python-ecobee-api==0.0.21 +python-ecobee-api==0.1.4 # homeassistant.components.eq3btsmart # python-eq3bt==0.1.9 @@ -1502,6 +1515,9 @@ python-gitlab==1.6.0 # homeassistant.components.hp_ilo python-hpilo==4.3 +# homeassistant.components.izone +python-izone==1.1.1 + # homeassistant.components.joaoapps_join python-join-api==0.0.4 @@ -1563,7 +1579,7 @@ python-velbus==2.0.27 python-vlc==1.1.2 # homeassistant.components.whois -python-whois==0.7.1 +python-whois==0.7.2 # homeassistant.components.wink python-wink==1.10.5 @@ -1575,7 +1591,7 @@ python_awair==0.0.4 python_opendata_transport==0.1.4 # homeassistant.components.egardia -pythonegardia==1.0.39 +pythonegardia==1.0.40 # homeassistant.components.tile pytile==2.0.6 @@ -1606,7 +1622,7 @@ pyuptimerobot==0.0.5 # pyuserinput==0.1.11 # homeassistant.components.vera -pyvera==0.3.4 +pyvera==0.3.6 # homeassistant.components.vesync pyvesync==1.1.0 @@ -1660,7 +1676,7 @@ recollect-waste==1.0.1 regenmaschine==1.5.1 # homeassistant.components.python_script -restrictedpython==4.0 +restrictedpython==5.0 # homeassistant.components.idteck_prox rfk101py==0.0.1 @@ -1714,7 +1730,7 @@ schiene==0.23 scsgate==0.1.0 # homeassistant.components.sendgrid -sendgrid==6.0.5 +sendgrid==6.1.0 # homeassistant.components.sensehat sense-hat==2.2.0 @@ -1726,13 +1742,13 @@ sense_energy==0.7.0 sharp_aquos_rc==0.3.2 # homeassistant.components.shodan -shodan==1.15.0 +shodan==1.19.0 # homeassistant.components.simplepush simplepush==1.1.4 # homeassistant.components.simplisafe -simplisafe-python==4.3.0 +simplisafe-python==5.0.1 # homeassistant.components.sisyphus sisyphus-control==2.2.1 @@ -1773,13 +1789,13 @@ snapcast==2.0.10 socialbladeclient==0.2 # homeassistant.components.solaredge_local -solaredge-local==0.1.4 +solaredge-local==0.2.0 # homeassistant.components.solaredge solaredge==0.0.2 # homeassistant.components.solax -solax==0.1.2 +solax==0.2.2 # homeassistant.components.honeywell somecomfort==0.5.2 @@ -1803,9 +1819,6 @@ spotipy-homeassistant==2.4.4.dev1 # homeassistant.components.sql sqlalchemy==1.3.8 -# homeassistant.components.srp_energy -srpenergy==1.0.6 - # homeassistant.components.starlingbank starlingbank==3.1 @@ -1872,7 +1885,7 @@ thingspeak==0.4.1 tikteck==0.4 # homeassistant.components.todoist -todoist-python==7.0.17 +todoist-python==8.0.0 # homeassistant.components.toon toonapilib==3.2.4 @@ -1901,9 +1914,6 @@ twilio==6.19.1 # homeassistant.components.upcloud upcloud-api==0.4.3 -# homeassistant.components.ups -upsmychoice==1.0.6 - # homeassistant.components.uscis uscisstatus==0.1.1 @@ -1960,6 +1970,9 @@ websockets==6.0 # homeassistant.components.wirelesstag wirelesstagpy==0.4.0 +# homeassistant.components.withings +withings-api==2.0.0b7 + # homeassistant.components.wunderlist wunderpy2==0.1.6 @@ -1973,7 +1986,7 @@ xboxapi==0.1.1 xfinity-gateway==0.0.4 # homeassistant.components.knx -xknx==0.11.1 +xknx==0.11.2 # homeassistant.components.bluesound # homeassistant.components.startca @@ -1985,6 +1998,9 @@ xmltodict==0.12.0 # homeassistant.components.xs1 xs1-api-client==2.3.5 +# homeassistant.components.yandex_transport +ya_ma==0.3.7 + # homeassistant.components.yweather yahooweather==0.10 @@ -1998,7 +2014,7 @@ yeelight==0.5.0 yeelightsunflower==0.0.10 # homeassistant.components.media_extractor -youtube_dl==2019.09.01 +youtube_dl==2019.09.28 # homeassistant.components.zengge zengge==0.2 @@ -2007,7 +2023,7 @@ zengge==0.2 zeroconf==0.23.0 # homeassistant.components.zha -zha-quirks==0.0.23 +zha-quirks==0.0.26 # homeassistant.components.zhong_hong zhong_hong_hvac==1.0.9 @@ -2016,16 +2032,16 @@ zhong_hong_hvac==1.0.9 ziggo-mediabox-xl==1.1.0 # homeassistant.components.zha -zigpy-deconz==0.3.0 +zigpy-deconz==0.5.0 # homeassistant.components.zha -zigpy-homeassistant==0.8.0 +zigpy-homeassistant==0.9.0 # homeassistant.components.zha -zigpy-xbee-homeassistant==0.4.0 +zigpy-xbee-homeassistant==0.5.0 # homeassistant.components.zha -zigpy-zigate==0.3.1 +zigpy-zigate==0.4.1 # homeassistant.components.zoneminder zm-py==0.3.3 diff --git a/requirements_test.txt b/requirements_test.txt index bfe459b0cfb..9da375b33c8 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -9,13 +9,14 @@ codecov==2.0.15 flake8-docstrings==1.3.1 flake8==3.7.8 mock-open==1.3.1 -mypy==0.720 -pre-commit==1.18.2 +mypy==0.730 +pre-commit==1.18.3 pydocstyle==4.0.1 pylint==2.3.1 +astroid==2.2.5 pytest-aiohttp==0.3.0 pytest-cov==2.7.1 pytest-sugar==0.9.2 pytest-timeout==1.3.3 -pytest==5.1.1 -requests_mock==1.6.0 +pytest==5.2.0 +requests_mock==1.7.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f879f546c07..2a8f51cd195 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -10,20 +10,21 @@ codecov==2.0.15 flake8-docstrings==1.3.1 flake8==3.7.8 mock-open==1.3.1 -mypy==0.720 -pre-commit==1.18.2 +mypy==0.730 +pre-commit==1.18.3 pydocstyle==4.0.1 pylint==2.3.1 +astroid==2.2.5 pytest-aiohttp==0.3.0 pytest-cov==2.7.1 pytest-sugar==0.9.2 pytest-timeout==1.3.3 -pytest==5.1.1 -requests_mock==1.6.0 +pytest==5.2.0 +requests_mock==1.7.0 # homeassistant.components.homekit -HAP-python==2.5.0 +HAP-python==2.6.0 # homeassistant.components.mobile_app # homeassistant.components.owntracks @@ -36,13 +37,13 @@ PyRMVtransport==0.1.3 PyTransportNSW==0.1.1 # homeassistant.components.yessssms -YesssSMS==0.2.3 +YesssSMS==0.4.1 # homeassistant.components.adguard adguardhome==0.2.1 # homeassistant.components.geonetnz_quakes -aio_geojson_geonetnz_quakes==0.9 +aio_geojson_geonetnz_quakes==0.10 # homeassistant.components.ambient_station aioambient==0.3.2 @@ -61,7 +62,7 @@ aioesphomeapi==2.2.0 aiohttp_cors==0.7.0 # homeassistant.components.hue -aiohue==1.9.1 +aiohue==1.9.2 # homeassistant.components.notion aionotion==1.1.0 @@ -73,13 +74,13 @@ aioswitcher==2019.4.26 aiounifi==11 # homeassistant.components.wwlln -aiowwlln==2.0.1 +aiowwlln==2.0.2 # homeassistant.components.ambiclimate ambiclimate==0.2.1 # homeassistant.components.androidtv -androidtv==0.0.27 +androidtv==0.0.30 # homeassistant.components.apns apns2==0.3.0 @@ -94,7 +95,7 @@ av==6.1.2 axis==25 # homeassistant.components.zha -bellows-homeassistant==0.9.1 +bellows-homeassistant==0.10.0 # homeassistant.components.caldav caldav==0.6.1 @@ -163,14 +164,17 @@ ha-ffmpeg==2.0 hangups==0.4.9 # homeassistant.components.cloud -hass-nabucasa==0.17 +hass-nabucasa==0.22 # homeassistant.components.mqtt -hbmqtt==0.9.4 +hbmqtt==0.9.5 # homeassistant.components.jewish_calendar hdate==0.9.0 +# homeassistant.components.here_travel_time +herepy==0.6.3.1 + # homeassistant.components.pi_hole hole==0.5.0 @@ -178,13 +182,13 @@ hole==0.5.0 holidays==0.9.11 # homeassistant.components.frontend -home-assistant-frontend==20190919.1 +home-assistant-frontend==20191002.2 # homeassistant.components.homekit_controller homekit[IP]==0.15.0 # homeassistant.components.homematicip_cloud -homematicip==0.10.10 +homematicip==0.10.12 # homeassistant.components.google # homeassistant.components.remember_the_milk @@ -197,7 +201,7 @@ huawei-lte-api==1.3.0 iaqualink==0.2.9 # homeassistant.components.influxdb -influxdb==5.2.0 +influxdb==5.2.3 # homeassistant.components.verisure jsonpath==0.75 @@ -224,9 +228,6 @@ minio==4.0.9 # homeassistant.components.ssdp netdisco==2.6.0 -# homeassistant.components.withings -nokia==1.2.0 - # homeassistant.components.iqvia # homeassistant.components.opencv # homeassistant.components.tensorflow @@ -249,6 +250,17 @@ pexpect==4.6.0 # homeassistant.components.pilight pilight==0.1.1 +# homeassistant.components.image_processing +# homeassistant.components.proxy +# homeassistant.components.qrcode +pillow==6.1.0 + +# homeassistant.components.plex +plexapi==3.0.6 + +# homeassistant.components.plex +plexauth==0.0.4 + # homeassistant.components.mhz19 # homeassistant.components.serial_pm pmsensor==0.4 @@ -282,7 +294,7 @@ pyblackbird==0.5 pychromecast==4.0.1 # homeassistant.components.deconz -pydeconz==62 +pydeconz==63 # homeassistant.components.zwave pydispatcher==2.0.5 @@ -309,7 +321,7 @@ pymfy==0.5.2 pymonoprice==0.3 # homeassistant.components.nws -pynws==0.7.4 +pynws==0.8.1 # homeassistant.components.nx584 pynx584==0.4 @@ -320,7 +332,7 @@ pyopenuv==1.0.9 # homeassistant.auth.mfa_modules.notify # homeassistant.auth.mfa_modules.totp # homeassistant.components.otp -pyotp==2.2.7 +pyotp==2.3.0 # homeassistant.components.ps4 pyps4-homeassistant==0.8.7 @@ -337,15 +349,24 @@ pysmartapp==0.3.2 # homeassistant.components.smartthings pysmartthings==0.6.9 +# homeassistant.components.soma +pysoma==0.0.10 + # homeassistant.components.sonos pysonos==0.0.23 # homeassistant.components.spc pyspcwebgw==0.4.0 +# homeassistant.components.ecobee +python-ecobee-api==0.1.4 + # homeassistant.components.darksky python-forecastio==1.4.0 +# homeassistant.components.izone +python-izone==1.1.1 + # homeassistant.components.nest python-nest==4.1.0 @@ -368,7 +389,7 @@ pywebpush==1.9.2 regenmaschine==1.5.1 # homeassistant.components.python_script -restrictedpython==4.0 +restrictedpython==5.0 # homeassistant.components.rflink rflink==0.0.46 @@ -380,7 +401,7 @@ ring_doorbell==0.2.3 rxv==0.6.0 # homeassistant.components.simplisafe -simplisafe-python==4.3.0 +simplisafe-python==5.0.1 # homeassistant.components.sleepiq sleepyq==0.7 @@ -398,15 +419,15 @@ somecomfort==0.5.2 # homeassistant.components.sql sqlalchemy==1.3.8 -# homeassistant.components.srp_energy -srpenergy==1.0.6 - # homeassistant.components.statsd statsd==3.2.1 # homeassistant.components.toon toonapilib==3.2.4 +# homeassistant.components.transmission +transmissionrpc==0.11 + # homeassistant.components.twentemilieu twentemilieu==0.1.0 @@ -424,8 +445,11 @@ vultr==0.1.2 # homeassistant.components.wake_on_lan wakeonlan==1.1.6 +# homeassistant.components.withings +withings-api==2.0.0b7 + # homeassistant.components.zeroconf zeroconf==0.23.0 # homeassistant.components.zha -zigpy-homeassistant==0.8.0 +zigpy-homeassistant==0.9.0 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index ff2943a583b..8a33baabc29 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -87,6 +87,7 @@ TEST_REQUIREMENTS = ( "haversine", "hbmqtt", "hdate", + "herepy", "hole", "holidays", "home-assistant-frontend", @@ -104,12 +105,14 @@ TEST_REQUIREMENTS = ( "mficlient", "minio", "netdisco", - "nokia", "numpy", "oauth2client", "paho-mqtt", "pexpect", "pilight", + "pillow", + "plexapi", + "plexauth", "pmsensor", "prometheus_client", "ptvsd", @@ -140,10 +143,13 @@ TEST_REQUIREMENTS = ( "pysma", "pysmartapp", "pysmartthings", + "pysoma", "pysonos", "pyspcwebgw", "python_awair", + "python-ecobee-api", "python-forecastio", + "python-izone", "python-nest", "python-velbus", "pythonwhois", @@ -168,12 +174,14 @@ TEST_REQUIREMENTS = ( "srpenergy", "statsd", "toonapilib", + "transmissionrpc", "twentemilieu", "uvcclient", "vsure", "vultr", "wakeonlan", "warrant", + "withings-api", "YesssSMS", "zeroconf", "zigpy-homeassistant", diff --git a/script/hassfest/codeowners.py b/script/hassfest/codeowners.py index 1341bd75d1b..6f63fab3fdb 100644 --- a/script/hassfest/codeowners.py +++ b/script/hassfest/codeowners.py @@ -4,7 +4,7 @@ from typing import Dict from .model import Integration, Config BASE = """ -# This file is generated by script/manifest/codeowners.py +# This file is generated by script/hassfest/codeowners.py # People marked here will be automatically requested for a review # when the code that they own is touched. # https://github.com/blog/2392-introducing-code-owners diff --git a/script/scaffold/__init__.py b/script/scaffold/__init__.py new file mode 100644 index 00000000000..2eca398d998 --- /dev/null +++ b/script/scaffold/__init__.py @@ -0,0 +1 @@ +"""Scaffold new integration.""" diff --git a/script/scaffold/__main__.py b/script/scaffold/__main__.py new file mode 100644 index 00000000000..2258840f430 --- /dev/null +++ b/script/scaffold/__main__.py @@ -0,0 +1,91 @@ +"""Validate manifests.""" +import argparse +from pathlib import Path +import subprocess +import sys + +from . import gather_info, generate, error, docs +from .const import COMPONENT_DIR + + +TEMPLATES = [ + p.name for p in (Path(__file__).parent / "templates").glob("*") if p.is_dir() +] + + +def valid_integration(integration): + """Test if it's a valid integration.""" + if not (COMPONENT_DIR / integration).exists(): + raise argparse.ArgumentTypeError( + f"The integration {integration} does not exist." + ) + + return integration + + +def get_arguments() -> argparse.Namespace: + """Get parsed passed in arguments.""" + parser = argparse.ArgumentParser(description="Home Assistant Scaffolder") + parser.add_argument("template", type=str, choices=TEMPLATES) + parser.add_argument( + "--develop", action="store_true", help="Automatically fill in info" + ) + parser.add_argument( + "--integration", type=valid_integration, help="Integration to target." + ) + + arguments = parser.parse_args() + + return arguments + + +def main(): + """Scaffold an integration.""" + if not Path("requirements_all.txt").is_file(): + print("Run from project root") + return 1 + + args = get_arguments() + + info = gather_info.gather_info(args) + + generate.generate(args.template, info) + + # If creating new integration, create config flow too + if args.template == "integration": + if info.authentication or not info.discoverable: + template = "config_flow" + else: + template = "config_flow_discovery" + + generate.generate(template, info) + + print("Running hassfest to pick up new information.") + subprocess.run("python -m script.hassfest", shell=True) + print() + + print("Running tests") + print(f"$ pytest -vvv tests/components/{info.domain}") + if ( + subprocess.run( + f"pytest -vvv tests/components/{info.domain}", shell=True + ).returncode + != 0 + ): + return 1 + print() + + print(f"Done!") + + docs.print_relevant_docs(args.template, info) + + return 0 + + +if __name__ == "__main__": + try: + sys.exit(main()) + except error.ExitApp as err: + print() + print(f"Fatal Error: {err.reason}") + sys.exit(err.exit_code) diff --git a/script/scaffold/const.py b/script/scaffold/const.py new file mode 100644 index 00000000000..cf66bb4e2ae --- /dev/null +++ b/script/scaffold/const.py @@ -0,0 +1,5 @@ +"""Constants for scaffolding.""" +from pathlib import Path + +COMPONENT_DIR = Path("homeassistant/components") +TESTS_DIR = Path("tests/components") diff --git a/script/scaffold/docs.py b/script/scaffold/docs.py new file mode 100644 index 00000000000..ab87799d6b2 --- /dev/null +++ b/script/scaffold/docs.py @@ -0,0 +1,70 @@ +"""Print links to relevant docs.""" +from .model import Info + + +def print_relevant_docs(template: str, info: Info) -> None: + """Print relevant docs.""" + if template == "integration": + print( + f""" +Your integration has been created at {info.integration_dir} . Next step is to fill in the blanks for the code marked with TODO. + +For a breakdown of each file, check the developer documentation at: +https://developers.home-assistant.io/docs/en/creating_integration_file_structure.html +""" + ) + + elif template == "config_flow": + print( + f""" +The config flow has been added to the {info.domain} integration. Next step is to fill in the blanks for the code marked with TODO. +""" + ) + + elif template == "reproduce_state": + print( + f""" +Reproduce state code has been added to the {info.domain} integration: + - {info.integration_dir / "reproduce_state.py"} + - {info.tests_dir / "test_reproduce_state.py"} + +You will now need to update the code to make sure that every attribute +that can occur in the state will cause the right service to be called. +""" + ) + + elif template == "device_trigger": + print( + f""" +Device trigger base has been added to the {info.domain} integration: + - {info.integration_dir / "device_trigger.py"} + - {info.tests_dir / "test_device_trigger.py"} + +You will now need to update the code to make sure that relevant triggers +are exposed. +""" + ) + + elif template == "device_condition": + print( + f""" +Device condition base has been added to the {info.domain} integration: + - {info.integration_dir / "device_condition.py"} + - {info.tests_dir / "test_device_condition.py"} + +You will now need to update the code to make sure that relevant condtions +are exposed. +""" + ) + + elif template == "device_action": + print( + f""" +Device action base has been added to the {info.domain} integration: + - {info.integration_dir / "device_action.py"} + - {info.tests_dir / "test_device_action.py"} + +You will now need to update the code to make sure that relevant services +are exposed as actions. +""" + ) diff --git a/script/scaffold/error.py b/script/scaffold/error.py new file mode 100644 index 00000000000..75a869572fd --- /dev/null +++ b/script/scaffold/error.py @@ -0,0 +1,10 @@ +"""Errors for scaffolding.""" + + +class ExitApp(Exception): + """Exception to indicate app should exit.""" + + def __init__(self, reason, exit_code=1): + """Initialize the exit app exception.""" + self.reason = reason + self.exit_code = exit_code diff --git a/script/scaffold/gather_info.py b/script/scaffold/gather_info.py new file mode 100644 index 00000000000..a7263daaf41 --- /dev/null +++ b/script/scaffold/gather_info.py @@ -0,0 +1,183 @@ +"""Gather info for scaffolding.""" +import json + +from homeassistant.util import slugify + +from .const import COMPONENT_DIR +from .model import Info +from .error import ExitApp + + +CHECK_EMPTY = ["Cannot be empty", lambda value: value] + + +def gather_info(arguments) -> Info: + """Gather info.""" + existing = arguments.template != "integration" + + if arguments.develop: + print("Running in developer mode. Automatically filling in info.") + print() + + if existing: + if arguments.develop: + return _load_existing_integration("develop") + + if arguments.integration: + return _load_existing_integration(arguments.integration) + + return gather_existing_integration() + + if arguments.develop: + return Info( + domain="develop", + name="Develop Hub", + codeowner="@developer", + requirement="aiodevelop==1.2.3", + ) + + return gather_new_integration() + + +def gather_new_integration() -> Info: + """Gather info about new integration from user.""" + return Info( + **_gather_info( + { + "domain": { + "prompt": "What is the domain?", + "validators": [ + CHECK_EMPTY, + [ + "Domains cannot contain spaces or special characters.", + lambda value: value == slugify(value), + ], + [ + "There already is an integration with this domain.", + lambda value: not (COMPONENT_DIR / value).exists(), + ], + ], + }, + "name": { + "prompt": "What is the name of your integration?", + "validators": [CHECK_EMPTY], + }, + "codeowner": { + "prompt": "What is your GitHub handle?", + "validators": [ + CHECK_EMPTY, + [ + 'GitHub handles need to start with an "@"', + lambda value: value.startswith("@"), + ], + ], + }, + "requirement": { + "prompt": "What PyPI package and version do you depend on? Leave blank for none.", + "validators": [ + [ + "Versions should be pinned using '=='.", + lambda value: not value or "==" in value, + ] + ], + }, + "authentication": { + "prompt": "Does Home Assistant need the user to authenticate to control the device/service? (yes/no)", + "default": "yes", + "validators": [ + [ + "Type either 'yes' or 'no'", + lambda value: value in ("yes", "no"), + ] + ], + "convertor": lambda value: value == "yes", + }, + "discoverable": { + "prompt": "Is the device/service discoverable on the local network? (yes/no)", + "default": "no", + "validators": [ + [ + "Type either 'yes' or 'no'", + lambda value: value in ("yes", "no"), + ] + ], + "convertor": lambda value: value == "yes", + }, + } + ) + ) + + +def gather_existing_integration() -> Info: + """Gather info about existing integration from user.""" + answers = _gather_info( + { + "domain": { + "prompt": "What is the domain?", + "validators": [ + CHECK_EMPTY, + [ + "Domains cannot contain spaces or special characters.", + lambda value: value == slugify(value), + ], + [ + "This integration does not exist.", + lambda value: (COMPONENT_DIR / value).exists(), + ], + ], + } + } + ) + + return _load_existing_integration(answers["domain"]) + + +def _load_existing_integration(domain) -> Info: + """Load an existing integration.""" + if not (COMPONENT_DIR / domain).exists(): + raise ExitApp("Integration does not exist", 1) + + manifest = json.loads((COMPONENT_DIR / domain / "manifest.json").read_text()) + + return Info(domain=domain, name=manifest["name"]) + + +def _gather_info(fields) -> dict: + """Gather info from user.""" + answers = {} + + for key, info in fields.items(): + hint = None + while key not in answers: + if hint is not None: + print() + print(f"Error: {hint}") + + try: + print() + msg = info["prompt"] + if "default" in info: + msg += f" [{info['default']}]" + value = input(f"{msg}\n> ") + except (KeyboardInterrupt, EOFError): + raise ExitApp("Interrupted!", 1) + + value = value.strip() + + if value == "" and "default" in info: + value = info["default"] + + hint = None + + for validator_hint, validator in info["validators"]: + if not validator(value): + hint = validator_hint + break + + if hint is None: + if "convertor" in info: + value = info["convertor"](value) + answers[key] = value + + print() + return answers diff --git a/script/scaffold/generate.py b/script/scaffold/generate.py new file mode 100644 index 00000000000..6bccf6529fe --- /dev/null +++ b/script/scaffold/generate.py @@ -0,0 +1,121 @@ +"""Generate an integration.""" +from pathlib import Path + +from .error import ExitApp +from .model import Info + +TEMPLATE_DIR = Path(__file__).parent / "templates" +TEMPLATE_INTEGRATION = TEMPLATE_DIR / "integration" +TEMPLATE_TESTS = TEMPLATE_DIR / "tests" + + +def generate(template: str, info: Info) -> None: + """Generate a template.""" + _validate(template, info) + + print(f"Scaffolding {template} for the {info.domain} integration...") + _ensure_tests_dir_exists(info) + _generate(TEMPLATE_DIR / template / "integration", info.integration_dir, info) + _generate(TEMPLATE_DIR / template / "tests", info.tests_dir, info) + _custom_tasks(template, info) + print() + + +def _validate(template, info): + """Validate we can run this task.""" + if template == "config_flow": + if (info.integration_dir / "config_flow.py").exists(): + raise ExitApp(f"Integration {info.domain} already has a config flow.") + + +def _generate(src_dir, target_dir, info: Info) -> None: + """Generate an integration.""" + replaces = {"NEW_DOMAIN": info.domain, "NEW_NAME": info.name} + + if not target_dir.exists(): + target_dir.mkdir() + + for source_file in src_dir.glob("**/*"): + content = source_file.read_text() + + for to_search, to_replace in replaces.items(): + content = content.replace(to_search, to_replace) + + target_file = target_dir / source_file.relative_to(src_dir) + print(f"Writing {target_file}") + target_file.write_text(content) + + +def _ensure_tests_dir_exists(info: Info) -> None: + """Ensure a test dir exists.""" + if info.tests_dir.exists(): + return + + info.tests_dir.mkdir() + print(f"Writing {info.tests_dir / '__init__.py'}") + (info.tests_dir / "__init__.py").write_text( + f'"""Tests for the {info.name} integration."""\n' + ) + + +def _custom_tasks(template, info) -> None: + """Handle custom tasks for templates.""" + if template == "integration": + changes = {"codeowners": [info.codeowner]} + + if info.requirement: + changes["requirements"] = [info.requirement] + + info.update_manifest(**changes) + + if template == "config_flow": + info.update_manifest(config_flow=True) + info.update_strings( + config={ + "title": info.name, + "step": { + "user": {"title": "Connect to the device", "data": {"host": "Host"}} + }, + "error": { + "cannot_connect": "Failed to connect, please try again", + "invalid_auth": "Invalid authentication", + "unknown": "Unexpected error", + }, + "abort": {"already_configured": "Device is already configured"}, + } + ) + + if template == "config_flow_discovery": + info.update_manifest(config_flow=True) + info.update_strings( + config={ + "title": info.name, + "step": { + "confirm": { + "title": info.name, + "description": f"Do you want to set up {info.name}?", + } + }, + "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.", + }, + } + ) + + if template in ("config_flow", "config_flow_discovery"): + init_file = info.integration_dir / "__init__.py" + init_file.write_text( + init_file.read_text() + + """ + +async def async_setup_entry(hass, entry): + \"\"\"Set up a config entry for NEW_NAME.\"\"\" + # TODO forward the entry for each platform that you want to set up. + # hass.async_create_task( + # hass.config_entries.async_forward_entry_setup(entry, "media_player") + # ) + + return True +""" + ) diff --git a/script/scaffold/model.py b/script/scaffold/model.py new file mode 100644 index 00000000000..68ab771122e --- /dev/null +++ b/script/scaffold/model.py @@ -0,0 +1,61 @@ +"""Models for scaffolding.""" +import json +from pathlib import Path + +import attr + +from .const import COMPONENT_DIR, TESTS_DIR + + +@attr.s +class Info: + """Info about new integration.""" + + domain: str = attr.ib() + name: str = attr.ib() + codeowner: str = attr.ib(default=None) + requirement: str = attr.ib(default=None) + authentication: str = attr.ib(default=None) + discoverable: str = attr.ib(default=None) + + @property + def integration_dir(self) -> Path: + """Return directory if integration.""" + return COMPONENT_DIR / self.domain + + @property + def tests_dir(self) -> Path: + """Return test directory.""" + return TESTS_DIR / self.domain + + @property + def manifest_path(self) -> Path: + """Path to the manifest.""" + return COMPONENT_DIR / self.domain / "manifest.json" + + def manifest(self) -> dict: + """Return integration manifest.""" + return json.loads(self.manifest_path.read_text()) + + def update_manifest(self, **kwargs) -> None: + """Update the integration manifest.""" + print(f"Updating {self.domain} manifest: {kwargs}") + self.manifest_path.write_text( + json.dumps({**self.manifest(), **kwargs}, indent=2) + ) + + @property + def strings_path(self) -> Path: + """Path to the strings.""" + return COMPONENT_DIR / self.domain / "strings.json" + + def strings(self) -> dict: + """Return integration strings.""" + if not self.strings_path.exists(): + return {} + return json.loads(self.strings_path.read_text()) + + def update_strings(self, **kwargs) -> None: + """Update the integration strings.""" + print(f"Updating {self.domain} strings: {list(kwargs)}") + self.strings_path.write_text(json.dumps({**self.strings(), **kwargs}, indent=2)) diff --git a/script/scaffold/templates/config_flow/integration/config_flow.py b/script/scaffold/templates/config_flow/integration/config_flow.py new file mode 100644 index 00000000000..e08851f47a0 --- /dev/null +++ b/script/scaffold/templates/config_flow/integration/config_flow.py @@ -0,0 +1,64 @@ +"""Config flow for NEW_NAME integration.""" +import logging + +import voluptuous as vol + +from homeassistant import core, config_entries, exceptions + +from .const import DOMAIN # pylint:disable=unused-import + +_LOGGER = logging.getLogger(__name__) + +# TODO adjust the data schema to the data that you need +DATA_SCHEMA = vol.Schema({"host": str, "username": str, "password": 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. + """ + # TODO validate the data can be used to set up a connection. + # If you cannot connect: + # throw CannotConnect + # If the authentication is wrong: + # InvalidAuth + + # Return some info we want to store in the config entry. + return {"title": "Name of the device"} + + +class DomainConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for NEW_NAME.""" + + VERSION = 1 + # TODO pick one of the available connection classes in homeassistant/config_entries.py + CONNECTION_CLASS = config_entries.CONN_CLASS_UNKNOWN + + async def async_step_user(self, user_input=None): + """Handle the initial step.""" + errors = {} + if user_input is not None: + 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 + ) + + +class CannotConnect(exceptions.HomeAssistantError): + """Error to indicate we cannot connect.""" + + +class InvalidAuth(exceptions.HomeAssistantError): + """Error to indicate there is invalid auth.""" diff --git a/script/scaffold/templates/config_flow/tests/test_config_flow.py b/script/scaffold/templates/config_flow/tests/test_config_flow.py new file mode 100644 index 00000000000..35d8a96ab2b --- /dev/null +++ b/script/scaffold/templates/config_flow/tests/test_config_flow.py @@ -0,0 +1,93 @@ +"""Test the NEW_NAME config flow.""" +from unittest.mock import patch + +from homeassistant import config_entries, setup +from homeassistant.components.NEW_DOMAIN.const import DOMAIN +from homeassistant.components.NEW_DOMAIN.config_flow import CannotConnect, InvalidAuth + +from tests.common import mock_coro + + +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.NEW_DOMAIN.config_flow.validate_input", + return_value=mock_coro({"title": "Test Title"}), + ), patch( + "homeassistant.components.NEW_DOMAIN.async_setup", return_value=mock_coro(True) + ) as mock_setup, patch( + "homeassistant.components.NEW_DOMAIN.async_setup_entry", + return_value=mock_coro(True), + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "host": "1.1.1.1", + "username": "test-username", + "password": "test-password", + }, + ) + + assert result2["type"] == "create_entry" + assert result2["title"] == "Test Title" + assert result2["data"] == { + "host": "1.1.1.1", + "username": "test-username", + "password": "test-password", + } + 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.NEW_DOMAIN.config_flow.validate_input", + side_effect=InvalidAuth, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "host": "1.1.1.1", + "username": "test-username", + "password": "test-password", + }, + ) + + 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.NEW_DOMAIN.config_flow.validate_input", + side_effect=CannotConnect, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "host": "1.1.1.1", + "username": "test-username", + "password": "test-password", + }, + ) + + assert result2["type"] == "form" + assert result2["errors"] == {"base": "cannot_connect"} diff --git a/script/scaffold/templates/config_flow_discovery/integration/config_flow.py b/script/scaffold/templates/config_flow_discovery/integration/config_flow.py new file mode 100644 index 00000000000..16d13aaa99f --- /dev/null +++ b/script/scaffold/templates/config_flow_discovery/integration/config_flow.py @@ -0,0 +1,18 @@ +"""Config flow for NEW_NAME.""" +import my_pypi_dependency + +from homeassistant.helpers import config_entry_flow +from homeassistant import config_entries +from .const import DOMAIN + + +async def _async_has_devices(hass) -> bool: + """Return if there are devices that can be discovered.""" + # TODO Check if there are any devices that can be discovered in the network. + devices = await hass.async_add_executor_job(my_pypi_dependency.discover) + return len(devices) > 0 + + +config_entry_flow.register_discovery_flow( + DOMAIN, "NEW_NAME", _async_has_devices, config_entries.CONN_CLASS_UNKNOWN +) diff --git a/script/scaffold/templates/device_action/integration/device_action.py b/script/scaffold/templates/device_action/integration/device_action.py new file mode 100644 index 00000000000..d5674f01b2d --- /dev/null +++ b/script/scaffold/templates/device_action/integration/device_action.py @@ -0,0 +1,84 @@ +"""Provides device automations for NEW_NAME.""" +from typing import Optional, List +import voluptuous as vol + +from homeassistant.const import ( + ATTR_ENTITY_ID, + CONF_DOMAIN, + CONF_TYPE, + CONF_DEVICE_ID, + CONF_ENTITY_ID, + SERVICE_TURN_ON, + SERVICE_TURN_OFF, +) +from homeassistant.core import HomeAssistant, Context +from homeassistant.helpers import entity_registry +import homeassistant.helpers.config_validation as cv +from . import DOMAIN + +# TODO specify your supported action types. +ACTION_TYPES = {"turn_on", "turn_off"} + +ACTION_SCHEMA = cv.DEVICE_ACTION_BASE_SCHEMA.extend( + { + vol.Required(CONF_TYPE): vol.In(ACTION_TYPES), + vol.Required(CONF_ENTITY_ID): cv.entity_domain(DOMAIN), + } +) + + +async def async_get_actions(hass: HomeAssistant, device_id: str) -> List[dict]: + """List device actions for NEW_NAME devices.""" + registry = await entity_registry.async_get_registry(hass) + actions = [] + + # TODO Read this comment and remove it. + # This example shows how to iterate over the entities of this device + # that match this integration. If your actions instead rely on + # calling services, do something like: + # zha_device = await _async_get_zha_device(hass, device_id) + # return zha_device.device_actions + + # Get all the integrations entities for this device + for entry in entity_registry.async_entries_for_device(registry, device_id): + if entry.domain != DOMAIN: + continue + + # Add actions for each entity that belongs to this integration + # TODO add your own actions. + actions.append( + { + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + CONF_TYPE: "turn_on", + } + ) + actions.append( + { + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + CONF_TYPE: "turn_off", + } + ) + + return actions + + +async def async_call_action_from_config( + hass: HomeAssistant, config: dict, variables: dict, context: Optional[Context] +) -> None: + """Execute a device action.""" + config = ACTION_SCHEMA(config) + + service_data = {ATTR_ENTITY_ID: config[CONF_ENTITY_ID]} + + if config[CONF_TYPE] == "turn_on": + service = SERVICE_TURN_ON + elif config[CONF_TYPE] == "turn_off": + service = SERVICE_TURN_OFF + + await hass.services.async_call( + DOMAIN, service, service_data, blocking=True, context=context + ) diff --git a/script/scaffold/templates/device_action/tests/test_device_action.py b/script/scaffold/templates/device_action/tests/test_device_action.py new file mode 100644 index 00000000000..f8a00bf1ec8 --- /dev/null +++ b/script/scaffold/templates/device_action/tests/test_device_action.py @@ -0,0 +1,103 @@ +"""The tests for NEW_NAME device actions.""" +import pytest + +from homeassistant.components.NEW_DOMAIN import DOMAIN +from homeassistant.setup import async_setup_component +import homeassistant.components.automation as automation +from homeassistant.helpers import device_registry + +from tests.common import ( + MockConfigEntry, + async_mock_service, + mock_device_registry, + mock_registry, + async_get_device_automations, +) + + +@pytest.fixture +def device_reg(hass): + """Return an empty, loaded, registry.""" + return mock_device_registry(hass) + + +@pytest.fixture +def entity_reg(hass): + """Return an empty, loaded, registry.""" + return mock_registry(hass) + + +async def test_get_actions(hass, device_reg, entity_reg): + """Test we get the expected actions from a switch.""" + config_entry = MockConfigEntry(domain="test", data={}) + config_entry.add_to_hass(hass) + device_entry = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + entity_reg.async_get_or_create(DOMAIN, "test", "5678", device_id=device_entry.id) + expected_actions = [ + { + "domain": DOMAIN, + "type": "turn_on", + "device_id": device_entry.id, + "entity_id": "NEW_DOMAIN.test_5678", + }, + { + "domain": DOMAIN, + "type": "turn_off", + "device_id": device_entry.id, + "entity_id": "NEW_DOMAIN.test_5678", + }, + ] + actions = await async_get_device_automations(hass, "action", device_entry.id) + assert actions == expected_actions + + +async def test_action(hass): + """Test for turn_on and turn_off actions.""" + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": { + "platform": "event", + "event_type": "test_event_turn_off", + }, + "action": { + "domain": DOMAIN, + "device_id": "abcdefgh", + "entity_id": "NEW_DOMAIN.entity", + "type": "turn_off", + }, + }, + { + "trigger": { + "platform": "event", + "event_type": "test_event_turn_on", + }, + "action": { + "domain": DOMAIN, + "device_id": "abcdefgh", + "entity_id": "NEW_DOMAIN.entity", + "type": "turn_on", + }, + }, + ] + }, + ) + + turn_off_calls = async_mock_service(hass, "NEW_DOMAIN", "turn_off") + turn_on_calls = async_mock_service(hass, "NEW_DOMAIN", "turn_on") + + hass.bus.async_fire("test_event_turn_off") + await hass.async_block_till_done() + assert len(turn_off_calls) == 1 + assert len(turn_on_calls) == 0 + + hass.bus.async_fire("test_event_turn_on") + await hass.async_block_till_done() + assert len(turn_off_calls) == 1 + assert len(turn_on_calls) == 1 diff --git a/script/scaffold/templates/device_condition/integration/device_condition.py b/script/scaffold/templates/device_condition/integration/device_condition.py new file mode 100644 index 00000000000..d19fa8817a0 --- /dev/null +++ b/script/scaffold/templates/device_condition/integration/device_condition.py @@ -0,0 +1,64 @@ +"""Provides device automations for NEW_NAME.""" +from typing import List +import voluptuous as vol + +from homeassistant.const import ( + ATTR_ENTITY_ID, + CONF_DOMAIN, + CONF_TYPE, + CONF_PLATFORM, + CONF_DEVICE_ID, + CONF_ENTITY_ID, + STATE_ON, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers import condition, entity_registry +from homeassistant.helpers.typing import ConfigType, TemplateVarsType +from homeassistant.helpers.config_validation import DEVICE_CONDITION_BASE_SCHEMA +from . import DOMAIN + +# TODO specify your supported condition types. +CONDITION_TYPES = {"is_on"} + +CONDITION_SCHEMA = DEVICE_CONDITION_BASE_SCHEMA.extend( + {vol.Required(CONF_TYPE): vol.In(CONDITION_TYPES)} +) + + +async def async_get_conditions(hass: HomeAssistant, device_id: str) -> List[str]: + """List device conditions for NEW_NAME devices.""" + registry = await entity_registry.async_get_registry(hass) + conditions = [] + + # Get all the integrations entities for this device + for entry in entity_registry.async_entries_for_device(registry, device_id): + if entry.domain != DOMAIN: + continue + + # Add conditions for each entity that belongs to this integration + # TODO add your own conditions. + conditions.append( + { + CONF_PLATFORM: "device", + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + CONF_TYPE: "is_on", + } + ) + + return conditions + + +def async_condition_from_config( + config: ConfigType, config_validation: bool +) -> condition.ConditionCheckerType: + """Create a function to test a device condition.""" + if config_validation: + config = CONDITION_SCHEMA(config) + + def test_is_on(hass: HomeAssistant, variables: TemplateVarsType) -> bool: + """Test if an entity is on.""" + return condition.state(hass, config[ATTR_ENTITY_ID], STATE_ON) + + return test_is_on diff --git a/script/scaffold/templates/device_condition/tests/test_device_condition.py b/script/scaffold/templates/device_condition/tests/test_device_condition.py new file mode 100644 index 00000000000..d9cef083510 --- /dev/null +++ b/script/scaffold/templates/device_condition/tests/test_device_condition.py @@ -0,0 +1,125 @@ +"""The tests for NEW_NAME device conditions.""" +import pytest + +from homeassistant.components.switch import DOMAIN +from homeassistant.const import STATE_ON, STATE_OFF +from homeassistant.setup import async_setup_component +import homeassistant.components.automation as automation +from homeassistant.helpers import device_registry + +from tests.common import ( + MockConfigEntry, + async_mock_service, + mock_device_registry, + mock_registry, + async_get_device_automations, +) + + +@pytest.fixture +def device_reg(hass): + """Return an empty, loaded, registry.""" + return mock_device_registry(hass) + + +@pytest.fixture +def entity_reg(hass): + """Return an empty, loaded, registry.""" + return mock_registry(hass) + + +@pytest.fixture +def calls(hass): + """Track calls to a mock serivce.""" + return async_mock_service(hass, "test", "automation") + + +async def test_get_conditions(hass, device_reg, entity_reg): + """Test we get the expected conditions from a switch.""" + config_entry = MockConfigEntry(domain="test", data={}) + config_entry.add_to_hass(hass) + device_entry = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + entity_reg.async_get_or_create(DOMAIN, "test", "5678", device_id=device_entry.id) + expected_conditions = [ + { + "condition": "device", + "domain": DOMAIN, + "type": "is_off", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_5678", + }, + { + "condition": "device", + "domain": DOMAIN, + "type": "is_on", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_5678", + }, + ] + conditions = await async_get_device_automations(hass, "condition", device_entry.id) + assert conditions == expected_conditions + + +async def test_if_state(hass, calls): + """Test for turn_on and turn_off conditions.""" + hass.states.async_set("NEW_DOMAIN.entity", STATE_ON) + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": {"platform": "event", "event_type": "test_event1"}, + "condition": [ + { + "condition": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": "NEW_DOMAIN.entity", + "type": "is_on", + } + ], + "action": { + "service": "test.automation", + "data_template": { + "some": "is_on - {{ trigger.platform }} - {{ trigger.event.event_type }}" + }, + }, + }, + { + "trigger": {"platform": "event", "event_type": "test_event2"}, + "condition": [ + { + "condition": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": "NEW_DOMAIN.entity", + "type": "is_off", + } + ], + "action": { + "service": "test.automation", + "data_template": { + "some": "is_off - {{ trigger.platform }} - {{ trigger.event.event_type }}" + }, + }, + }, + ] + }, + ) + hass.bus.async_fire("test_event1") + hass.bus.async_fire("test_event2") + await hass.async_block_till_done() + assert len(calls) == 1 + assert calls[0].data["some"] == "is_on - event - test_event1" + + hass.states.async_set("NEW_DOMAIN.entity", STATE_OFF) + hass.bus.async_fire("test_event1") + hass.bus.async_fire("test_event2") + await hass.async_block_till_done() + assert len(calls) == 2 + assert calls[1].data["some"] == "is_off - event - test_event2" diff --git a/script/scaffold/templates/device_trigger/integration/device_trigger.py b/script/scaffold/templates/device_trigger/integration/device_trigger.py new file mode 100644 index 00000000000..f7e9fc091f8 --- /dev/null +++ b/script/scaffold/templates/device_trigger/integration/device_trigger.py @@ -0,0 +1,100 @@ +"""Provides device automations for NEW_NAME.""" +from typing import List +import voluptuous as vol + +from homeassistant.const import ( + CONF_DOMAIN, + CONF_TYPE, + CONF_PLATFORM, + CONF_DEVICE_ID, + CONF_ENTITY_ID, + STATE_ON, + STATE_OFF, +) +from homeassistant.core import HomeAssistant, CALLBACK_TYPE +from homeassistant.helpers import entity_registry +from homeassistant.helpers.typing import ConfigType +from homeassistant.components.automation import state, AutomationActionType +from homeassistant.components.device_automation import TRIGGER_BASE_SCHEMA +from . import DOMAIN + +# TODO specify your supported trigger types. +TRIGGER_TYPES = {"turned_on", "turned_off"} + +TRIGGER_SCHEMA = TRIGGER_BASE_SCHEMA.extend( + {vol.Required(CONF_TYPE): vol.In(TRIGGER_TYPES)} +) + + +async def async_get_triggers(hass: HomeAssistant, device_id: str) -> List[dict]: + """List device triggers for NEW_NAME devices.""" + registry = await entity_registry.async_get_registry(hass) + triggers = [] + + # TODO Read this comment and remove it. + # This example shows how to iterate over the entities of this device + # that match this integration. If your triggers instead rely on + # events fired by devices without entities, do something like: + # zha_device = await _async_get_zha_device(hass, device_id) + # return zha_device.device_triggers + + # Get all the integrations entities for this device + for entry in entity_registry.async_entries_for_device(registry, device_id): + if entry.domain != DOMAIN: + continue + + # Add triggers for each entity that belongs to this integration + # TODO add your own triggers. + triggers.append( + { + CONF_PLATFORM: "device", + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + CONF_TYPE: "turned_on", + } + ) + triggers.append( + { + CONF_PLATFORM: "device", + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + CONF_TYPE: "turned_off", + } + ) + + return triggers + + +async def async_attach_trigger( + hass: HomeAssistant, + config: ConfigType, + action: AutomationActionType, + automation_info: dict, +) -> CALLBACK_TYPE: + """Attach a trigger.""" + config = TRIGGER_SCHEMA(config) + + # TODO Implement your own logic to attach triggers. + # Generally we suggest to re-use the existing state or event + # triggers from the automation integration. + + if config[CONF_TYPE] == "turned_on": + from_state = STATE_OFF + to_state = STATE_ON + else: + from_state = STATE_ON + to_state = STATE_OFF + + return state.async_attach_trigger( + hass, + { + CONF_ENTITY_ID: config[CONF_ENTITY_ID], + state.CONF_FROM: from_state, + state.CONF_TO: to_state, + }, + action, + automation_info, + platform_type="device", + ) diff --git a/script/scaffold/templates/device_trigger/tests/test_device_trigger.py b/script/scaffold/templates/device_trigger/tests/test_device_trigger.py new file mode 100644 index 00000000000..c22197bb136 --- /dev/null +++ b/script/scaffold/templates/device_trigger/tests/test_device_trigger.py @@ -0,0 +1,131 @@ +"""The tests for NEW_NAME device triggers.""" +import pytest + +from homeassistant.components.switch import DOMAIN +from homeassistant.const import STATE_ON, STATE_OFF +from homeassistant.setup import async_setup_component +import homeassistant.components.automation as automation +from homeassistant.helpers import device_registry + +from tests.common import ( + MockConfigEntry, + async_mock_service, + mock_device_registry, + mock_registry, + async_get_device_automations, +) + + +@pytest.fixture +def device_reg(hass): + """Return an empty, loaded, registry.""" + return mock_device_registry(hass) + + +@pytest.fixture +def entity_reg(hass): + """Return an empty, loaded, registry.""" + return mock_registry(hass) + + +@pytest.fixture +def calls(hass): + """Track calls to a mock serivce.""" + return async_mock_service(hass, "test", "automation") + + +async def test_get_triggers(hass, device_reg, entity_reg): + """Test we get the expected triggers from a switch.""" + config_entry = MockConfigEntry(domain="test", data={}) + config_entry.add_to_hass(hass) + device_entry = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + entity_reg.async_get_or_create(DOMAIN, "test", "5678", device_id=device_entry.id) + expected_triggers = [ + { + "platform": "device", + "domain": DOMAIN, + "type": "turned_off", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_5678", + }, + { + "platform": "device", + "domain": DOMAIN, + "type": "turned_on", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_5678", + }, + ] + triggers = await async_get_device_automations(hass, "trigger", device_entry.id) + assert triggers == expected_triggers + + +async def test_if_fires_on_state_change(hass, calls): + """Test for turn_on and turn_off triggers firing.""" + hass.states.async_set("NEW_DOMAIN.entity", STATE_OFF) + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": { + "platform": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": "NEW_DOMAIN.entity", + "type": "turned_on", + }, + "action": { + "service": "test.automation", + "data_template": { + "some": ( + "turn_on - {{ trigger.platform}} - " + "{{ trigger.entity_id}} - {{ trigger.from_state.state}} - " + "{{ trigger.to_state.state}} - {{ trigger.for }}" + ) + }, + }, + }, + { + "trigger": { + "platform": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": "NEW_DOMAIN.entity", + "type": "turned_off", + }, + "action": { + "service": "test.automation", + "data_template": { + "some": ( + "turn_off - {{ trigger.platform}} - " + "{{ trigger.entity_id}} - {{ trigger.from_state.state}} - " + "{{ trigger.to_state.state}} - {{ trigger.for }}" + ) + }, + }, + }, + ] + }, + ) + + # Fake that the entity is turning on. + hass.states.async_set("NEW_DOMAIN.entity", STATE_ON) + await hass.async_block_till_done() + assert len(calls) == 1 + assert calls[0].data["some"] == "turn_on - device - {} - off - on - None".format( + "NEW_DOMAIN.entity" + ) + + # Fake that the entity is turning off. + hass.states.async_set("NEW_DOMAIN.entity", STATE_OFF) + await hass.async_block_till_done() + assert len(calls) == 2 + assert calls[1].data["some"] == "turn_off - device - {} - on - off - None".format( + "NEW_DOMAIN.entity" + ) diff --git a/script/scaffold/templates/integration/integration/__init__.py b/script/scaffold/templates/integration/integration/__init__.py new file mode 100644 index 00000000000..7ab8b736782 --- /dev/null +++ b/script/scaffold/templates/integration/integration/__init__.py @@ -0,0 +1,12 @@ +"""The NEW_NAME integration.""" +import voluptuous as vol + +from .const import DOMAIN + + +CONFIG_SCHEMA = vol.Schema({vol.Optional(DOMAIN): {}}) + + +async def async_setup(hass, config): + """Set up the NEW_NAME integration.""" + return True diff --git a/script/scaffold/templates/integration/integration/const.py b/script/scaffold/templates/integration/integration/const.py new file mode 100644 index 00000000000..e8a1c494d49 --- /dev/null +++ b/script/scaffold/templates/integration/integration/const.py @@ -0,0 +1,3 @@ +"""Constants for the NEW_NAME integration.""" + +DOMAIN = "NEW_DOMAIN" diff --git a/script/scaffold/templates/integration/integration/manifest.json b/script/scaffold/templates/integration/integration/manifest.json new file mode 100644 index 00000000000..0bc54519ce9 --- /dev/null +++ b/script/scaffold/templates/integration/integration/manifest.json @@ -0,0 +1,11 @@ +{ + "domain": "NEW_DOMAIN", + "name": "NEW_NAME", + "config_flow": false, + "documentation": "https://www.home-assistant.io/integrations/NEW_DOMAIN", + "requirements": [], + "ssdp": {}, + "homekit": {}, + "dependencies": [], + "codeowners": [] +} diff --git a/script/scaffold/templates/reproduce_state/integration/reproduce_state.py b/script/scaffold/templates/reproduce_state/integration/reproduce_state.py new file mode 100644 index 00000000000..3449009818b --- /dev/null +++ b/script/scaffold/templates/reproduce_state/integration/reproduce_state.py @@ -0,0 +1,78 @@ +"""Reproduce an NEW_NAME state.""" +import asyncio +import logging +from typing import Iterable, Optional + +from homeassistant.const import ( + ATTR_ENTITY_ID, + STATE_ON, + STATE_OFF, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, +) +from homeassistant.core import Context, State +from homeassistant.helpers.typing import HomeAssistantType + +from . import DOMAIN + +_LOGGER = logging.getLogger(__name__) + +# TODO add valid states here +VALID_STATES = {STATE_ON, STATE_OFF} + + +async def _async_reproduce_state( + hass: HomeAssistantType, state: State, context: Optional[Context] = 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 + and + # TODO this is an example attribute + cur_state.attributes.get("color") == state.attributes.get("color") + ): + return + + service_data = {ATTR_ENTITY_ID: state.entity_id} + + # TODO determine the services to call to achieve desired state + if state.state == STATE_ON: + service = SERVICE_TURN_ON + if "color" in state.attributes: + service_data["color"] = state.attributes["color"] + + 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 +) -> None: + """Reproduce NEW_NAME states.""" + # TODO pick one and remove other one + + # Reproduce states in parallel. + await asyncio.gather( + *(_async_reproduce_state(hass, state, context) for state in states) + ) + + # Alternative: Reproduce states in sequence + # for state in states: + # await _async_reproduce_state(hass, state, context) diff --git a/script/scaffold/templates/reproduce_state/tests/test_reproduce_state.py b/script/scaffold/templates/reproduce_state/tests/test_reproduce_state.py new file mode 100644 index 00000000000..ff15625ad7c --- /dev/null +++ b/script/scaffold/templates/reproduce_state/tests/test_reproduce_state.py @@ -0,0 +1,56 @@ +"""Test reproduce state for NEW_NAME.""" +from homeassistant.core import State + +from tests.common import async_mock_service + + +async def test_reproducing_states(hass, caplog): + """Test reproducing NEW_NAME states.""" + hass.states.async_set("NEW_DOMAIN.entity_off", "off", {}) + hass.states.async_set("NEW_DOMAIN.entity_on", "on", {"color": "red"}) + + turn_on_calls = async_mock_service(hass, "NEW_DOMAIN", "turn_on") + turn_off_calls = async_mock_service(hass, "NEW_DOMAIN", "turn_off") + + # These calls should do nothing as entities already in desired state + await hass.helpers.state.async_reproduce_state( + [ + State("NEW_DOMAIN.entity_off", "off"), + State("NEW_DOMAIN.entity_on", "on", {"color": "red"}), + ], + blocking=True, + ) + + 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("NEW_DOMAIN.entity_off", "not_supported")], blocking=True + ) + + 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("NEW_DOMAIN.entity_on", "off"), + State("NEW_DOMAIN.entity_off", "on", {"color": "red"}), + # Should not raise + State("NEW_DOMAIN.non_existing", "on"), + ], + blocking=True, + ) + + assert len(turn_on_calls) == 1 + assert turn_on_calls[0].domain == "NEW_DOMAIN" + assert turn_on_calls[0].data == { + "entity_id": "NEW_DOMAIN.entity_off", + "color": "red", + } + + assert len(turn_off_calls) == 1 + assert turn_off_calls[0].domain == "NEW_DOMAIN" + assert turn_off_calls[0].data == {"entity_id": "NEW_DOMAIN.entity_on"} diff --git a/setup.cfg b/setup.cfg index 49f738cf969..4c9c892b93f 100644 --- a/setup.cfg +++ b/setup.cfg @@ -27,11 +27,13 @@ max-line-length = 88 # W503: Line break occurred before a binary operator # E203: Whitespace before ':' # D202 No blank lines allowed after function docstring +# W504 line break after binary operator ignore = E501, W503, E203, - D202 + D202, + W504 [isort] # https://github.com/timothycrosley/isort diff --git a/setup.py b/setup.py index 5ab8d74c64c..23a8a808f43 100755 --- a/setup.py +++ b/setup.py @@ -31,26 +31,26 @@ PROJECT_URLS = { PACKAGES = find_packages(exclude=["tests", "tests.*"]) REQUIRES = [ - "aiohttp==3.5.4", + "aiohttp==3.6.1", "astral==1.10.1", "async_timeout==3.0.1", - "attrs==19.1.0", + "attrs==19.2.0", "bcrypt==3.1.7", "certifi>=2019.6.16", 'contextvars==2.4;python_version<"3.7"', - "importlib-metadata==0.19", + "importlib-metadata==0.23", "jinja2>=2.10.1", "PyJWT==1.7.1", # PyJWT has loose dependency. We want the latest one. "cryptography==2.7", "pip>=8.0.3", - "python-slugify==3.0.3", + "python-slugify==3.0.4", "pytz>=2019.02", "pyyaml==5.1.2", "requests==2.22.0", "ruamel.yaml==0.15.100", "voluptuous==0.11.7", - "voluptuous-serialize==2.2.0", + "voluptuous-serialize==2.3.0", ] MIN_PY_VERSION = ".".join(map(str, hass_const.REQUIRED_PYTHON_VER)) diff --git a/tests/common.py b/tests/common.py index fda5c743222..0684e6daafc 100644 --- a/tests/common.py +++ b/tests/common.py @@ -1,5 +1,6 @@ """Test the helper method for writing tests.""" import asyncio +import collections import functools as ft import json import logging @@ -53,8 +54,11 @@ from homeassistant.helpers import ( from homeassistant.helpers.json import JSONEncoder from homeassistant.setup import async_setup_component, setup_component from homeassistant.util.unit_system import METRIC_SYSTEM -from homeassistant.util.async_ import run_callback_threadsafe, run_coroutine_threadsafe - +from homeassistant.util.async_ import run_callback_threadsafe +from homeassistant.components.device_automation import ( # noqa + _async_get_device_automations as async_get_device_automations, + _async_get_device_automation_capabilities as async_get_device_automation_capabilities, +) _TEST_INSTANCE_PORT = SERVER_PORT _LOGGER = logging.getLogger(__name__) @@ -90,7 +94,9 @@ def threadsafe_coroutine_factory(func): def threadsafe(*args, **kwargs): """Call func threadsafe.""" hass = args[0] - return run_coroutine_threadsafe(func(*args, **kwargs), hass.loop).result() + return asyncio.run_coroutine_threadsafe( + func(*args, **kwargs), hass.loop + ).result() return threadsafe @@ -123,7 +129,7 @@ def get_test_home_assistant(): def start_hass(*mocks): """Start hass.""" - run_coroutine_threadsafe(hass.async_start(), loop).result() + asyncio.run_coroutine_threadsafe(hass.async_start(), loop).result() def stop_hass(): """Stop hass.""" @@ -1045,3 +1051,85 @@ def async_mock_signal(hass, signal): hass.helpers.dispatcher.async_dispatcher_connect(signal, mock_signal_handler) return calls + + +class hashdict(dict): + """ + hashable dict implementation, suitable for use as a key into other dicts. + + >>> h1 = hashdict({"apples": 1, "bananas":2}) + >>> h2 = hashdict({"bananas": 3, "mangoes": 5}) + >>> h1+h2 + hashdict(apples=1, bananas=3, mangoes=5) + >>> d1 = {} + >>> d1[h1] = "salad" + >>> d1[h1] + 'salad' + >>> d1[h2] + Traceback (most recent call last): + ... + KeyError: hashdict(bananas=3, mangoes=5) + + based on answers from + http://stackoverflow.com/questions/1151658/python-hashable-dicts + + """ + + def __key(self): # noqa: D105 no docstring + return tuple(sorted(self.items())) + + def __repr__(self): # noqa: D105 no docstring + return ", ".join("{0}={1}".format(str(i[0]), repr(i[1])) for i in self.__key()) + + def __hash__(self): # noqa: D105 no docstring + return hash(self.__key()) + + def __setitem__(self, key, value): # noqa: D105 no docstring + raise TypeError( + "{0} does not support item assignment".format(self.__class__.__name__) + ) + + def __delitem__(self, key): # noqa: D105 no docstring + raise TypeError( + "{0} does not support item assignment".format(self.__class__.__name__) + ) + + def clear(self): # noqa: D102 no docstring + raise TypeError( + "{0} does not support item assignment".format(self.__class__.__name__) + ) + + def pop(self, *args, **kwargs): # noqa: D102 no docstring + raise TypeError( + "{0} does not support item assignment".format(self.__class__.__name__) + ) + + def popitem(self, *args, **kwargs): # noqa: D102 no docstring + raise TypeError( + "{0} does not support item assignment".format(self.__class__.__name__) + ) + + def setdefault(self, *args, **kwargs): # noqa: D102 no docstring + raise TypeError( + "{0} does not support item assignment".format(self.__class__.__name__) + ) + + def update(self, *args, **kwargs): # noqa: D102 no docstring + raise TypeError( + "{0} does not support item assignment".format(self.__class__.__name__) + ) + + # update is not ok because it mutates the object + # __add__ is ok because it creates a new object + # while the new object is under construction, it's ok to mutate it + def __add__(self, right): # noqa: D105 no docstring + result = hashdict(self) + dict.update(result, right) + return result + + +def assert_lists_same(a, b): + """Compare two lists, ignoring order.""" + assert collections.Counter([hashdict(i) for i in a]) == collections.Counter( + [hashdict(i) for i in b] + ) diff --git a/tests/components/alexa/test_capabilities.py b/tests/components/alexa/test_capabilities.py index 357e0e3026d..d53f145e6ff 100644 --- a/tests/components/alexa/test_capabilities.py +++ b/tests/components/alexa/test_capabilities.py @@ -9,8 +9,9 @@ from homeassistant.const import ( STATE_UNKNOWN, STATE_UNAVAILABLE, ) -from homeassistant.components import climate +from homeassistant.components.climate import const as climate from homeassistant.components.alexa import smart_home +from homeassistant.components.alexa.errors import UnsupportedProperty from tests.common import async_mock_service from . import ( @@ -293,8 +294,8 @@ async def test_report_colored_temp_light_state(hass): ) properties = await reported_properties(hass, "light.test_off") - properties.assert_equal( - "Alexa.ColorTemperatureController", "colorTemperatureInKelvin", 0 + properties.assert_not_has_property( + "Alexa.ColorTemperatureController", "colorTemperatureInKelvin" ) @@ -378,6 +379,112 @@ async def test_report_cover_percentage_state(hass): properties.assert_equal("Alexa.PercentageController", "percentage", 0) +async def test_report_climate_state(hass): + """Test ThermostatController reports state correctly.""" + for auto_modes in (climate.HVAC_MODE_AUTO, climate.HVAC_MODE_HEAT_COOL): + hass.states.async_set( + "climate.downstairs", + auto_modes, + { + "friendly_name": "Climate Downstairs", + "supported_features": 91, + climate.ATTR_CURRENT_TEMPERATURE: 34, + ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, + }, + ) + properties = await reported_properties(hass, "climate.downstairs") + properties.assert_equal("Alexa.ThermostatController", "thermostatMode", "AUTO") + properties.assert_equal( + "Alexa.TemperatureSensor", + "temperature", + {"value": 34.0, "scale": "CELSIUS"}, + ) + + for off_modes in ( + climate.HVAC_MODE_OFF, + climate.HVAC_MODE_FAN_ONLY, + climate.HVAC_MODE_DRY, + ): + hass.states.async_set( + "climate.downstairs", + off_modes, + { + "friendly_name": "Climate Downstairs", + "supported_features": 91, + climate.ATTR_CURRENT_TEMPERATURE: 34, + ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, + }, + ) + properties = await reported_properties(hass, "climate.downstairs") + properties.assert_equal("Alexa.ThermostatController", "thermostatMode", "OFF") + properties.assert_equal( + "Alexa.TemperatureSensor", + "temperature", + {"value": 34.0, "scale": "CELSIUS"}, + ) + + hass.states.async_set( + "climate.heat", + "heat", + { + "friendly_name": "Climate Heat", + "supported_features": 91, + climate.ATTR_CURRENT_TEMPERATURE: 34, + ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, + }, + ) + properties = await reported_properties(hass, "climate.heat") + properties.assert_equal("Alexa.ThermostatController", "thermostatMode", "HEAT") + properties.assert_equal( + "Alexa.TemperatureSensor", "temperature", {"value": 34.0, "scale": "CELSIUS"} + ) + + hass.states.async_set( + "climate.cool", + "cool", + { + "friendly_name": "Climate Cool", + "supported_features": 91, + climate.ATTR_CURRENT_TEMPERATURE: 34, + ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, + }, + ) + properties = await reported_properties(hass, "climate.cool") + properties.assert_equal("Alexa.ThermostatController", "thermostatMode", "COOL") + properties.assert_equal( + "Alexa.TemperatureSensor", "temperature", {"value": 34.0, "scale": "CELSIUS"} + ) + + hass.states.async_set( + "climate.unavailable", + "unavailable", + {"friendly_name": "Climate Unavailable", "supported_features": 91}, + ) + properties = await reported_properties(hass, "climate.unavailable") + properties.assert_not_has_property("Alexa.ThermostatController", "thermostatMode") + + hass.states.async_set( + "climate.unsupported", + "blablabla", + { + "friendly_name": "Climate Unsupported", + "supported_features": 91, + climate.ATTR_CURRENT_TEMPERATURE: 34, + ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, + }, + ) + with pytest.raises(UnsupportedProperty): + properties = await reported_properties(hass, "climate.unsupported") + properties.assert_not_has_property( + "Alexa.ThermostatController", "thermostatMode" + ) + properties.assert_equal( + "Alexa.TemperatureSensor", + "temperature", + {"value": 34.0, "scale": "CELSIUS"}, + ) + + async def test_temperature_sensor_sensor(hass): """Test TemperatureSensor reports sensor temperature correctly.""" for bad_value in (STATE_UNKNOWN, STATE_UNAVAILABLE, "not-number"): diff --git a/tests/components/alexa/test_smart_home.py b/tests/components/alexa/test_smart_home.py index bd5a4d25edd..e5e5b8ab7ae 100644 --- a/tests/components/alexa/test_smart_home.py +++ b/tests/components/alexa/test_smart_home.py @@ -308,7 +308,7 @@ async def test_fan(hass): appliance = await discovery_test(device, hass) assert appliance["endpointId"] == "fan#test_1" - assert appliance["displayCategories"][0] == "OTHER" + assert appliance["displayCategories"][0] == "FAN" assert appliance["friendlyName"] == "Test fan 1" assert_endpoint_capabilities( appliance, "Alexa.PowerController", "Alexa.EndpointHealth" @@ -333,7 +333,7 @@ async def test_variable_fan(hass): appliance = await discovery_test(device, hass) assert appliance["endpointId"] == "fan#test_2" - assert appliance["displayCategories"][0] == "OTHER" + assert appliance["displayCategories"][0] == "FAN" assert appliance["friendlyName"] == "Test fan 2" assert_endpoint_capabilities( @@ -1162,14 +1162,16 @@ async def test_entity_config(hass): request = get_new_request("Alexa.Discovery", "Discover") hass.states.async_set("light.test_1", "on", {"friendly_name": "Test light 1"}) + hass.states.async_set("scene.test_1", "scening", {"friendly_name": "Test 1"}) alexa_config = MockConfig(hass) alexa_config.entity_config = { "light.test_1": { - "name": "Config name", + "name": "Config *name*", "display_categories": "SWITCH", - "description": "Config description", - } + "description": "Config >! 1 }}", self.hass), None, None, + None, MATCH_ALL, None, None, @@ -298,6 +305,7 @@ class TestBinarySensorTemplate(unittest.TestCase): template_hlpr.Template("{{ 1 > 1 }}", self.hass), None, None, + None, MATCH_ALL, None, None, @@ -428,6 +436,59 @@ async def test_template_delay_off(hass): assert state.state == "on" +async def test_available_without_availability_template(hass): + """Ensure availability is true without an availability_template.""" + config = { + "binary_sensor": { + "platform": "template", + "sensors": { + "test": { + "friendly_name": "virtual thingy", + "value_template": "true", + "device_class": "motion", + "delay_off": 5, + } + }, + } + } + await setup.async_setup_component(hass, "binary_sensor", config) + await hass.async_start() + await hass.async_block_till_done() + + assert hass.states.get("binary_sensor.test").state != STATE_UNAVAILABLE + + +async def test_availability_template(hass): + """Test availability template.""" + config = { + "binary_sensor": { + "platform": "template", + "sensors": { + "test": { + "friendly_name": "virtual thingy", + "value_template": "true", + "device_class": "motion", + "delay_off": 5, + "availability_template": "{{ is_state('sensor.test_state','on') }}", + } + }, + } + } + await setup.async_setup_component(hass, "binary_sensor", config) + await hass.async_start() + await hass.async_block_till_done() + + hass.states.async_set("sensor.test_state", STATE_OFF) + await hass.async_block_till_done() + + assert hass.states.get("binary_sensor.test").state == STATE_UNAVAILABLE + + hass.states.async_set("sensor.test_state", STATE_ON) + await hass.async_block_till_done() + + assert hass.states.get("binary_sensor.test").state != STATE_UNAVAILABLE + + async def test_invalid_attribute_template(hass, caplog): """Test that errors are logged if rendering template fails.""" hass.states.async_set("binary_sensor.test_sensor", "true") @@ -458,6 +519,32 @@ async def test_invalid_attribute_template(hass, caplog): assert ("Error rendering attribute test_attribute") in caplog.text +async def test_invalid_availability_template_keeps_component_available(hass, caplog): + """Test that an invalid availability keeps the device available.""" + + await setup.async_setup_component( + hass, + "binary_sensor", + { + "binary_sensor": { + "platform": "template", + "sensors": { + "my_sensor": { + "value_template": "{{ states.binary_sensor.test_sensor }}", + "availability_template": "{{ x - 12 }}", + } + }, + } + }, + ) + + await hass.async_start() + await hass.async_block_till_done() + + assert hass.states.get("binary_sensor.my_sensor").state != STATE_UNAVAILABLE + assert ("UndefinedError: 'x' is undefined") in caplog.text + + async def test_no_update_template_match_all(hass, caplog): """Test that we do not update sensors that match on all.""" hass.states.async_set("binary_sensor.test_sensor", "true") diff --git a/tests/components/template/test_cover.py b/tests/components/template/test_cover.py index 247ee25027c..d3be01cbdc3 100644 --- a/tests/components/template/test_cover.py +++ b/tests/components/template/test_cover.py @@ -16,7 +16,10 @@ from homeassistant.const import ( SERVICE_SET_COVER_TILT_POSITION, SERVICE_STOP_COVER, STATE_CLOSED, + STATE_UNAVAILABLE, STATE_OPEN, + STATE_ON, + STATE_OFF, ) from tests.common import assert_setup_component, async_mock_service @@ -839,6 +842,112 @@ async def test_entity_picture_template(hass, calls): assert state.attributes["entity_picture"] == "/local/cover.png" +async def test_availability_template(hass, calls): + """Test availability template.""" + with assert_setup_component(1, "cover"): + assert await setup.async_setup_component( + hass, + "cover", + { + "cover": { + "platform": "template", + "covers": { + "test_template_cover": { + "value_template": "open", + "open_cover": { + "service": "cover.open_cover", + "entity_id": "cover.test_state", + }, + "close_cover": { + "service": "cover.close_cover", + "entity_id": "cover.test_state", + }, + "availability_template": "{{ is_state('availability_state.state','on') }}", + } + }, + } + }, + ) + + await hass.async_start() + await hass.async_block_till_done() + + hass.states.async_set("availability_state.state", STATE_OFF) + await hass.async_block_till_done() + + assert hass.states.get("cover.test_template_cover").state == STATE_UNAVAILABLE + + hass.states.async_set("availability_state.state", STATE_ON) + await hass.async_block_till_done() + + assert hass.states.get("cover.test_template_cover").state != STATE_UNAVAILABLE + + +async def test_availability_without_availability_template(hass, calls): + """Test that component is availble if there is no.""" + assert await setup.async_setup_component( + hass, + "cover", + { + "cover": { + "platform": "template", + "covers": { + "test_template_cover": { + "value_template": "open", + "open_cover": { + "service": "cover.open_cover", + "entity_id": "cover.test_state", + }, + "close_cover": { + "service": "cover.close_cover", + "entity_id": "cover.test_state", + }, + } + }, + } + }, + ) + + await hass.async_start() + await hass.async_block_till_done() + + state = hass.states.get("cover.test_template_cover") + assert state.state != STATE_UNAVAILABLE + + +async def test_invalid_availability_template_keeps_component_available(hass, caplog): + """Test that an invalid availability keeps the device available.""" + assert await setup.async_setup_component( + hass, + "cover", + { + "cover": { + "platform": "template", + "covers": { + "test_template_cover": { + "availability_template": "{{ x - 12 }}", + "value_template": "open", + "open_cover": { + "service": "cover.open_cover", + "entity_id": "cover.test_state", + }, + "close_cover": { + "service": "cover.close_cover", + "entity_id": "cover.test_state", + }, + } + }, + } + }, + ) + + await hass.async_start() + await hass.async_block_till_done() + + assert hass.states.get("cover.test_template_cover") != STATE_UNAVAILABLE + assert ("UndefinedError: 'x' is undefined") in caplog.text + + async def test_device_class(hass, calls): """Test device class.""" with assert_setup_component(1, "cover"): diff --git a/tests/components/template/test_fan.py b/tests/components/template/test_fan.py index b80522b37e2..5753684795b 100644 --- a/tests/components/template/test_fan.py +++ b/tests/components/template/test_fan.py @@ -5,7 +5,7 @@ import pytest import voluptuous as vol from homeassistant import setup -from homeassistant.const import STATE_ON, STATE_OFF +from homeassistant.const import STATE_ON, STATE_OFF, STATE_UNAVAILABLE from homeassistant.components.fan import ( ATTR_SPEED, ATTR_OSCILLATING, @@ -26,6 +26,8 @@ _LOGGER = logging.getLogger(__name__) _TEST_FAN = "fan.test_fan" # Represent for fan's state _STATE_INPUT_BOOLEAN = "input_boolean.state" +# Represent for fan's state +_STATE_AVAILABILITY_BOOLEAN = "availability_boolean.state" # Represent for fan's speed _SPEED_INPUT_SELECT = "input_select.speed" # Represent for fan's oscillating @@ -214,6 +216,49 @@ async def test_templates_with_entities(hass, calls): _verify(hass, STATE_ON, SPEED_MEDIUM, True, DIRECTION_FORWARD) +async def test_availability_template_with_entities(hass, calls): + """Test availability tempalates with values from other entities.""" + + with assert_setup_component(1, "fan"): + assert await setup.async_setup_component( + hass, + "fan", + { + "fan": { + "platform": "template", + "fans": { + "test_fan": { + "availability_template": "{{ is_state('availability_boolean.state', 'on') }}", + "value_template": "{{ 'on' }}", + "speed_template": "{{ 'medium' }}", + "oscillating_template": "{{ 1 == 1 }}", + "direction_template": "{{ 'forward' }}", + "turn_on": {"service": "script.fan_on"}, + "turn_off": {"service": "script.fan_off"}, + } + }, + } + }, + ) + + await hass.async_start() + await hass.async_block_till_done() + + # When template returns true.. + hass.states.async_set(_STATE_AVAILABILITY_BOOLEAN, STATE_ON) + await hass.async_block_till_done() + + # Device State should not be unavailable + assert hass.states.get(_TEST_FAN).state != STATE_UNAVAILABLE + + # When Availability template returns false + hass.states.async_set(_STATE_AVAILABILITY_BOOLEAN, STATE_OFF) + await hass.async_block_till_done() + + # device state should be unavailable + assert hass.states.get(_TEST_FAN).state == STATE_UNAVAILABLE + + async def test_templates_with_valid_values(hass, calls): """Test templates with valid values.""" with assert_setup_component(1, "fan"): @@ -272,6 +317,39 @@ async def test_templates_invalid_values(hass, calls): _verify(hass, STATE_OFF, None, None, None) +async def test_invalid_availability_template_keeps_component_available(hass, caplog): + """Test that an invalid availability keeps the device available.""" + + with assert_setup_component(1, "fan"): + assert await setup.async_setup_component( + hass, + "fan", + { + "fan": { + "platform": "template", + "fans": { + "test_fan": { + "value_template": "{{ 'on' }}", + "availability_template": "{{ x - 12 }}", + "speed_template": "{{ states('input_select.speed') }}", + "oscillating_template": "{{ states('input_select.osc') }}", + "direction_template": "{{ states('input_select.direction') }}", + "turn_on": {"service": "script.fan_on"}, + "turn_off": {"service": "script.fan_off"}, + } + }, + } + }, + ) + + await hass.async_start() + await hass.async_block_till_done() + + assert hass.states.get("fan.test_fan").state != STATE_UNAVAILABLE + assert ("Could not render availability_template template") in caplog.text + assert ("UndefinedError: 'x' is undefined") in caplog.text + + # End of template tests # diff --git a/tests/components/template/test_light.py b/tests/components/template/test_light.py index 87fd8cd4db3..c2dd49a76fb 100644 --- a/tests/components/template/test_light.py +++ b/tests/components/template/test_light.py @@ -4,13 +4,16 @@ import logging from homeassistant.core import callback from homeassistant import setup from homeassistant.components.light import ATTR_BRIGHTNESS -from homeassistant.const import STATE_ON, STATE_OFF +from homeassistant.const import STATE_ON, STATE_OFF, STATE_UNAVAILABLE from tests.common import get_test_home_assistant, assert_setup_component from tests.components.light import common _LOGGER = logging.getLogger(__name__) +# Represent for light's availability +_STATE_AVAILABILITY_BOOLEAN = "availability_boolean.state" + class TestTemplateLight: """Test the Template light.""" @@ -774,3 +777,92 @@ class TestTemplateLight: state = self.hass.states.get("light.test_template_light") assert state.attributes["entity_picture"] == "/local/light.png" + + +async def test_available_template_with_entities(hass): + """Test availability templates with values from other entities.""" + + await setup.async_setup_component( + hass, + "light", + { + "light": { + "platform": "template", + "lights": { + "test_template_light": { + "availability_template": "{{ is_state('availability_boolean.state', 'on') }}", + "turn_on": { + "service": "light.turn_on", + "entity_id": "light.test_state", + }, + "turn_off": { + "service": "light.turn_off", + "entity_id": "light.test_state", + }, + "set_level": { + "service": "light.turn_on", + "data_template": { + "entity_id": "light.test_state", + "brightness": "{{brightness}}", + }, + }, + } + }, + } + }, + ) + await hass.async_start() + await hass.async_block_till_done() + + # When template returns true.. + hass.states.async_set(_STATE_AVAILABILITY_BOOLEAN, STATE_ON) + await hass.async_block_till_done() + + # Device State should not be unavailable + assert hass.states.get("light.test_template_light").state != STATE_UNAVAILABLE + + # When Availability template returns false + hass.states.async_set(_STATE_AVAILABILITY_BOOLEAN, STATE_OFF) + await hass.async_block_till_done() + + # device state should be unavailable + assert hass.states.get("light.test_template_light").state == STATE_UNAVAILABLE + + +async def test_invalid_availability_template_keeps_component_available(hass, caplog): + """Test that an invalid availability keeps the device available.""" + await setup.async_setup_component( + hass, + "light", + { + "light": { + "platform": "template", + "lights": { + "test_template_light": { + "availability_template": "{{ x - 12 }}", + "turn_on": { + "service": "light.turn_on", + "entity_id": "light.test_state", + }, + "turn_off": { + "service": "light.turn_off", + "entity_id": "light.test_state", + }, + "set_level": { + "service": "light.turn_on", + "data_template": { + "entity_id": "light.test_state", + "brightness": "{{brightness}}", + }, + }, + } + }, + } + }, + ) + + await hass.async_start() + await hass.async_block_till_done() + + assert hass.states.get("light.test_template_light").state != STATE_UNAVAILABLE + assert ("UndefinedError: 'x' is undefined") in caplog.text diff --git a/tests/components/template/test_lock.py b/tests/components/template/test_lock.py index 24cde24051a..d1d30207375 100644 --- a/tests/components/template/test_lock.py +++ b/tests/components/template/test_lock.py @@ -5,7 +5,7 @@ from homeassistant.core import callback from homeassistant import setup from homeassistant.components import lock from homeassistant.const import ATTR_ENTITY_ID -from homeassistant.const import STATE_ON, STATE_OFF +from homeassistant.const import STATE_ON, STATE_OFF, STATE_UNAVAILABLE from tests.common import get_test_home_assistant, assert_setup_component @@ -254,9 +254,9 @@ class TestTemplateLock: assert state.state == lock.STATE_UNLOCKED assert ( - "Template lock Template Lock has no entity ids configured " + "Template lock 'Template Lock' has no entity ids configured " "to track nor were we able to extract the entities to track " - "from the value_template template. This entity will only " + "from the 'value_template' template. This entity will only " "be able to be updated manually." ) in caplog.text @@ -332,3 +332,67 @@ class TestTemplateLock: self.hass.block_till_done() assert len(self.calls) == 1 + + +async def test_available_template_with_entities(hass): + """Test availability templates with values from other entities.""" + + await setup.async_setup_component( + hass, + "lock", + { + "lock": { + "platform": "template", + "value_template": "{{ 'on' }}", + "lock": {"service": "switch.turn_on", "entity_id": "switch.test_state"}, + "unlock": { + "service": "switch.turn_off", + "entity_id": "switch.test_state", + }, + "availability_template": "{{ is_state('availability_state.state', 'on') }}", + } + }, + ) + + await hass.async_start() + await hass.async_block_till_done() + + # When template returns true.. + hass.states.async_set("availability_state.state", STATE_ON) + await hass.async_block_till_done() + + # Device State should not be unavailable + assert hass.states.get("lock.template_lock").state != STATE_UNAVAILABLE + + # When Availability template returns false + hass.states.async_set("availability_state.state", STATE_OFF) + await hass.async_block_till_done() + + # device state should be unavailable + assert hass.states.get("lock.template_lock").state == STATE_UNAVAILABLE + + +async def test_invalid_availability_template_keeps_component_available(hass, caplog): + """Test that an invalid availability keeps the device available.""" + await setup.async_setup_component( + hass, + "lock", + { + "lock": { + "platform": "template", + "value_template": "{{ 1 + 1 }}", + "availability_template": "{{ x - 12 }}", + "lock": {"service": "switch.turn_on", "entity_id": "switch.test_state"}, + "unlock": { + "service": "switch.turn_off", + "entity_id": "switch.test_state", + }, + } + }, + ) + + await hass.async_start() + await hass.async_block_till_done() + + assert hass.states.get("lock.template_lock").state != STATE_UNAVAILABLE + assert ("UndefinedError: 'x' is undefined") in caplog.text diff --git a/tests/components/template/test_sensor.py b/tests/components/template/test_sensor.py index 9223399bee7..b3813da1766 100644 --- a/tests/components/template/test_sensor.py +++ b/tests/components/template/test_sensor.py @@ -3,6 +3,7 @@ from homeassistant.const import EVENT_HOMEASSISTANT_START from homeassistant.setup import setup_component, async_setup_component from tests.common import get_test_home_assistant, assert_setup_component +from homeassistant.const import STATE_UNAVAILABLE, STATE_ON, STATE_OFF class TestTemplateSensor: @@ -251,7 +252,7 @@ class TestTemplateSensor: self.hass.block_till_done() state = self.hass.states.get("sensor.test_template_sensor") - assert state.state == "unknown" + assert state.state == STATE_UNAVAILABLE def test_invalid_name_does_not_create(self): """Test invalid name.""" @@ -377,6 +378,44 @@ class TestTemplateSensor: assert "device_class" not in state.attributes +async def test_available_template_with_entities(hass): + """Test availability tempalates with values from other entities.""" + hass.states.async_set("sensor.availability_sensor", STATE_OFF) + with assert_setup_component(1, "sensor"): + assert await async_setup_component( + hass, + "sensor", + { + "sensor": { + "platform": "template", + "sensors": { + "test_template_sensor": { + "value_template": "{{ states.sensor.test_sensor.state }}", + "availability_template": "{{ is_state('sensor.availability_sensor', 'on') }}", + } + }, + } + }, + ) + + await hass.async_start() + await hass.async_block_till_done() + + # When template returns true.. + hass.states.async_set("sensor.availability_sensor", STATE_ON) + await hass.async_block_till_done() + + # Device State should not be unavailable + assert hass.states.get("sensor.test_template_sensor").state != STATE_UNAVAILABLE + + # When Availability template returns false + hass.states.async_set("sensor.availability_sensor", STATE_OFF) + await hass.async_block_till_done() + + # device state should be unavailable + assert hass.states.get("sensor.test_template_sensor").state == STATE_UNAVAILABLE + + async def test_invalid_attribute_template(hass, caplog): """Test that errors are logged if rendering template fails.""" hass.states.async_set("sensor.test_sensor", "startup") @@ -405,6 +444,32 @@ async def test_invalid_attribute_template(hass, caplog): assert ("Error rendering attribute test_attribute") in caplog.text +async def test_invalid_availability_template_keeps_component_available(hass, caplog): + """Test that an invalid availability keeps the device available.""" + + await async_setup_component( + hass, + "sensor", + { + "sensor": { + "platform": "template", + "sensors": { + "my_sensor": { + "value_template": "{{ states.sensor.test_state.state }}", + "availability_template": "{{ x - 12 }}", + } + }, + } + }, + ) + + await hass.async_start() + await hass.async_block_till_done() + + assert hass.states.get("sensor.my_sensor").state != STATE_UNAVAILABLE + assert ("UndefinedError: 'x' is undefined") in caplog.text + + async def test_no_template_match_all(hass, caplog): """Test that we do not allow sensors that match on all.""" hass.states.async_set("sensor.test_sensor", "startup") diff --git a/tests/components/template/test_switch.py b/tests/components/template/test_switch.py index 9a07a935d12..3adc5dcad46 100644 --- a/tests/components/template/test_switch.py +++ b/tests/components/template/test_switch.py @@ -1,7 +1,7 @@ """The tests for the Template switch platform.""" from homeassistant.core import callback from homeassistant import setup -from homeassistant.const import STATE_ON, STATE_OFF +from homeassistant.const import STATE_ON, STATE_OFF, STATE_UNAVAILABLE from tests.common import get_test_home_assistant, assert_setup_component from tests.components.switch import common @@ -474,3 +474,76 @@ class TestTemplateSwitch: self.hass.block_till_done() assert len(self.calls) == 1 + + +async def test_available_template_with_entities(hass): + """Test availability templates with values from other entities.""" + await setup.async_setup_component( + hass, + "switch", + { + "switch": { + "platform": "template", + "switches": { + "test_template_switch": { + "value_template": "{{ 1 == 1 }}", + "turn_on": { + "service": "switch.turn_on", + "entity_id": "switch.test_state", + }, + "turn_off": { + "service": "switch.turn_off", + "entity_id": "switch.test_state", + }, + "availability_template": "{{ is_state('availability_state.state', 'on') }}", + } + }, + } + }, + ) + + await hass.async_start() + await hass.async_block_till_done() + + hass.states.async_set("availability_state.state", STATE_ON) + await hass.async_block_till_done() + + assert hass.states.get("switch.test_template_switch").state != STATE_UNAVAILABLE + + hass.states.async_set("availability_state.state", STATE_OFF) + await hass.async_block_till_done() + + assert hass.states.get("switch.test_template_switch").state == STATE_UNAVAILABLE + + +async def test_invalid_availability_template_keeps_component_available(hass, caplog): + """Test that an invalid availability keeps the device available.""" + await setup.async_setup_component( + hass, + "switch", + { + "switch": { + "platform": "template", + "switches": { + "test_template_switch": { + "value_template": "{{ true }}", + "turn_on": { + "service": "switch.turn_on", + "entity_id": "switch.test_state", + }, + "turn_off": { + "service": "switch.turn_off", + "entity_id": "switch.test_state", + }, + "availability_template": "{{ x - 12 }}", + } + }, + } + }, + ) + + await hass.async_start() + await hass.async_block_till_done() + + assert hass.states.get("switch.test_template_switch").state != STATE_UNAVAILABLE + assert ("UndefinedError: 'x' is undefined") in caplog.text diff --git a/tests/components/template/test_vacuum.py b/tests/components/template/test_vacuum.py index 9e3c535f136..da0e8e59ede 100644 --- a/tests/components/template/test_vacuum.py +++ b/tests/components/template/test_vacuum.py @@ -3,7 +3,7 @@ import logging import pytest from homeassistant import setup -from homeassistant.const import STATE_ON, STATE_UNKNOWN +from homeassistant.const import STATE_ON, STATE_OFF, STATE_UNKNOWN, STATE_UNAVAILABLE from homeassistant.components.vacuum import ( ATTR_BATTERY_LEVEL, STATE_CLEANING, @@ -210,6 +210,68 @@ async def test_invalid_templates(hass, calls): _verify(hass, STATE_UNKNOWN, None) +async def test_available_template_with_entities(hass, calls): + """Test availability templates with values from other entities.""" + + assert await setup.async_setup_component( + hass, + "vacuum", + { + "vacuum": { + "platform": "template", + "vacuums": { + "test_template_vacuum": { + "availability_template": "{{ is_state('availability_state.state', 'on') }}", + "start": {"service": "script.vacuum_start"}, + } + }, + } + }, + ) + + await hass.async_start() + await hass.async_block_till_done() + + # When template returns true.. + hass.states.async_set("availability_state.state", STATE_ON) + await hass.async_block_till_done() + + # Device State should not be unavailable + assert hass.states.get("vacuum.test_template_vacuum").state != STATE_UNAVAILABLE + + # When Availability template returns false + hass.states.async_set("availability_state.state", STATE_OFF) + await hass.async_block_till_done() + + # device state should be unavailable + assert hass.states.get("vacuum.test_template_vacuum").state == STATE_UNAVAILABLE + + +async def test_invalid_availability_template_keeps_component_available(hass, caplog): + """Test that an invalid availability keeps the device available.""" + assert await setup.async_setup_component( + hass, + "vacuum", + { + "vacuum": { + "platform": "template", + "vacuums": { + "test_template_vacuum": { + "availability_template": "{{ x - 12 }}", + "start": {"service": "script.vacuum_start"}, + } + }, + } + }, + ) + + await hass.async_start() + await hass.async_block_till_done() + + assert hass.states.get("vacuum.test_template_vacuum") != STATE_UNAVAILABLE + assert ("UndefinedError: 'x' is undefined") in caplog.text + + # End of template tests # diff --git a/tests/components/transmission/__init__.py b/tests/components/transmission/__init__.py new file mode 100644 index 00000000000..b8f8d8c847f --- /dev/null +++ b/tests/components/transmission/__init__.py @@ -0,0 +1 @@ +"""Tests for Transmission.""" diff --git a/tests/components/transmission/test_config_flow.py b/tests/components/transmission/test_config_flow.py new file mode 100644 index 00000000000..e79f5c8ac96 --- /dev/null +++ b/tests/components/transmission/test_config_flow.py @@ -0,0 +1,245 @@ +"""Tests for Met.no config flow.""" +from datetime import timedelta +from unittest.mock import patch + +import pytest +from transmissionrpc.error import TransmissionError + +from homeassistant import data_entry_flow +from homeassistant.components.transmission import config_flow +from homeassistant.components.transmission.const import ( + DEFAULT_NAME, + DEFAULT_PORT, + DEFAULT_SCAN_INTERVAL, + DOMAIN, +) +from homeassistant.const import ( + CONF_HOST, + CONF_NAME, + CONF_PASSWORD, + CONF_PORT, + CONF_SCAN_INTERVAL, + CONF_USERNAME, +) + +from tests.common import MockConfigEntry + +NAME = "Transmission" +HOST = "192.168.1.100" +USERNAME = "username" +PASSWORD = "password" +PORT = 9091 +SCAN_INTERVAL = 10 + + +@pytest.fixture(name="api") +def mock_transmission_api(): + """Mock an api.""" + with patch("transmissionrpc.Client"): + yield + + +@pytest.fixture(name="auth_error") +def mock_api_authentication_error(): + """Mock an api.""" + with patch( + "transmissionrpc.Client", side_effect=TransmissionError("401: Unauthorized") + ): + yield + + +@pytest.fixture(name="conn_error") +def mock_api_connection_error(): + """Mock an api.""" + with patch( + "transmissionrpc.Client", + side_effect=TransmissionError("111: Connection refused"), + ): + yield + + +@pytest.fixture(name="unknown_error") +def mock_api_unknown_error(): + """Mock an api.""" + with patch("transmissionrpc.Client", side_effect=TransmissionError): + yield + + +def init_config_flow(hass): + """Init a configuration flow.""" + flow = config_flow.TransmissionFlowHandler() + flow.hass = hass + return flow + + +async def test_flow_works(hass, api): + """Test user config.""" + flow = init_config_flow(hass) + + result = await flow.async_step_user() + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "user" + + # test with required fields only + result = await flow.async_step_user( + {CONF_NAME: NAME, CONF_HOST: HOST, CONF_PORT: PORT} + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == NAME + assert result["data"][CONF_NAME] == NAME + assert result["data"][CONF_HOST] == HOST + assert result["data"][CONF_PORT] == PORT + assert result["data"]["options"][CONF_SCAN_INTERVAL] == DEFAULT_SCAN_INTERVAL + + # test with all provided + result = await flow.async_step_user( + { + CONF_NAME: NAME, + CONF_HOST: HOST, + CONF_USERNAME: USERNAME, + CONF_PASSWORD: PASSWORD, + CONF_PORT: PORT, + } + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == NAME + assert result["data"][CONF_NAME] == NAME + assert result["data"][CONF_HOST] == HOST + assert result["data"][CONF_USERNAME] == USERNAME + assert result["data"][CONF_PASSWORD] == PASSWORD + assert result["data"][CONF_PORT] == PORT + assert result["data"]["options"][CONF_SCAN_INTERVAL] == DEFAULT_SCAN_INTERVAL + + +async def test_options(hass): + """Test updating options.""" + entry = MockConfigEntry( + domain=DOMAIN, + title=CONF_NAME, + data={ + "name": DEFAULT_NAME, + "host": HOST, + "username": USERNAME, + "password": PASSWORD, + "port": DEFAULT_PORT, + "options": {CONF_SCAN_INTERVAL: DEFAULT_SCAN_INTERVAL}, + }, + options={CONF_SCAN_INTERVAL: DEFAULT_SCAN_INTERVAL}, + ) + flow = init_config_flow(hass) + options_flow = flow.async_get_options_flow(entry) + + result = await options_flow.async_step_init() + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "init" + result = await options_flow.async_step_init({CONF_SCAN_INTERVAL: 10}) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["data"][CONF_SCAN_INTERVAL] == 10 + + +async def test_import(hass, api): + """Test import step.""" + flow = init_config_flow(hass) + + # import with minimum fields only + result = await flow.async_step_import( + { + CONF_NAME: DEFAULT_NAME, + CONF_HOST: HOST, + CONF_PORT: DEFAULT_PORT, + CONF_SCAN_INTERVAL: timedelta(seconds=DEFAULT_SCAN_INTERVAL), + } + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == DEFAULT_NAME + assert result["data"][CONF_NAME] == DEFAULT_NAME + assert result["data"][CONF_HOST] == HOST + assert result["data"][CONF_PORT] == DEFAULT_PORT + assert result["data"]["options"][CONF_SCAN_INTERVAL] == DEFAULT_SCAN_INTERVAL + + # import with all + result = await flow.async_step_import( + { + CONF_NAME: NAME, + CONF_HOST: HOST, + CONF_USERNAME: USERNAME, + CONF_PASSWORD: PASSWORD, + CONF_PORT: PORT, + CONF_SCAN_INTERVAL: timedelta(seconds=SCAN_INTERVAL), + } + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == NAME + assert result["data"][CONF_NAME] == NAME + assert result["data"][CONF_HOST] == HOST + assert result["data"][CONF_USERNAME] == USERNAME + assert result["data"][CONF_PASSWORD] == PASSWORD + assert result["data"][CONF_PORT] == PORT + assert result["data"]["options"][CONF_SCAN_INTERVAL] == SCAN_INTERVAL + + +async def test_integration_already_exists(hass, api): + """Test we only allow a single config flow.""" + MockConfigEntry(domain=DOMAIN).add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "user"} + ) + assert result["type"] == "abort" + assert result["reason"] == "one_instance_allowed" + + +async def test_error_on_wrong_credentials(hass, auth_error): + """Test with wrong credentials.""" + flow = init_config_flow(hass) + + result = await flow.async_step_user( + { + CONF_NAME: NAME, + CONF_HOST: HOST, + CONF_USERNAME: USERNAME, + CONF_PASSWORD: PASSWORD, + CONF_PORT: PORT, + } + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == { + CONF_USERNAME: "wrong_credentials", + CONF_PASSWORD: "wrong_credentials", + } + + +async def test_error_on_connection_failure(hass, conn_error): + """Test when connection to host fails.""" + flow = init_config_flow(hass) + + result = await flow.async_step_user( + { + CONF_NAME: NAME, + CONF_HOST: HOST, + CONF_USERNAME: USERNAME, + CONF_PASSWORD: PASSWORD, + CONF_PORT: PORT, + } + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {"base": "cannot_connect"} + + +async def test_error_on_unknwon_error(hass, unknown_error): + """Test when connection to host fails.""" + flow = init_config_flow(hass) + + result = await flow.async_step_user( + { + CONF_NAME: NAME, + CONF_HOST: HOST, + CONF_USERNAME: USERNAME, + CONF_PASSWORD: PASSWORD, + CONF_PORT: PORT, + } + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {"base": "cannot_connect"} diff --git a/tests/components/unifi/test_controller.py b/tests/components/unifi/test_controller.py index b28044bc3c7..e73719205f7 100644 --- a/tests/components/unifi/test_controller.py +++ b/tests/components/unifi/test_controller.py @@ -8,6 +8,7 @@ from homeassistant.components.unifi.const import ( CONF_CONTROLLER, CONF_SITE_ID, UNIFI_CONFIG, + UNIFI_WIRELESS_CLIENTS, ) from homeassistant.const import ( CONF_HOST, @@ -49,7 +50,8 @@ async def test_controller_setup(): controller.CONF_DETECTION_TIME: 30, controller.CONF_SSID_FILTER: ["ssid"], } - ] + ], + UNIFI_WIRELESS_CLIENTS: Mock(), } entry = Mock() entry.data = ENTRY_CONFIG @@ -57,6 +59,7 @@ async def test_controller_setup(): api = Mock() api.initialize.return_value = mock_coro(True) api.sites.return_value = mock_coro(CONTROLLER_SITES) + api.clients = [] unifi_controller = controller.UniFiController(hass, entry) @@ -100,7 +103,8 @@ async def test_controller_site(): async def test_controller_mac(): """Test that it is possible to identify controller mac.""" hass = Mock() - hass.data = {UNIFI_CONFIG: {}} + hass.data = {UNIFI_CONFIG: {}, UNIFI_WIRELESS_CLIENTS: Mock()} + hass.data[UNIFI_WIRELESS_CLIENTS].get_data.return_value = set() entry = Mock() entry.data = ENTRY_CONFIG entry.options = {} @@ -123,7 +127,7 @@ async def test_controller_mac(): async def test_controller_no_mac(): """Test that it works to not find the controllers mac.""" hass = Mock() - hass.data = {UNIFI_CONFIG: {}} + hass.data = {UNIFI_CONFIG: {}, UNIFI_WIRELESS_CLIENTS: Mock()} entry = Mock() entry.data = ENTRY_CONFIG entry.options = {} @@ -133,6 +137,7 @@ async def test_controller_no_mac(): api.initialize.return_value = mock_coro(True) api.clients = {"client1": client} api.sites.return_value = mock_coro(CONTROLLER_SITES) + api.clients = {} unifi_controller = controller.UniFiController(hass, entry) @@ -195,13 +200,14 @@ async def test_reset_if_entry_had_wrong_auth(): async def test_reset_unloads_entry_if_setup(): """Calling reset when the entry has been setup.""" hass = Mock() - hass.data = {UNIFI_CONFIG: {}} + hass.data = {UNIFI_CONFIG: {}, UNIFI_WIRELESS_CLIENTS: Mock()} entry = Mock() entry.data = ENTRY_CONFIG entry.options = {} api = Mock() api.initialize.return_value = mock_coro(True) api.sites.return_value = mock_coro(CONTROLLER_SITES) + api.clients = [] unifi_controller = controller.UniFiController(hass, entry) diff --git a/tests/components/unifi/test_device_tracker.py b/tests/components/unifi/test_device_tracker.py index 969c2a734d3..3a2b37487af 100644 --- a/tests/components/unifi/test_device_tracker.py +++ b/tests/components/unifi/test_device_tracker.py @@ -1,9 +1,11 @@ """The tests for the UniFi device tracker platform.""" from collections import deque from copy import copy -from unittest.mock import Mock + from datetime import timedelta +from asynctest import Mock + import pytest from aiounifi.clients import Clients, ClientsAll @@ -19,6 +21,7 @@ from homeassistant.components.unifi.const import ( CONF_TRACK_WIRED_CLIENTS, CONTROLLER_ID as CONF_CONTROLLER_ID, UNIFI_CONFIG, + UNIFI_WIRELESS_CLIENTS, ) from homeassistant.const import ( CONF_HOST, @@ -96,7 +99,7 @@ CONTROLLER_DATA = { CONF_PASSWORD: "mock-pswd", CONF_PORT: 1234, CONF_SITE_ID: "mock-site", - CONF_VERIFY_SSL: True, + CONF_VERIFY_SSL: False, } ENTRY_CONFIG = {CONF_CONTROLLER: CONTROLLER_DATA} @@ -108,7 +111,9 @@ CONTROLLER_ID = CONF_CONTROLLER_ID.format(host="mock-host", site="mock-site") def mock_controller(hass): """Mock a UniFi Controller.""" hass.data[UNIFI_CONFIG] = {} + hass.data[UNIFI_WIRELESS_CLIENTS] = Mock() controller = unifi.UniFiController(hass, None) + controller.wireless_clients = set() controller.api = Mock() controller.mock_requests = [] @@ -163,12 +168,12 @@ async def setup_controller(hass, mock_controller, options={}): async def test_platform_manually_configured(hass): - """Test that we do not discover anything or try to set up a bridge.""" + """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"}} ) - is True + is False ) assert unifi.DOMAIN not in hass.data @@ -253,6 +258,45 @@ async def test_tracked_devices(hass, mock_controller): assert device_1 is None +async def test_wireless_client_go_wired_issue(hass, mock_controller): + """Test the solution to catch wireless device go wired UniFi issue. + + UniFi has a known issue that when a wireless device goes away it sometimes gets marked as wired. + """ + client_1_client = copy(CLIENT_1) + client_1_client["last_seen"] = dt_util.as_timestamp(dt_util.utcnow()) + mock_controller.mock_client_responses.append([client_1_client]) + mock_controller.mock_device_responses.append({}) + + await setup_controller(hass, mock_controller) + assert len(mock_controller.mock_requests) == 2 + assert len(hass.states.async_all()) == 3 + + client_1 = hass.states.get("device_tracker.client_1") + assert client_1 is not None + assert client_1.state == "home" + + client_1_client["is_wired"] = True + client_1_client["last_seen"] = dt_util.as_timestamp(dt_util.utcnow()) + mock_controller.mock_client_responses.append([client_1_client]) + mock_controller.mock_device_responses.append({}) + await mock_controller.async_update() + await hass.async_block_till_done() + + client_1 = hass.states.get("device_tracker.client_1") + assert client_1.state == "not_home" + + client_1_client["is_wired"] = False + client_1_client["last_seen"] = dt_util.as_timestamp(dt_util.utcnow()) + mock_controller.mock_client_responses.append([client_1_client]) + mock_controller.mock_device_responses.append({}) + await mock_controller.async_update() + await hass.async_block_till_done() + + client_1 = hass.states.get("device_tracker.client_1") + assert client_1.state == "home" + + async def test_restoring_client(hass, mock_controller): """Test the update_items function with some clients.""" mock_controller.mock_client_responses.append([CLIENT_2]) diff --git a/tests/components/unifi/test_switch.py b/tests/components/unifi/test_switch.py index e660e57fc67..7ea5e0680b9 100644 --- a/tests/components/unifi/test_switch.py +++ b/tests/components/unifi/test_switch.py @@ -17,6 +17,7 @@ from homeassistant.components.unifi.const import ( CONF_SITE_ID, CONTROLLER_ID as CONF_CONTROLLER_ID, UNIFI_CONFIG, + UNIFI_WIRELESS_CLIENTS, ) from homeassistant.helpers import entity_registry from homeassistant.setup import async_setup_component @@ -221,7 +222,9 @@ CONTROLLER_ID = CONF_CONTROLLER_ID.format(host="mock-host", site="mock-site") def mock_controller(hass): """Mock a UniFi Controller.""" hass.data[UNIFI_CONFIG] = {} + hass.data[UNIFI_WIRELESS_CLIENTS] = Mock() controller = unifi.UniFiController(hass, None) + controller.wireless_clients = set() controller._site_role = "admin" @@ -326,7 +329,7 @@ async def test_switches(hass, mock_controller): await setup_controller(hass, mock_controller, options) assert len(mock_controller.mock_requests) == 3 - assert len(hass.states.async_all()) == 5 + assert len(hass.states.async_all()) == 4 switch_1 = hass.states.get("switch.poe_client_1") assert switch_1 is not None diff --git a/tests/components/universal/test_media_player.py b/tests/components/universal/test_media_player.py index fd6c0f73303..67d826f576b 100644 --- a/tests/components/universal/test_media_player.py +++ b/tests/components/universal/test_media_player.py @@ -1,4 +1,5 @@ """The tests for the Universal Media player platform.""" +import asyncio from copy import copy import unittest @@ -10,7 +11,6 @@ import homeassistant.components.input_number as input_number import homeassistant.components.input_select as input_select import homeassistant.components.media_player as media_player import homeassistant.components.universal.media_player as universal -from homeassistant.util.async_ import run_coroutine_threadsafe from tests.common import mock_service, get_test_home_assistant @@ -298,7 +298,7 @@ class TestMediaPlayer(unittest.TestCase): setup_ok = True try: - run_coroutine_threadsafe( + asyncio.run_coroutine_threadsafe( universal.async_setup_platform( self.hass, validate_config(bad_config), add_entities ), @@ -309,7 +309,7 @@ class TestMediaPlayer(unittest.TestCase): assert not setup_ok assert 0 == len(entities) - run_coroutine_threadsafe( + asyncio.run_coroutine_threadsafe( universal.async_setup_platform( self.hass, validate_config(config), add_entities ), @@ -369,26 +369,26 @@ class TestMediaPlayer(unittest.TestCase): ump = universal.UniversalMediaPlayer(self.hass, **config) ump.entity_id = media_player.ENTITY_ID_FORMAT.format(config["name"]) - run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() + asyncio.run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() assert ump._child_state is None self.mock_mp_1._state = STATE_PLAYING self.mock_mp_1.schedule_update_ha_state() self.hass.block_till_done() - run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() + asyncio.run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() assert self.mock_mp_1.entity_id == ump._child_state.entity_id self.mock_mp_2._state = STATE_PLAYING self.mock_mp_2.schedule_update_ha_state() self.hass.block_till_done() - run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() + asyncio.run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() assert self.mock_mp_1.entity_id == ump._child_state.entity_id self.mock_mp_1._state = STATE_OFF self.mock_mp_1.schedule_update_ha_state() self.hass.block_till_done() - run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() + asyncio.run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() assert self.mock_mp_2.entity_id == ump._child_state.entity_id def test_name(self): @@ -413,14 +413,14 @@ class TestMediaPlayer(unittest.TestCase): ump = universal.UniversalMediaPlayer(self.hass, **config) ump.entity_id = media_player.ENTITY_ID_FORMAT.format(config["name"]) - run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() + asyncio.run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() assert ump.state, STATE_OFF self.mock_mp_1._state = STATE_PLAYING self.mock_mp_1.schedule_update_ha_state() self.hass.block_till_done() - run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() + asyncio.run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() assert STATE_PLAYING == ump.state def test_state_with_children_and_attrs(self): @@ -429,22 +429,22 @@ class TestMediaPlayer(unittest.TestCase): ump = universal.UniversalMediaPlayer(self.hass, **config) ump.entity_id = media_player.ENTITY_ID_FORMAT.format(config["name"]) - run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() + asyncio.run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() assert STATE_OFF == ump.state self.hass.states.set(self.mock_state_switch_id, STATE_ON) - run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() + asyncio.run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() assert STATE_ON == ump.state self.mock_mp_1._state = STATE_PLAYING self.mock_mp_1.schedule_update_ha_state() self.hass.block_till_done() - run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() + asyncio.run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() assert STATE_PLAYING == ump.state self.hass.states.set(self.mock_state_switch_id, STATE_OFF) - run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() + asyncio.run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() assert STATE_OFF == ump.state def test_volume_level(self): @@ -453,20 +453,20 @@ class TestMediaPlayer(unittest.TestCase): ump = universal.UniversalMediaPlayer(self.hass, **config) ump.entity_id = media_player.ENTITY_ID_FORMAT.format(config["name"]) - run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() + asyncio.run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() assert ump.volume_level is None self.mock_mp_1._state = STATE_PLAYING self.mock_mp_1.schedule_update_ha_state() self.hass.block_till_done() - run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() + asyncio.run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() assert 0 == ump.volume_level self.mock_mp_1._volume_level = 1 self.mock_mp_1.schedule_update_ha_state() self.hass.block_till_done() - run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() + asyncio.run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() assert 1 == ump.volume_level def test_media_image_url(self): @@ -476,7 +476,7 @@ class TestMediaPlayer(unittest.TestCase): ump = universal.UniversalMediaPlayer(self.hass, **config) ump.entity_id = media_player.ENTITY_ID_FORMAT.format(config["name"]) - run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() + asyncio.run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() assert ump.media_image_url is None @@ -484,7 +484,7 @@ class TestMediaPlayer(unittest.TestCase): self.mock_mp_1._media_image_url = test_url self.mock_mp_1.schedule_update_ha_state() self.hass.block_till_done() - run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() + asyncio.run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() # mock_mp_1 will convert the url to the api proxy url. This test # ensures ump passes through the same url without an additional proxy. assert self.mock_mp_1.entity_picture == ump.entity_picture @@ -495,20 +495,20 @@ class TestMediaPlayer(unittest.TestCase): ump = universal.UniversalMediaPlayer(self.hass, **config) ump.entity_id = media_player.ENTITY_ID_FORMAT.format(config["name"]) - run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() + asyncio.run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() assert not ump.is_volume_muted self.mock_mp_1._state = STATE_PLAYING self.mock_mp_1.schedule_update_ha_state() self.hass.block_till_done() - run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() + asyncio.run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() assert not ump.is_volume_muted self.mock_mp_1._is_volume_muted = True self.mock_mp_1.schedule_update_ha_state() self.hass.block_till_done() - run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() + asyncio.run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() assert ump.is_volume_muted def test_source_list_children_and_attr(self): @@ -561,7 +561,7 @@ class TestMediaPlayer(unittest.TestCase): ump = universal.UniversalMediaPlayer(self.hass, **config) ump.entity_id = media_player.ENTITY_ID_FORMAT.format(config["name"]) - run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() + asyncio.run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() assert 0 == ump.supported_features @@ -569,7 +569,7 @@ class TestMediaPlayer(unittest.TestCase): self.mock_mp_1._state = STATE_PLAYING self.mock_mp_1.schedule_update_ha_state() self.hass.block_till_done() - run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() + asyncio.run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() assert 512 == ump.supported_features def test_supported_features_children_and_cmds(self): @@ -590,12 +590,12 @@ class TestMediaPlayer(unittest.TestCase): ump = universal.UniversalMediaPlayer(self.hass, **config) ump.entity_id = media_player.ENTITY_ID_FORMAT.format(config["name"]) - run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() + asyncio.run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() self.mock_mp_1._state = STATE_PLAYING self.mock_mp_1.schedule_update_ha_state() self.hass.block_till_done() - run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() + asyncio.run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() check_flags = ( universal.SUPPORT_TURN_ON @@ -615,16 +615,16 @@ class TestMediaPlayer(unittest.TestCase): ump = universal.UniversalMediaPlayer(self.hass, **config) ump.entity_id = media_player.ENTITY_ID_FORMAT.format(config["name"]) - run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() + asyncio.run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() self.mock_mp_1._state = STATE_OFF self.mock_mp_1.schedule_update_ha_state() self.mock_mp_2._state = STATE_OFF self.mock_mp_2.schedule_update_ha_state() self.hass.block_till_done() - run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() + asyncio.run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() - run_coroutine_threadsafe(ump.async_turn_off(), self.hass.loop).result() + asyncio.run_coroutine_threadsafe(ump.async_turn_off(), self.hass.loop).result() assert 0 == len(self.mock_mp_1.service_calls["turn_off"]) assert 0 == len(self.mock_mp_2.service_calls["turn_off"]) @@ -634,67 +634,85 @@ class TestMediaPlayer(unittest.TestCase): ump = universal.UniversalMediaPlayer(self.hass, **config) ump.entity_id = media_player.ENTITY_ID_FORMAT.format(config["name"]) - run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() + asyncio.run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() self.mock_mp_2._state = STATE_PLAYING self.mock_mp_2.schedule_update_ha_state() self.hass.block_till_done() - run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() + asyncio.run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() - run_coroutine_threadsafe(ump.async_turn_off(), self.hass.loop).result() + asyncio.run_coroutine_threadsafe(ump.async_turn_off(), self.hass.loop).result() assert 1 == len(self.mock_mp_2.service_calls["turn_off"]) - run_coroutine_threadsafe(ump.async_turn_on(), self.hass.loop).result() + asyncio.run_coroutine_threadsafe(ump.async_turn_on(), self.hass.loop).result() assert 1 == len(self.mock_mp_2.service_calls["turn_on"]) - run_coroutine_threadsafe(ump.async_mute_volume(True), self.hass.loop).result() + asyncio.run_coroutine_threadsafe( + ump.async_mute_volume(True), self.hass.loop + ).result() assert 1 == len(self.mock_mp_2.service_calls["mute_volume"]) - run_coroutine_threadsafe( + asyncio.run_coroutine_threadsafe( ump.async_set_volume_level(0.5), self.hass.loop ).result() assert 1 == len(self.mock_mp_2.service_calls["set_volume_level"]) - run_coroutine_threadsafe(ump.async_media_play(), self.hass.loop).result() + asyncio.run_coroutine_threadsafe( + ump.async_media_play(), self.hass.loop + ).result() assert 1 == len(self.mock_mp_2.service_calls["media_play"]) - run_coroutine_threadsafe(ump.async_media_pause(), self.hass.loop).result() + asyncio.run_coroutine_threadsafe( + ump.async_media_pause(), self.hass.loop + ).result() assert 1 == len(self.mock_mp_2.service_calls["media_pause"]) - run_coroutine_threadsafe( + asyncio.run_coroutine_threadsafe( ump.async_media_previous_track(), self.hass.loop ).result() assert 1 == len(self.mock_mp_2.service_calls["media_previous_track"]) - run_coroutine_threadsafe(ump.async_media_next_track(), self.hass.loop).result() + asyncio.run_coroutine_threadsafe( + ump.async_media_next_track(), self.hass.loop + ).result() assert 1 == len(self.mock_mp_2.service_calls["media_next_track"]) - run_coroutine_threadsafe(ump.async_media_seek(100), self.hass.loop).result() + asyncio.run_coroutine_threadsafe( + ump.async_media_seek(100), self.hass.loop + ).result() assert 1 == len(self.mock_mp_2.service_calls["media_seek"]) - run_coroutine_threadsafe( + asyncio.run_coroutine_threadsafe( ump.async_play_media("movie", "batman"), self.hass.loop ).result() assert 1 == len(self.mock_mp_2.service_calls["play_media"]) - run_coroutine_threadsafe(ump.async_volume_up(), self.hass.loop).result() + asyncio.run_coroutine_threadsafe(ump.async_volume_up(), self.hass.loop).result() assert 1 == len(self.mock_mp_2.service_calls["volume_up"]) - run_coroutine_threadsafe(ump.async_volume_down(), self.hass.loop).result() + asyncio.run_coroutine_threadsafe( + ump.async_volume_down(), self.hass.loop + ).result() assert 1 == len(self.mock_mp_2.service_calls["volume_down"]) - run_coroutine_threadsafe(ump.async_media_play_pause(), self.hass.loop).result() + asyncio.run_coroutine_threadsafe( + ump.async_media_play_pause(), self.hass.loop + ).result() assert 1 == len(self.mock_mp_2.service_calls["media_play_pause"]) - run_coroutine_threadsafe( + asyncio.run_coroutine_threadsafe( ump.async_select_source("dvd"), self.hass.loop ).result() assert 1 == len(self.mock_mp_2.service_calls["select_source"]) - run_coroutine_threadsafe(ump.async_clear_playlist(), self.hass.loop).result() + asyncio.run_coroutine_threadsafe( + ump.async_clear_playlist(), self.hass.loop + ).result() assert 1 == len(self.mock_mp_2.service_calls["clear_playlist"]) - run_coroutine_threadsafe(ump.async_set_shuffle(True), self.hass.loop).result() + asyncio.run_coroutine_threadsafe( + ump.async_set_shuffle(True), self.hass.loop + ).result() assert 1 == len(self.mock_mp_2.service_calls["shuffle_set"]) def test_service_call_to_command(self): @@ -707,12 +725,12 @@ class TestMediaPlayer(unittest.TestCase): ump = universal.UniversalMediaPlayer(self.hass, **config) ump.entity_id = media_player.ENTITY_ID_FORMAT.format(config["name"]) - run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() + asyncio.run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() self.mock_mp_2._state = STATE_PLAYING self.mock_mp_2.schedule_update_ha_state() self.hass.block_till_done() - run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() + asyncio.run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() - run_coroutine_threadsafe(ump.async_turn_off(), self.hass.loop).result() + asyncio.run_coroutine_threadsafe(ump.async_turn_off(), self.hass.loop).result() assert 1 == len(service) diff --git a/tests/components/uptime/test_sensor.py b/tests/components/uptime/test_sensor.py index 32d73c70d45..b3dcddfba6a 100644 --- a/tests/components/uptime/test_sensor.py +++ b/tests/components/uptime/test_sensor.py @@ -1,9 +1,9 @@ """The tests for the uptime sensor platform.""" +import asyncio import unittest from unittest.mock import patch from datetime import timedelta -from homeassistant.util.async_ import run_coroutine_threadsafe from homeassistant.setup import setup_component from homeassistant.components.uptime.sensor import UptimeSensor from tests.common import get_test_home_assistant @@ -46,11 +46,15 @@ class TestUptimeSensor(unittest.TestCase): assert sensor.unit_of_measurement == "days" new_time = sensor.initial + timedelta(days=1) with patch("homeassistant.util.dt.now", return_value=new_time): - run_coroutine_threadsafe(sensor.async_update(), self.hass.loop).result() + asyncio.run_coroutine_threadsafe( + sensor.async_update(), self.hass.loop + ).result() assert sensor.state == 1.00 new_time = sensor.initial + timedelta(days=111.499) with patch("homeassistant.util.dt.now", return_value=new_time): - run_coroutine_threadsafe(sensor.async_update(), self.hass.loop).result() + asyncio.run_coroutine_threadsafe( + sensor.async_update(), self.hass.loop + ).result() assert sensor.state == 111.50 def test_uptime_sensor_hours_output(self): @@ -59,11 +63,15 @@ class TestUptimeSensor(unittest.TestCase): assert sensor.unit_of_measurement == "hours" new_time = sensor.initial + timedelta(hours=16) with patch("homeassistant.util.dt.now", return_value=new_time): - run_coroutine_threadsafe(sensor.async_update(), self.hass.loop).result() + asyncio.run_coroutine_threadsafe( + sensor.async_update(), self.hass.loop + ).result() assert sensor.state == 16.00 new_time = sensor.initial + timedelta(hours=72.499) with patch("homeassistant.util.dt.now", return_value=new_time): - run_coroutine_threadsafe(sensor.async_update(), self.hass.loop).result() + asyncio.run_coroutine_threadsafe( + sensor.async_update(), self.hass.loop + ).result() assert sensor.state == 72.50 def test_uptime_sensor_minutes_output(self): @@ -72,9 +80,13 @@ class TestUptimeSensor(unittest.TestCase): assert sensor.unit_of_measurement == "minutes" new_time = sensor.initial + timedelta(minutes=16) with patch("homeassistant.util.dt.now", return_value=new_time): - run_coroutine_threadsafe(sensor.async_update(), self.hass.loop).result() + asyncio.run_coroutine_threadsafe( + sensor.async_update(), self.hass.loop + ).result() assert sensor.state == 16.00 new_time = sensor.initial + timedelta(minutes=12.499) with patch("homeassistant.util.dt.now", return_value=new_time): - run_coroutine_threadsafe(sensor.async_update(), self.hass.loop).result() + asyncio.run_coroutine_threadsafe( + sensor.async_update(), self.hass.loop + ).result() assert sensor.state == 12.50 diff --git a/tests/components/withings/common.py b/tests/components/withings/common.py index b8406c39711..f3839a1be55 100644 --- a/tests/components/withings/common.py +++ b/tests/components/withings/common.py @@ -1,7 +1,7 @@ """Common data for for the withings component tests.""" import time -import nokia +import withings_api as withings import homeassistant.components.withings.const as const @@ -92,7 +92,7 @@ def new_measure(type_str, value, unit): } -def nokia_sleep_response(states): +def withings_sleep_response(states): """Create a sleep response based on states.""" data = [] for state in states: @@ -104,10 +104,10 @@ def nokia_sleep_response(states): ) ) - return nokia.NokiaSleep(new_sleep_data("aa", data)) + return withings.WithingsSleep(new_sleep_data("aa", data)) -NOKIA_MEASURES_RESPONSE = nokia.NokiaMeasures( +WITHINGS_MEASURES_RESPONSE = withings.WithingsMeasures( { "updatetime": "", "timezone": "", @@ -174,7 +174,7 @@ NOKIA_MEASURES_RESPONSE = nokia.NokiaMeasures( ) -NOKIA_SLEEP_RESPONSE = nokia_sleep_response( +WITHINGS_SLEEP_RESPONSE = withings_sleep_response( [ const.MEASURE_TYPE_SLEEP_STATE_AWAKE, const.MEASURE_TYPE_SLEEP_STATE_LIGHT, @@ -183,7 +183,7 @@ NOKIA_SLEEP_RESPONSE = nokia_sleep_response( ] ) -NOKIA_SLEEP_SUMMARY_RESPONSE = nokia.NokiaSleepSummary( +WITHINGS_SLEEP_SUMMARY_RESPONSE = withings.WithingsSleepSummary( { "series": [ new_sleep_summary( diff --git a/tests/components/withings/conftest.py b/tests/components/withings/conftest.py index 7cbe3dc1cd4..0aa6af0d7c0 100644 --- a/tests/components/withings/conftest.py +++ b/tests/components/withings/conftest.py @@ -3,7 +3,7 @@ import time from typing import Awaitable, Callable, List import asynctest -import nokia +import withings_api as withings import pytest import homeassistant.components.api as api @@ -15,9 +15,9 @@ from homeassistant.const import CONF_UNIT_SYSTEM, CONF_UNIT_SYSTEM_METRIC from homeassistant.setup import async_setup_component from .common import ( - NOKIA_MEASURES_RESPONSE, - NOKIA_SLEEP_RESPONSE, - NOKIA_SLEEP_SUMMARY_RESPONSE, + WITHINGS_MEASURES_RESPONSE, + WITHINGS_SLEEP_RESPONSE, + WITHINGS_SLEEP_SUMMARY_RESPONSE, ) @@ -34,17 +34,17 @@ class WithingsFactoryConfig: measures: List[str] = None, unit_system: str = None, throttle_interval: int = const.THROTTLE_INTERVAL, - nokia_request_response="DATA", - nokia_measures_response: nokia.NokiaMeasures = NOKIA_MEASURES_RESPONSE, - nokia_sleep_response: nokia.NokiaSleep = NOKIA_SLEEP_RESPONSE, - nokia_sleep_summary_response: nokia.NokiaSleepSummary = NOKIA_SLEEP_SUMMARY_RESPONSE, + withings_request_response="DATA", + withings_measures_response: withings.WithingsMeasures = WITHINGS_MEASURES_RESPONSE, + withings_sleep_response: withings.WithingsSleep = WITHINGS_SLEEP_RESPONSE, + withings_sleep_summary_response: withings.WithingsSleepSummary = WITHINGS_SLEEP_SUMMARY_RESPONSE, ) -> None: """Constructor.""" self._throttle_interval = throttle_interval - self._nokia_request_response = nokia_request_response - self._nokia_measures_response = nokia_measures_response - self._nokia_sleep_response = nokia_sleep_response - self._nokia_sleep_summary_response = nokia_sleep_summary_response + self._withings_request_response = withings_request_response + self._withings_measures_response = withings_measures_response + self._withings_sleep_response = withings_sleep_response + self._withings_sleep_summary_response = withings_sleep_summary_response self._withings_config = { const.CLIENT_ID: "my_client_id", const.CLIENT_SECRET: "my_client_secret", @@ -103,24 +103,24 @@ class WithingsFactoryConfig: return self._throttle_interval @property - def nokia_request_response(self): + def withings_request_response(self): """Request response.""" - return self._nokia_request_response + return self._withings_request_response @property - def nokia_measures_response(self) -> nokia.NokiaMeasures: + def withings_measures_response(self) -> withings.WithingsMeasures: """Measures response.""" - return self._nokia_measures_response + return self._withings_measures_response @property - def nokia_sleep_response(self) -> nokia.NokiaSleep: + def withings_sleep_response(self) -> withings.WithingsSleep: """Sleep response.""" - return self._nokia_sleep_response + return self._withings_sleep_response @property - def nokia_sleep_summary_response(self) -> nokia.NokiaSleepSummary: + def withings_sleep_summary_response(self) -> withings.WithingsSleepSummary: """Sleep summary response.""" - return self._nokia_sleep_summary_response + return self._withings_sleep_summary_response class WithingsFactoryData: @@ -130,21 +130,21 @@ class WithingsFactoryData: self, hass, flow_id, - nokia_auth_get_credentials_mock, - nokia_api_request_mock, - nokia_api_get_measures_mock, - nokia_api_get_sleep_mock, - nokia_api_get_sleep_summary_mock, + withings_auth_get_credentials_mock, + withings_api_request_mock, + withings_api_get_measures_mock, + withings_api_get_sleep_mock, + withings_api_get_sleep_summary_mock, data_manager_get_throttle_interval_mock, ): """Constructor.""" self._hass = hass self._flow_id = flow_id - self._nokia_auth_get_credentials_mock = nokia_auth_get_credentials_mock - self._nokia_api_request_mock = nokia_api_request_mock - self._nokia_api_get_measures_mock = nokia_api_get_measures_mock - self._nokia_api_get_sleep_mock = nokia_api_get_sleep_mock - self._nokia_api_get_sleep_summary_mock = nokia_api_get_sleep_summary_mock + self._withings_auth_get_credentials_mock = withings_auth_get_credentials_mock + self._withings_api_request_mock = withings_api_request_mock + self._withings_api_get_measures_mock = withings_api_get_measures_mock + self._withings_api_get_sleep_mock = withings_api_get_sleep_mock + self._withings_api_get_sleep_summary_mock = withings_api_get_sleep_summary_mock self._data_manager_get_throttle_interval_mock = ( data_manager_get_throttle_interval_mock ) @@ -160,29 +160,29 @@ class WithingsFactoryData: return self._flow_id @property - def nokia_auth_get_credentials_mock(self): + def withings_auth_get_credentials_mock(self): """Get auth credentials mock.""" - return self._nokia_auth_get_credentials_mock + return self._withings_auth_get_credentials_mock @property - def nokia_api_request_mock(self): + def withings_api_request_mock(self): """Get request mock.""" - return self._nokia_api_request_mock + return self._withings_api_request_mock @property - def nokia_api_get_measures_mock(self): + def withings_api_get_measures_mock(self): """Get measures mock.""" - return self._nokia_api_get_measures_mock + return self._withings_api_get_measures_mock @property - def nokia_api_get_sleep_mock(self): + def withings_api_get_sleep_mock(self): """Get sleep mock.""" - return self._nokia_api_get_sleep_mock + return self._withings_api_get_sleep_mock @property - def nokia_api_get_sleep_summary_mock(self): + def withings_api_get_sleep_summary_mock(self): """Get sleep summary mock.""" - return self._nokia_api_get_sleep_summary_mock + return self._withings_api_get_sleep_summary_mock @property def data_manager_get_throttle_interval_mock(self): @@ -243,9 +243,9 @@ def withings_factory_fixture(request, hass) -> WithingsFactory: assert await async_setup_component(hass, http.DOMAIN, config.hass_config) assert await async_setup_component(hass, api.DOMAIN, config.hass_config) - nokia_auth_get_credentials_patch = asynctest.patch( - "nokia.NokiaAuth.get_credentials", - return_value=nokia.NokiaCredentials( + withings_auth_get_credentials_patch = asynctest.patch( + "withings_api.WithingsAuth.get_credentials", + return_value=withings.WithingsCredentials( access_token="my_access_token", token_expiry=time.time() + 600, token_type="my_token_type", @@ -255,28 +255,33 @@ def withings_factory_fixture(request, hass) -> WithingsFactory: consumer_secret=config.withings_config.get(const.CLIENT_SECRET), ), ) - nokia_auth_get_credentials_mock = nokia_auth_get_credentials_patch.start() + withings_auth_get_credentials_mock = withings_auth_get_credentials_patch.start() - nokia_api_request_patch = asynctest.patch( - "nokia.NokiaApi.request", return_value=config.nokia_request_response + withings_api_request_patch = asynctest.patch( + "withings_api.WithingsApi.request", + return_value=config.withings_request_response, ) - nokia_api_request_mock = nokia_api_request_patch.start() + withings_api_request_mock = withings_api_request_patch.start() - nokia_api_get_measures_patch = asynctest.patch( - "nokia.NokiaApi.get_measures", return_value=config.nokia_measures_response + withings_api_get_measures_patch = asynctest.patch( + "withings_api.WithingsApi.get_measures", + return_value=config.withings_measures_response, ) - nokia_api_get_measures_mock = nokia_api_get_measures_patch.start() + withings_api_get_measures_mock = withings_api_get_measures_patch.start() - nokia_api_get_sleep_patch = asynctest.patch( - "nokia.NokiaApi.get_sleep", return_value=config.nokia_sleep_response + withings_api_get_sleep_patch = asynctest.patch( + "withings_api.WithingsApi.get_sleep", + return_value=config.withings_sleep_response, ) - nokia_api_get_sleep_mock = nokia_api_get_sleep_patch.start() + withings_api_get_sleep_mock = withings_api_get_sleep_patch.start() - nokia_api_get_sleep_summary_patch = asynctest.patch( - "nokia.NokiaApi.get_sleep_summary", - return_value=config.nokia_sleep_summary_response, + withings_api_get_sleep_summary_patch = asynctest.patch( + "withings_api.WithingsApi.get_sleep_summary", + return_value=config.withings_sleep_summary_response, + ) + withings_api_get_sleep_summary_mock = ( + withings_api_get_sleep_summary_patch.start() ) - nokia_api_get_sleep_summary_mock = nokia_api_get_sleep_summary_patch.start() data_manager_get_throttle_interval_patch = asynctest.patch( "homeassistant.components.withings.common.WithingsDataManager" @@ -295,11 +300,11 @@ def withings_factory_fixture(request, hass) -> WithingsFactory: patches.extend( [ - nokia_auth_get_credentials_patch, - nokia_api_request_patch, - nokia_api_get_measures_patch, - nokia_api_get_sleep_patch, - nokia_api_get_sleep_summary_patch, + withings_auth_get_credentials_patch, + withings_api_request_patch, + withings_api_get_measures_patch, + withings_api_get_sleep_patch, + withings_api_get_sleep_summary_patch, data_manager_get_throttle_interval_patch, get_measures_patch, ] @@ -328,11 +333,11 @@ def withings_factory_fixture(request, hass) -> WithingsFactory: return WithingsFactoryData( hass, flow_id, - nokia_auth_get_credentials_mock, - nokia_api_request_mock, - nokia_api_get_measures_mock, - nokia_api_get_sleep_mock, - nokia_api_get_sleep_summary_mock, + withings_auth_get_credentials_mock, + withings_api_request_mock, + withings_api_get_measures_mock, + withings_api_get_sleep_mock, + withings_api_get_sleep_summary_mock, data_manager_get_throttle_interval_mock, ) diff --git a/tests/components/withings/test_common.py b/tests/components/withings/test_common.py index a22689f92bb..9f2480f9094 100644 --- a/tests/components/withings/test_common.py +++ b/tests/components/withings/test_common.py @@ -1,6 +1,6 @@ """Tests for the Withings component.""" from asynctest import MagicMock -import nokia +import withings_api as withings from oauthlib.oauth2.rfc6749.errors import MissingTokenError import pytest from requests_oauthlib import TokenUpdated @@ -13,19 +13,19 @@ from homeassistant.components.withings.common import ( from homeassistant.exceptions import PlatformNotReady -@pytest.fixture(name="nokia_api") -def nokia_api_fixture(): - """Provide nokia api.""" - nokia_api = nokia.NokiaApi.__new__(nokia.NokiaApi) - nokia_api.get_measures = MagicMock() - nokia_api.get_sleep = MagicMock() - return nokia_api +@pytest.fixture(name="withings_api") +def withings_api_fixture(): + """Provide withings api.""" + withings_api = withings.WithingsApi.__new__(withings.WithingsApi) + withings_api.get_measures = MagicMock() + withings_api.get_sleep = MagicMock() + return withings_api @pytest.fixture(name="data_manager") -def data_manager_fixture(hass, nokia_api: nokia.NokiaApi): +def data_manager_fixture(hass, withings_api: withings.WithingsApi): """Provide data manager.""" - return WithingsDataManager(hass, "My Profile", nokia_api) + return WithingsDataManager(hass, "My Profile", withings_api) def test_print_service(): diff --git a/tests/components/withings/test_sensor.py b/tests/components/withings/test_sensor.py index da77910097b..697d0a8b864 100644 --- a/tests/components/withings/test_sensor.py +++ b/tests/components/withings/test_sensor.py @@ -2,7 +2,12 @@ from unittest.mock import MagicMock, patch import asynctest -from nokia import NokiaApi, NokiaMeasures, NokiaSleep, NokiaSleepSummary +from withings_api import ( + WithingsApi, + WithingsMeasures, + WithingsSleep, + WithingsSleepSummary, +) import pytest from homeassistant.components.withings import DOMAIN @@ -15,7 +20,7 @@ from homeassistant.helpers.entity_component import async_update_entity from homeassistant.helpers.typing import HomeAssistantType from homeassistant.util import slugify -from .common import nokia_sleep_response +from .common import withings_sleep_response from .conftest import WithingsFactory, WithingsFactoryConfig @@ -120,9 +125,9 @@ async def test_health_sensor_state_none( data = await withings_factory( WithingsFactoryConfig( measures=measure, - nokia_measures_response=None, - nokia_sleep_response=None, - nokia_sleep_summary_response=None, + withings_measures_response=None, + withings_sleep_response=None, + withings_sleep_summary_response=None, ) ) @@ -153,9 +158,9 @@ async def test_health_sensor_state_empty( data = await withings_factory( WithingsFactoryConfig( measures=measure, - nokia_measures_response=NokiaMeasures({"measuregrps": []}), - nokia_sleep_response=NokiaSleep({"series": []}), - nokia_sleep_summary_response=NokiaSleepSummary({"series": []}), + withings_measures_response=WithingsMeasures({"measuregrps": []}), + withings_sleep_response=WithingsSleep({"series": []}), + withings_sleep_summary_response=WithingsSleepSummary({"series": []}), ) ) @@ -201,7 +206,8 @@ async def test_sleep_state_throttled( data = await withings_factory( WithingsFactoryConfig( - measures=[measure], nokia_sleep_response=nokia_sleep_response(sleep_states) + measures=[measure], + withings_sleep_response=withings_sleep_response(sleep_states), ) ) @@ -257,16 +263,16 @@ async def test_async_setup_entry_credentials_saver(hass: HomeAssistantType): "expires_in": "2", } - original_nokia_api = NokiaApi - nokia_api_instance = None + original_withings_api = WithingsApi + withings_api_instance = None - def new_nokia_api(*args, **kwargs): - nonlocal nokia_api_instance - nokia_api_instance = original_nokia_api(*args, **kwargs) - nokia_api_instance.request = MagicMock() - return nokia_api_instance + def new_withings_api(*args, **kwargs): + nonlocal withings_api_instance + withings_api_instance = original_withings_api(*args, **kwargs) + withings_api_instance.request = MagicMock() + return withings_api_instance - nokia_api_patch = patch("nokia.NokiaApi", side_effect=new_nokia_api) + withings_api_patch = patch("withings_api.WithingsApi", side_effect=new_withings_api) session_patch = patch("requests_oauthlib.OAuth2Session") client_patch = patch("oauthlib.oauth2.WebApplicationClient") update_entry_patch = patch.object( @@ -275,7 +281,7 @@ async def test_async_setup_entry_credentials_saver(hass: HomeAssistantType): wraps=hass.config_entries.async_update_entry, ) - with session_patch, client_patch, nokia_api_patch, update_entry_patch: + with session_patch, client_patch, withings_api_patch, update_entry_patch: async_add_entities = MagicMock() hass.config_entries.async_update_entry = MagicMock() config_entry = ConfigEntry( @@ -298,7 +304,7 @@ async def test_async_setup_entry_credentials_saver(hass: HomeAssistantType): await async_setup_entry(hass, config_entry, async_add_entities) - nokia_api_instance.set_token(expected_creds) + withings_api_instance.set_token(expected_creds) new_creds = config_entry.data[const.CREDENTIALS] assert new_creds["access_token"] == "my_access_token2" diff --git a/tests/components/yandex_transport/__init__.py b/tests/components/yandex_transport/__init__.py new file mode 100644 index 00000000000..fe6b0db52d3 --- /dev/null +++ b/tests/components/yandex_transport/__init__.py @@ -0,0 +1 @@ +"""Tests for the yandex transport platform.""" diff --git a/tests/components/yandex_transport/test_yandex_transport_sensor.py b/tests/components/yandex_transport/test_yandex_transport_sensor.py new file mode 100644 index 00000000000..50d945e7fae --- /dev/null +++ b/tests/components/yandex_transport/test_yandex_transport_sensor.py @@ -0,0 +1,88 @@ +"""Tests for the yandex transport platform.""" + +import json +import pytest + +import homeassistant.components.sensor as sensor +import homeassistant.util.dt as dt_util +from homeassistant.const import CONF_NAME +from tests.common import ( + assert_setup_component, + async_setup_component, + MockDependency, + load_fixture, +) + +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 + instance.get_stop_info.return_value = REPLY + yield instance + + +STOP_ID = 9639579 +ROUTES = ["194", "т36", "т47", "м10"] +NAME = "test_name" +TEST_CONFIG = { + "sensor": { + "platform": "yandex_transport", + "stop_id": 9639579, + "routes": ROUTES, + "name": NAME, + } +} + +FILTERED_ATTRS = { + "т36": ["21:43", "21:47", "22:02"], + "т47": ["21:40", "22:01"], + "м10": ["21:48", "22:00"], + "stop_name": "7-й автобусный парк", + "attribution": "Data provided by maps.yandex.ru", +} + +RESULT_STATE = dt_util.utc_from_timestamp(1568659253).isoformat(timespec="seconds") + + +async def assert_setup_sensor(hass, config, count=1): + """Set up the sensor and assert it's been created.""" + with assert_setup_component(count): + assert await async_setup_component(hass, sensor.DOMAIN, config) + + +async def test_setup_platform_valid_config(hass, mock_requester): + """Test that sensor is set up properly with valid config.""" + await assert_setup_sensor(hass, TEST_CONFIG) + + +async def test_setup_platform_invalid_config(hass, mock_requester): + """Check an invalid configuration.""" + await assert_setup_sensor( + hass, {"sensor": {"platform": "yandex_transport", "stopid": 1234}}, count=0 + ) + + +async def test_name(hass, mock_requester): + """Return the name if set in the configuration.""" + await assert_setup_sensor(hass, TEST_CONFIG) + state = hass.states.get("sensor.test_name") + assert state.name == TEST_CONFIG["sensor"][CONF_NAME] + + +async def test_state(hass, mock_requester): + """Return the contents of _state.""" + await assert_setup_sensor(hass, TEST_CONFIG) + state = hass.states.get("sensor.test_name") + assert state.state == RESULT_STATE + + +async def test_filtered_attributes(hass, mock_requester): + """Return the contents of attributes.""" + await assert_setup_sensor(hass, TEST_CONFIG) + state = hass.states.get("sensor.test_name") + state_attrs = {key: state.attributes[key] for key in FILTERED_ATTRS} + assert state_attrs == FILTERED_ATTRS diff --git a/tests/components/yessssms/test_notify.py b/tests/components/yessssms/test_notify.py index 3d11cdedc67..5cc204ccc4d 100644 --- a/tests/components/yessssms/test_notify.py +++ b/tests/components/yessssms/test_notify.py @@ -1,7 +1,148 @@ """The tests for the notify yessssms platform.""" import unittest +from unittest.mock import patch + +import pytest import requests_mock + +from homeassistant.setup import async_setup_component import homeassistant.components.yessssms.notify as yessssms +from homeassistant.components.yessssms.const import CONF_PROVIDER + +from homeassistant.const import CONF_PASSWORD, CONF_RECIPIENT, CONF_USERNAME + + +@pytest.fixture(name="config") +def config_data(): + """Set valid config data.""" + config = { + "notify": { + "platform": "yessssms", + "name": "sms", + CONF_USERNAME: "06641234567", + CONF_PASSWORD: "secretPassword", + CONF_RECIPIENT: "06509876543", + CONF_PROVIDER: "educom", + } + } + return config + + +@pytest.fixture(name="valid_settings") +def init_valid_settings(hass, config): + """Initialize component with valid settings.""" + return async_setup_component(hass, "notify", config) + + +@pytest.fixture(name="invalid_provider_settings") +def init_invalid_provider_settings(hass, config): + """Set invalid provider data and initalize component.""" + config["notify"][CONF_PROVIDER] = "FantasyMobile" # invalid provider + return async_setup_component(hass, "notify", config) + + +@pytest.fixture(name="invalid_login_data") +def mock_invalid_login_data(): + """Mock invalid login data.""" + path = "homeassistant.components.yessssms.notify.YesssSMS.login_data_valid" + with patch(path, return_value=False): + yield + + +@pytest.fixture(name="valid_login_data") +def mock_valid_login_data(): + """Mock valid login data.""" + path = "homeassistant.components.yessssms.notify.YesssSMS.login_data_valid" + with patch(path, return_value=True): + yield + + +@pytest.fixture(name="connection_error") +def mock_connection_error(): + """Mock a connection error.""" + path = "homeassistant.components.yessssms.notify.YesssSMS.login_data_valid" + with patch(path, side_effect=yessssms.YesssSMS.ConnectionError()): + yield + + +async def test_unsupported_provider_error(hass, caplog, invalid_provider_settings): + """Test for error on unsupported provider.""" + await invalid_provider_settings + for record in caplog.records: + if ( + record.levelname == "ERROR" + and record.name == "homeassistant.components.yessssms.notify" + ): + assert ( + "Unknown provider: provider (fantasymobile) is not known to YesssSMS" + in record.message + ) + assert ( + "Unknown provider: provider (fantasymobile) is not known to YesssSMS" + in caplog.text + ) + assert not hass.services.has_service("notify", "sms") + + +async def test_false_login_data_error(hass, caplog, valid_settings, invalid_login_data): + """Test login data check error.""" + await valid_settings + assert not hass.services.has_service("notify", "sms") + for record in caplog.records: + if ( + record.levelname == "ERROR" + and record.name == "homeassistant.components.yessssms.notify" + ): + assert ( + "Login data is not valid! Please double check your login data at" + in record.message + ) + + +async def test_init_success(hass, caplog, valid_settings, valid_login_data): + """Test for successful init of yessssms.""" + await valid_settings + assert hass.services.has_service("notify", "sms") + messages = [] + for record in caplog.records: + if ( + record.levelname == "DEBUG" + and record.name == "homeassistant.components.yessssms.notify" + ): + messages.append(record.message) + assert "Login data for 'educom' valid" in messages[0] + assert ( + "initialized; library version: {}".format(yessssms.YesssSMS("", "").version()) + in messages[1] + ) + + +async def test_connection_error_on_init(hass, caplog, valid_settings, connection_error): + """Test for connection error on init.""" + await valid_settings + assert hass.services.has_service("notify", "sms") + for record in caplog.records: + if ( + record.levelname == "WARNING" + and record.name == "homeassistant.components.yessssms.notify" + ): + assert ( + "Connection Error, could not verify login data for '{}'".format( + "educom" + ) + in record.message + ) + for record in caplog.records: + if ( + record.levelname == "DEBUG" + and record.name == "homeassistant.components.yessssms.notify" + ): + assert ( + "initialized; library version: {}".format( + yessssms.YesssSMS("", "").version() + ) + in record.message + ) class TestNotifyYesssSMS(unittest.TestCase): @@ -12,7 +153,8 @@ class TestNotifyYesssSMS(unittest.TestCase): login = "06641234567" passwd = "testpasswd" recipient = "06501234567" - self.yessssms = yessssms.YesssSMSNotificationService(login, passwd, recipient) + client = yessssms.YesssSMS(login, passwd) + self.yessssms = yessssms.YesssSMSNotificationService(client, recipient) @requests_mock.Mocker() def test_login_error(self, mock): @@ -197,7 +339,7 @@ class TestNotifyYesssSMS(unittest.TestCase): "POST", # pylint: disable=protected-access self.yessssms.yesss._login_url, - exc=ConnectionError, + exc=yessssms.YesssSMS.ConnectionError, ) message = "Testing YesssSMS platform :)" @@ -209,4 +351,4 @@ class TestNotifyYesssSMS(unittest.TestCase): self.assertTrue(mock.called) self.assertEqual(mock.call_count, 1) - self.assertIn("unable to connect", context.output[0]) + self.assertIn("cannot connect to provider", context.output[0]) diff --git a/tests/components/zha/test_binary_sensor.py b/tests/components/zha/test_binary_sensor.py index 7ba7d94d251..47f81787acd 100644 --- a/tests/components/zha/test_binary_sensor.py +++ b/tests/components/zha/test_binary_sensor.py @@ -74,13 +74,13 @@ async def async_test_binary_sensor_on_off(hass, cluster, entity_id): """Test getting on and off messages for binary sensors.""" # binary sensor on attr = make_attribute(0, 1) - cluster.handle_message(False, 1, 0x0A, [[attr]]) + cluster.handle_message(1, 0x0A, [[attr]]) await hass.async_block_till_done() assert hass.states.get(entity_id).state == STATE_ON # binary sensor off attr.value.value = 0 - cluster.handle_message(False, 0, 0x0A, [[attr]]) + cluster.handle_message(0, 0x0A, [[attr]]) await hass.async_block_till_done() assert hass.states.get(entity_id).state == STATE_OFF diff --git a/tests/components/zha/test_device_action.py b/tests/components/zha/test_device_action.py new file mode 100644 index 00000000000..6e7bc6ab4b1 --- /dev/null +++ b/tests/components/zha/test_device_action.py @@ -0,0 +1,133 @@ +"""The test for zha device automation actions.""" +from unittest.mock import patch + +import pytest + +import homeassistant.components.automation as automation +from homeassistant.components.device_automation import ( + _async_get_device_automations as async_get_device_automations, +) +from homeassistant.components.zha import DOMAIN +from homeassistant.components.zha.core.const import CHANNEL_ON_OFF +from homeassistant.helpers.device_registry import async_get_registry +from homeassistant.setup import async_setup_component + +from .common import async_enable_traffic, async_init_zigpy_device + +from tests.common import async_mock_service, mock_coro + +SHORT_PRESS = "remote_button_short_press" +COMMAND = "command" +COMMAND_SINGLE = "single" + + +@pytest.fixture +def calls(hass): + """Track calls to a mock serivce.""" + return async_mock_service(hass, "zha", "warning_device_warn") + + +async def test_get_actions(hass, config_entry, zha_gateway): + """Test we get the expected actions from a zha device.""" + from zigpy.zcl.clusters.general import Basic + from zigpy.zcl.clusters.security import IasZone, IasWd + + # create zigpy device + zigpy_device = await async_init_zigpy_device( + hass, + [Basic.cluster_id, IasZone.cluster_id, IasWd.cluster_id], + [], + None, + zha_gateway, + ) + + await hass.config_entries.async_forward_entry_setup(config_entry, DOMAIN) + await hass.async_block_till_done() + hass.config_entries._entries.append(config_entry) + + zha_device = zha_gateway.get_device(zigpy_device.ieee) + ieee_address = str(zha_device.ieee) + + ha_device_registry = await async_get_registry(hass) + reg_device = ha_device_registry.async_get_device({(DOMAIN, ieee_address)}, set()) + + actions = await async_get_device_automations(hass, "action", reg_device.id) + + expected_actions = [ + {"domain": DOMAIN, "type": "squawk", "device_id": reg_device.id}, + {"domain": DOMAIN, "type": "warn", "device_id": reg_device.id}, + ] + + assert actions == expected_actions + + +async def test_action(hass, config_entry, zha_gateway, calls): + """Test for executing a zha device action.""" + + from zigpy.zcl.clusters.general import Basic, OnOff + from zigpy.zcl.clusters.security import IasZone, IasWd + from zigpy.zcl.foundation import Status + + # create zigpy device + zigpy_device = await async_init_zigpy_device( + hass, + [Basic.cluster_id, IasZone.cluster_id, IasWd.cluster_id], + [OnOff.cluster_id], + None, + zha_gateway, + ) + + zigpy_device.device_automation_triggers = { + (SHORT_PRESS, SHORT_PRESS): {COMMAND: COMMAND_SINGLE} + } + + await hass.config_entries.async_forward_entry_setup(config_entry, "switch") + await hass.async_block_till_done() + + hass.config_entries._entries.append(config_entry) + + zha_device = zha_gateway.get_device(zigpy_device.ieee) + ieee_address = str(zha_device.ieee) + + ha_device_registry = await async_get_registry(hass) + reg_device = ha_device_registry.async_get_device({(DOMAIN, ieee_address)}, set()) + + # allow traffic to flow through the gateway and device + await async_enable_traffic(hass, zha_gateway, [zha_device]) + + with patch( + "zigpy.zcl.Cluster.request", return_value=mock_coro([0x00, Status.SUCCESS]) + ): + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": { + "device_id": reg_device.id, + "domain": "zha", + "platform": "device", + "type": SHORT_PRESS, + "subtype": SHORT_PRESS, + }, + "action": { + "domain": DOMAIN, + "device_id": reg_device.id, + "type": "warn", + }, + } + ] + }, + ) + + await hass.async_block_till_done() + + on_off_channel = zha_device.cluster_channels[CHANNEL_ON_OFF] + on_off_channel.zha_send_event(on_off_channel.cluster, COMMAND_SINGLE, []) + await hass.async_block_till_done() + + assert len(calls) == 1 + assert calls[0].domain == DOMAIN + assert calls[0].service == "warning_device_warn" + assert calls[0].data["ieee"] == ieee_address diff --git a/tests/components/zha/test_device_tracker.py b/tests/components/zha/test_device_tracker.py index b4c313041f3..6a7638d9f86 100644 --- a/tests/components/zha/test_device_tracker.py +++ b/tests/components/zha/test_device_tracker.py @@ -67,10 +67,10 @@ async def test_device_tracker(hass, config_entry, zha_gateway): # turn state flip attr = make_attribute(0x0020, 23) - cluster.handle_message(False, 1, 0x0A, [[attr]]) + cluster.handle_message(1, 0x0A, [[attr]]) attr = make_attribute(0x0021, 200) - cluster.handle_message(False, 1, 0x0A, [[attr]]) + cluster.handle_message(1, 0x0A, [[attr]]) zigpy_device.last_seen = time.time() + 10 next_update = dt_util.utcnow() + timedelta(seconds=30) diff --git a/tests/components/zha/test_device_trigger.py b/tests/components/zha/test_device_trigger.py new file mode 100644 index 00000000000..2f4ddb6b8b2 --- /dev/null +++ b/tests/components/zha/test_device_trigger.py @@ -0,0 +1,301 @@ +"""ZHA device automation trigger tests.""" +from unittest.mock import patch + +import pytest + +import homeassistant.components.automation as automation +from homeassistant.components.switch import DOMAIN +from homeassistant.components.zha.core.const import CHANNEL_ON_OFF +from homeassistant.helpers.device_registry import async_get_registry +from homeassistant.setup import async_setup_component + +from .common import async_enable_traffic, async_init_zigpy_device + +from tests.common import async_mock_service, async_get_device_automations + +ON = 1 +OFF = 0 +SHAKEN = "device_shaken" +COMMAND = "command" +COMMAND_SHAKE = "shake" +COMMAND_HOLD = "hold" +COMMAND_SINGLE = "single" +COMMAND_DOUBLE = "double" +DOUBLE_PRESS = "remote_button_double_press" +SHORT_PRESS = "remote_button_short_press" +LONG_PRESS = "remote_button_long_press" +LONG_RELEASE = "remote_button_long_release" + + +def _same_lists(list_a, list_b): + if len(list_a) != len(list_b): + return False + + for item in list_a: + if item not in list_b: + return False + return True + + +@pytest.fixture +def calls(hass): + """Track calls to a mock serivce.""" + return async_mock_service(hass, "test", "automation") + + +async def test_triggers(hass, config_entry, zha_gateway): + """Test zha device triggers.""" + from zigpy.zcl.clusters.general import OnOff, Basic + + # create zigpy device + zigpy_device = await async_init_zigpy_device( + hass, [Basic.cluster_id], [OnOff.cluster_id], None, zha_gateway + ) + + zigpy_device.device_automation_triggers = { + (SHAKEN, SHAKEN): {COMMAND: COMMAND_SHAKE}, + (DOUBLE_PRESS, DOUBLE_PRESS): {COMMAND: COMMAND_DOUBLE}, + (SHORT_PRESS, SHORT_PRESS): {COMMAND: COMMAND_SINGLE}, + (LONG_PRESS, LONG_PRESS): {COMMAND: COMMAND_HOLD}, + (LONG_RELEASE, LONG_RELEASE): {COMMAND: COMMAND_HOLD}, + } + + await hass.config_entries.async_forward_entry_setup(config_entry, DOMAIN) + await hass.async_block_till_done() + hass.config_entries._entries.append(config_entry) + + zha_device = zha_gateway.get_device(zigpy_device.ieee) + ieee_address = str(zha_device.ieee) + + ha_device_registry = await async_get_registry(hass) + reg_device = ha_device_registry.async_get_device({("zha", ieee_address)}, set()) + + triggers = await async_get_device_automations(hass, "trigger", reg_device.id) + + expected_triggers = [ + { + "device_id": reg_device.id, + "domain": "zha", + "platform": "device", + "type": SHAKEN, + "subtype": SHAKEN, + }, + { + "device_id": reg_device.id, + "domain": "zha", + "platform": "device", + "type": DOUBLE_PRESS, + "subtype": DOUBLE_PRESS, + }, + { + "device_id": reg_device.id, + "domain": "zha", + "platform": "device", + "type": SHORT_PRESS, + "subtype": SHORT_PRESS, + }, + { + "device_id": reg_device.id, + "domain": "zha", + "platform": "device", + "type": LONG_PRESS, + "subtype": LONG_PRESS, + }, + { + "device_id": reg_device.id, + "domain": "zha", + "platform": "device", + "type": LONG_RELEASE, + "subtype": LONG_RELEASE, + }, + ] + assert _same_lists(triggers, expected_triggers) + + +async def test_no_triggers(hass, config_entry, zha_gateway): + """Test zha device with no triggers.""" + from zigpy.zcl.clusters.general import OnOff, Basic + + # create zigpy device + zigpy_device = await async_init_zigpy_device( + hass, [Basic.cluster_id], [OnOff.cluster_id], None, zha_gateway + ) + + await hass.config_entries.async_forward_entry_setup(config_entry, DOMAIN) + await hass.async_block_till_done() + hass.config_entries._entries.append(config_entry) + + zha_device = zha_gateway.get_device(zigpy_device.ieee) + ieee_address = str(zha_device.ieee) + + ha_device_registry = await async_get_registry(hass) + reg_device = ha_device_registry.async_get_device({("zha", ieee_address)}, set()) + + triggers = await async_get_device_automations(hass, "trigger", reg_device.id) + assert triggers == [] + + +async def test_if_fires_on_event(hass, config_entry, zha_gateway, calls): + """Test for remote triggers firing.""" + from zigpy.zcl.clusters.general import OnOff, Basic + + # create zigpy device + zigpy_device = await async_init_zigpy_device( + hass, [Basic.cluster_id], [OnOff.cluster_id], None, zha_gateway + ) + + zigpy_device.device_automation_triggers = { + (SHAKEN, SHAKEN): {COMMAND: COMMAND_SHAKE}, + (DOUBLE_PRESS, DOUBLE_PRESS): {COMMAND: COMMAND_DOUBLE}, + (SHORT_PRESS, SHORT_PRESS): {COMMAND: COMMAND_SINGLE}, + (LONG_PRESS, LONG_PRESS): {COMMAND: COMMAND_HOLD}, + (LONG_RELEASE, LONG_RELEASE): {COMMAND: COMMAND_HOLD}, + } + + await hass.config_entries.async_forward_entry_setup(config_entry, DOMAIN) + await hass.async_block_till_done() + hass.config_entries._entries.append(config_entry) + + zha_device = zha_gateway.get_device(zigpy_device.ieee) + + # allow traffic to flow through the gateway and device + await async_enable_traffic(hass, zha_gateway, [zha_device]) + + ieee_address = str(zha_device.ieee) + ha_device_registry = await async_get_registry(hass) + reg_device = ha_device_registry.async_get_device({("zha", ieee_address)}, set()) + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": { + "device_id": reg_device.id, + "domain": "zha", + "platform": "device", + "type": SHORT_PRESS, + "subtype": SHORT_PRESS, + }, + "action": { + "service": "test.automation", + "data": {"message": "service called"}, + }, + } + ] + }, + ) + + await hass.async_block_till_done() + + on_off_channel = zha_device.cluster_channels[CHANNEL_ON_OFF] + on_off_channel.zha_send_event(on_off_channel.cluster, COMMAND_SINGLE, []) + await hass.async_block_till_done() + + assert len(calls) == 1 + assert calls[0].data["message"] == "service called" + + +async def test_exception_no_triggers(hass, config_entry, zha_gateway, calls): + """Test for exception on event triggers firing.""" + from zigpy.zcl.clusters.general import OnOff, Basic + + # create zigpy device + zigpy_device = await async_init_zigpy_device( + hass, [Basic.cluster_id], [OnOff.cluster_id], None, zha_gateway + ) + + await hass.config_entries.async_forward_entry_setup(config_entry, DOMAIN) + await hass.async_block_till_done() + hass.config_entries._entries.append(config_entry) + + zha_device = zha_gateway.get_device(zigpy_device.ieee) + + # allow traffic to flow through the gateway and device + await async_enable_traffic(hass, zha_gateway, [zha_device]) + + ieee_address = str(zha_device.ieee) + ha_device_registry = await async_get_registry(hass) + reg_device = ha_device_registry.async_get_device({("zha", ieee_address)}, set()) + + with patch("logging.Logger.error") as mock: + await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": { + "device_id": reg_device.id, + "domain": "zha", + "platform": "device", + "type": "junk", + "subtype": "junk", + }, + "action": { + "service": "test.automation", + "data": {"message": "service called"}, + }, + } + ] + }, + ) + await hass.async_block_till_done() + mock.assert_called_with("Error setting up trigger %s", "automation 0") + + +async def test_exception_bad_trigger(hass, config_entry, zha_gateway, calls): + """Test for exception on event triggers firing.""" + from zigpy.zcl.clusters.general import OnOff, Basic + + # create zigpy device + zigpy_device = await async_init_zigpy_device( + hass, [Basic.cluster_id], [OnOff.cluster_id], None, zha_gateway + ) + + zigpy_device.device_automation_triggers = { + (SHAKEN, SHAKEN): {COMMAND: COMMAND_SHAKE}, + (DOUBLE_PRESS, DOUBLE_PRESS): {COMMAND: COMMAND_DOUBLE}, + (SHORT_PRESS, SHORT_PRESS): {COMMAND: COMMAND_SINGLE}, + (LONG_PRESS, LONG_PRESS): {COMMAND: COMMAND_HOLD}, + (LONG_RELEASE, LONG_RELEASE): {COMMAND: COMMAND_HOLD}, + } + + await hass.config_entries.async_forward_entry_setup(config_entry, DOMAIN) + await hass.async_block_till_done() + hass.config_entries._entries.append(config_entry) + + zha_device = zha_gateway.get_device(zigpy_device.ieee) + + # allow traffic to flow through the gateway and device + await async_enable_traffic(hass, zha_gateway, [zha_device]) + + ieee_address = str(zha_device.ieee) + ha_device_registry = await async_get_registry(hass) + reg_device = ha_device_registry.async_get_device({("zha", ieee_address)}, set()) + + with patch("logging.Logger.error") as mock: + await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": { + "device_id": reg_device.id, + "domain": "zha", + "platform": "device", + "type": "junk", + "subtype": "junk", + }, + "action": { + "service": "test.automation", + "data": {"message": "service called"}, + }, + } + ] + }, + ) + await hass.async_block_till_done() + mock.assert_called_with("Error setting up trigger %s", "automation 0") diff --git a/tests/components/zha/test_fan.py b/tests/components/zha/test_fan.py index a8c55890acf..3fe5e7937c8 100644 --- a/tests/components/zha/test_fan.py +++ b/tests/components/zha/test_fan.py @@ -44,13 +44,13 @@ async def test_fan(hass, config_entry, zha_gateway): # turn on at fan attr = make_attribute(0, 1) - cluster.handle_message(False, 1, 0x0A, [[attr]]) + cluster.handle_message(1, 0x0A, [[attr]]) await hass.async_block_till_done() assert hass.states.get(entity_id).state == STATE_ON # turn off at fan attr.value.value = 0 - cluster.handle_message(False, 0, 0x0A, [[attr]]) + cluster.handle_message(0, 0x0A, [[attr]]) await hass.async_block_till_done() assert hass.states.get(entity_id).state == STATE_OFF diff --git a/tests/components/zha/test_light.py b/tests/components/zha/test_light.py index 2b167d52ac6..08c6cfe18cf 100644 --- a/tests/components/zha/test_light.py +++ b/tests/components/zha/test_light.py @@ -123,13 +123,13 @@ async def async_test_on_off_from_light(hass, cluster, entity_id): """Test on off functionality from the light.""" # turn on at light attr = make_attribute(0, 1) - cluster.handle_message(False, 1, 0x0A, [[attr]]) + cluster.handle_message(1, 0x0A, [[attr]]) await hass.async_block_till_done() assert hass.states.get(entity_id).state == STATE_ON # turn off at light attr.value.value = 0 - cluster.handle_message(False, 0, 0x0A, [[attr]]) + cluster.handle_message(0, 0x0A, [[attr]]) await hass.async_block_till_done() assert hass.states.get(entity_id).state == STATE_OFF @@ -138,7 +138,7 @@ async def async_test_on_from_light(hass, cluster, entity_id): """Test on off functionality from the light.""" # turn on at light attr = make_attribute(0, 1) - cluster.handle_message(False, 1, 0x0A, [[attr]]) + cluster.handle_message(1, 0x0A, [[attr]]) await hass.async_block_till_done() assert hass.states.get(entity_id).state == STATE_ON @@ -230,7 +230,7 @@ async def async_test_level_on_off_from_hass( 4, (types.uint8_t, types.uint16_t), 10, - 5.0, + 0, expect_reply=True, manufacturer=None, ) @@ -243,7 +243,7 @@ async def async_test_level_on_off_from_hass( async def async_test_dimmer_from_light(hass, cluster, entity_id, level, expected_state): """Test dimmer functionality from the light.""" attr = make_attribute(0, level) - cluster.handle_message(False, 1, 0x0A, [[attr]]) + cluster.handle_message(1, 0x0A, [[attr]]) 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 diff --git a/tests/components/zha/test_lock.py b/tests/components/zha/test_lock.py index 49a60a0f760..7381b557310 100644 --- a/tests/components/zha/test_lock.py +++ b/tests/components/zha/test_lock.py @@ -43,13 +43,13 @@ async def test_lock(hass, config_entry, zha_gateway): # set state to locked attr = make_attribute(0, 1) - cluster.handle_message(False, 1, 0x0A, [[attr]]) + cluster.handle_message(1, 0x0A, [[attr]]) await hass.async_block_till_done() assert hass.states.get(entity_id).state == STATE_LOCKED # set state to unlocked attr.value.value = 2 - cluster.handle_message(False, 0, 0x0A, [[attr]]) + cluster.handle_message(0, 0x0A, [[attr]]) await hass.async_block_till_done() assert hass.states.get(entity_id).state == STATE_UNLOCKED diff --git a/tests/components/zha/test_sensor.py b/tests/components/zha/test_sensor.py index dc3ea35229f..faa44f34927 100644 --- a/tests/components/zha/test_sensor.py +++ b/tests/components/zha/test_sensor.py @@ -177,7 +177,7 @@ async def send_attribute_report(hass, cluster, attrid, value): device is paired to the zigbee network. """ attr = make_attribute(attrid, value) - cluster.handle_message(False, 1, 0x0A, [[attr]]) + cluster.handle_message(1, 0x0A, [[attr]]) await hass.async_block_till_done() diff --git a/tests/components/zha/test_switch.py b/tests/components/zha/test_switch.py index 07d5bd8ca7c..ac6bc73b809 100644 --- a/tests/components/zha/test_switch.py +++ b/tests/components/zha/test_switch.py @@ -44,13 +44,13 @@ async def test_switch(hass, config_entry, zha_gateway): # turn on at switch attr = make_attribute(0, 1) - cluster.handle_message(False, 1, 0x0A, [[attr]]) + cluster.handle_message(1, 0x0A, [[attr]]) await hass.async_block_till_done() assert hass.states.get(entity_id).state == STATE_ON # turn off at switch attr.value.value = 0 - cluster.handle_message(False, 0, 0x0A, [[attr]]) + cluster.handle_message(0, 0x0A, [[attr]]) await hass.async_block_till_done() assert hass.states.get(entity_id).state == STATE_OFF diff --git a/tests/components/zwave/test_sensor.py b/tests/components/zwave/test_sensor.py index f18a66d9a5b..cec93f5af4a 100644 --- a/tests/components/zwave/test_sensor.py +++ b/tests/components/zwave/test_sensor.py @@ -53,6 +53,23 @@ def test_get_device_detects_multilevel_meter(mock_openzwave): assert isinstance(device, sensor.ZWaveMultilevelSensor) +def test_get_device_detects_battery_sensor(mock_openzwave): + """Test get_device returns a Z-Wave battery sensor.""" + + node = MockNode(command_classes=[const.COMMAND_CLASS_BATTERY]) + value = MockValue( + data=0, + node=node, + type=const.TYPE_DECIMAL, + command_class=const.COMMAND_CLASS_BATTERY, + ) + values = MockEntityValues(primary=value) + + device = sensor.get_device(node=node, values=values, node_config={}) + assert isinstance(device, sensor.ZWaveBatterySensor) + assert device.device_class == homeassistant.const.DEVICE_CLASS_BATTERY + + def test_multilevelsensor_value_changed_temp_fahrenheit(mock_openzwave): """Test value changed for Z-Wave multilevel sensor for temperature.""" node = MockNode( diff --git a/tests/conftest.py b/tests/conftest.py index 36c0f52f41a..5e1bbc76fb5 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -13,7 +13,8 @@ from homeassistant.util import location from homeassistant.auth.const import GROUP_ID_ADMIN, GROUP_ID_READ_ONLY from homeassistant.auth.providers import legacy_api_password, homeassistant -from tests.common import ( +pytest.register_assert_rewrite("tests.common") +from tests.common import ( # noqa: E402 module level import not at top of file async_test_home_assistant, INSTANCES, mock_coro, @@ -21,7 +22,9 @@ from tests.common import ( MockUser, CLIENT_ID, ) -from tests.test_util.aiohttp import mock_aiohttp_client +from tests.test_util.aiohttp import ( + mock_aiohttp_client, +) # noqa: E402 module level import not at top of file if os.environ.get("UVLOOP") == "1": import uvloop diff --git a/tests/fixtures/here_travel_time/attribution_response.json b/tests/fixtures/here_travel_time/attribution_response.json new file mode 100644 index 00000000000..9b682f6c51f --- /dev/null +++ b/tests/fixtures/here_travel_time/attribution_response.json @@ -0,0 +1,276 @@ +{ + "response": { + "metaInfo": { + "timestamp": "2019-09-21T15:17:31Z", + "mapVersion": "8.30.100.154", + "moduleVersion": "7.2.201937-5251", + "interfaceVersion": "2.6.70", + "availableMapVersion": [ + "8.30.100.154" + ] + }, + "route": [ + { + "waypoint": [ + { + "linkId": "+565790671", + "mappedPosition": { + "latitude": 50.0378591, + "longitude": 14.3924721 + }, + "originalPosition": { + "latitude": 50.0377513, + "longitude": 14.3923344 + }, + "type": "stopOver", + "spot": 0.3, + "sideOfStreet": "left", + "mappedRoadName": "V Bokách III", + "label": "V Bokách III", + "shapeIndex": 0, + "source": "user" + }, + { + "linkId": "+748931502", + "mappedPosition": { + "latitude": 50.0798786, + "longitude": 14.4260037 + }, + "originalPosition": { + "latitude": 50.0799383, + "longitude": 14.4258216 + }, + "type": "stopOver", + "spot": 1.0, + "sideOfStreet": "left", + "mappedRoadName": "Štěpánská", + "label": "Štěpánská", + "shapeIndex": 116, + "source": "user" + } + ], + "mode": { + "type": "shortest", + "transportModes": [ + "publicTransportTimeTable" + ], + "trafficMode": "enabled", + "feature": [] + }, + "leg": [ + { + "start": { + "linkId": "+565790671", + "mappedPosition": { + "latitude": 50.0378591, + "longitude": 14.3924721 + }, + "originalPosition": { + "latitude": 50.0377513, + "longitude": 14.3923344 + }, + "type": "stopOver", + "spot": 0.3, + "sideOfStreet": "left", + "mappedRoadName": "V Bokách III", + "label": "V Bokách III", + "shapeIndex": 0, + "source": "user" + }, + "end": { + "linkId": "+748931502", + "mappedPosition": { + "latitude": 50.0798786, + "longitude": 14.4260037 + }, + "originalPosition": { + "latitude": 50.0799383, + "longitude": 14.4258216 + }, + "type": "stopOver", + "spot": 1.0, + "sideOfStreet": "left", + "mappedRoadName": "Štěpánská", + "label": "Štěpánská", + "shapeIndex": 116, + "source": "user" + }, + "length": 7835, + "travelTime": 2413, + "maneuver": [ + { + "position": { + "latitude": 50.0378591, + "longitude": 14.3924721 + }, + "instruction": "Head northwest on Kosořská. Go for 28 m.", + "travelTime": 32, + "length": 28, + "id": "M1", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 50.0380039, + "longitude": 14.3921542 + }, + "instruction": "Turn left onto Kosořská. Go for 24 m.", + "travelTime": 24, + "length": 24, + "id": "M2", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 50.0380039, + "longitude": 14.3918109 + }, + "instruction": "Take the street on the left, Slivenecká. Go for 343 m.", + "travelTime": 354, + "length": 343, + "id": "M3", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 50.0376499, + "longitude": 14.3871975 + }, + "instruction": "Turn left onto Slivenecká. Go for 64 m.", + "travelTime": 72, + "length": 64, + "id": "M4", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 50.0373602, + "longitude": 14.3879807 + }, + "instruction": "Turn right onto Slivenecká. Go for 91 m.", + "travelTime": 95, + "length": 91, + "id": "M5", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 50.0365448, + "longitude": 14.3878305 + }, + "instruction": "Turn left onto K Barrandovu. Go for 124 m.", + "travelTime": 126, + "length": 124, + "id": "M6", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 50.0363168, + "longitude": 14.3894618 + }, + "instruction": "Go to the Tram station Geologicka and take the rail 5 toward Ústřední dílny DP. Follow for 13 stations.", + "travelTime": 1440, + "length": 6911, + "id": "M7", + "stopName": "Geologicka", + "_type": "PublicTransportManeuverType" + }, + { + "position": { + "latitude": 50.0800508, + "longitude": 14.423403 + }, + "instruction": "Get off at Vodickova.", + "travelTime": 0, + "length": 0, + "id": "M8", + "stopName": "Vodickova", + "nextRoadName": "Vodičkova", + "_type": "PublicTransportManeuverType" + }, + { + "position": { + "latitude": 50.0800508, + "longitude": 14.423403 + }, + "instruction": "Head northeast on Vodičkova. Go for 65 m.", + "travelTime": 74, + "length": 65, + "id": "M9", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 50.0804901, + "longitude": 14.4239759 + }, + "instruction": "Turn right onto V Jámě. Go for 163 m.", + "travelTime": 174, + "length": 163, + "id": "M10", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 50.0796962, + "longitude": 14.4258857 + }, + "instruction": "Turn left onto Štěpánská. Go for 22 m.", + "travelTime": 22, + "length": 22, + "id": "M11", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 50.0798786, + "longitude": 14.4260037 + }, + "instruction": "Arrive at Štěpánská. Your destination is on the left.", + "travelTime": 0, + "length": 0, + "id": "M12", + "_type": "PrivateTransportManeuverType" + } + ] + } + ], + "publicTransportLine": [ + { + "lineName": "5", + "lineForeground": "#F5ADCE", + "lineBackground": "#F5ADCE", + "companyName": "HERE Technologies", + "destination": "Ústřední dílny DP", + "type": "railLight", + "id": "L1" + } + ], + "summary": { + "distance": 7835, + "baseTime": 2413, + "flags": [ + "noThroughRoad", + "builtUpArea" + ], + "text": "The trip takes 7.8 km and 40 mins.", + "travelTime": 2413, + "departure": "2019-09-21T17:16:17+02:00", + "timetableExpiration": "2019-09-21T00:00:00Z", + "_type": "PublicTransportRouteSummaryType" + } + } + ], + "language": "en-us", + "sourceAttribution": { + "attribution": "With the support of HERE Technologies. All information is provided without warranty of any kind.", + "supplier": [ + { + "title": "HERE Technologies", + "href": "https://transit.api.here.com/r?appId=Mt1bOYh3m9uxE7r3wuUx&u=https://wego.here.com" + } + ] + } + } +} \ No newline at end of file diff --git a/tests/fixtures/here_travel_time/bike_response.json b/tests/fixtures/here_travel_time/bike_response.json new file mode 100644 index 00000000000..a3af39129d0 --- /dev/null +++ b/tests/fixtures/here_travel_time/bike_response.json @@ -0,0 +1,274 @@ +{ + "response": { + "metaInfo": { + "timestamp": "2019-07-24T10:17:40Z", + "mapVersion": "8.30.98.154", + "moduleVersion": "7.2.201929-4522", + "interfaceVersion": "2.6.64", + "availableMapVersion": [ + "8.30.98.154" + ] + }, + "route": [ + { + "waypoint": [ + { + "linkId": "-1230414527", + "mappedPosition": { + "latitude": 41.9797859, + "longitude": -87.8790879 + }, + "originalPosition": { + "latitude": 41.9798, + "longitude": -87.8801 + }, + "type": "stopOver", + "spot": 0.5079365, + "sideOfStreet": "right", + "mappedRoadName": "Mannheim Rd", + "label": "Mannheim Rd - US-12", + "shapeIndex": 0, + "source": "user" + }, + { + "linkId": "+924115108", + "mappedPosition": { + "latitude": 41.90413, + "longitude": -87.9223502 + }, + "originalPosition": { + "latitude": 41.9043, + "longitude": -87.9216001 + }, + "type": "stopOver", + "spot": 0.1925926, + "sideOfStreet": "right", + "mappedRoadName": "", + "label": "", + "shapeIndex": 87, + "source": "user" + } + ], + "mode": { + "type": "fastest", + "transportModes": [ + "bicycle" + ], + "trafficMode": "enabled", + "feature": [] + }, + "leg": [ + { + "start": { + "linkId": "-1230414527", + "mappedPosition": { + "latitude": 41.9797859, + "longitude": -87.8790879 + }, + "originalPosition": { + "latitude": 41.9798, + "longitude": -87.8801 + }, + "type": "stopOver", + "spot": 0.5079365, + "sideOfStreet": "right", + "mappedRoadName": "Mannheim Rd", + "label": "Mannheim Rd - US-12", + "shapeIndex": 0, + "source": "user" + }, + "end": { + "linkId": "+924115108", + "mappedPosition": { + "latitude": 41.90413, + "longitude": -87.9223502 + }, + "originalPosition": { + "latitude": 41.9043, + "longitude": -87.9216001 + }, + "type": "stopOver", + "spot": 0.1925926, + "sideOfStreet": "right", + "mappedRoadName": "", + "label": "", + "shapeIndex": 87, + "source": "user" + }, + "length": 12613, + "travelTime": 3292, + "maneuver": [ + { + "position": { + "latitude": 41.9797859, + "longitude": -87.8790879 + }, + "instruction": "Head south on Mannheim Rd (US-12/US-45). Go for 2.6 km.", + "travelTime": 646, + "length": 2648, + "id": "M1", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 41.9579244, + "longitude": -87.8838551 + }, + "instruction": "Keep left onto Mannheim Rd (US-12/US-45). Go for 2.4 km.", + "travelTime": 621, + "length": 2427, + "id": "M2", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 41.9364238, + "longitude": -87.8849387 + }, + "instruction": "Turn right onto W Belmont Ave. Go for 595 m.", + "travelTime": 158, + "length": 595, + "id": "M3", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 41.9362521, + "longitude": -87.8921163 + }, + "instruction": "Turn left onto Cullerton St. Go for 669 m.", + "travelTime": 180, + "length": 669, + "id": "M4", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 41.9305658, + "longitude": -87.8932428 + }, + "instruction": "Continue on N Landen Dr. Go for 976 m.", + "travelTime": 246, + "length": 976, + "id": "M5", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 41.9217896, + "longitude": -87.8928781 + }, + "instruction": "Turn right onto E Fullerton Ave. Go for 904 m.", + "travelTime": 238, + "length": 904, + "id": "M6", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 41.921618, + "longitude": -87.9038107 + }, + "instruction": "Turn left onto N Wolf Rd. Go for 1.6 km.", + "travelTime": 417, + "length": 1604, + "id": "M7", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 41.907177, + "longitude": -87.9032314 + }, + "instruction": "Turn right onto W North Ave (IL-64). Go for 2.0 km.", + "travelTime": 574, + "length": 2031, + "id": "M8", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 41.9065225, + "longitude": -87.9277039 + }, + "instruction": "Turn left onto N Clinton Ave. Go for 275 m.", + "travelTime": 78, + "length": 275, + "id": "M9", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 41.9040549, + "longitude": -87.9277253 + }, + "instruction": "Turn left onto E Third St. Go for 249 m.", + "travelTime": 63, + "length": 249, + "id": "M10", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 41.9040334, + "longitude": -87.9247105 + }, + "instruction": "Continue on N Caroline Ave. Go for 96 m.", + "travelTime": 37, + "length": 96, + "id": "M11", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 41.9038832, + "longitude": -87.9236054 + }, + "instruction": "Turn slightly left. Go for 113 m.", + "travelTime": 28, + "length": 113, + "id": "M12", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 41.9039047, + "longitude": -87.9222536 + }, + "instruction": "Turn left. Go for 26 m.", + "travelTime": 6, + "length": 26, + "id": "M13", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 41.90413, + "longitude": -87.9223502 + }, + "instruction": "Arrive at your destination on the right.", + "travelTime": 0, + "length": 0, + "id": "M14", + "_type": "PrivateTransportManeuverType" + } + ] + } + ], + "summary": { + "distance": 12613, + "baseTime": 3292, + "flags": [ + "noThroughRoad", + "builtUpArea", + "park" + ], + "text": "The trip takes 12.6 km and 55 mins.", + "travelTime": 3292, + "_type": "RouteSummaryType" + } + } + ], + "language": "en-us" + } +} \ No newline at end of file diff --git a/tests/fixtures/here_travel_time/car_enabled_response.json b/tests/fixtures/here_travel_time/car_enabled_response.json new file mode 100644 index 00000000000..08da738f046 --- /dev/null +++ b/tests/fixtures/here_travel_time/car_enabled_response.json @@ -0,0 +1,298 @@ +{ + "response": { + "metaInfo": { + "timestamp": "2019-07-21T21:21:31Z", + "mapVersion": "8.30.98.154", + "moduleVersion": "7.2.201928-4478", + "interfaceVersion": "2.6.64", + "availableMapVersion": [ + "8.30.98.154" + ] + }, + "route": [ + { + "waypoint": [ + { + "linkId": "-1128310200", + "mappedPosition": { + "latitude": 38.9026523, + "longitude": -77.048338 + }, + "originalPosition": { + "latitude": 38.9029809, + "longitude": -77.048338 + }, + "type": "stopOver", + "spot": 0.3538462, + "sideOfStreet": "right", + "mappedRoadName": "K St NW", + "label": "K St NW", + "shapeIndex": 0, + "source": "user" + }, + { + "linkId": "-18459081", + "mappedPosition": { + "latitude": 39.0422511, + "longitude": -77.1193526 + }, + "originalPosition": { + "latitude": 39.042158, + "longitude": -77.119116 + }, + "type": "stopOver", + "spot": 0.7253521, + "sideOfStreet": "left", + "mappedRoadName": "Commonwealth Dr", + "label": "Commonwealth Dr", + "shapeIndex": 283, + "source": "user" + } + ], + "mode": { + "type": "fastest", + "transportModes": [ + "car" + ], + "trafficMode": "enabled", + "feature": [] + }, + "leg": [ + { + "start": { + "linkId": "-1128310200", + "mappedPosition": { + "latitude": 38.9026523, + "longitude": -77.048338 + }, + "originalPosition": { + "latitude": 38.9029809, + "longitude": -77.048338 + }, + "type": "stopOver", + "spot": 0.3538462, + "sideOfStreet": "right", + "mappedRoadName": "K St NW", + "label": "K St NW", + "shapeIndex": 0, + "source": "user" + }, + "end": { + "linkId": "-18459081", + "mappedPosition": { + "latitude": 39.0422511, + "longitude": -77.1193526 + }, + "originalPosition": { + "latitude": 39.042158, + "longitude": -77.119116 + }, + "type": "stopOver", + "spot": 0.7253521, + "sideOfStreet": "left", + "mappedRoadName": "Commonwealth Dr", + "label": "Commonwealth Dr", + "shapeIndex": 283, + "source": "user" + }, + "length": 23381, + "travelTime": 1817, + "maneuver": [ + { + "position": { + "latitude": 38.9026523, + "longitude": -77.048338 + }, + "instruction": "Head toward 22nd St NW on K St NW. Go for 140 m.", + "travelTime": 36, + "length": 140, + "id": "M1", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 38.9027703, + "longitude": -77.0494902 + }, + "instruction": "Take the 3rd exit from Washington Cir NW roundabout onto K St NW. Go for 325 m.", + "travelTime": 81, + "length": 325, + "id": "M2", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 38.9026523, + "longitude": -77.0529449 + }, + "instruction": "Keep left onto K St NW (US-29). Go for 201 m.", + "travelTime": 29, + "length": 201, + "id": "M3", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 38.9025235, + "longitude": -77.0552516 + }, + "instruction": "Keep right onto Whitehurst Fwy (US-29). Go for 1.4 km.", + "travelTime": 143, + "length": 1381, + "id": "M4", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 38.9050448, + "longitude": -77.0701969 + }, + "instruction": "Turn left onto M St NW. Go for 784 m.", + "travelTime": 80, + "length": 784, + "id": "M5", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 38.9060318, + "longitude": -77.0790696 + }, + "instruction": "Turn slightly left onto Canal Rd NW. Go for 4.2 km.", + "travelTime": 287, + "length": 4230, + "id": "M6", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 38.9303219, + "longitude": -77.1117926 + }, + "instruction": "Continue on Clara Barton Pkwy. Go for 844 m.", + "travelTime": 55, + "length": 844, + "id": "M7", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 38.9368558, + "longitude": -77.1166742 + }, + "instruction": "Continue on Clara Barton Pkwy. Go for 4.7 km.", + "travelTime": 294, + "length": 4652, + "id": "M8", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 38.9706838, + "longitude": -77.1461463 + }, + "instruction": "Keep right onto Cabin John Pkwy N toward I-495 N. Go for 2.1 km.", + "travelTime": 90, + "length": 2069, + "id": "M9", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 38.9858222, + "longitude": -77.1571326 + }, + "instruction": "Take left ramp onto I-495 N (Capital Beltway). Go for 2.9 km.", + "travelTime": 129, + "length": 2890, + "id": "M10", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 39.0104449, + "longitude": -77.1508026 + }, + "instruction": "Keep left onto I-270-SPUR toward I-270/Rockville/Frederick. Go for 1.1 km.", + "travelTime": 48, + "length": 1136, + "id": "M11", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 39.0192747, + "longitude": -77.144773 + }, + "instruction": "Take exit 1 toward Democracy Blvd/Old Georgetown Rd/MD-187 onto Democracy Blvd. Go for 1.8 km.", + "travelTime": 205, + "length": 1818, + "id": "M12", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 39.0247464, + "longitude": -77.1253431 + }, + "instruction": "Turn left onto Old Georgetown Rd (MD-187). Go for 2.3 km.", + "travelTime": 230, + "length": 2340, + "id": "M13", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 39.0447772, + "longitude": -77.1203649 + }, + "instruction": "Turn right onto Nicholson Ln. Go for 208 m.", + "travelTime": 31, + "length": 208, + "id": "M14", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 39.0448952, + "longitude": -77.1179724 + }, + "instruction": "Turn right onto Commonwealth Dr. Go for 341 m.", + "travelTime": 75, + "length": 341, + "id": "M15", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 39.0422511, + "longitude": -77.1193526 + }, + "instruction": "Arrive at Commonwealth Dr. Your destination is on the left.", + "travelTime": 4, + "length": 22, + "id": "M16", + "_type": "PrivateTransportManeuverType" + } + ] + } + ], + "summary": { + "distance": 23381, + "trafficTime": 1782, + "baseTime": 1712, + "flags": [ + "noThroughRoad", + "motorway", + "builtUpArea", + "park" + ], + "text": "The trip takes 23.4 km and 30 mins.", + "travelTime": 1782, + "_type": "RouteSummaryType" + } + } + ], + "language": "en-us" + } +} \ No newline at end of file diff --git a/tests/fixtures/here_travel_time/car_response.json b/tests/fixtures/here_travel_time/car_response.json new file mode 100644 index 00000000000..bda8454f3f3 --- /dev/null +++ b/tests/fixtures/here_travel_time/car_response.json @@ -0,0 +1,299 @@ +{ + "response": { + "metaInfo": { + "timestamp": "2019-07-19T07:38:39Z", + "mapVersion": "8.30.98.154", + "moduleVersion": "7.2.201928-4446", + "interfaceVersion": "2.6.64", + "availableMapVersion": [ + "8.30.98.154" + ] + }, + "route": [ + { + "waypoint": [ + { + "linkId": "+732182239", + "mappedPosition": { + "latitude": 38.9, + "longitude": -77.0488358 + }, + "originalPosition": { + "latitude": 38.9, + "longitude": -77.0483301 + }, + "type": "stopOver", + "spot": 0.4946237, + "sideOfStreet": "right", + "mappedRoadName": "22nd St NW", + "label": "22nd St NW", + "shapeIndex": 0, + "source": "user" + }, + { + "linkId": "+942865877", + "mappedPosition": { + "latitude": 38.9999735, + "longitude": -77.100141 + }, + "originalPosition": { + "latitude": 38.9999999, + "longitude": -77.1000001 + }, + "type": "stopOver", + "spot": 1, + "sideOfStreet": "left", + "mappedRoadName": "Service Rd S", + "label": "Service Rd S", + "shapeIndex": 279, + "source": "user" + } + ], + "mode": { + "type": "fastest", + "transportModes": [ + "car" + ], + "trafficMode": "enabled", + "feature": [] + }, + "leg": [ + { + "start": { + "linkId": "+732182239", + "mappedPosition": { + "latitude": 38.9, + "longitude": -77.0488358 + }, + "originalPosition": { + "latitude": 38.9, + "longitude": -77.0483301 + }, + "type": "stopOver", + "spot": 0.4946237, + "sideOfStreet": "right", + "mappedRoadName": "22nd St NW", + "label": "22nd St NW", + "shapeIndex": 0, + "source": "user" + }, + "end": { + "linkId": "+942865877", + "mappedPosition": { + "latitude": 38.9999735, + "longitude": -77.100141 + }, + "originalPosition": { + "latitude": 38.9999999, + "longitude": -77.1000001 + }, + "type": "stopOver", + "spot": 1, + "sideOfStreet": "left", + "mappedRoadName": "Service Rd S", + "label": "Service Rd S", + "shapeIndex": 279, + "source": "user" + }, + "length": 23903, + "travelTime": 1884, + "maneuver": [ + { + "position": { + "latitude": 38.9, + "longitude": -77.0488358 + }, + "instruction": "Head toward I St NW on 22nd St NW. Go for 279 m.", + "travelTime": 95, + "length": 279, + "id": "M1", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 38.9021051, + "longitude": -77.048825 + }, + "instruction": "Turn left toward Pennsylvania Ave NW. Go for 71 m.", + "travelTime": 21, + "length": 71, + "id": "M2", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 38.902545, + "longitude": -77.0494151 + }, + "instruction": "Take the 3rd exit from Washington Cir NW roundabout onto K St NW. Go for 352 m.", + "travelTime": 90, + "length": 352, + "id": "M3", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 38.9026523, + "longitude": -77.0529449 + }, + "instruction": "Keep left onto K St NW (US-29). Go for 201 m.", + "travelTime": 30, + "length": 201, + "id": "M4", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 38.9025235, + "longitude": -77.0552516 + }, + "instruction": "Keep right onto Whitehurst Fwy (US-29). Go for 1.4 km.", + "travelTime": 131, + "length": 1381, + "id": "M5", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 38.9050448, + "longitude": -77.0701969 + }, + "instruction": "Turn left onto M St NW. Go for 784 m.", + "travelTime": 78, + "length": 784, + "id": "M6", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 38.9060318, + "longitude": -77.0790696 + }, + "instruction": "Turn slightly left onto Canal Rd NW. Go for 4.2 km.", + "travelTime": 277, + "length": 4230, + "id": "M7", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 38.9303219, + "longitude": -77.1117926 + }, + "instruction": "Continue on Clara Barton Pkwy. Go for 844 m.", + "travelTime": 55, + "length": 844, + "id": "M8", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 38.9368558, + "longitude": -77.1166742 + }, + "instruction": "Continue on Clara Barton Pkwy. Go for 4.7 km.", + "travelTime": 298, + "length": 4652, + "id": "M9", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 38.9706838, + "longitude": -77.1461463 + }, + "instruction": "Keep right onto Cabin John Pkwy N toward I-495 N. Go for 2.1 km.", + "travelTime": 91, + "length": 2069, + "id": "M10", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 38.9858222, + "longitude": -77.1571326 + }, + "instruction": "Take left ramp onto I-495 N (Capital Beltway). Go for 5.5 km.", + "travelTime": 238, + "length": 5538, + "id": "M11", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 39.0153587, + "longitude": -77.1221781 + }, + "instruction": "Take exit 36 toward Bethesda onto MD-187 S (Old Georgetown Rd). Go for 2.4 km.", + "travelTime": 211, + "length": 2365, + "id": "M12", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 38.9981818, + "longitude": -77.1093571 + }, + "instruction": "Turn left onto Lincoln Dr. Go for 506 m.", + "travelTime": 127, + "length": 506, + "id": "M13", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 38.9987397, + "longitude": -77.1037138 + }, + "instruction": "Turn right onto Service Rd W. Go for 121 m.", + "travelTime": 36, + "length": 121, + "id": "M14", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 38.9976454, + "longitude": -77.1036172 + }, + "instruction": "Turn left onto Service Rd S. Go for 510 m.", + "travelTime": 106, + "length": 510, + "id": "M15", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 38.9999735, + "longitude": -77.100141 + }, + "instruction": "Arrive at Service Rd S. Your destination is on the left.", + "travelTime": 0, + "length": 0, + "id": "M16", + "_type": "PrivateTransportManeuverType" + } + ] + } + ], + "summary": { + "distance": 23903, + "trafficTime": 1861, + "baseTime": 1803, + "flags": [ + "noThroughRoad", + "motorway", + "builtUpArea", + "park", + "privateRoad" + ], + "text": "The trip takes 23.9 km and 31 mins.", + "travelTime": 1861, + "_type": "RouteSummaryType" + } + } + ], + "language": "en-us" + } +} \ No newline at end of file diff --git a/tests/fixtures/here_travel_time/car_shortest_response.json b/tests/fixtures/here_travel_time/car_shortest_response.json new file mode 100644 index 00000000000..765c438c1cd --- /dev/null +++ b/tests/fixtures/here_travel_time/car_shortest_response.json @@ -0,0 +1,231 @@ +{ + "response": { + "metaInfo": { + "timestamp": "2019-07-21T21:05:28Z", + "mapVersion": "8.30.98.154", + "moduleVersion": "7.2.201928-4478", + "interfaceVersion": "2.6.64", + "availableMapVersion": [ + "8.30.98.154" + ] + }, + "route": [ + { + "waypoint": [ + { + "linkId": "-1128310200", + "mappedPosition": { + "latitude": 38.9026523, + "longitude": -77.048338 + }, + "originalPosition": { + "latitude": 38.9029809, + "longitude": -77.048338 + }, + "type": "stopOver", + "spot": 0.3538462, + "sideOfStreet": "right", + "mappedRoadName": "K St NW", + "label": "K St NW", + "shapeIndex": 0, + "source": "user" + }, + { + "linkId": "-18459081", + "mappedPosition": { + "latitude": 39.0422511, + "longitude": -77.1193526 + }, + "originalPosition": { + "latitude": 39.042158, + "longitude": -77.119116 + }, + "type": "stopOver", + "spot": 0.7253521, + "sideOfStreet": "left", + "mappedRoadName": "Commonwealth Dr", + "label": "Commonwealth Dr", + "shapeIndex": 162, + "source": "user" + } + ], + "mode": { + "type": "shortest", + "transportModes": [ + "car" + ], + "trafficMode": "enabled", + "feature": [] + }, + "leg": [ + { + "start": { + "linkId": "-1128310200", + "mappedPosition": { + "latitude": 38.9026523, + "longitude": -77.048338 + }, + "originalPosition": { + "latitude": 38.9029809, + "longitude": -77.048338 + }, + "type": "stopOver", + "spot": 0.3538462, + "sideOfStreet": "right", + "mappedRoadName": "K St NW", + "label": "K St NW", + "shapeIndex": 0, + "source": "user" + }, + "end": { + "linkId": "-18459081", + "mappedPosition": { + "latitude": 39.0422511, + "longitude": -77.1193526 + }, + "originalPosition": { + "latitude": 39.042158, + "longitude": -77.119116 + }, + "type": "stopOver", + "spot": 0.7253521, + "sideOfStreet": "left", + "mappedRoadName": "Commonwealth Dr", + "label": "Commonwealth Dr", + "shapeIndex": 162, + "source": "user" + }, + "length": 18388, + "travelTime": 2493, + "maneuver": [ + { + "position": { + "latitude": 38.9026523, + "longitude": -77.048338 + }, + "instruction": "Head west on K St NW. Go for 79 m.", + "travelTime": 22, + "length": 79, + "id": "M1", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 38.9026523, + "longitude": -77.048825 + }, + "instruction": "Turn right onto 22nd St NW. Go for 141 m.", + "travelTime": 79, + "length": 141, + "id": "M2", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 38.9039075, + "longitude": -77.048825 + }, + "instruction": "Keep left onto 22nd St NW. Go for 841 m.", + "travelTime": 256, + "length": 841, + "id": "M3", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 38.9114928, + "longitude": -77.0487821 + }, + "instruction": "Turn left onto Massachusetts Ave NW. Go for 145 m.", + "travelTime": 22, + "length": 145, + "id": "M4", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 38.9120293, + "longitude": -77.0502949 + }, + "instruction": "Take the 1st exit from Massachusetts Ave NW roundabout onto Massachusetts Ave NW. Go for 2.8 km.", + "travelTime": 301, + "length": 2773, + "id": "M5", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 38.9286053, + "longitude": -77.073158 + }, + "instruction": "Turn right onto Wisconsin Ave NW. Go for 3.8 km.", + "travelTime": 610, + "length": 3801, + "id": "M6", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 38.9607918, + "longitude": -77.0857322 + }, + "instruction": "Continue on Wisconsin Ave (MD-355). Go for 9.7 km.", + "travelTime": 1013, + "length": 9686, + "id": "M7", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 39.0447664, + "longitude": -77.1116638 + }, + "instruction": "Turn left onto Nicholson Ln. Go for 559 m.", + "travelTime": 111, + "length": 559, + "id": "M8", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 39.0448952, + "longitude": -77.1179724 + }, + "instruction": "Turn left onto Commonwealth Dr. Go for 341 m.", + "travelTime": 75, + "length": 341, + "id": "M9", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 39.0422511, + "longitude": -77.1193526 + }, + "instruction": "Arrive at Commonwealth Dr. Your destination is on the left.", + "travelTime": 4, + "length": 22, + "id": "M10", + "_type": "PrivateTransportManeuverType" + } + ] + } + ], + "summary": { + "distance": 18388, + "trafficTime": 2427, + "baseTime": 2150, + "flags": [ + "noThroughRoad", + "builtUpArea", + "park" + ], + "text": "The trip takes 18.4 km and 40 mins.", + "travelTime": 2427, + "_type": "RouteSummaryType" + } + } + ], + "language": "en-us" + } +} \ No newline at end of file diff --git a/tests/fixtures/here_travel_time/pedestrian_response.json b/tests/fixtures/here_travel_time/pedestrian_response.json new file mode 100644 index 00000000000..07881e8bd3d --- /dev/null +++ b/tests/fixtures/here_travel_time/pedestrian_response.json @@ -0,0 +1,308 @@ +{ + "response": { + "metaInfo": { + "timestamp": "2019-07-21T18:40:10Z", + "mapVersion": "8.30.98.154", + "moduleVersion": "7.2.201928-4478", + "interfaceVersion": "2.6.64", + "availableMapVersion": [ + "8.30.98.154" + ] + }, + "route": [ + { + "waypoint": [ + { + "linkId": "-1230414527", + "mappedPosition": { + "latitude": 41.9797859, + "longitude": -87.8790879 + }, + "originalPosition": { + "latitude": 41.9798, + "longitude": -87.8801 + }, + "type": "stopOver", + "spot": 0.5079365, + "sideOfStreet": "right", + "mappedRoadName": "Mannheim Rd", + "label": "Mannheim Rd - US-12", + "shapeIndex": 0, + "source": "user" + }, + { + "linkId": "+924115108", + "mappedPosition": { + "latitude": 41.90413, + "longitude": -87.9223502 + }, + "originalPosition": { + "latitude": 41.9043, + "longitude": -87.9216001 + }, + "type": "stopOver", + "spot": 0.1925926, + "sideOfStreet": "right", + "mappedRoadName": "", + "label": "", + "shapeIndex": 122, + "source": "user" + } + ], + "mode": { + "type": "fastest", + "transportModes": [ + "pedestrian" + ], + "trafficMode": "disabled", + "feature": [] + }, + "leg": [ + { + "start": { + "linkId": "-1230414527", + "mappedPosition": { + "latitude": 41.9797859, + "longitude": -87.8790879 + }, + "originalPosition": { + "latitude": 41.9798, + "longitude": -87.8801 + }, + "type": "stopOver", + "spot": 0.5079365, + "sideOfStreet": "right", + "mappedRoadName": "Mannheim Rd", + "label": "Mannheim Rd - US-12", + "shapeIndex": 0, + "source": "user" + }, + "end": { + "linkId": "+924115108", + "mappedPosition": { + "latitude": 41.90413, + "longitude": -87.9223502 + }, + "originalPosition": { + "latitude": 41.9043, + "longitude": -87.9216001 + }, + "type": "stopOver", + "spot": 0.1925926, + "sideOfStreet": "right", + "mappedRoadName": "", + "label": "", + "shapeIndex": 122, + "source": "user" + }, + "length": 12533, + "travelTime": 12631, + "maneuver": [ + { + "position": { + "latitude": 41.9797859, + "longitude": -87.8790879 + }, + "instruction": "Head south on Mannheim Rd. Go for 848 m.", + "travelTime": 848, + "length": 848, + "id": "M1", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 41.9722581, + "longitude": -87.8776109 + }, + "instruction": "Take the street on the left, Mannheim Rd. Go for 4.2 km.", + "travelTime": 4239, + "length": 4227, + "id": "M2", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 41.9364238, + "longitude": -87.8849387 + }, + "instruction": "Turn right onto W Belmont Ave. Go for 595 m.", + "travelTime": 605, + "length": 595, + "id": "M3", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 41.9362521, + "longitude": -87.8921163 + }, + "instruction": "Turn left onto Cullerton St. Go for 406 m.", + "travelTime": 411, + "length": 406, + "id": "M4", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 41.9326043, + "longitude": -87.8919983 + }, + "instruction": "Turn right onto Cullerton St. Go for 1.2 km.", + "travelTime": 1249, + "length": 1239, + "id": "M5", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 41.9217896, + "longitude": -87.8928781 + }, + "instruction": "Turn right onto E Fullerton Ave. Go for 786 m.", + "travelTime": 796, + "length": 786, + "id": "M6", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 41.9216394, + "longitude": -87.9023838 + }, + "instruction": "Turn left onto La Porte Ave. Go for 424 m.", + "travelTime": 430, + "length": 424, + "id": "M7", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 41.9180024, + "longitude": -87.9028559 + }, + "instruction": "Turn right onto E Palmer Ave. Go for 864 m.", + "travelTime": 875, + "length": 864, + "id": "M8", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 41.9175196, + "longitude": -87.9132199 + }, + "instruction": "Turn left onto N Railroad Ave. Go for 1.2 km.", + "travelTime": 1180, + "length": 1170, + "id": "M9", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 41.9070268, + "longitude": -87.9130161 + }, + "instruction": "Turn right onto W North Ave. Go for 638 m.", + "travelTime": 638, + "length": 638, + "id": "M10", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 41.9068551, + "longitude": -87.9207087 + }, + "instruction": "Take the street on the left, E North Ave. Go for 354 m.", + "travelTime": 354, + "length": 354, + "id": "M11", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 41.9065869, + "longitude": -87.9249573 + }, + "instruction": "Take the street on the left, E North Ave. Go for 228 m.", + "travelTime": 242, + "length": 228, + "id": "M12", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 41.9065225, + "longitude": -87.9277039 + }, + "instruction": "Turn left. Go for 409 m.", + "travelTime": 419, + "length": 409, + "id": "M13", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 41.9040334, + "longitude": -87.9260409 + }, + "instruction": "Turn left onto E Third St. Go for 206 m.", + "travelTime": 206, + "length": 206, + "id": "M14", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 41.9038832, + "longitude": -87.9236054 + }, + "instruction": "Turn left. Go for 113 m.", + "travelTime": 113, + "length": 113, + "id": "M15", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 41.9039047, + "longitude": -87.9222536 + }, + "instruction": "Turn left. Go for 26 m.", + "travelTime": 26, + "length": 26, + "id": "M16", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 41.90413, + "longitude": -87.9223502 + }, + "instruction": "Arrive at your destination on the right.", + "travelTime": 0, + "length": 0, + "id": "M17", + "_type": "PrivateTransportManeuverType" + } + ] + } + ], + "summary": { + "distance": 12533, + "baseTime": 12631, + "flags": [ + "noThroughRoad", + "builtUpArea", + "park", + "privateRoad" + ], + "text": "The trip takes 12.5 km and 3:31 h.", + "travelTime": 12631, + "_type": "RouteSummaryType" + } + } + ], + "language": "en-us" + } +} \ No newline at end of file diff --git a/tests/fixtures/here_travel_time/public_response.json b/tests/fixtures/here_travel_time/public_response.json new file mode 100644 index 00000000000..149b4d06c39 --- /dev/null +++ b/tests/fixtures/here_travel_time/public_response.json @@ -0,0 +1,294 @@ +{ + "response": { + "metaInfo": { + "timestamp": "2019-07-21T18:40:37Z", + "mapVersion": "8.30.98.154", + "moduleVersion": "7.2.201928-4478", + "interfaceVersion": "2.6.64", + "availableMapVersion": [ + "8.30.98.154" + ] + }, + "route": [ + { + "waypoint": [ + { + "linkId": "-1230414527", + "mappedPosition": { + "latitude": 41.9797859, + "longitude": -87.8790879 + }, + "originalPosition": { + "latitude": 41.9798, + "longitude": -87.8801 + }, + "type": "stopOver", + "spot": 0.5079365, + "sideOfStreet": "right", + "mappedRoadName": "Mannheim Rd", + "label": "Mannheim Rd - US-12", + "shapeIndex": 0, + "source": "user" + }, + { + "linkId": "+924115108", + "mappedPosition": { + "latitude": 41.90413, + "longitude": -87.9223502 + }, + "originalPosition": { + "latitude": 41.9043, + "longitude": -87.9216001 + }, + "type": "stopOver", + "spot": 0.1925926, + "sideOfStreet": "right", + "mappedRoadName": "", + "label": "", + "shapeIndex": 191, + "source": "user" + } + ], + "mode": { + "type": "fastest", + "transportModes": [ + "publicTransport" + ], + "trafficMode": "disabled", + "feature": [] + }, + "leg": [ + { + "start": { + "linkId": "-1230414527", + "mappedPosition": { + "latitude": 41.9797859, + "longitude": -87.8790879 + }, + "originalPosition": { + "latitude": 41.9798, + "longitude": -87.8801 + }, + "type": "stopOver", + "spot": 0.5079365, + "sideOfStreet": "right", + "mappedRoadName": "Mannheim Rd", + "label": "Mannheim Rd - US-12", + "shapeIndex": 0, + "source": "user" + }, + "end": { + "linkId": "+924115108", + "mappedPosition": { + "latitude": 41.90413, + "longitude": -87.9223502 + }, + "originalPosition": { + "latitude": 41.9043, + "longitude": -87.9216001 + }, + "type": "stopOver", + "spot": 0.1925926, + "sideOfStreet": "right", + "mappedRoadName": "", + "label": "", + "shapeIndex": 191, + "source": "user" + }, + "length": 22325, + "travelTime": 5350, + "maneuver": [ + { + "position": { + "latitude": 41.9797859, + "longitude": -87.8790879 + }, + "instruction": "Head south on Mannheim Rd. Go for 848 m.", + "travelTime": 848, + "length": 848, + "id": "M1", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 41.9722581, + "longitude": -87.8776109 + }, + "instruction": "Take the street on the left, Mannheim Rd. Go for 825 m.", + "travelTime": 825, + "length": 825, + "id": "M2", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 41.9650483, + "longitude": -87.8769565 + }, + "instruction": "Go to the stop Mannheim/Lawrence and take the bus 332 toward Palmer/Schiller. Follow for 7 stops.", + "travelTime": 475, + "length": 4360, + "id": "M3", + "stopName": "Mannheim/Lawrence", + "_type": "PublicTransportManeuverType" + }, + { + "position": { + "latitude": 41.9541478, + "longitude": -87.9133594 + }, + "instruction": "Get off at Irving Park/Taft.", + "travelTime": 0, + "length": 0, + "id": "M4", + "stopName": "Irving Park/Taft", + "_type": "PublicTransportManeuverType" + }, + { + "position": { + "latitude": 41.9541478, + "longitude": -87.9133594 + }, + "instruction": "Take the bus 332 toward Cargo Rd./Delta Cargo. Follow for 1 stop.", + "travelTime": 155, + "length": 3505, + "id": "M5", + "stopName": "Irving Park/Taft", + "_type": "PublicTransportManeuverType" + }, + { + "position": { + "latitude": 41.9599199, + "longitude": -87.9162776 + }, + "instruction": "Get off at Cargo Rd./S. Access Rd./Lufthansa.", + "travelTime": 0, + "length": 0, + "id": "M6", + "stopName": "Cargo Rd./S. Access Rd./Lufthansa", + "_type": "PublicTransportManeuverType" + }, + { + "position": { + "latitude": 41.9599199, + "longitude": -87.9162776 + }, + "instruction": "Take the bus 332 toward Palmer/Schiller. Follow for 41 stops.", + "travelTime": 1510, + "length": 11261, + "id": "M7", + "stopName": "Cargo Rd./S. Access Rd./Lufthansa", + "_type": "PublicTransportManeuverType" + }, + { + "position": { + "latitude": 41.9041729, + "longitude": -87.9399669 + }, + "instruction": "Get off at York/Third.", + "travelTime": 0, + "length": 0, + "id": "M8", + "stopName": "York/Third", + "nextRoadName": "N York St", + "_type": "PublicTransportManeuverType" + }, + { + "position": { + "latitude": 41.9041729, + "longitude": -87.9399669 + }, + "instruction": "Head east on N York St. Go for 33 m.", + "travelTime": 43, + "length": 33, + "id": "M9", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 41.9039476, + "longitude": -87.9398811 + }, + "instruction": "Turn left onto E Third St. Go for 1.4 km.", + "travelTime": 1355, + "length": 1354, + "id": "M10", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 41.9038832, + "longitude": -87.9236054 + }, + "instruction": "Turn left. Go for 113 m.", + "travelTime": 113, + "length": 113, + "id": "M11", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 41.9039047, + "longitude": -87.9222536 + }, + "instruction": "Turn left. Go for 26 m.", + "travelTime": 26, + "length": 26, + "id": "M12", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 41.90413, + "longitude": -87.9223502 + }, + "instruction": "Arrive at your destination on the right.", + "travelTime": 0, + "length": 0, + "id": "M13", + "_type": "PrivateTransportManeuverType" + } + ] + } + ], + "publicTransportLine": [ + { + "lineName": "332", + "companyName": "", + "destination": "Palmer/Schiller", + "type": "busPublic", + "id": "L1" + }, + { + "lineName": "332", + "companyName": "", + "destination": "Cargo Rd./Delta Cargo", + "type": "busPublic", + "id": "L2" + }, + { + "lineName": "332", + "companyName": "", + "destination": "Palmer/Schiller", + "type": "busPublic", + "id": "L3" + } + ], + "summary": { + "distance": 22325, + "baseTime": 5350, + "flags": [ + "noThroughRoad", + "builtUpArea", + "park" + ], + "text": "The trip takes 22.3 km and 1:29 h.", + "travelTime": 5350, + "departure": "1970-01-01T00:00:00Z", + "_type": "PublicTransportRouteSummaryType" + } + } + ], + "language": "en-us" + } +} \ No newline at end of file diff --git a/tests/fixtures/here_travel_time/public_time_table_response.json b/tests/fixtures/here_travel_time/public_time_table_response.json new file mode 100644 index 00000000000..52df0d4eb35 --- /dev/null +++ b/tests/fixtures/here_travel_time/public_time_table_response.json @@ -0,0 +1,308 @@ +{ + "response": { + "metaInfo": { + "timestamp": "2019-08-06T06:43:24Z", + "mapVersion": "8.30.99.152", + "moduleVersion": "7.2.201931-4739", + "interfaceVersion": "2.6.66", + "availableMapVersion": [ + "8.30.99.152" + ] + }, + "route": [ + { + "waypoint": [ + { + "linkId": "-1230414527", + "mappedPosition": { + "latitude": 41.9797859, + "longitude": -87.8790879 + }, + "originalPosition": { + "latitude": 41.9798, + "longitude": -87.8801 + }, + "type": "stopOver", + "spot": 0.5079365, + "sideOfStreet": "right", + "mappedRoadName": "Mannheim Rd", + "label": "Mannheim Rd - US-12", + "shapeIndex": 0, + "source": "user" + }, + { + "linkId": "+924115108", + "mappedPosition": { + "latitude": 41.90413, + "longitude": -87.9223502 + }, + "originalPosition": { + "latitude": 41.9043, + "longitude": -87.9216001 + }, + "type": "stopOver", + "spot": 0.1925926, + "sideOfStreet": "right", + "mappedRoadName": "", + "label": "", + "shapeIndex": 111, + "source": "user" + } + ], + "mode": { + "type": "fastest", + "transportModes": [ + "publicTransportTimeTable" + ], + "trafficMode": "disabled", + "feature": [] + }, + "leg": [ + { + "start": { + "linkId": "-1230414527", + "mappedPosition": { + "latitude": 41.9797859, + "longitude": -87.8790879 + }, + "originalPosition": { + "latitude": 41.9798, + "longitude": -87.8801 + }, + "type": "stopOver", + "spot": 0.5079365, + "sideOfStreet": "right", + "mappedRoadName": "Mannheim Rd", + "label": "Mannheim Rd - US-12", + "shapeIndex": 0, + "source": "user" + }, + "end": { + "linkId": "+924115108", + "mappedPosition": { + "latitude": 41.90413, + "longitude": -87.9223502 + }, + "originalPosition": { + "latitude": 41.9043, + "longitude": -87.9216001 + }, + "type": "stopOver", + "spot": 0.1925926, + "sideOfStreet": "right", + "mappedRoadName": "", + "label": "", + "shapeIndex": 111, + "source": "user" + }, + "length": 14775, + "travelTime": 4784, + "maneuver": [ + { + "position": { + "latitude": 41.9797859, + "longitude": -87.8790879 + }, + "instruction": "Head south on Mannheim Rd. Go for 848 m.", + "travelTime": 848, + "length": 848, + "id": "M1", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 41.9722581, + "longitude": -87.8776109 + }, + "instruction": "Take the street on the left, Mannheim Rd. Go for 812 m.", + "travelTime": 812, + "length": 812, + "id": "M2", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 41.965051, + "longitude": -87.8769591 + }, + "instruction": "Go to the Bus stop Mannheim/Lawrence and take the bus 330 toward Archer/Harlem (Terminal). Follow for 33 stops.", + "travelTime": 900, + "length": 7815, + "id": "M3", + "stopName": "Mannheim/Lawrence", + "_type": "PublicTransportManeuverType" + }, + { + "position": { + "latitude": 41.896836, + "longitude": -87.883771 + }, + "instruction": "Get off at Mannheim/Lake.", + "travelTime": 0, + "length": 0, + "id": "M4", + "stopName": "Mannheim/Lake", + "_type": "PublicTransportManeuverType" + }, + { + "position": { + "latitude": 41.896836, + "longitude": -87.883771 + }, + "instruction": "Walk to Bus Lake/Mannheim.", + "travelTime": 300, + "length": 72, + "id": "M5", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 41.897263, + "longitude": -87.8842648 + }, + "instruction": "Take the bus 309 toward Elmhurst Metra Station. Follow for 18 stops.", + "travelTime": 1020, + "length": 4362, + "id": "M6", + "stopName": "Lake/Mannheim", + "_type": "PublicTransportManeuverType" + }, + { + "position": { + "latitude": 41.9066347, + "longitude": -87.928671 + }, + "instruction": "Get off at North/Berteau.", + "travelTime": 0, + "length": 0, + "id": "M7", + "stopName": "North/Berteau", + "nextRoadName": "E Berteau Ave", + "_type": "PublicTransportManeuverType" + }, + { + "position": { + "latitude": 41.9066347, + "longitude": -87.928671 + }, + "instruction": "Head north on E Berteau Ave. Go for 23 m.", + "travelTime": 40, + "length": 23, + "id": "M8", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 41.9067693, + "longitude": -87.9284549 + }, + "instruction": "Turn right onto E Berteau Ave. Go for 40 m.", + "travelTime": 44, + "length": 40, + "id": "M9", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 41.9065011, + "longitude": -87.9282939 + }, + "instruction": "Turn left onto E North Ave. Go for 49 m.", + "travelTime": 56, + "length": 49, + "id": "M10", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 41.9065225, + "longitude": -87.9277039 + }, + "instruction": "Turn slightly right. Go for 409 m.", + "travelTime": 419, + "length": 409, + "id": "M11", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 41.9040334, + "longitude": -87.9260409 + }, + "instruction": "Turn left onto E Third St. Go for 206 m.", + "travelTime": 206, + "length": 206, + "id": "M12", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 41.9038832, + "longitude": -87.9236054 + }, + "instruction": "Turn left. Go for 113 m.", + "travelTime": 113, + "length": 113, + "id": "M13", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 41.9039047, + "longitude": -87.9222536 + }, + "instruction": "Turn left. Go for 26 m.", + "travelTime": 26, + "length": 26, + "id": "M14", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 41.90413, + "longitude": -87.9223502 + }, + "instruction": "Arrive at your destination on the right.", + "travelTime": 0, + "length": 0, + "id": "M15", + "_type": "PrivateTransportManeuverType" + } + ] + } + ], + "publicTransportLine": [ + { + "lineName": "330", + "companyName": "PACE", + "destination": "Archer/Harlem (Terminal)", + "type": "busPublic", + "id": "L1" + }, + { + "lineName": "309", + "companyName": "PACE", + "destination": "Elmhurst Metra Station", + "type": "busPublic", + "id": "L2" + } + ], + "summary": { + "distance": 14775, + "baseTime": 4784, + "flags": [ + "noThroughRoad", + "builtUpArea", + "park" + ], + "text": "The trip takes 14.8 km and 1:20 h.", + "travelTime": 4784, + "departure": "2019-08-06T05:09:20-05:00", + "timetableExpiration": "2019-08-04T00:00:00Z", + "_type": "PublicTransportRouteSummaryType" + } + } + ], + "language": "en-us" + } +} \ No newline at end of file diff --git a/tests/fixtures/here_travel_time/routing_error_invalid_credentials.json b/tests/fixtures/here_travel_time/routing_error_invalid_credentials.json new file mode 100644 index 00000000000..81fb246178c --- /dev/null +++ b/tests/fixtures/here_travel_time/routing_error_invalid_credentials.json @@ -0,0 +1,15 @@ +{ + "_type": "ns2:RoutingServiceErrorType", + "type": "PermissionError", + "subtype": "InvalidCredentials", + "details": "This is not a valid app_id and app_code pair. Please verify that the values are not swapped between the app_id and app_code and the values provisioned by HERE (either by your customer representative or via http://developer.here.com/myapps) were copied correctly into the request.", + "metaInfo": { + "timestamp": "2019-07-10T09:43:14Z", + "mapVersion": "8.30.98.152", + "moduleVersion": "7.2.201927-4307", + "interfaceVersion": "2.6.64", + "availableMapVersion": [ + "8.30.98.152" + ] + } +} \ No newline at end of file diff --git a/tests/fixtures/here_travel_time/routing_error_no_route_found.json b/tests/fixtures/here_travel_time/routing_error_no_route_found.json new file mode 100644 index 00000000000..a776fa91c43 --- /dev/null +++ b/tests/fixtures/here_travel_time/routing_error_no_route_found.json @@ -0,0 +1,21 @@ +{ + "_type": "ns2:RoutingServiceErrorType", + "type": "ApplicationError", + "subtype": "NoRouteFound", + "details": "Error is NGEO_ERROR_ROUTE_NO_END_POINT", + "additionalData": [ + { + "key": "error_code", + "value": "NGEO_ERROR_ROUTE_NO_END_POINT" + } + ], + "metaInfo": { + "timestamp": "2019-07-10T09:51:04Z", + "mapVersion": "8.30.98.152", + "moduleVersion": "7.2.201927-4307", + "interfaceVersion": "2.6.64", + "availableMapVersion": [ + "8.30.98.152" + ] + } +} \ No newline at end of file diff --git a/tests/fixtures/here_travel_time/truck_response.json b/tests/fixtures/here_travel_time/truck_response.json new file mode 100644 index 00000000000..a302d564902 --- /dev/null +++ b/tests/fixtures/here_travel_time/truck_response.json @@ -0,0 +1,187 @@ +{ + "response": { + "metaInfo": { + "timestamp": "2019-07-21T14:25:00Z", + "mapVersion": "8.30.98.154", + "moduleVersion": "7.2.201928-4478", + "interfaceVersion": "2.6.64", + "availableMapVersion": [ + "8.30.98.154" + ] + }, + "route": [ + { + "waypoint": [ + { + "linkId": "+930461269", + "mappedPosition": { + "latitude": 41.9800687, + "longitude": -87.8805614 + }, + "originalPosition": { + "latitude": 41.9798, + "longitude": -87.8801 + }, + "type": "stopOver", + "spot": 0.5555556, + "sideOfStreet": "right", + "mappedRoadName": "", + "label": "", + "shapeIndex": 0, + "source": "user" + }, + { + "linkId": "-1035319462", + "mappedPosition": { + "latitude": 41.9042909, + "longitude": -87.9216528 + }, + "originalPosition": { + "latitude": 41.9043, + "longitude": -87.9216001 + }, + "type": "stopOver", + "spot": 0, + "sideOfStreet": "left", + "mappedRoadName": "Eisenhower Expy E", + "label": "Eisenhower Expy E - I-290", + "shapeIndex": 135, + "source": "user" + } + ], + "mode": { + "type": "fastest", + "transportModes": [ + "truck" + ], + "trafficMode": "disabled", + "feature": [] + }, + "leg": [ + { + "start": { + "linkId": "+930461269", + "mappedPosition": { + "latitude": 41.9800687, + "longitude": -87.8805614 + }, + "originalPosition": { + "latitude": 41.9798, + "longitude": -87.8801 + }, + "type": "stopOver", + "spot": 0.5555556, + "sideOfStreet": "right", + "mappedRoadName": "", + "label": "", + "shapeIndex": 0, + "source": "user" + }, + "end": { + "linkId": "-1035319462", + "mappedPosition": { + "latitude": 41.9042909, + "longitude": -87.9216528 + }, + "originalPosition": { + "latitude": 41.9043, + "longitude": -87.9216001 + }, + "type": "stopOver", + "spot": 0, + "sideOfStreet": "left", + "mappedRoadName": "Eisenhower Expy E", + "label": "Eisenhower Expy E - I-290", + "shapeIndex": 135, + "source": "user" + }, + "length": 13049, + "travelTime": 812, + "maneuver": [ + { + "position": { + "latitude": 41.9800687, + "longitude": -87.8805614 + }, + "instruction": "Take ramp onto I-190. Go for 631 m.", + "travelTime": 53, + "length": 631, + "id": "M1", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 41.98259, + "longitude": -87.8744352 + }, + "instruction": "Take exit 1D toward Indiana onto I-294 S (Tri-State Tollway). Go for 10.9 km.", + "travelTime": 573, + "length": 10872, + "id": "M2", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 41.9059324, + "longitude": -87.9199362 + }, + "instruction": "Take exit 33 toward Rockford/US-20/IL-64 onto I-290 W (Eisenhower Expy W). Go for 475 m.", + "travelTime": 54, + "length": 475, + "id": "M3", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 41.9067156, + "longitude": -87.9237771 + }, + "instruction": "Take exit 13B toward North Ave onto IL-64 W (E North Ave). Go for 435 m.", + "travelTime": 51, + "length": 435, + "id": "M4", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 41.9065869, + "longitude": -87.9249573 + }, + "instruction": "Take ramp onto I-290 E (Eisenhower Expy E) toward Chicago/I-294 S. Go for 636 m.", + "travelTime": 81, + "length": 636, + "id": "M5", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 41.9042909, + "longitude": -87.9216528 + }, + "instruction": "Arrive at Eisenhower Expy E (I-290). Your destination is on the left.", + "travelTime": 0, + "length": 0, + "id": "M6", + "_type": "PrivateTransportManeuverType" + } + ] + } + ], + "summary": { + "distance": 13049, + "trafficTime": 812, + "baseTime": 812, + "flags": [ + "tollroad", + "motorway", + "builtUpArea" + ], + "text": "The trip takes 13.0 km and 14 mins.", + "travelTime": 812, + "_type": "RouteSummaryType" + } + } + ], + "language": "en-us" + } +} \ No newline at end of file diff --git a/tests/fixtures/yandex_transport_reply.json b/tests/fixtures/yandex_transport_reply.json new file mode 100644 index 00000000000..c5e4857297a --- /dev/null +++ b/tests/fixtures/yandex_transport_reply.json @@ -0,0 +1,2106 @@ +{ + "data": { + "geometries": [ + { + "type": "Point", + "coordinates": [ + 37.565280044, + 55.851959656 + ] + } + ], + "geometry": { + "type": "Point", + "coordinates": [ + 37.565280044, + 55.851959656 + ] + }, + "properties": { + "name": "7-й автобусный парк", + "description": "7-й автобусный парк", + "currentTime": "Mon Sep 16 2019 21:40:40 GMT+0300 (Moscow Standard Time)", + "StopMetaData": { + "id": "stop__9639579", + "name": "7-й автобусный парк", + "type": "urban", + "region": { + "id": 213, + "type": 6, + "parent_id": 1, + "capital_id": 0, + "geo_parent_id": 0, + "city_id": 213, + "name": "moscow", + "native_name": "", + "iso_name": "RU MOW", + "is_main": true, + "en_name": "Moscow", + "short_en_name": "MSK", + "phone_code": "495 499", + "phone_code_old": "095", + "zip_code": "", + "population": 12506468, + "synonyms": "Moskau, Moskva", + "latitude": 55.753215, + "longitude": 37.622504, + "latitude_size": 0.878654, + "longitude_size": 1.164423, + "zoom": 10, + "tzname": "Europe/Moscow", + "official_languages": "ru", + "widespread_languages": "ru", + "suggest_list": [], + "is_eu": false, + "services_names": [ + "bs", + "yaca", + "weather", + "afisha", + "maps", + "tv", + "ad", + "etrain", + "subway", + "delivery", + "route" + ], + "ename": "moscow", + "bounds": [ + [ + 37.0402925, + 55.31141404514547 + ], + [ + 38.2047155, + 56.190068045145466 + ] + ], + "names": { + "ablative": "", + "accusative": "Москву", + "dative": "Москве", + "directional": "", + "genitive": "Москвы", + "instrumental": "Москвой", + "locative": "", + "nominative": "Москва", + "preposition": "в", + "prepositional": "Москве" + }, + "parent": { + "id": 1, + "type": 5, + "parent_id": 3, + "capital_id": 213, + "geo_parent_id": 0, + "city_id": 213, + "name": "moscow-and-moscow-oblast", + "native_name": "", + "iso_name": "RU-MOS", + "is_main": true, + "en_name": "Moscow and Moscow Oblast", + "short_en_name": "RU-MOS", + "phone_code": "495 496 498 499", + "phone_code_old": "", + "zip_code": "", + "population": 7503385, + "synonyms": "Московская область, Подмосковье, Podmoskovye", + "latitude": 55.815792, + "longitude": 37.380031, + "latitude_size": 2.705659, + "longitude_size": 5.060749, + "zoom": 8, + "tzname": "Europe/Moscow", + "official_languages": "ru", + "widespread_languages": "ru", + "suggest_list": [ + 213, + 10716, + 10747, + 10758, + 20728, + 10740, + 10738, + 20523, + 10735, + 10734, + 10743, + 21622 + ], + "is_eu": false, + "services_names": [ + "bs", + "yaca", + "ad" + ], + "ename": "moscow-and-moscow-oblast", + "bounds": [ + [ + 34.8496565, + 54.439456064325434 + ], + [ + 39.9104055, + 57.14511506432543 + ] + ], + "names": { + "ablative": "", + "accusative": "Москву и Московскую область", + "dative": "Москве и Московской области", + "directional": "", + "genitive": "Москвы и Московской области", + "instrumental": "Москвой и Московской областью", + "locative": "", + "nominative": "Москва и Московская область", + "preposition": "в", + "prepositional": "Москве и Московской области" + }, + "parent": { + "id": 225, + "type": 3, + "parent_id": 10001, + "capital_id": 213, + "geo_parent_id": 0, + "city_id": 213, + "name": "russia", + "native_name": "", + "iso_name": "RU", + "is_main": false, + "en_name": "Russia", + "short_en_name": "RU", + "phone_code": "7", + "phone_code_old": "", + "zip_code": "", + "population": 146880432, + "synonyms": "Russian Federation,Российская Федерация", + "latitude": 61.698653, + "longitude": 99.505405, + "latitude_size": 40.700127, + "longitude_size": 171.643239, + "zoom": 3, + "tzname": "", + "official_languages": "ru", + "widespread_languages": "ru", + "suggest_list": [ + 213, + 2, + 65, + 54, + 47, + 43, + 66, + 51, + 56, + 172, + 39, + 62 + ], + "is_eu": false, + "services_names": [ + "bs", + "yaca", + "ad" + ], + "ename": "russia", + "bounds": [ + [ + 13.683785499999999, + 35.290400699917846 + ], + [ + -174.6729755, + 75.99052769991785 + ] + ], + "names": { + "ablative": "", + "accusative": "Россию", + "dative": "России", + "directional": "", + "genitive": "России", + "instrumental": "Россией", + "locative": "", + "nominative": "Россия", + "preposition": "в", + "prepositional": "России" + } + } + } + }, + "Transport": [ + { + "lineId": "2036925416", + "name": "194", + "Types": [ + "bus" + ], + "type": "bus", + "threads": [ + { + "threadId": "2036927196", + "EssentialStops": [ + { + "id": "stop__9711780", + "name": "Метро Петровско-Разумовская" + }, + { + "id": "stop__9648742", + "name": "Коровино" + } + ], + "BriefSchedule": { + "Events": [ + { + "Scheduled": { + "value": "1568659860", + "tzOffset": 10800, + "text": "21:51" + } + }, + { + "Scheduled": { + "value": "1568660760", + "tzOffset": 10800, + "text": "22:06" + } + }, + { + "Scheduled": { + "value": "1568661840", + "tzOffset": 10800, + "text": "22:24" + } + } + ], + "departureTime": "21:51" + } + } + ], + "threadId": "2036927196", + "EssentialStops": [ + { + "id": "stop__9711780", + "name": "Метро Петровско-Разумовская" + }, + { + "id": "stop__9648742", + "name": "Коровино" + } + ], + "BriefSchedule": { + "Events": [ + { + "Scheduled": { + "value": "1568659860", + "tzOffset": 10800, + "text": "21:51" + } + }, + { + "Scheduled": { + "value": "1568660760", + "tzOffset": 10800, + "text": "22:06" + } + }, + { + "Scheduled": { + "value": "1568661840", + "tzOffset": 10800, + "text": "22:24" + } + } + ], + "departureTime": "21:51" + } + }, + { + "lineId": "213_114_bus_mosgortrans", + "name": "114", + "Types": [ + "bus" + ], + "type": "bus", + "threads": [ + { + "threadId": "213B_114_bus_mosgortrans", + "EssentialStops": [ + { + "id": "stop__9647199", + "name": "Метро Войковская" + }, + { + "id": "stop__9639588", + "name": "Коровинское шоссе" + } + ], + "BriefSchedule": { + "Events": [], + "Frequency": { + "text": "15 мин", + "value": 900, + "begin": { + "value": "1568603405", + "tzOffset": 10800, + "text": "6:10" + }, + "end": { + "value": "1568672165", + "tzOffset": 10800, + "text": "1:16" + } + } + } + } + ], + "threadId": "213B_114_bus_mosgortrans", + "EssentialStops": [ + { + "id": "stop__9647199", + "name": "Метро Войковская" + }, + { + "id": "stop__9639588", + "name": "Коровинское шоссе" + } + ], + "BriefSchedule": { + "Events": [], + "Frequency": { + "text": "15 мин", + "value": 900, + "begin": { + "value": "1568603405", + "tzOffset": 10800, + "text": "6:10" + }, + "end": { + "value": "1568672165", + "tzOffset": 10800, + "text": "1:16" + } + } + } + }, + { + "lineId": "213_154_bus_mosgortrans", + "name": "154", + "Types": [ + "bus" + ], + "type": "bus", + "threads": [ + { + "threadId": "213B_154_bus_mosgortrans", + "EssentialStops": [ + { + "id": "stop__9642548", + "name": "ВДНХ (южная)" + }, + { + "id": "stop__9711744", + "name": "Станция Ховрино" + } + ], + "BriefSchedule": { + "Events": [ + { + "Scheduled": { + "value": "1568659260", + "tzOffset": 10800, + "text": "21:41" + }, + "Estimated": { + "value": "1568659252", + "tzOffset": 10800, + "text": "21:40" + }, + "vehicleId": "codd%5Fnew|1054764%5F191500" + }, + { + "Scheduled": { + "value": "1568660580", + "tzOffset": 10800, + "text": "22:03" + } + }, + { + "Scheduled": { + "value": "1568661900", + "tzOffset": 10800, + "text": "22:25" + } + } + ], + "departureTime": "21:41" + } + } + ], + "threadId": "213B_154_bus_mosgortrans", + "EssentialStops": [ + { + "id": "stop__9642548", + "name": "ВДНХ (южная)" + }, + { + "id": "stop__9711744", + "name": "Станция Ховрино" + } + ], + "BriefSchedule": { + "Events": [ + { + "Scheduled": { + "value": "1568659260", + "tzOffset": 10800, + "text": "21:41" + }, + "Estimated": { + "value": "1568659252", + "tzOffset": 10800, + "text": "21:40" + }, + "vehicleId": "codd%5Fnew|1054764%5F191500" + }, + { + "Scheduled": { + "value": "1568660580", + "tzOffset": 10800, + "text": "22:03" + } + }, + { + "Scheduled": { + "value": "1568661900", + "tzOffset": 10800, + "text": "22:25" + } + } + ], + "departureTime": "21:41" + } + }, + { + "lineId": "213_179_bus_mosgortrans", + "name": "179", + "Types": [ + "bus" + ], + "type": "bus", + "threads": [ + { + "threadId": "213B_179_bus_mosgortrans", + "EssentialStops": [ + { + "id": "stop__9647199", + "name": "Метро Войковская" + }, + { + "id": "stop__9639480", + "name": "Платформа Лианозово" + } + ], + "BriefSchedule": { + "Events": [ + { + "Scheduled": { + "value": "1568659920", + "tzOffset": 10800, + "text": "21:52" + }, + "Estimated": { + "value": "1568659351", + "tzOffset": 10800, + "text": "21:42" + }, + "vehicleId": "codd%5Fnew|59832%5F31359" + }, + { + "Scheduled": { + "value": "1568660760", + "tzOffset": 10800, + "text": "22:06" + } + }, + { + "Scheduled": { + "value": "1568661660", + "tzOffset": 10800, + "text": "22:21" + } + } + ], + "departureTime": "21:52" + } + } + ], + "threadId": "213B_179_bus_mosgortrans", + "EssentialStops": [ + { + "id": "stop__9647199", + "name": "Метро Войковская" + }, + { + "id": "stop__9639480", + "name": "Платформа Лианозово" + } + ], + "BriefSchedule": { + "Events": [ + { + "Scheduled": { + "value": "1568659920", + "tzOffset": 10800, + "text": "21:52" + }, + "Estimated": { + "value": "1568659351", + "tzOffset": 10800, + "text": "21:42" + }, + "vehicleId": "codd%5Fnew|59832%5F31359" + }, + { + "Scheduled": { + "value": "1568660760", + "tzOffset": 10800, + "text": "22:06" + } + }, + { + "Scheduled": { + "value": "1568661660", + "tzOffset": 10800, + "text": "22:21" + } + } + ], + "departureTime": "21:52" + } + }, + { + "lineId": "213_191m_minibus_default", + "name": "591", + "Types": [ + "bus" + ], + "type": "bus", + "threads": [ + { + "threadId": "213A_191m_minibus_default", + "EssentialStops": [ + { + "id": "stop__9647199", + "name": "Метро Войковская" + }, + { + "id": "stop__9711744", + "name": "Станция Ховрино" + } + ], + "BriefSchedule": { + "Events": [ + { + "Estimated": { + "value": "1568660525", + "tzOffset": 10800, + "text": "22:02" + }, + "vehicleId": "codd%5Fnew|38278%5F9345312" + } + ], + "Frequency": { + "text": "22 мин", + "value": 1320, + "begin": { + "value": "1568602033", + "tzOffset": 10800, + "text": "5:47" + }, + "end": { + "value": "1568672233", + "tzOffset": 10800, + "text": "1:17" + } + } + } + } + ], + "threadId": "213A_191m_minibus_default", + "EssentialStops": [ + { + "id": "stop__9647199", + "name": "Метро Войковская" + }, + { + "id": "stop__9711744", + "name": "Станция Ховрино" + } + ], + "BriefSchedule": { + "Events": [ + { + "Estimated": { + "value": "1568660525", + "tzOffset": 10800, + "text": "22:02" + }, + "vehicleId": "codd%5Fnew|38278%5F9345312" + } + ], + "Frequency": { + "text": "22 мин", + "value": 1320, + "begin": { + "value": "1568602033", + "tzOffset": 10800, + "text": "5:47" + }, + "end": { + "value": "1568672233", + "tzOffset": 10800, + "text": "1:17" + } + } + } + }, + { + "lineId": "213_206m_minibus_default", + "name": "206к", + "Types": [ + "bus" + ], + "type": "bus", + "threads": [ + { + "threadId": "213A_206m_minibus_default", + "EssentialStops": [ + { + "id": "stop__9640756", + "name": "Метро Петровско-Разумовская" + }, + { + "id": "stop__9640553", + "name": "Лобненская улица" + } + ], + "BriefSchedule": { + "Events": [], + "Frequency": { + "text": "22 мин", + "value": 1320, + "begin": { + "value": "1568601239", + "tzOffset": 10800, + "text": "5:33" + }, + "end": { + "value": "1568671439", + "tzOffset": 10800, + "text": "1:03" + } + } + } + } + ], + "threadId": "213A_206m_minibus_default", + "EssentialStops": [ + { + "id": "stop__9640756", + "name": "Метро Петровско-Разумовская" + }, + { + "id": "stop__9640553", + "name": "Лобненская улица" + } + ], + "BriefSchedule": { + "Events": [], + "Frequency": { + "text": "22 мин", + "value": 1320, + "begin": { + "value": "1568601239", + "tzOffset": 10800, + "text": "5:33" + }, + "end": { + "value": "1568671439", + "tzOffset": 10800, + "text": "1:03" + } + } + } + }, + { + "lineId": "213_215_bus_mosgortrans", + "name": "215", + "Types": [ + "bus" + ], + "type": "bus", + "threads": [ + { + "threadId": "213B_215_bus_mosgortrans", + "EssentialStops": [ + { + "id": "stop__9711780", + "name": "Метро Петровско-Разумовская" + }, + { + "id": "stop__9711744", + "name": "Станция Ховрино" + } + ], + "BriefSchedule": { + "Events": [], + "Frequency": { + "text": "27 мин", + "value": 1620, + "begin": { + "value": "1568601276", + "tzOffset": 10800, + "text": "5:34" + }, + "end": { + "value": "1568671476", + "tzOffset": 10800, + "text": "1:04" + } + } + } + } + ], + "threadId": "213B_215_bus_mosgortrans", + "EssentialStops": [ + { + "id": "stop__9711780", + "name": "Метро Петровско-Разумовская" + }, + { + "id": "stop__9711744", + "name": "Станция Ховрино" + } + ], + "BriefSchedule": { + "Events": [], + "Frequency": { + "text": "27 мин", + "value": 1620, + "begin": { + "value": "1568601276", + "tzOffset": 10800, + "text": "5:34" + }, + "end": { + "value": "1568671476", + "tzOffset": 10800, + "text": "1:04" + } + } + } + }, + { + "lineId": "213_282_bus_mosgortrans", + "name": "282", + "Types": [ + "bus" + ], + "type": "bus", + "threads": [ + { + "threadId": "213A_282_bus_mosgortrans", + "EssentialStops": [ + { + "id": "stop__9641102", + "name": "Улица Корнейчука" + }, + { + "id": "2532226085", + "name": "Метро Войковская" + } + ], + "BriefSchedule": { + "Events": [ + { + "Estimated": { + "value": "1568659888", + "tzOffset": 10800, + "text": "21:51" + }, + "vehicleId": "codd%5Fnew|34874%5F9345408" + } + ], + "Frequency": { + "text": "15 мин", + "value": 900, + "begin": { + "value": "1568602180", + "tzOffset": 10800, + "text": "5:49" + }, + "end": { + "value": "1568673460", + "tzOffset": 10800, + "text": "1:37" + } + } + } + } + ], + "threadId": "213A_282_bus_mosgortrans", + "EssentialStops": [ + { + "id": "stop__9641102", + "name": "Улица Корнейчука" + }, + { + "id": "2532226085", + "name": "Метро Войковская" + } + ], + "BriefSchedule": { + "Events": [ + { + "Estimated": { + "value": "1568659888", + "tzOffset": 10800, + "text": "21:51" + }, + "vehicleId": "codd%5Fnew|34874%5F9345408" + } + ], + "Frequency": { + "text": "15 мин", + "value": 900, + "begin": { + "value": "1568602180", + "tzOffset": 10800, + "text": "5:49" + }, + "end": { + "value": "1568673460", + "tzOffset": 10800, + "text": "1:37" + } + } + } + }, + { + "lineId": "213_294m_minibus_default", + "name": "994", + "Types": [ + "bus" + ], + "type": "bus", + "threads": [ + { + "threadId": "213A_294m_minibus_default", + "EssentialStops": [ + { + "id": "stop__9640756", + "name": "Метро Петровско-Разумовская" + }, + { + "id": "stop__9649459", + "name": "Метро Алтуфьево" + } + ], + "BriefSchedule": { + "Events": [], + "Frequency": { + "text": "30 мин", + "value": 1800, + "begin": { + "value": "1568601527", + "tzOffset": 10800, + "text": "5:38" + }, + "end": { + "value": "1568671727", + "tzOffset": 10800, + "text": "1:08" + } + } + } + } + ], + "threadId": "213A_294m_minibus_default", + "EssentialStops": [ + { + "id": "stop__9640756", + "name": "Метро Петровско-Разумовская" + }, + { + "id": "stop__9649459", + "name": "Метро Алтуфьево" + } + ], + "BriefSchedule": { + "Events": [], + "Frequency": { + "text": "30 мин", + "value": 1800, + "begin": { + "value": "1568601527", + "tzOffset": 10800, + "text": "5:38" + }, + "end": { + "value": "1568671727", + "tzOffset": 10800, + "text": "1:08" + } + } + } + }, + { + "lineId": "213_36_trolleybus_mosgortrans", + "name": "т36", + "Types": [ + "bus" + ], + "type": "bus", + "threads": [ + { + "threadId": "213A_36_trolleybus_mosgortrans", + "EssentialStops": [ + { + "id": "stop__9642550", + "name": "ВДНХ (южная)" + }, + { + "id": "stop__9640641", + "name": "Дмитровское шоссе, 155" + } + ], + "BriefSchedule": { + "Events": [ + { + "Scheduled": { + "value": "1568659680", + "tzOffset": 10800, + "text": "21:48" + }, + "Estimated": { + "value": "1568659426", + "tzOffset": 10800, + "text": "21:43" + }, + "vehicleId": "codd%5Fnew|1084829%5F430260" + }, + { + "Scheduled": { + "value": "1568660520", + "tzOffset": 10800, + "text": "22:02" + }, + "Estimated": { + "value": "1568659656", + "tzOffset": 10800, + "text": "21:47" + }, + "vehicleId": "codd%5Fnew|1117016%5F430280" + }, + { + "Scheduled": { + "value": "1568661900", + "tzOffset": 10800, + "text": "22:25" + }, + "Estimated": { + "value": "1568660538", + "tzOffset": 10800, + "text": "22:02" + }, + "vehicleId": "codd%5Fnew|1054576%5F430226" + } + ], + "departureTime": "21:48" + } + } + ], + "threadId": "213A_36_trolleybus_mosgortrans", + "EssentialStops": [ + { + "id": "stop__9642550", + "name": "ВДНХ (южная)" + }, + { + "id": "stop__9640641", + "name": "Дмитровское шоссе, 155" + } + ], + "BriefSchedule": { + "Events": [ + { + "Scheduled": { + "value": "1568659680", + "tzOffset": 10800, + "text": "21:48" + }, + "Estimated": { + "value": "1568659426", + "tzOffset": 10800, + "text": "21:43" + }, + "vehicleId": "codd%5Fnew|1084829%5F430260" + }, + { + "Scheduled": { + "value": "1568660520", + "tzOffset": 10800, + "text": "22:02" + }, + "Estimated": { + "value": "1568659656", + "tzOffset": 10800, + "text": "21:47" + }, + "vehicleId": "codd%5Fnew|1117016%5F430280" + }, + { + "Scheduled": { + "value": "1568661900", + "tzOffset": 10800, + "text": "22:25" + }, + "Estimated": { + "value": "1568660538", + "tzOffset": 10800, + "text": "22:02" + }, + "vehicleId": "codd%5Fnew|1054576%5F430226" + } + ], + "departureTime": "21:48" + } + }, + { + "lineId": "213_47_trolleybus_mosgortrans", + "name": "т47", + "Types": [ + "bus" + ], + "type": "bus", + "threads": [ + { + "threadId": "213B_47_trolleybus_mosgortrans", + "EssentialStops": [ + { + "id": "stop__9639568", + "name": "Бескудниковский переулок" + }, + { + "id": "stop__9641903", + "name": "Бескудниковский переулок" + } + ], + "BriefSchedule": { + "Events": [ + { + "Scheduled": { + "value": "1568659980", + "tzOffset": 10800, + "text": "21:53" + }, + "Estimated": { + "value": "1568659253", + "tzOffset": 10800, + "text": "21:40" + }, + "vehicleId": "codd%5Fnew|1112219%5F430329" + }, + { + "Scheduled": { + "value": "1568660940", + "tzOffset": 10800, + "text": "22:09" + }, + "Estimated": { + "value": "1568660519", + "tzOffset": 10800, + "text": "22:01" + }, + "vehicleId": "codd%5Fnew|1139620%5F430382" + }, + { + "Scheduled": { + "value": "1568663580", + "tzOffset": 10800, + "text": "22:53" + } + } + ], + "departureTime": "21:53" + } + } + ], + "threadId": "213B_47_trolleybus_mosgortrans", + "EssentialStops": [ + { + "id": "stop__9639568", + "name": "Бескудниковский переулок" + }, + { + "id": "stop__9641903", + "name": "Бескудниковский переулок" + } + ], + "BriefSchedule": { + "Events": [ + { + "Scheduled": { + "value": "1568659980", + "tzOffset": 10800, + "text": "21:53" + }, + "Estimated": { + "value": "1568659253", + "tzOffset": 10800, + "text": "21:40" + }, + "vehicleId": "codd%5Fnew|1112219%5F430329" + }, + { + "Scheduled": { + "value": "1568660940", + "tzOffset": 10800, + "text": "22:09" + }, + "Estimated": { + "value": "1568660519", + "tzOffset": 10800, + "text": "22:01" + }, + "vehicleId": "codd%5Fnew|1139620%5F430382" + }, + { + "Scheduled": { + "value": "1568663580", + "tzOffset": 10800, + "text": "22:53" + } + } + ], + "departureTime": "21:53" + } + }, + { + "lineId": "213_56_trolleybus_mosgortrans", + "name": "т56", + "Types": [ + "bus" + ], + "type": "bus", + "threads": [ + { + "threadId": "213A_56_trolleybus_mosgortrans", + "EssentialStops": [ + { + "id": "stop__9639561", + "name": "Коровинское шоссе" + }, + { + "id": "stop__9639588", + "name": "Коровинское шоссе" + } + ], + "BriefSchedule": { + "Events": [ + { + "Estimated": { + "value": "1568660675", + "tzOffset": 10800, + "text": "22:04" + }, + "vehicleId": "codd%5Fnew|146304%5F31207" + } + ], + "Frequency": { + "text": "8 мин", + "value": 480, + "begin": { + "value": "1568606244", + "tzOffset": 10800, + "text": "6:57" + }, + "end": { + "value": "1568670144", + "tzOffset": 10800, + "text": "0:42" + } + } + } + } + ], + "threadId": "213A_56_trolleybus_mosgortrans", + "EssentialStops": [ + { + "id": "stop__9639561", + "name": "Коровинское шоссе" + }, + { + "id": "stop__9639588", + "name": "Коровинское шоссе" + } + ], + "BriefSchedule": { + "Events": [ + { + "Estimated": { + "value": "1568660675", + "tzOffset": 10800, + "text": "22:04" + }, + "vehicleId": "codd%5Fnew|146304%5F31207" + } + ], + "Frequency": { + "text": "8 мин", + "value": 480, + "begin": { + "value": "1568606244", + "tzOffset": 10800, + "text": "6:57" + }, + "end": { + "value": "1568670144", + "tzOffset": 10800, + "text": "0:42" + } + } + } + }, + { + "lineId": "213_63_bus_mosgortrans", + "name": "63", + "Types": [ + "bus" + ], + "type": "bus", + "threads": [ + { + "threadId": "213A_63_bus_mosgortrans", + "EssentialStops": [ + { + "id": "stop__9640554", + "name": "Лобненская улица" + }, + { + "id": "stop__9640553", + "name": "Лобненская улица" + } + ], + "BriefSchedule": { + "Events": [ + { + "Estimated": { + "value": "1568659369", + "tzOffset": 10800, + "text": "21:42" + }, + "vehicleId": "codd%5Fnew|38921%5F9215306" + }, + { + "Estimated": { + "value": "1568660136", + "tzOffset": 10800, + "text": "21:55" + }, + "vehicleId": "codd%5Fnew|38918%5F9215303" + } + ], + "Frequency": { + "text": "17 мин", + "value": 1020, + "begin": { + "value": "1568600987", + "tzOffset": 10800, + "text": "5:29" + }, + "end": { + "value": "1568670227", + "tzOffset": 10800, + "text": "0:43" + } + } + } + } + ], + "threadId": "213A_63_bus_mosgortrans", + "EssentialStops": [ + { + "id": "stop__9640554", + "name": "Лобненская улица" + }, + { + "id": "stop__9640553", + "name": "Лобненская улица" + } + ], + "BriefSchedule": { + "Events": [ + { + "Estimated": { + "value": "1568659369", + "tzOffset": 10800, + "text": "21:42" + }, + "vehicleId": "codd%5Fnew|38921%5F9215306" + }, + { + "Estimated": { + "value": "1568660136", + "tzOffset": 10800, + "text": "21:55" + }, + "vehicleId": "codd%5Fnew|38918%5F9215303" + } + ], + "Frequency": { + "text": "17 мин", + "value": 1020, + "begin": { + "value": "1568600987", + "tzOffset": 10800, + "text": "5:29" + }, + "end": { + "value": "1568670227", + "tzOffset": 10800, + "text": "0:43" + } + } + } + }, + { + "lineId": "213_677_bus_mosgortrans", + "name": "677", + "Types": [ + "bus" + ], + "type": "bus", + "threads": [ + { + "threadId": "213B_677_bus_mosgortrans", + "EssentialStops": [ + { + "id": "stop__9639495", + "name": "Метро Петровско-Разумовская" + }, + { + "id": "stop__9639480", + "name": "Платформа Лианозово" + } + ], + "BriefSchedule": { + "Events": [ + { + "Estimated": { + "value": "1568659369", + "tzOffset": 10800, + "text": "21:42" + }, + "vehicleId": "codd%5Fnew|11731%5F31376" + } + ], + "Frequency": { + "text": "4 мин", + "value": 240, + "begin": { + "value": "1568600940", + "tzOffset": 10800, + "text": "5:29" + }, + "end": { + "value": "1568672640", + "tzOffset": 10800, + "text": "1:24" + } + } + } + } + ], + "threadId": "213B_677_bus_mosgortrans", + "EssentialStops": [ + { + "id": "stop__9639495", + "name": "Метро Петровско-Разумовская" + }, + { + "id": "stop__9639480", + "name": "Платформа Лианозово" + } + ], + "BriefSchedule": { + "Events": [ + { + "Estimated": { + "value": "1568659369", + "tzOffset": 10800, + "text": "21:42" + }, + "vehicleId": "codd%5Fnew|11731%5F31376" + } + ], + "Frequency": { + "text": "4 мин", + "value": 240, + "begin": { + "value": "1568600940", + "tzOffset": 10800, + "text": "5:29" + }, + "end": { + "value": "1568672640", + "tzOffset": 10800, + "text": "1:24" + } + } + } + }, + { + "lineId": "213_692_bus_mosgortrans", + "name": "692", + "Types": [ + "bus" + ], + "type": "bus", + "threads": [ + { + "threadId": "2036928706", + "EssentialStops": [ + { + "id": "3163417967", + "name": "Платформа Дегунино" + }, + { + "id": "3163417967", + "name": "Платформа Дегунино" + } + ], + "BriefSchedule": { + "Events": [ + { + "Scheduled": { + "value": "1568660280", + "tzOffset": 10800, + "text": "21:58" + }, + "Estimated": { + "value": "1568660255", + "tzOffset": 10800, + "text": "21:57" + }, + "vehicleId": "codd%5Fnew|63029%5F31485" + }, + { + "Scheduled": { + "value": "1568693340", + "tzOffset": 10800, + "text": "7:09" + } + }, + { + "Scheduled": { + "value": "1568696940", + "tzOffset": 10800, + "text": "8:09" + } + } + ], + "departureTime": "21:58" + } + } + ], + "threadId": "2036928706", + "EssentialStops": [ + { + "id": "3163417967", + "name": "Платформа Дегунино" + }, + { + "id": "3163417967", + "name": "Платформа Дегунино" + } + ], + "BriefSchedule": { + "Events": [ + { + "Scheduled": { + "value": "1568660280", + "tzOffset": 10800, + "text": "21:58" + }, + "Estimated": { + "value": "1568660255", + "tzOffset": 10800, + "text": "21:57" + }, + "vehicleId": "codd%5Fnew|63029%5F31485" + }, + { + "Scheduled": { + "value": "1568693340", + "tzOffset": 10800, + "text": "7:09" + } + }, + { + "Scheduled": { + "value": "1568696940", + "tzOffset": 10800, + "text": "8:09" + } + } + ], + "departureTime": "21:58" + } + }, + { + "lineId": "213_78_trolleybus_mosgortrans", + "name": "т78", + "Types": [ + "bus" + ], + "type": "bus", + "threads": [ + { + "threadId": "213A_78_trolleybus_mosgortrans", + "EssentialStops": [ + { + "id": "stop__9887464", + "name": "9-я Северная линия" + }, + { + "id": "stop__9887464", + "name": "9-я Северная линия" + } + ], + "BriefSchedule": { + "Events": [ + { + "Scheduled": { + "value": "1568659620", + "tzOffset": 10800, + "text": "21:47" + }, + "Estimated": { + "value": "1568659898", + "tzOffset": 10800, + "text": "21:51" + }, + "vehicleId": "codd%5Fnew|147522%5F31184" + }, + { + "Scheduled": { + "value": "1568660760", + "tzOffset": 10800, + "text": "22:06" + } + }, + { + "Scheduled": { + "value": "1568661900", + "tzOffset": 10800, + "text": "22:25" + } + } + ], + "departureTime": "21:47" + } + } + ], + "threadId": "213A_78_trolleybus_mosgortrans", + "EssentialStops": [ + { + "id": "stop__9887464", + "name": "9-я Северная линия" + }, + { + "id": "stop__9887464", + "name": "9-я Северная линия" + } + ], + "BriefSchedule": { + "Events": [ + { + "Scheduled": { + "value": "1568659620", + "tzOffset": 10800, + "text": "21:47" + }, + "Estimated": { + "value": "1568659898", + "tzOffset": 10800, + "text": "21:51" + }, + "vehicleId": "codd%5Fnew|147522%5F31184" + }, + { + "Scheduled": { + "value": "1568660760", + "tzOffset": 10800, + "text": "22:06" + } + }, + { + "Scheduled": { + "value": "1568661900", + "tzOffset": 10800, + "text": "22:25" + } + } + ], + "departureTime": "21:47" + } + }, + { + "lineId": "213_82_bus_mosgortrans", + "name": "82", + "Types": [ + "bus" + ], + "type": "bus", + "threads": [ + { + "threadId": "2036925244", + "EssentialStops": [ + { + "id": "2310890052", + "name": "Метро Верхние Лихоборы" + }, + { + "id": "2310890052", + "name": "Метро Верхние Лихоборы" + } + ], + "BriefSchedule": { + "Events": [ + { + "Scheduled": { + "value": "1568659680", + "tzOffset": 10800, + "text": "21:48" + } + }, + { + "Scheduled": { + "value": "1568661780", + "tzOffset": 10800, + "text": "22:23" + } + }, + { + "Scheduled": { + "value": "1568663760", + "tzOffset": 10800, + "text": "22:56" + } + } + ], + "departureTime": "21:48" + } + } + ], + "threadId": "2036925244", + "EssentialStops": [ + { + "id": "2310890052", + "name": "Метро Верхние Лихоборы" + }, + { + "id": "2310890052", + "name": "Метро Верхние Лихоборы" + } + ], + "BriefSchedule": { + "Events": [ + { + "Scheduled": { + "value": "1568659680", + "tzOffset": 10800, + "text": "21:48" + } + }, + { + "Scheduled": { + "value": "1568661780", + "tzOffset": 10800, + "text": "22:23" + } + }, + { + "Scheduled": { + "value": "1568663760", + "tzOffset": 10800, + "text": "22:56" + } + } + ], + "departureTime": "21:48" + } + }, + { + "lineId": "2465131598", + "name": "179к", + "Types": [ + "bus" + ], + "type": "bus", + "threads": [ + { + "threadId": "2465131758", + "EssentialStops": [ + { + "id": "stop__9640244", + "name": "Платформа Лианозово" + }, + { + "id": "stop__9639480", + "name": "Платформа Лианозово" + } + ], + "BriefSchedule": { + "Events": [ + { + "Scheduled": { + "value": "1568659500", + "tzOffset": 10800, + "text": "21:45" + } + }, + { + "Scheduled": { + "value": "1568659980", + "tzOffset": 10800, + "text": "21:53" + } + }, + { + "Scheduled": { + "value": "1568660880", + "tzOffset": 10800, + "text": "22:08" + } + } + ], + "departureTime": "21:45" + } + } + ], + "threadId": "2465131758", + "EssentialStops": [ + { + "id": "stop__9640244", + "name": "Платформа Лианозово" + }, + { + "id": "stop__9639480", + "name": "Платформа Лианозово" + } + ], + "BriefSchedule": { + "Events": [ + { + "Scheduled": { + "value": "1568659500", + "tzOffset": 10800, + "text": "21:45" + } + }, + { + "Scheduled": { + "value": "1568659980", + "tzOffset": 10800, + "text": "21:53" + } + }, + { + "Scheduled": { + "value": "1568660880", + "tzOffset": 10800, + "text": "22:08" + } + } + ], + "departureTime": "21:45" + } + }, + { + "lineId": "466_bus_default", + "name": "466", + "Types": [ + "bus" + ], + "type": "bus", + "threads": [ + { + "threadId": "466B_bus_default", + "EssentialStops": [ + { + "id": "stop__9640546", + "name": "Станция Бескудниково" + }, + { + "id": "stop__9640545", + "name": "Станция Бескудниково" + } + ], + "BriefSchedule": { + "Events": [], + "Frequency": { + "text": "22 мин", + "value": 1320, + "begin": { + "value": "1568604647", + "tzOffset": 10800, + "text": "6:30" + }, + "end": { + "value": "1568675447", + "tzOffset": 10800, + "text": "2:10" + } + } + } + } + ], + "threadId": "466B_bus_default", + "EssentialStops": [ + { + "id": "stop__9640546", + "name": "Станция Бескудниково" + }, + { + "id": "stop__9640545", + "name": "Станция Бескудниково" + } + ], + "BriefSchedule": { + "Events": [], + "Frequency": { + "text": "22 мин", + "value": 1320, + "begin": { + "value": "1568604647", + "tzOffset": 10800, + "text": "6:30" + }, + "end": { + "value": "1568675447", + "tzOffset": 10800, + "text": "2:10" + } + } + } + }, + { + "lineId": "677k_bus_default", + "name": "677к", + "Types": [ + "bus" + ], + "type": "bus", + "threads": [ + { + "threadId": "677kA_bus_default", + "EssentialStops": [ + { + "id": "stop__9640244", + "name": "Платформа Лианозово" + }, + { + "id": "stop__9639480", + "name": "Платформа Лианозово" + } + ], + "BriefSchedule": { + "Events": [ + { + "Scheduled": { + "value": "1568659920", + "tzOffset": 10800, + "text": "21:52" + }, + "Estimated": { + "value": "1568660003", + "tzOffset": 10800, + "text": "21:53" + }, + "vehicleId": "codd%5Fnew|130308%5F31319" + }, + { + "Scheduled": { + "value": "1568661240", + "tzOffset": 10800, + "text": "22:14" + } + }, + { + "Scheduled": { + "value": "1568662500", + "tzOffset": 10800, + "text": "22:35" + } + } + ], + "departureTime": "21:52" + } + } + ], + "threadId": "677kA_bus_default", + "EssentialStops": [ + { + "id": "stop__9640244", + "name": "Платформа Лианозово" + }, + { + "id": "stop__9639480", + "name": "Платформа Лианозово" + } + ], + "BriefSchedule": { + "Events": [ + { + "Scheduled": { + "value": "1568659920", + "tzOffset": 10800, + "text": "21:52" + }, + "Estimated": { + "value": "1568660003", + "tzOffset": 10800, + "text": "21:53" + }, + "vehicleId": "codd%5Fnew|130308%5F31319" + }, + { + "Scheduled": { + "value": "1568661240", + "tzOffset": 10800, + "text": "22:14" + } + }, + { + "Scheduled": { + "value": "1568662500", + "tzOffset": 10800, + "text": "22:35" + } + } + ], + "departureTime": "21:52" + } + }, + { + "lineId": "m10_bus_default", + "name": "м10", + "Types": [ + "bus" + ], + "type": "bus", + "threads": [ + { + "threadId": "2036926048", + "EssentialStops": [ + { + "id": "stop__9640554", + "name": "Лобненская улица" + }, + { + "id": "stop__9640553", + "name": "Лобненская улица" + } + ], + "BriefSchedule": { + "Events": [ + { + "Estimated": { + "value": "1568659718", + "tzOffset": 10800, + "text": "21:48" + }, + "vehicleId": "codd%5Fnew|146260%5F31212" + }, + { + "Estimated": { + "value": "1568660422", + "tzOffset": 10800, + "text": "22:00" + }, + "vehicleId": "codd%5Fnew|13997%5F31247" + } + ], + "Frequency": { + "text": "15 мин", + "value": 900, + "begin": { + "value": "1568606903", + "tzOffset": 10800, + "text": "7:08" + }, + "end": { + "value": "1568675183", + "tzOffset": 10800, + "text": "2:06" + } + } + } + } + ], + "threadId": "2036926048", + "EssentialStops": [ + { + "id": "stop__9640554", + "name": "Лобненская улица" + }, + { + "id": "stop__9640553", + "name": "Лобненская улица" + } + ], + "BriefSchedule": { + "Events": [ + { + "Estimated": { + "value": "1568659718", + "tzOffset": 10800, + "text": "21:48" + }, + "vehicleId": "codd%5Fnew|146260%5F31212" + }, + { + "Estimated": { + "value": "1568660422", + "tzOffset": 10800, + "text": "22:00" + }, + "vehicleId": "codd%5Fnew|13997%5F31247" + } + ], + "Frequency": { + "text": "15 мин", + "value": 900, + "begin": { + "value": "1568606903", + "tzOffset": 10800, + "text": "7:08" + }, + "end": { + "value": "1568675183", + "tzOffset": 10800, + "text": "2:06" + } + } + } + } + ] + } + }, + "toponymSeoname": "dmitrovskoye_shosse" + } +} diff --git a/tests/helpers/test_check_config.py b/tests/helpers/test_check_config.py index 9e5ea15293a..5228f0d4882 100644 --- a/tests/helpers/test_check_config.py +++ b/tests/helpers/test_check_config.py @@ -75,7 +75,7 @@ async def test_component_platform_not_found(hass, loop): assert res.keys() == {"homeassistant"} assert res.errors[0] == CheckConfigError( - "Component error: beer - Integration beer not found.", None, None + "Component error: beer - Integration 'beer' not found.", None, None ) # Only 1 error expected @@ -95,7 +95,7 @@ async def test_component_platform_not_found_2(hass, loop): assert res["light"] == [] assert res.errors[0] == CheckConfigError( - "Platform error light.beer - Integration beer not found.", None, None + "Platform error light.beer - Integration 'beer' not found.", None, None ) # Only 1 error expected diff --git a/tests/helpers/test_device_registry.py b/tests/helpers/test_device_registry.py index b854b62853c..1b146e9cb12 100644 --- a/tests/helpers/test_device_registry.py +++ b/tests/helpers/test_device_registry.py @@ -409,6 +409,64 @@ async def test_update(registry): assert updated_entry.via_device_id == "98765B" +async def test_update_remove_config_entries(hass, registry, update_events): + """Make sure we do not get duplicate entries.""" + entry = registry.async_get_or_create( + config_entry_id="123", + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + identifiers={("bridgeid", "0123")}, + manufacturer="manufacturer", + model="model", + ) + entry2 = registry.async_get_or_create( + config_entry_id="456", + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + identifiers={("bridgeid", "0123")}, + manufacturer="manufacturer", + model="model", + ) + entry3 = registry.async_get_or_create( + config_entry_id="123", + connections={(device_registry.CONNECTION_NETWORK_MAC, "34:56:78:CD:EF:12")}, + identifiers={("bridgeid", "4567")}, + manufacturer="manufacturer", + model="model", + ) + + assert len(registry.devices) == 2 + assert entry.id == entry2.id + assert entry.id != entry3.id + assert entry2.config_entries == {"123", "456"} + + updated_entry = registry.async_update_device( + entry2.id, remove_config_entry_id="123" + ) + removed_entry = registry.async_update_device( + entry3.id, remove_config_entry_id="123" + ) + + assert updated_entry.config_entries == {"456"} + assert removed_entry is None + + removed_entry = registry.async_get_device({("bridgeid", "4567")}, set()) + + assert removed_entry is None + + await hass.async_block_till_done() + + assert len(update_events) == 5 + assert update_events[0]["action"] == "create" + assert update_events[0]["device_id"] == entry.id + assert update_events[1]["action"] == "update" + assert update_events[1]["device_id"] == entry2.id + assert update_events[2]["action"] == "create" + assert update_events[2]["device_id"] == entry3.id + assert update_events[3]["action"] == "update" + assert update_events[3]["device_id"] == entry.id + assert update_events[4]["action"] == "remove" + assert update_events[4]["device_id"] == entry3.id + + async def test_loading_race_condition(hass): """Test only one storage load called when concurrent loading occurred .""" with asynctest.patch( diff --git a/tests/scripts/test_check_config.py b/tests/scripts/test_check_config.py index bd4f37bd135..18143c088be 100644 --- a/tests/scripts/test_check_config.py +++ b/tests/scripts/test_check_config.py @@ -63,7 +63,7 @@ def test_component_platform_not_found(isfile_patch, loop): assert res["components"].keys() == {"homeassistant"} assert res["except"] == { check_config.ERROR_STR: [ - "Component error: beer - Integration beer not found." + "Component error: beer - Integration 'beer' not found." ] } assert res["secret_cache"] == {} @@ -77,7 +77,7 @@ def test_component_platform_not_found(isfile_patch, loop): assert res["components"]["light"] == [] assert res["except"] == { check_config.ERROR_STR: [ - "Platform error light.beer - Integration beer not found." + "Platform error light.beer - Integration 'beer' not found." ] } assert res["secret_cache"] == {} diff --git a/tests/test_core.py b/tests/test_core.py index e81ce7a4a5a..5ac13027f28 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -15,7 +15,6 @@ import pytest import homeassistant.core as ha from homeassistant.exceptions import InvalidEntityFormatError, InvalidStateError -from homeassistant.util.async_ import run_coroutine_threadsafe import homeassistant.util.dt as dt_util from homeassistant.util.unit_system import METRIC_SYSTEM from homeassistant.const import ( @@ -191,7 +190,7 @@ class TestHomeAssistant(unittest.TestCase): for _ in range(3): self.hass.add_job(test_coro()) - run_coroutine_threadsafe( + asyncio.run_coroutine_threadsafe( asyncio.wait(self.hass._pending_tasks), loop=self.hass.loop ).result() @@ -216,7 +215,9 @@ class TestHomeAssistant(unittest.TestCase): yield from asyncio.sleep(0) yield from asyncio.sleep(0) - run_coroutine_threadsafe(wait_finish_callback(), self.hass.loop).result() + asyncio.run_coroutine_threadsafe( + wait_finish_callback(), self.hass.loop + ).result() assert len(self.hass._pending_tasks) == 2 self.hass.block_till_done() @@ -239,7 +240,9 @@ class TestHomeAssistant(unittest.TestCase): for _ in range(2): self.hass.add_job(test_executor) - run_coroutine_threadsafe(wait_finish_callback(), self.hass.loop).result() + asyncio.run_coroutine_threadsafe( + wait_finish_callback(), self.hass.loop + ).result() assert len(self.hass._pending_tasks) == 2 self.hass.block_till_done() @@ -263,7 +266,9 @@ class TestHomeAssistant(unittest.TestCase): for _ in range(2): self.hass.add_job(test_callback) - run_coroutine_threadsafe(wait_finish_callback(), self.hass.loop).result() + asyncio.run_coroutine_threadsafe( + wait_finish_callback(), self.hass.loop + ).result() self.hass.block_till_done() diff --git a/tests/test_util/aiohttp.py b/tests/test_util/aiohttp.py index 98fc70f3bf5..ce13ca5a594 100644 --- a/tests/test_util/aiohttp.py +++ b/tests/test_util/aiohttp.py @@ -244,8 +244,12 @@ class AiohttpClientMockResponse: def raise_for_status(self): """Raise error if status is 400 or higher.""" if self.status >= 400: + request_info = mock.Mock(real_url="http://example.com") raise ClientResponseError( - None, None, code=self.status, headers=self.headers + request_info=request_info, + history=None, + code=self.status, + headers=self.headers, ) def close(self): diff --git a/tests/testing_config/custom_components/test/binary_sensor.py b/tests/testing_config/custom_components/test/binary_sensor.py new file mode 100644 index 00000000000..5052b8e47f1 --- /dev/null +++ b/tests/testing_config/custom_components/test/binary_sensor.py @@ -0,0 +1,50 @@ +""" +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 BinarySensorDevice, DEVICE_CLASSES +from tests.common import MockEntity + + +ENTITIES = {} + + +def init(empty=False): + """Initialize the platform with entities.""" + global ENTITIES + + ENTITIES = ( + {} + if empty + else { + device_class: MockBinarySensor( + name=f"{device_class} sensor", + is_on=True, + unique_id=f"unique_{device_class}", + device_class=device_class, + ) + for device_class in DEVICE_CLASSES + } + ) + + +async def async_setup_platform( + hass, config, async_add_entities_callback, discovery_info=None +): + """Return mock entities.""" + async_add_entities_callback(list(ENTITIES.values())) + + +class MockBinarySensor(MockEntity, BinarySensorDevice): + """Mock Binary Sensor class.""" + + @property + def is_on(self): + """Return true if the binary sensor is on.""" + return self._handle("is_on") + + @property + def device_class(self): + """Return the class of this sensor.""" + return self._handle("device_class") diff --git a/tests/testing_config/custom_components/test/sensor.py b/tests/testing_config/custom_components/test/sensor.py new file mode 100644 index 00000000000..651ee17bd65 --- /dev/null +++ b/tests/testing_config/custom_components/test/sensor.py @@ -0,0 +1,64 @@ +""" +Provide a mock sensor platform. + +Call init before using it in your tests to ensure clean test data. +""" +import homeassistant.components.sensor as sensor +from tests.common import MockEntity + + +DEVICE_CLASSES = list(sensor.DEVICE_CLASSES) +DEVICE_CLASSES.append("none") + +UNITS_OF_MEASUREMENT = { + sensor.DEVICE_CLASS_BATTERY: "%", # % of battery that is left + sensor.DEVICE_CLASS_HUMIDITY: "%", # % of humidity in the air + sensor.DEVICE_CLASS_ILLUMINANCE: "lm", # current light level (lx/lm) + sensor.DEVICE_CLASS_SIGNAL_STRENGTH: "dB", # signal strength (dB/dBm) + sensor.DEVICE_CLASS_TEMPERATURE: "C", # temperature (C/F) + sensor.DEVICE_CLASS_TIMESTAMP: "hh:mm:ss", # timestamp (ISO8601) + sensor.DEVICE_CLASS_PRESSURE: "hPa", # pressure (hPa/mbar) + sensor.DEVICE_CLASS_POWER: "kW", # power (W/kW) +} + +ENTITIES = {} + + +def init(empty=False): + """Initialize the platform with entities.""" + global ENTITIES + + ENTITIES = ( + {} + if empty + else { + device_class: MockSensor( + name=f"{device_class} sensor", + unique_id=f"unique_{device_class}", + device_class=device_class, + unit_of_measurement=UNITS_OF_MEASUREMENT.get(device_class), + ) + for device_class in DEVICE_CLASSES + } + ) + + +async def async_setup_platform( + hass, config, async_add_entities_callback, discovery_info=None +): + """Return mock entities.""" + async_add_entities_callback(list(ENTITIES.values())) + + +class MockSensor(MockEntity): + """Mock Sensor class.""" + + @property + def device_class(self): + """Return the class of this sensor.""" + return self._handle("device_class") + + @property + def unit_of_measurement(self): + """Return the unit_of_measurement of this sensor.""" + return self._handle("unit_of_measurement") diff --git a/tests/util/test_async.py b/tests/util/test_async.py index 023c42cc817..8dede61869c 100644 --- a/tests/util/test_async.py +++ b/tests/util/test_async.py @@ -9,43 +9,6 @@ import pytest from homeassistant.util import async_ as hasync -@patch("asyncio.coroutines.iscoroutine") -@patch("concurrent.futures.Future") -@patch("threading.get_ident") -def test_run_coroutine_threadsafe_from_inside_event_loop( - mock_ident, _, mock_iscoroutine -): - """Testing calling run_coroutine_threadsafe from inside an event loop.""" - coro = MagicMock() - loop = MagicMock() - - loop._thread_ident = None - mock_ident.return_value = 5 - mock_iscoroutine.return_value = True - hasync.run_coroutine_threadsafe(coro, loop) - assert len(loop.call_soon_threadsafe.mock_calls) == 1 - - loop._thread_ident = 5 - mock_ident.return_value = 5 - mock_iscoroutine.return_value = True - with pytest.raises(RuntimeError): - hasync.run_coroutine_threadsafe(coro, loop) - assert len(loop.call_soon_threadsafe.mock_calls) == 1 - - loop._thread_ident = 1 - mock_ident.return_value = 5 - mock_iscoroutine.return_value = False - with pytest.raises(TypeError): - hasync.run_coroutine_threadsafe(coro, loop) - assert len(loop.call_soon_threadsafe.mock_calls) == 1 - - loop._thread_ident = 1 - mock_ident.return_value = 5 - mock_iscoroutine.return_value = True - hasync.run_coroutine_threadsafe(coro, loop) - assert len(loop.call_soon_threadsafe.mock_calls) == 2 - - @patch("asyncio.coroutines.iscoroutine") @patch("concurrent.futures.Future") @patch("threading.get_ident") @@ -187,49 +150,6 @@ class RunThreadsafeTests(TestCase): finally: future.done() or future.cancel() - def test_run_coroutine_threadsafe(self): - """Test coroutine submission from a thread to an event loop.""" - future = self.loop.run_in_executor(None, self.target_coroutine) - result = self.loop.run_until_complete(future) - self.assertEqual(result, 3) - - def test_run_coroutine_threadsafe_with_exception(self): - """Test coroutine submission from thread to event loop on exception.""" - future = self.loop.run_in_executor(None, self.target_coroutine, True) - with self.assertRaises(RuntimeError) as exc_context: - self.loop.run_until_complete(future) - self.assertIn("Fail!", exc_context.exception.args) - - def test_run_coroutine_threadsafe_with_invalid(self): - """Test coroutine submission from thread to event loop on invalid.""" - callback = lambda: self.target_coroutine(invalid=True) # noqa - future = self.loop.run_in_executor(None, callback) - with self.assertRaises(ValueError) as exc_context: - self.loop.run_until_complete(future) - self.assertIn("Invalid!", exc_context.exception.args) - - def test_run_coroutine_threadsafe_with_timeout(self): - """Test coroutine submission from thread to event loop on timeout.""" - callback = lambda: self.target_coroutine(timeout=0) # noqa - future = self.loop.run_in_executor(None, callback) - with self.assertRaises(asyncio.TimeoutError): - self.loop.run_until_complete(future) - self.run_briefly(self.loop) - # Check that there's no pending task (add has been cancelled) - if sys.version_info[:2] >= (3, 7): - all_tasks = asyncio.all_tasks - else: - all_tasks = asyncio.Task.all_tasks - for task in all_tasks(self.loop): - self.assertTrue(task.done()) - - def test_run_coroutine_threadsafe_task_cancelled(self): - """Test coroutine submission from tread to event loop on cancel.""" - callback = lambda: self.target_coroutine(cancel=True) # noqa - future = self.loop.run_in_executor(None, callback) - with self.assertRaises(asyncio.CancelledError): - self.loop.run_until_complete(future) - def test_run_callback_threadsafe(self): """Test callback submission from a thread to an event loop.""" future = self.loop.run_in_executor(None, self.target_callback)