diff --git a/.coveragerc b/.coveragerc index b0d6a40c7f7f..7f519f8970a0 100644 --- a/.coveragerc +++ b/.coveragerc @@ -62,6 +62,7 @@ omit = homeassistant/components/asterisk_cdr/mailbox.py homeassistant/components/asterisk_mbox/* homeassistant/components/asuswrt/device_tracker.py + homeassistant/components/aten_pe/* homeassistant/components/atome/* homeassistant/components/august/* homeassistant/components/aurora_abb_powerone/sensor.py @@ -83,9 +84,9 @@ omit = homeassistant/components/blinkt/light.py homeassistant/components/blockchain/sensor.py homeassistant/components/bloomsky/* - homeassistant/components/bluesound/media_player.py + homeassistant/components/bluesound/* homeassistant/components/bluetooth_le_tracker/device_tracker.py - homeassistant/components/bluetooth_tracker/device_tracker.py + homeassistant/components/bluetooth_tracker/* homeassistant/components/bme280/sensor.py homeassistant/components/bme680/sensor.py homeassistant/components/bmw_connected_drive/* @@ -93,6 +94,7 @@ omit = homeassistant/components/bom/sensor.py homeassistant/components/bom/weather.py homeassistant/components/braviatv/media_player.py + homeassistant/components/broadlink/remote.py homeassistant/components/broadlink/sensor.py homeassistant/components/broadlink/switch.py homeassistant/components/brottsplatskartan/sensor.py @@ -109,7 +111,7 @@ omit = homeassistant/components/cast/* homeassistant/components/cert_expiry/sensor.py homeassistant/components/cert_expiry/helper.py - homeassistant/components/channels/media_player.py + homeassistant/components/channels/* homeassistant/components/cisco_ios/device_tracker.py homeassistant/components/cisco_mobility_express/device_tracker.py homeassistant/components/cisco_webex_teams/notify.py @@ -163,6 +165,7 @@ omit = homeassistant/components/doorbird/* homeassistant/components/dovado/* homeassistant/components/downloader/* + homeassistant/components/dsmr_reader/* homeassistant/components/dte_energy_bridge/sensor.py homeassistant/components/dublin_bus_transport/sensor.py homeassistant/components/duke_energy/sensor.py @@ -178,7 +181,7 @@ omit = homeassistant/components/ecobee/notify.py homeassistant/components/ecobee/sensor.py homeassistant/components/ecobee/weather.py - homeassistant/components/econet/water_heater.py + homeassistant/components/econet/* homeassistant/components/ecovacs/* homeassistant/components/eddystone_temperature/sensor.py homeassistant/components/edimax/switch.py @@ -199,6 +202,7 @@ omit = homeassistant/components/envirophat/sensor.py homeassistant/components/envisalink/* homeassistant/components/ephember/climate.py + homeassistant/components/epson/const.py homeassistant/components/epson/media_player.py homeassistant/components/epsonworkforce/sensor.py homeassistant/components/eq3btsmart/climate.py @@ -229,6 +233,7 @@ omit = homeassistant/components/flexit/climate.py homeassistant/components/flic/binary_sensor.py homeassistant/components/flock/notify.py + homeassistant/components/flume/* homeassistant/components/flunearyou/sensor.py homeassistant/components/flux_led/light.py homeassistant/components/folder/sensor.py @@ -282,12 +287,13 @@ omit = homeassistant/components/hangouts/hangouts_bot.py homeassistant/components/hangouts/hangups_utils.py homeassistant/components/harman_kardon_avr/media_player.py - homeassistant/components/harmony/remote.py + homeassistant/components/harmony/* homeassistant/components/haveibeenpwned/sensor.py homeassistant/components/hdmi_cec/* homeassistant/components/heatmiser/climate.py homeassistant/components/hikvision/binary_sensor.py homeassistant/components/hikvisioncam/switch.py + homeassistant/components/hisense_aehw4a1/* homeassistant/components/hitron_coda/device_tracker.py homeassistant/components/hive/* homeassistant/components/hlk_sw16/* @@ -313,7 +319,7 @@ omit = homeassistant/components/iaqualink/light.py homeassistant/components/iaqualink/sensor.py homeassistant/components/iaqualink/switch.py - homeassistant/components/icloud/device_tracker.py + homeassistant/components/icloud/* homeassistant/components/izone/climate.py homeassistant/components/izone/discovery.py homeassistant/components/izone/__init__.py @@ -409,6 +415,7 @@ omit = homeassistant/components/miflora/sensor.py homeassistant/components/mikrotik/* homeassistant/components/mill/climate.py + homeassistant/components/mill/const.py homeassistant/components/minio/* homeassistant/components/mitemp_bt/sensor.py homeassistant/components/mjpeg/camera.py @@ -525,6 +532,7 @@ omit = homeassistant/components/proliphix/climate.py homeassistant/components/prometheus/* homeassistant/components/prowl/notify.py + homeassistant/components/proxmoxve/* homeassistant/components/proxy/camera.py homeassistant/components/ptvsd/* homeassistant/components/pulseaudio_loopback/switch.py @@ -605,6 +613,7 @@ omit = homeassistant/components/simplepush/notify.py homeassistant/components/simplisafe/__init__.py homeassistant/components/simplisafe/alarm_control_panel.py + homeassistant/components/simplisafe/lock.py homeassistant/components/simulated/sensor.py homeassistant/components/sisyphus/* homeassistant/components/sky_hub/device_tracker.py @@ -632,7 +641,7 @@ omit = homeassistant/components/somfy/* homeassistant/components/somfy_mylink/* homeassistant/components/sonarr/sensor.py - homeassistant/components/songpal/media_player.py + homeassistant/components/songpal/* homeassistant/components/sonos/* homeassistant/components/sony_projector/switch.py homeassistant/components/spc/* @@ -640,7 +649,8 @@ omit = homeassistant/components/spider/* homeassistant/components/spotcrime/sensor.py homeassistant/components/spotify/media_player.py - homeassistant/components/squeezebox/media_player.py + homeassistant/components/squeezebox/* + homeassistant/components/starline/* homeassistant/components/starlingbank/sensor.py homeassistant/components/steam_online/sensor.py homeassistant/components/stiebel_eltron/* @@ -686,6 +696,7 @@ omit = homeassistant/components/tile/device_tracker.py homeassistant/components/time_date/sensor.py homeassistant/components/todoist/calendar.py + homeassistant/components/todoist/const.py homeassistant/components/tof/sensor.py homeassistant/components/tomato/device_tracker.py homeassistant/components/toon/* @@ -740,6 +751,7 @@ omit = homeassistant/components/venstar/climate.py homeassistant/components/vera/* homeassistant/components/verisure/* + homeassistant/components/versasense/* homeassistant/components/vesync/__init__.py homeassistant/components/vesync/common.py homeassistant/components/vesync/const.py @@ -761,7 +773,6 @@ omit = homeassistant/components/waze_travel_time/sensor.py homeassistant/components/webostv/* homeassistant/components/wemo/* - homeassistant/components/wemo/fan.py homeassistant/components/whois/sensor.py homeassistant/components/wink/* homeassistant/components/wirelesstag/* @@ -781,7 +792,6 @@ omit = homeassistant/components/xmpp/notify.py homeassistant/components/xs1/* 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/* diff --git a/.pre-commit-config-all.yaml b/.pre-commit-config-all.yaml index 3910835ae9d6..f7b29cddc4fb 100644 --- a/.pre-commit-config-all.yaml +++ b/.pre-commit-config-all.yaml @@ -18,7 +18,7 @@ repos: - --safe - --quiet files: ^((homeassistant|script|tests)/.+)?[^/]+\.py$ -- repo: https://gitlab.com/pycqa/flake8 +- repo: https://github.com/PyCQA/flake8 rev: 3.7.9 hooks: - id: flake8 @@ -26,6 +26,15 @@ repos: - flake8-docstrings==1.5.0 - pydocstyle==4.0.1 files: ^(homeassistant|script|tests)/.+\.py$ +- repo: https://github.com/PyCQA/bandit + rev: 1.6.2 + hooks: + - id: bandit + args: + - --quiet + - --format=custom + - --configfile=tests/bandit.yaml + files: ^(homeassistant|script|tests)/.+\.py$ # Using a local "system" mypy instead of the mypy hook, because its # results depend on what is installed. And the mypy hook runs in a # virtualenv of its own, meaning we'd need to install and maintain diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 3220ac848668..216bac95f292 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -22,3 +22,12 @@ repos: - flake8-docstrings==1.5.0 - pydocstyle==4.0.1 files: ^(homeassistant|script|tests)/.+\.py$ +- repo: https://github.com/PyCQA/bandit + rev: 1.6.2 + hooks: + - id: bandit + args: + - --quiet + - --format=custom + - --configfile=tests/bandit.yaml + files: ^(homeassistant|script|tests)/.+\.py$ diff --git a/CODEOWNERS b/CODEOWNERS index ec92d9186796..8078aadf6419 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -17,7 +17,6 @@ homeassistant/components/abode/* @shred86 homeassistant/components/adguard/* @frenck homeassistant/components/airly/* @bieniu homeassistant/components/airvisual/* @bachya -homeassistant/components/alarm_control_panel/* @colinodell homeassistant/components/alexa/* @home-assistant/cloud @ochlocracy homeassistant/components/almond/* @gcampax @balloob homeassistant/components/alpha_vantage/* @fabaff @@ -33,6 +32,7 @@ homeassistant/components/arcam_fmj/* @elupus homeassistant/components/arduino/* @fabaff homeassistant/components/arest/* @fabaff homeassistant/components/asuswrt/* @kennedyshead +homeassistant/components/aten_pe/* @mtdcr homeassistant/components/atome/* @baqs homeassistant/components/aurora_abb_powerone/* @davet2001 homeassistant/components/auth/* @home-assistant/core @@ -50,7 +50,7 @@ homeassistant/components/bizkaibus/* @UgaitzEtxebarria homeassistant/components/blink/* @fronzbot homeassistant/components/bmw_connected_drive/* @gerard33 homeassistant/components/braviatv/* @robbiet480 -homeassistant/components/broadlink/* @danielhiversen +homeassistant/components/broadlink/* @danielhiversen @felipediel homeassistant/components/brunt/* @eavanvalkenburg homeassistant/components/bt_smarthub/* @jxwolstenholme homeassistant/components/buienradar/* @mjj4791 @ties @@ -79,6 +79,7 @@ homeassistant/components/device_automation/* @home-assistant/core homeassistant/components/digital_ocean/* @fabaff homeassistant/components/discogs/* @thibmaek homeassistant/components/doorbird/* @oblogic7 +homeassistant/components/dsmr_reader/* @depl0y homeassistant/components/dweet/* @fabaff homeassistant/components/ecobee/* @marthoc homeassistant/components/ecovacs/* @OverloadUT @@ -86,8 +87,10 @@ homeassistant/components/egardia/* @jeroenterheerdt homeassistant/components/eight_sleep/* @mezz64 homeassistant/components/elv/* @majuss homeassistant/components/emby/* @mezz64 +homeassistant/components/emulated_hue/* @NobleKangaroo homeassistant/components/enigma2/* @fbradyirl homeassistant/components/enocean/* @bdurrer +homeassistant/components/entur_public_transport/* @hfurubotten homeassistant/components/environment_canada/* @michaeldavie homeassistant/components/ephember/* @ttroy50 homeassistant/components/epsonworkforce/* @ThaStealth @@ -95,11 +98,13 @@ homeassistant/components/eq3btsmart/* @rytilahti homeassistant/components/esphome/* @OttoWinter homeassistant/components/essent/* @TheLastProject homeassistant/components/evohome/* @zxdavb +homeassistant/components/fastdotcom/* @rohankapoorcom homeassistant/components/file/* @fabaff homeassistant/components/filter/* @dgomes homeassistant/components/fitbit/* @robbiet480 homeassistant/components/fixer/* @fabaff homeassistant/components/flock/* @fabaff +homeassistant/components/flume/* @ChrisMandich homeassistant/components/flunearyou/* @bachya homeassistant/components/fortigate/* @kifeo homeassistant/components/fortios/* @kimfrellsen @@ -112,6 +117,7 @@ homeassistant/components/gearbest/* @HerrHofrat homeassistant/components/geniushub/* @zxdavb homeassistant/components/geo_rss_events/* @exxamalte homeassistant/components/geonetnz_quakes/* @exxamalte +homeassistant/components/geonetnz_volcano/* @exxamalte homeassistant/components/gitter/* @fabaff homeassistant/components/glances/* @fabaff @engrbm87 homeassistant/components/gntp/* @robbiet480 @@ -125,10 +131,12 @@ homeassistant/components/growatt_server/* @indykoning homeassistant/components/gtfs/* @robbiet480 homeassistant/components/harmony/* @ehendrix23 homeassistant/components/hassio/* @home-assistant/hass-io +homeassistant/components/heatmiser/* @andylockran homeassistant/components/heos/* @andrewsayre homeassistant/components/here_travel_time/* @eifinger homeassistant/components/hikvision/* @mezz64 homeassistant/components/hikvisioncam/* @fbradyirl +homeassistant/components/hisense_aehw4a1/* @bannhead homeassistant/components/history/* @home-assistant/core homeassistant/components/history_graph/* @andrey-git homeassistant/components/hive/* @Rendili @KJonline @@ -152,7 +160,9 @@ homeassistant/components/input_number/* @home-assistant/core homeassistant/components/input_select/* @home-assistant/core homeassistant/components/input_text/* @home-assistant/core homeassistant/components/integration/* @dgomes +homeassistant/components/intent/* @home-assistant/core homeassistant/components/ios/* @robbiet480 +homeassistant/components/iperf3/* @rohankapoorcom homeassistant/components/ipma/* @dgomes homeassistant/components/iqvia/* @bachya homeassistant/components/irish_rail_transport/* @ttroy50 @@ -178,6 +188,7 @@ homeassistant/components/logi_circle/* @evanjd homeassistant/components/lovelace/* @home-assistant/frontend homeassistant/components/luci/* @fbradyirl @mzdrale homeassistant/components/luftdaten/* @fabaff +homeassistant/components/lupusec/* @majuss homeassistant/components/lutron/* @JonGilmore homeassistant/components/mastodon/* @fabaff homeassistant/components/matrix/* @tinloaf @@ -192,6 +203,7 @@ homeassistant/components/mill/* @danielhiversen homeassistant/components/min_max/* @fabaff homeassistant/components/minio/* @tkislan homeassistant/components/mobile_app/* @robbiet480 +homeassistant/components/modbus/* @adamchengtkc homeassistant/components/monoprice/* @etsinko homeassistant/components/moon/* @fabaff homeassistant/components/mpd/* @fabaff @@ -205,6 +217,7 @@ homeassistant/components/ness_alarm/* @nickw444 homeassistant/components/nest/* @awarecan homeassistant/components/netdata/* @fabaff homeassistant/components/nextbus/* @vividboarder +homeassistant/components/nilu/* @hfurubotten homeassistant/components/nissan_leaf/* @filcole homeassistant/components/nmbs/* @thibmaek homeassistant/components/no_ip/* @fabaff @@ -236,6 +249,7 @@ homeassistant/components/plant/* @ChristianKuehnel homeassistant/components/plex/* @jjlawren homeassistant/components/plugwise/* @laetificat @CoMPaTech @bouwew homeassistant/components/point/* @fredrike +homeassistant/components/proxmoxve/* @k4ds3 homeassistant/components/ps4/* @ktnrg45 homeassistant/components/ptvsd/* @swamp-ig homeassistant/components/push/* @dgomes @@ -280,8 +294,10 @@ homeassistant/components/soma/* @ratsept homeassistant/components/somfy/* @tetienne homeassistant/components/songpal/* @rytilahti homeassistant/components/spaceapi/* @fabaff +homeassistant/components/speedtestdotnet/* @rohankapoorcom homeassistant/components/spider/* @peternijssen homeassistant/components/sql/* @dgomes +homeassistant/components/starline/* @anonym-tsk homeassistant/components/statistics/* @fabaff homeassistant/components/stiebel_eltron/* @fucm homeassistant/components/stream/* @hunterjm @@ -297,6 +313,7 @@ homeassistant/components/switchmate/* @danielhiversen homeassistant/components/syncthru/* @nielstron homeassistant/components/synology_srm/* @aerialls homeassistant/components/syslog/* @fabaff +homeassistant/components/tado/* @michaelarnauts homeassistant/components/tahoma/* @philklei homeassistant/components/tautulli/* @ludeeus homeassistant/components/tellduslive/* @fredrike @@ -330,6 +347,7 @@ homeassistant/components/usgs_earthquakes_feed/* @exxamalte homeassistant/components/utility_meter/* @dgomes homeassistant/components/velbus/* @cereal2nd homeassistant/components/velux/* @Julius2342 +homeassistant/components/versasense/* @flamm3blemuff1n homeassistant/components/version/* @fabaff homeassistant/components/vesync/* @markperdue @webdjoe homeassistant/components/vicare/* @oischinger diff --git a/azure-pipelines-ci.yml b/azure-pipelines-ci.yml index 37473b926201..cbad0c9af08a 100644 --- a/azure-pipelines-ci.yml +++ b/azure-pipelines-ci.yml @@ -50,6 +50,10 @@ stages: . venv/bin/activate pre-commit run flake8 --all-files displayName: 'Run flake8' + - script: | + . venv/bin/activate + pre-commit run bandit --all-files + displayName: 'Run bandit' - job: 'Validate' pool: vmImage: 'ubuntu-latest' @@ -158,7 +162,7 @@ stages: python -m venv venv . venv/bin/activate - pip install -U pip setuptools + pip install -U pip setuptools wheel pip install -r requirements_all.txt -c homeassistant/package_constraints.txt pip install -r requirements_test.txt -c homeassistant/package_constraints.txt - script: | diff --git a/homeassistant/__main__.py b/homeassistant/__main__.py index b416b1f98d3d..40336c195924 100644 --- a/homeassistant/__main__.py +++ b/homeassistant/__main__.py @@ -15,12 +15,10 @@ def set_loop() -> None: - """Attempt to use uvloop.""" + """Attempt to use different loop.""" import asyncio from asyncio.events import BaseDefaultEventLoopPolicy - policy = None - if sys.platform == "win32": if hasattr(asyncio, "WindowsProactorEventLoopPolicy"): # pylint: disable=no-member @@ -33,15 +31,7 @@ class ProactorPolicy(BaseDefaultEventLoopPolicy): _loop_factory = asyncio.ProactorEventLoop policy = ProactorPolicy() - else: - try: - import uvloop - except ImportError: - pass - else: - policy = uvloop.EventLoopPolicy() - if policy is not None: asyncio.set_event_loop_policy(policy) @@ -272,7 +262,6 @@ def cmdline() -> List[str]: async def setup_and_run_hass(config_dir: str, args: argparse.Namespace) -> int: """Set up HASS and run.""" - # pylint: disable=redefined-outer-name from homeassistant import bootstrap, core hass = core.HomeAssistant() diff --git a/homeassistant/auth/mfa_modules/__init__.py b/homeassistant/auth/mfa_modules/__init__.py index 9d49f67df82d..9020b0b321ec 100644 --- a/homeassistant/auth/mfa_modules/__init__.py +++ b/homeassistant/auth/mfa_modules/__init__.py @@ -42,7 +42,7 @@ def __init__(self, hass: HomeAssistant, config: Dict[str, Any]) -> None: self.config = config @property - def id(self) -> str: # pylint: disable=invalid-name + def id(self) -> str: """Return id of the auth module. Default is same as type diff --git a/homeassistant/auth/permissions/__init__.py b/homeassistant/auth/permissions/__init__.py index 25253a1601cd..36cf9c4f420c 100644 --- a/homeassistant/auth/permissions/__init__.py +++ b/homeassistant/auth/permissions/__init__.py @@ -1,18 +1,6 @@ """Permissions for Home Assistant.""" import logging -from typing import ( # noqa: F401 - cast, - Any, - Callable, - Dict, - List, - Mapping, - Optional, - Set, - Tuple, - Union, - TYPE_CHECKING, -) +from typing import Any, Callable, Optional import voluptuous as vol @@ -20,7 +8,7 @@ from .models import PermissionLookup from .types import PolicyType from .entities import ENTITY_POLICY_SCHEMA, compile_entities -from .merge import merge_policies # noqa +from .merge import merge_policies # noqa: F401 from .util import test_all @@ -70,15 +58,12 @@ def _entity_func(self) -> Callable[[str, str], bool]: def __eq__(self, other: Any) -> bool: """Equals check.""" - # pylint: disable=protected-access return isinstance(other, PolicyPermissions) and other._policy == self._policy class _OwnerPermissions(AbstractPermissions): """Owner permissions.""" - # pylint: disable=no-self-use - def access_all_entities(self, key: str) -> bool: """Check if we have a certain access to all entities.""" return True diff --git a/homeassistant/auth/permissions/models.py b/homeassistant/auth/permissions/models.py index 31bea635bbe3..1224ea07b23a 100644 --- a/homeassistant/auth/permissions/models.py +++ b/homeassistant/auth/permissions/models.py @@ -5,8 +5,8 @@ if TYPE_CHECKING: # pylint: disable=unused-import - from homeassistant.helpers import entity_registry as ent_reg # noqa - from homeassistant.helpers import device_registry as dev_reg # noqa + from homeassistant.helpers import entity_registry as ent_reg # noqa: F401 + from homeassistant.helpers import device_registry as dev_reg # noqa: F401 @attr.s(slots=True) diff --git a/homeassistant/auth/permissions/util.py b/homeassistant/auth/permissions/util.py index 109a5dc04ae2..4d38e0a639c0 100644 --- a/homeassistant/auth/permissions/util.py +++ b/homeassistant/auth/permissions/util.py @@ -21,8 +21,9 @@ def lookup_all( def compile_policy( policy: CategoryType, subcategories: SubCatLookupType, perm_lookup: PermissionLookup -) -> Callable[[str, str], bool]: # noqa +) -> Callable[[str, str], bool]: """Compile policy into a function that tests policy. + Subcategories are mapping key -> lookup function, ordered by highest priority first. """ @@ -80,7 +81,7 @@ def apply_policy_funcs(object_id: str, key: str) -> bool: def _gen_dict_test_func( perm_lookup: PermissionLookup, lookup_func: LookupFunc, lookup_dict: SubCategoryDict -) -> Callable[[str, str], Optional[bool]]: # noqa +) -> Callable[[str, str], Optional[bool]]: """Generate a lookup function.""" def test_value(object_id: str, key: str) -> Optional[bool]: diff --git a/homeassistant/auth/providers/__init__.py b/homeassistant/auth/providers/__init__.py index 3e25003ad00e..cbce31529022 100644 --- a/homeassistant/auth/providers/__init__.py +++ b/homeassistant/auth/providers/__init__.py @@ -48,7 +48,7 @@ def __init__( self.config = config @property - def id(self) -> Optional[str]: # pylint: disable=invalid-name + def id(self) -> Optional[str]: """Return id of the auth provider. Optional, can be None. diff --git a/homeassistant/components/abode/.translations/pt.json b/homeassistant/components/abode/.translations/pt.json index 512bf59906c9..4a371c706f77 100644 --- a/homeassistant/components/abode/.translations/pt.json +++ b/homeassistant/components/abode/.translations/pt.json @@ -6,6 +6,7 @@ "step": { "user": { "data": { + "password": "Palavra-passe", "username": "Endere\u00e7o de e-mail" } } diff --git a/homeassistant/components/abode/.translations/ru.json b/homeassistant/components/abode/.translations/ru.json index f39e6b1443b0..590f76627313 100644 --- a/homeassistant/components/abode/.translations/ru.json +++ b/homeassistant/components/abode/.translations/ru.json @@ -5,8 +5,8 @@ }, "error": { "connection_error": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a Abode.", - "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." + "identifier_exists": "\u0423\u0447\u0451\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\u0451\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435." }, "step": { "user": { diff --git a/homeassistant/components/abode/alarm_control_panel.py b/homeassistant/components/abode/alarm_control_panel.py index f1ff08f3a0ac..88a072bd79cd 100644 --- a/homeassistant/components/abode/alarm_control_panel.py +++ b/homeassistant/components/abode/alarm_control_panel.py @@ -2,6 +2,10 @@ import logging import homeassistant.components.alarm_control_panel as alarm +from homeassistant.components.alarm_control_panel.const import ( + SUPPORT_ALARM_ARM_AWAY, + SUPPORT_ALARM_ARM_HOME, +) from homeassistant.const import ( ATTR_ATTRIBUTION, STATE_ALARM_ARMED_AWAY, @@ -51,6 +55,11 @@ def state(self): state = None return state + @property + def supported_features(self) -> int: + """Return the list of supported features.""" + return SUPPORT_ALARM_ARM_HOME | SUPPORT_ALARM_ARM_AWAY + def alarm_disarm(self, code=None): """Send disarm command.""" self._device.set_standby() diff --git a/homeassistant/components/abode/config_flow.py b/homeassistant/components/abode/config_flow.py index bf48e4546b30..b8e8548db318 100644 --- a/homeassistant/components/abode/config_flow.py +++ b/homeassistant/components/abode/config_flow.py @@ -10,7 +10,7 @@ from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import callback -from .const import DOMAIN, DEFAULT_CACHEDB # pylint: disable=W0611 +from .const import DOMAIN, DEFAULT_CACHEDB # pylint: disable=unused-import CONF_POLLING = "polling" diff --git a/homeassistant/components/adguard/.translations/pt.json b/homeassistant/components/adguard/.translations/pt.json index f681da4210f8..77ce7025f70c 100644 --- a/homeassistant/components/adguard/.translations/pt.json +++ b/homeassistant/components/adguard/.translations/pt.json @@ -4,7 +4,9 @@ "user": { "data": { "host": "Servidor", - "port": "Porta" + "password": "Palavra-passe", + "port": "Porta", + "username": "Nome de Utilizador" } } } diff --git a/homeassistant/components/aftership/sensor.py b/homeassistant/components/aftership/sensor.py index c41e5aec7b51..ff5edc92ba07 100644 --- a/homeassistant/components/aftership/sensor.py +++ b/homeassistant/components/aftership/sensor.py @@ -2,6 +2,7 @@ from datetime import timedelta import logging +from pyaftership.tracker import Tracking import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -11,6 +12,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle + from .const import DOMAIN _LOGGER = logging.getLogger(__name__) @@ -56,8 +58,6 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the AfterShip sensor platform.""" - from pyaftership.tracker import Tracking - apikey = config[CONF_API_KEY] name = config[CONF_NAME] diff --git a/homeassistant/components/air_quality/__init__.py b/homeassistant/components/air_quality/__init__.py index 3d8bb92572a9..00308c40b362 100644 --- a/homeassistant/components/air_quality/__init__.py +++ b/homeassistant/components/air_quality/__init__.py @@ -3,7 +3,7 @@ import logging from homeassistant.helpers.entity_component import EntityComponent -from homeassistant.helpers.config_validation import ( # noqa +from homeassistant.helpers.config_validation import ( # noqa: F401 PLATFORM_SCHEMA, PLATFORM_SCHEMA_BASE, ) diff --git a/homeassistant/components/alarm_control_panel/.translations/bg.json b/homeassistant/components/alarm_control_panel/.translations/bg.json index 29700793770a..a9342c8c4772 100644 --- a/homeassistant/components/alarm_control_panel/.translations/bg.json +++ b/homeassistant/components/alarm_control_panel/.translations/bg.json @@ -6,6 +6,13 @@ "arm_night": "\u0421\u043b\u043e\u0436\u0438 {entity_name} \u043f\u043e\u0434 \u043e\u0445\u0440\u0430\u043d\u0430 \u0432 \u043d\u043e\u0449\u0435\u043d \u0440\u0435\u0436\u0438\u043c", "disarm": "\u0414\u0435\u0430\u043a\u0442\u0438\u0432\u0438\u0440\u0430\u0439 {entity_name}", "trigger": "\u0417\u0430\u0434\u0435\u0439\u0441\u0442\u0432\u0430\u043d\u0435 {entity_name}" + }, + "trigger_type": { + "armed_away": "{entity_name} \u043f\u043e\u0434 \u043e\u0445\u0440\u0430\u043d\u0430", + "armed_home": "{entity_name} \u043f\u043e\u0434 \u043e\u0445\u0440\u0430\u043d\u0430 - \u0432\u043a\u044a\u0449\u0438", + "armed_night": "{entity_name} \u043f\u043e\u0434 \u043e\u0445\u0440\u0430\u043d\u0430 - \u043d\u043e\u0449", + "disarmed": "{entity_name} \u0434\u0435\u0430\u043a\u0442\u0438\u0432\u0438\u0440\u0430\u043d\u0430", + "triggered": "{entity_name} \u0437\u0430\u0434\u0435\u0439\u0441\u0442\u0432\u0430\u043d\u0430" } } } \ No newline at end of file diff --git a/homeassistant/components/alarm_control_panel/.translations/ca.json b/homeassistant/components/alarm_control_panel/.translations/ca.json index 8d95d5f64859..d60cf3173c7f 100644 --- a/homeassistant/components/alarm_control_panel/.translations/ca.json +++ b/homeassistant/components/alarm_control_panel/.translations/ca.json @@ -6,6 +6,13 @@ "arm_night": "Activa {entity_name} nocturn", "disarm": "Desactiva {entity_name}", "trigger": "Dispara {entity_name}" + }, + "trigger_type": { + "armed_away": "{entity_name} activada en mode a fora", + "armed_home": "{entity_name} activada en mode a casa", + "armed_night": "{entity_name} activada en mode nocturn", + "disarmed": "{entity_name} desactivada", + "triggered": "{entity_name} disparat/ada" } } } \ No newline at end of file diff --git a/homeassistant/components/alarm_control_panel/.translations/en.json b/homeassistant/components/alarm_control_panel/.translations/en.json index b8eeb1d2e8c5..a00e81feb921 100644 --- a/homeassistant/components/alarm_control_panel/.translations/en.json +++ b/homeassistant/components/alarm_control_panel/.translations/en.json @@ -6,6 +6,13 @@ "arm_night": "Arm {entity_name} night", "disarm": "Disarm {entity_name}", "trigger": "Trigger {entity_name}" + }, + "trigger_type": { + "armed_away": "{entity_name} armed away", + "armed_home": "{entity_name} armed home", + "armed_night": "{entity_name} armed night", + "disarmed": "{entity_name} disarmed", + "triggered": "{entity_name} triggered" } } } \ No newline at end of file diff --git a/homeassistant/components/alarm_control_panel/.translations/it.json b/homeassistant/components/alarm_control_panel/.translations/it.json index e39967e9dacc..78a3f0b07e5f 100644 --- a/homeassistant/components/alarm_control_panel/.translations/it.json +++ b/homeassistant/components/alarm_control_panel/.translations/it.json @@ -6,6 +6,13 @@ "arm_night": "Armare {entity_name} notte", "disarm": "Disarmare {entity_name}", "trigger": "Attivazione {entity_name}" + }, + "trigger_type": { + "armed_away": "{entity_name} armata modalit\u00e0 fuori casa", + "armed_home": "{entity_name} armata modalit\u00e0 a casa", + "armed_night": "{entity_name} armata modalit\u00e0 notte", + "disarmed": "{entity_name} disarmato", + "triggered": "{entity_name} attivato" } } } \ No newline at end of file diff --git a/homeassistant/components/alarm_control_panel/.translations/lb.json b/homeassistant/components/alarm_control_panel/.translations/lb.json index ff265a52c388..add11f5b8fe6 100644 --- a/homeassistant/components/alarm_control_panel/.translations/lb.json +++ b/homeassistant/components/alarm_control_panel/.translations/lb.json @@ -6,6 +6,13 @@ "arm_night": "{entity_name} fir Nuecht uschalten", "disarm": "{entity_name} entsch\u00e4rfen", "trigger": "{entity_name} ausl\u00e9isen" + }, + "trigger_type": { + "armed_away": "{entity_name} ugeschalt fir Ennerwee", + "armed_home": "{entity_name} ugeschalt fir Doheem", + "armed_night": "{entity_name} ugeschalt fir Nuecht", + "disarmed": "{entity_name} entsch\u00e4rft", + "triggered": "{entity_name} ausgel\u00e9ist" } } } \ No newline at end of file diff --git a/homeassistant/components/alarm_control_panel/.translations/no.json b/homeassistant/components/alarm_control_panel/.translations/no.json index 93833f33d415..108d273a0f07 100644 --- a/homeassistant/components/alarm_control_panel/.translations/no.json +++ b/homeassistant/components/alarm_control_panel/.translations/no.json @@ -6,6 +6,13 @@ "arm_night": "Aktiver {entity_name} natt", "disarm": "Deaktiver {entity_name}", "trigger": "Utl\u00f8ser {entity_name}" + }, + "trigger_type": { + "armed_away": "{entity_name} borte sikkring ", + "armed_home": "{entity_name} hjemme sikkring", + "armed_night": "{entity_name} natt sikkring", + "disarmed": "{entity_name} deaktivert", + "triggered": "{entity_name} utl\u00f8st" } } } \ No newline at end of file diff --git a/homeassistant/components/alarm_control_panel/.translations/pl.json b/homeassistant/components/alarm_control_panel/.translations/pl.json index a5dc326c2673..024a0861c1c0 100644 --- a/homeassistant/components/alarm_control_panel/.translations/pl.json +++ b/homeassistant/components/alarm_control_panel/.translations/pl.json @@ -6,6 +6,13 @@ "arm_night": "uzbr\u00f3j (noc) {entity_name}", "disarm": "rozbr\u00f3j {entity_name}", "trigger": "wyzw\u00f3l {entity_name}" + }, + "trigger_type": { + "armed_away": "{entity_name} zostanie uzbrojony (poza domem)", + "armed_home": "{entity_name} zostanie uzbrojony (w domu)", + "armed_night": "{entity_name} zostanie uzbrojony (noc)", + "disarmed": "{entity_name} zostanie rozbrojony", + "triggered": "{entity_name} zostanie wyzwolony" } } } \ No newline at end of file diff --git a/homeassistant/components/alarm_control_panel/.translations/ru.json b/homeassistant/components/alarm_control_panel/.translations/ru.json index e573ce709183..f9a0e859e111 100644 --- a/homeassistant/components/alarm_control_panel/.translations/ru.json +++ b/homeassistant/components/alarm_control_panel/.translations/ru.json @@ -6,6 +6,13 @@ "arm_night": "\u0412\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u0440\u0435\u0436\u0438\u043c \u043e\u0445\u0440\u0430\u043d\u044b \"\u041d\u043e\u0447\u044c\" \u043d\u0430 \u043f\u0430\u043d\u0435\u043b\u0438 {entity_name}", "disarm": "\u041e\u0442\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u043e\u0445\u0440\u0430\u043d\u0443 \u043d\u0430 \u043f\u0430\u043d\u0435\u043b\u0438 {entity_name}", "trigger": "{entity_name} \u0441\u0440\u0430\u0431\u0430\u0442\u044b\u0432\u0430\u0435\u0442" + }, + "trigger_type": { + "armed_away": "\u0412\u043a\u043b\u044e\u0447\u0435\u043d \u0440\u0435\u0436\u0438\u043c \u043e\u0445\u0440\u0430\u043d\u044b \"\u041d\u0435 \u0434\u043e\u043c\u0430\" \u043d\u0430 \u043f\u0430\u043d\u0435\u043b\u0438 {entity_name}", + "armed_home": "\u0412\u043a\u043b\u044e\u0447\u0435\u043d \u0440\u0435\u0436\u0438\u043c \u043e\u0445\u0440\u0430\u043d\u044b \"\u0414\u043e\u043c\u0430\" \u043d\u0430 \u043f\u0430\u043d\u0435\u043b\u0438 {entity_name}", + "armed_night": "\u0412\u043a\u043b\u044e\u0447\u0435\u043d \u0440\u0435\u0436\u0438\u043c \u043e\u0445\u0440\u0430\u043d\u044b \"\u041d\u043e\u0447\u044c\" \u043d\u0430 \u043f\u0430\u043d\u0435\u043b\u0438 {entity_name}", + "disarmed": "\u041e\u0442\u043a\u043b\u044e\u0447\u0435\u043d\u0430 \u043e\u0445\u0440\u0430\u043d\u0430 \u043d\u0430 \u043f\u0430\u043d\u0435\u043b\u0438 {entity_name}", + "triggered": "{entity_name} \u0441\u0440\u0430\u0431\u0430\u0442\u044b\u0432\u0430\u0435\u0442" } } } \ No newline at end of file diff --git a/homeassistant/components/alarm_control_panel/.translations/sl.json b/homeassistant/components/alarm_control_panel/.translations/sl.json index 9bf01fc62de9..855c50ab8273 100644 --- a/homeassistant/components/alarm_control_panel/.translations/sl.json +++ b/homeassistant/components/alarm_control_panel/.translations/sl.json @@ -6,6 +6,13 @@ "arm_night": "Vklju\u010di {entity_name} no\u010d", "disarm": "Razoro\u017ei {entity_name}", "trigger": "Spro\u017ei {entity_name}" + }, + "trigger_type": { + "armed_away": "{entity_name} oboro\u017een - zdoma", + "armed_home": "{entity_name} oboro\u017een - dom", + "armed_night": "{entity_name} oboro\u017een - no\u010d", + "disarmed": "{entity_name} razoro\u017een", + "triggered": "{entity_name} spro\u017een" } } } \ No newline at end of file diff --git a/homeassistant/components/alarm_control_panel/.translations/zh-Hant.json b/homeassistant/components/alarm_control_panel/.translations/zh-Hant.json index c52288802d11..72c0b65436dd 100644 --- a/homeassistant/components/alarm_control_panel/.translations/zh-Hant.json +++ b/homeassistant/components/alarm_control_panel/.translations/zh-Hant.json @@ -6,6 +6,13 @@ "arm_night": "\u8a2d\u5b9a {entity_name} \u591c\u9593\u6a21\u5f0f", "disarm": "\u89e3\u9664 {entity_name}", "trigger": "\u89f8\u767c {entity_name}" + }, + "trigger_type": { + "armed_away": "{entity_name} \u8a2d\u5b9a\u5916\u51fa", + "armed_home": "{entity_name} \u8a2d\u5b9a\u5728\u5bb6", + "armed_night": "{entity_name} \u8a2d\u5b9a\u591c\u9593", + "disarmed": "{entity_name} \u5df2\u89e3\u9664", + "triggered": "{entity_name} \u5df2\u89f8\u767c" } } } \ No newline at end of file diff --git a/homeassistant/components/alarm_control_panel/__init__.py b/homeassistant/components/alarm_control_panel/__init__.py index ab0b810ee83e..dfac0fd192f9 100644 --- a/homeassistant/components/alarm_control_panel/__init__.py +++ b/homeassistant/components/alarm_control_panel/__init__.py @@ -1,4 +1,5 @@ """Component to interface with an alarm control panel.""" +from abc import abstractmethod from datetime import timedelta import logging @@ -7,22 +8,30 @@ from homeassistant.const import ( ATTR_CODE, ATTR_CODE_FORMAT, - SERVICE_ALARM_TRIGGER, - SERVICE_ALARM_DISARM, - SERVICE_ALARM_ARM_HOME, SERVICE_ALARM_ARM_AWAY, - SERVICE_ALARM_ARM_NIGHT, SERVICE_ALARM_ARM_CUSTOM_BYPASS, + SERVICE_ALARM_ARM_HOME, + SERVICE_ALARM_ARM_NIGHT, + SERVICE_ALARM_DISARM, + SERVICE_ALARM_TRIGGER, ) -from homeassistant.helpers.config_validation import ( # noqa - ENTITY_SERVICE_SCHEMA, +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.config_validation import ( # noqa: F401 + make_entity_service_schema, PLATFORM_SCHEMA, PLATFORM_SCHEMA_BASE, ) -import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent +from .const import ( + SUPPORT_ALARM_ARM_AWAY, + SUPPORT_ALARM_ARM_CUSTOM_BYPASS, + SUPPORT_ALARM_ARM_HOME, + SUPPORT_ALARM_ARM_NIGHT, + SUPPORT_ALARM_TRIGGER, +) + DOMAIN = "alarm_control_panel" SCAN_INTERVAL = timedelta(seconds=30) ATTR_CHANGED_BY = "changed_by" @@ -32,9 +41,7 @@ ENTITY_ID_FORMAT = DOMAIN + ".{}" -ALARM_SERVICE_SCHEMA = ENTITY_SERVICE_SCHEMA.extend( - {vol.Optional(ATTR_CODE): cv.string} -) +ALARM_SERVICE_SCHEMA = make_entity_service_schema({vol.Optional(ATTR_CODE): cv.string}) async def async_setup(hass, config): @@ -49,21 +56,34 @@ async def async_setup(hass, config): SERVICE_ALARM_DISARM, ALARM_SERVICE_SCHEMA, "async_alarm_disarm" ) component.async_register_entity_service( - SERVICE_ALARM_ARM_HOME, ALARM_SERVICE_SCHEMA, "async_alarm_arm_home" + SERVICE_ALARM_ARM_HOME, + ALARM_SERVICE_SCHEMA, + "async_alarm_arm_home", + [SUPPORT_ALARM_ARM_HOME], ) component.async_register_entity_service( - SERVICE_ALARM_ARM_AWAY, ALARM_SERVICE_SCHEMA, "async_alarm_arm_away" + SERVICE_ALARM_ARM_AWAY, + ALARM_SERVICE_SCHEMA, + "async_alarm_arm_away", + [SUPPORT_ALARM_ARM_AWAY], ) component.async_register_entity_service( - SERVICE_ALARM_ARM_NIGHT, ALARM_SERVICE_SCHEMA, "async_alarm_arm_night" + SERVICE_ALARM_ARM_NIGHT, + ALARM_SERVICE_SCHEMA, + "async_alarm_arm_night", + [SUPPORT_ALARM_ARM_NIGHT], ) component.async_register_entity_service( SERVICE_ALARM_ARM_CUSTOM_BYPASS, ALARM_SERVICE_SCHEMA, "async_alarm_arm_custom_bypass", + [SUPPORT_ALARM_ARM_CUSTOM_BYPASS], ) component.async_register_entity_service( - SERVICE_ALARM_TRIGGER, ALARM_SERVICE_SCHEMA, "async_alarm_trigger" + SERVICE_ALARM_TRIGGER, + ALARM_SERVICE_SCHEMA, + "async_alarm_trigger", + [SUPPORT_ALARM_TRIGGER], ) return True @@ -79,7 +99,6 @@ async def async_unload_entry(hass, entry): return await hass.data[DOMAIN].async_unload_entry(entry) -# pylint: disable=no-self-use class AlarmControlPanel(Entity): """An abstract class for alarm control devices.""" @@ -164,6 +183,11 @@ def async_alarm_arm_custom_bypass(self, code=None): """ return self.hass.async_add_executor_job(self.alarm_arm_custom_bypass, code) + @property + @abstractmethod + def supported_features(self) -> int: + """Return the list of supported features.""" + @property def state_attributes(self): """Return the state attributes.""" diff --git a/homeassistant/components/alarm_control_panel/const.py b/homeassistant/components/alarm_control_panel/const.py new file mode 100644 index 000000000000..77f7846fc347 --- /dev/null +++ b/homeassistant/components/alarm_control_panel/const.py @@ -0,0 +1,7 @@ +"""Provides the constants needed for component.""" + +SUPPORT_ALARM_ARM_HOME = 1 +SUPPORT_ALARM_ARM_AWAY = 2 +SUPPORT_ALARM_ARM_NIGHT = 4 +SUPPORT_ALARM_TRIGGER = 8 +SUPPORT_ALARM_ARM_CUSTOM_BYPASS = 16 diff --git a/homeassistant/components/alarm_control_panel/device_action.py b/homeassistant/components/alarm_control_panel/device_action.py index a3c2b4822611..81e444ae16f0 100644 --- a/homeassistant/components/alarm_control_panel/device_action.py +++ b/homeassistant/components/alarm_control_panel/device_action.py @@ -1,5 +1,6 @@ """Provides device automations for Alarm control panel.""" -from typing import Optional, List +from typing import List, Optional + import voluptuous as vol from homeassistant.const import ( @@ -16,10 +17,17 @@ SERVICE_ALARM_DISARM, SERVICE_ALARM_TRIGGER, ) -from homeassistant.core import HomeAssistant, Context +from homeassistant.core import Context, HomeAssistant from homeassistant.helpers import entity_registry import homeassistant.helpers.config_validation as cv + from . import ATTR_CODE_ARM_REQUIRED, DOMAIN +from .const import ( + SUPPORT_ALARM_ARM_AWAY, + SUPPORT_ALARM_ARM_HOME, + SUPPORT_ALARM_ARM_NIGHT, + SUPPORT_ALARM_TRIGGER, +) ACTION_TYPES = {"arm_away", "arm_home", "arm_night", "disarm", "trigger"} @@ -42,31 +50,42 @@ async def async_get_actions(hass: HomeAssistant, device_id: str) -> List[dict]: if entry.domain != DOMAIN: continue + state = hass.states.get(entry.entity_id) + + # We need a state or else we can't populate the HVAC and preset modes. + if state is None: + continue + + supported_features = state.attributes["supported_features"] + # Add actions for each entity that belongs to this integration - actions.append( - { - CONF_DEVICE_ID: device_id, - CONF_DOMAIN: DOMAIN, - CONF_ENTITY_ID: entry.entity_id, - CONF_TYPE: "arm_away", - } - ) - actions.append( - { - CONF_DEVICE_ID: device_id, - CONF_DOMAIN: DOMAIN, - CONF_ENTITY_ID: entry.entity_id, - CONF_TYPE: "arm_home", - } - ) - actions.append( - { - CONF_DEVICE_ID: device_id, - CONF_DOMAIN: DOMAIN, - CONF_ENTITY_ID: entry.entity_id, - CONF_TYPE: "arm_night", - } - ) + if supported_features & SUPPORT_ALARM_ARM_AWAY: + actions.append( + { + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + CONF_TYPE: "arm_away", + } + ) + if supported_features & SUPPORT_ALARM_ARM_HOME: + actions.append( + { + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + CONF_TYPE: "arm_home", + } + ) + if supported_features & SUPPORT_ALARM_ARM_NIGHT: + actions.append( + { + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + CONF_TYPE: "arm_night", + } + ) actions.append( { CONF_DEVICE_ID: device_id, @@ -75,14 +94,15 @@ async def async_get_actions(hass: HomeAssistant, device_id: str) -> List[dict]: CONF_TYPE: "disarm", } ) - actions.append( - { - CONF_DEVICE_ID: device_id, - CONF_DOMAIN: DOMAIN, - CONF_ENTITY_ID: entry.entity_id, - CONF_TYPE: "trigger", - } - ) + if supported_features & SUPPORT_ALARM_TRIGGER: + actions.append( + { + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + CONF_TYPE: "trigger", + } + ) return actions diff --git a/homeassistant/components/alarm_control_panel/device_trigger.py b/homeassistant/components/alarm_control_panel/device_trigger.py new file mode 100644 index 000000000000..95ae17aaaf56 --- /dev/null +++ b/homeassistant/components/alarm_control_panel/device_trigger.py @@ -0,0 +1,151 @@ +"""Provides device automations for Alarm control panel.""" +from typing import List + +import voluptuous as vol + +from homeassistant.components.alarm_control_panel.const import ( + SUPPORT_ALARM_ARM_AWAY, + SUPPORT_ALARM_ARM_HOME, + SUPPORT_ALARM_ARM_NIGHT, +) +from homeassistant.components.automation import AutomationActionType, state +from homeassistant.components.device_automation import TRIGGER_BASE_SCHEMA +from homeassistant.const import ( + CONF_DEVICE_ID, + CONF_DOMAIN, + CONF_ENTITY_ID, + CONF_PLATFORM, + CONF_TYPE, + STATE_ALARM_ARMED_AWAY, + STATE_ALARM_ARMED_HOME, + STATE_ALARM_ARMED_NIGHT, + STATE_ALARM_DISARMED, + STATE_ALARM_PENDING, + STATE_ALARM_TRIGGERED, +) +from homeassistant.core import CALLBACK_TYPE, HomeAssistant +from homeassistant.helpers import config_validation as cv, entity_registry +from homeassistant.helpers.typing import ConfigType + +from . import DOMAIN + +TRIGGER_TYPES = { + "triggered", + "disarmed", + "armed_home", + "armed_away", + "armed_night", +} + +TRIGGER_SCHEMA = TRIGGER_BASE_SCHEMA.extend( + { + vol.Required(CONF_ENTITY_ID): cv.entity_id, + vol.Required(CONF_TYPE): vol.In(TRIGGER_TYPES), + } +) + + +async def async_get_triggers(hass: HomeAssistant, device_id: str) -> List[dict]: + """List device triggers for Alarm control panel devices.""" + registry = await entity_registry.async_get_registry(hass) + 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 + + entity_state = hass.states.get(entry.entity_id) + + # We need a state or else we can't populate the HVAC and preset modes. + if entity_state is None: + continue + + supported_features = entity_state.attributes["supported_features"] + + # Add triggers for each entity that belongs to this integration + triggers += [ + { + CONF_PLATFORM: "device", + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + CONF_TYPE: "disarmed", + }, + { + CONF_PLATFORM: "device", + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + CONF_TYPE: "triggered", + }, + ] + if supported_features & SUPPORT_ALARM_ARM_HOME: + triggers.append( + { + CONF_PLATFORM: "device", + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + CONF_TYPE: "armed_home", + } + ) + if supported_features & SUPPORT_ALARM_ARM_AWAY: + triggers.append( + { + CONF_PLATFORM: "device", + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + CONF_TYPE: "armed_away", + } + ) + if supported_features & SUPPORT_ALARM_ARM_NIGHT: + triggers.append( + { + CONF_PLATFORM: "device", + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + CONF_TYPE: "armed_night", + } + ) + + 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) + + if config[CONF_TYPE] == "triggered": + from_state = STATE_ALARM_PENDING + to_state = STATE_ALARM_TRIGGERED + elif config[CONF_TYPE] == "disarmed": + from_state = STATE_ALARM_TRIGGERED + to_state = STATE_ALARM_DISARMED + elif config[CONF_TYPE] == "armed_home": + from_state = STATE_ALARM_PENDING + to_state = STATE_ALARM_ARMED_HOME + elif config[CONF_TYPE] == "armed_away": + from_state = STATE_ALARM_PENDING + to_state = STATE_ALARM_ARMED_AWAY + elif config[CONF_TYPE] == "armed_night": + from_state = STATE_ALARM_PENDING + to_state = STATE_ALARM_ARMED_NIGHT + + state_config = { + state.CONF_PLATFORM: "state", + CONF_ENTITY_ID: config[CONF_ENTITY_ID], + state.CONF_FROM: from_state, + state.CONF_TO: to_state, + } + state_config = state.TRIGGER_SCHEMA(state_config) + return await state.async_attach_trigger( + hass, state_config, action, automation_info, platform_type="device" + ) diff --git a/homeassistant/components/alarm_control_panel/manifest.json b/homeassistant/components/alarm_control_panel/manifest.json index 04ef58769dad..e877fe90a17c 100644 --- a/homeassistant/components/alarm_control_panel/manifest.json +++ b/homeassistant/components/alarm_control_panel/manifest.json @@ -4,7 +4,5 @@ "documentation": "https://www.home-assistant.io/integrations/alarm_control_panel", "requirements": [], "dependencies": [], - "codeowners": [ - "@colinodell" - ] + "codeowners": [] } diff --git a/homeassistant/components/alarm_control_panel/services.yaml b/homeassistant/components/alarm_control_panel/services.yaml index 9abf2189ed3c..b31cb718b3f8 100644 --- a/homeassistant/components/alarm_control_panel/services.yaml +++ b/homeassistant/components/alarm_control_panel/services.yaml @@ -59,85 +59,3 @@ alarm_trigger: code: description: An optional code to trigger the alarm control panel with. example: 1234 - -envisalink_alarm_keypress: - description: Send custom keypresses to the alarm. - fields: - entity_id: - description: Name of the alarm control panel to trigger. - example: 'alarm_control_panel.downstairs' - keypress: - description: 'String to send to the alarm panel (1-6 characters).' - example: '*71' - -alarmdecoder_alarm_toggle_chime: - description: Send the alarm the toggle chime command. - fields: - entity_id: - description: Name of the alarm control panel to trigger. - example: 'alarm_control_panel.downstairs' - code: - description: A required code to toggle the alarm control panel chime with. - example: 1234 - -ifttt_push_alarm_state: - description: Update the alarm state to the specified value. - fields: - entity_id: - description: Name of the alarm control panel which state has to be updated. - example: 'alarm_control_panel.downstairs' - state: - description: The state to which the alarm control panel has to be set. - example: 'armed_night' - -elkm1_alarm_arm_vacation: - description: Arm the ElkM1 in vacation mode. - fields: - entity_id: - description: Name of alarm control panel to arm. - example: 'alarm_control_panel.main' - code: - description: An code to arm the alarm control panel. - example: 1234 - -elkm1_alarm_arm_home_instant: - description: Arm the ElkM1 in home instant mode. - fields: - entity_id: - description: Name of alarm control panel to arm. - example: 'alarm_control_panel.main' - code: - description: An code to arm the alarm control panel. - example: 1234 - -elkm1_alarm_arm_night_instant: - description: Arm the ElkM1 in night instant mode. - fields: - entity_id: - description: Name of alarm control panel to arm. - example: 'alarm_control_panel.main' - code: - description: An code to arm the alarm control panel. - example: 1234 - -elkm1_alarm_display_message: - description: Display a message on all of the ElkM1 keypads for an area. - fields: - entity_id: - description: Name of alarm control panel to display messages on. - example: 'alarm_control_panel.main' - clear: - description: 0=clear message, 1=clear message with * key, 2=Display until timeout; default 2 - example: 1 - beep: - description: 0=no beep, 1=beep; default 0 - example: 1 - timeout: - description: Time to display message, 0=forever, max 65535, default 0 - example: 4242 - line1: - description: Up to 16 characters of text (truncated if too long). Default blank. - example: The answer to life, - line2: - description: Up to 16 characters of text (truncated if too long). Default blank. - example: the universe, and everything. diff --git a/homeassistant/components/alarm_control_panel/strings.json b/homeassistant/components/alarm_control_panel/strings.json index f67635776dd8..cbca15c8cf66 100644 --- a/homeassistant/components/alarm_control_panel/strings.json +++ b/homeassistant/components/alarm_control_panel/strings.json @@ -6,6 +6,13 @@ "arm_night": "Arm {entity_name} night", "disarm": "Disarm {entity_name}", "trigger": "Trigger {entity_name}" + }, + "trigger_type": { + "triggered": "{entity_name} triggered", + "disarmed": "{entity_name} disarmed", + "armed_home": "{entity_name} armed home", + "armed_away": "{entity_name} armed away", + "armed_night": "{entity_name} armed night" } } } \ No newline at end of file diff --git a/homeassistant/components/alarmdecoder/__init__.py b/homeassistant/components/alarmdecoder/__init__.py index 61cb0effe53e..93c0746a812e 100644 --- a/homeassistant/components/alarmdecoder/__init__.py +++ b/homeassistant/components/alarmdecoder/__init__.py @@ -1,14 +1,17 @@ """Support for AlarmDecoder devices.""" +from datetime import timedelta import logging -from datetime import timedelta +from alarmdecoder import AlarmDecoder +from alarmdecoder.devices import SerialDevice, SocketDevice, USBDevice +from alarmdecoder.util import NoDeviceError import voluptuous as vol +from homeassistant.components.binary_sensor import DEVICE_CLASSES_SCHEMA +from homeassistant.const import CONF_HOST, EVENT_HOMEASSISTANT_STOP import homeassistant.helpers.config_validation as cv -from homeassistant.const import EVENT_HOMEASSISTANT_STOP, CONF_HOST from homeassistant.helpers.discovery import load_platform from homeassistant.util import dt as dt_util -from homeassistant.components.binary_sensor import DEVICE_CLASSES_SCHEMA _LOGGER = logging.getLogger(__name__) @@ -109,9 +112,6 @@ def setup(hass, config): """Set up for the AlarmDecoder devices.""" - from alarmdecoder import AlarmDecoder - from alarmdecoder.devices import SocketDevice, SerialDevice, USBDevice - conf = config.get(DOMAIN) restart = False @@ -134,8 +134,6 @@ def stop_alarmdecoder(event): def open_connection(now=None): """Open a connection to AlarmDecoder.""" - from alarmdecoder.util import NoDeviceError - nonlocal restart try: controller.open(baud) diff --git a/homeassistant/components/alarmdecoder/alarm_control_panel.py b/homeassistant/components/alarmdecoder/alarm_control_panel.py index 288c1dfd1c75..d2e9fd136a82 100644 --- a/homeassistant/components/alarmdecoder/alarm_control_panel.py +++ b/homeassistant/components/alarmdecoder/alarm_control_panel.py @@ -3,7 +3,15 @@ import voluptuous as vol -import homeassistant.components.alarm_control_panel as alarm +from homeassistant.components.alarm_control_panel import ( + AlarmControlPanel, + FORMAT_NUMBER, +) +from homeassistant.components.alarm_control_panel.const import ( + SUPPORT_ALARM_ARM_AWAY, + SUPPORT_ALARM_ARM_HOME, + SUPPORT_ALARM_ARM_NIGHT, +) from homeassistant.const import ( ATTR_CODE, STATE_ALARM_ARMED_AWAY, @@ -13,11 +21,11 @@ ) import homeassistant.helpers.config_validation as cv -from . import DATA_AD, DOMAIN as DOMAIN_ALARMDECODER, SIGNAL_PANEL_MESSAGE +from . import DATA_AD, DOMAIN, SIGNAL_PANEL_MESSAGE _LOGGER = logging.getLogger(__name__) -SERVICE_ALARM_TOGGLE_CHIME = "alarmdecoder_alarm_toggle_chime" +SERVICE_ALARM_TOGGLE_CHIME = "alarm_toggle_chime" ALARM_TOGGLE_CHIME_SCHEMA = vol.Schema({vol.Required(ATTR_CODE): cv.string}) SERVICE_ALARM_KEYPRESS = "alarm_keypress" @@ -36,7 +44,7 @@ def alarm_toggle_chime_handler(service): device.alarm_toggle_chime(code) hass.services.register( - alarm.DOMAIN, + DOMAIN, SERVICE_ALARM_TOGGLE_CHIME, alarm_toggle_chime_handler, schema=ALARM_TOGGLE_CHIME_SCHEMA, @@ -48,14 +56,14 @@ def alarm_keypress_handler(service): device.alarm_keypress(keypress) hass.services.register( - DOMAIN_ALARMDECODER, + DOMAIN, SERVICE_ALARM_KEYPRESS, alarm_keypress_handler, schema=ALARM_KEYPRESS_SCHEMA, ) -class AlarmDecoderAlarmPanel(alarm.AlarmControlPanel): +class AlarmDecoderAlarmPanel(AlarmControlPanel): """Representation of an AlarmDecoder-based alarm panel.""" def __init__(self): @@ -115,13 +123,18 @@ def should_poll(self): @property def code_format(self): """Return one or more digits/characters.""" - return alarm.FORMAT_NUMBER + return FORMAT_NUMBER @property def state(self): """Return the state of the device.""" return self._state + @property + def supported_features(self) -> int: + """Return the list of supported features.""" + return SUPPORT_ALARM_ARM_HOME | SUPPORT_ALARM_ARM_AWAY | SUPPORT_ALARM_ARM_NIGHT + @property def device_state_attributes(self): """Return the state attributes.""" diff --git a/homeassistant/components/alarmdecoder/services.yaml b/homeassistant/components/alarmdecoder/services.yaml index 55451d42f137..12268d48bb71 100644 --- a/homeassistant/components/alarmdecoder/services.yaml +++ b/homeassistant/components/alarmdecoder/services.yaml @@ -7,3 +7,13 @@ alarm_keypress: keypress: description: 'String to send to the alarm panel.' example: '*71' + +alarm_toggle_chime: + description: Send the alarm the toggle chime command. + fields: + entity_id: + description: Name of the alarm control panel to trigger. + example: 'alarm_control_panel.downstairs' + code: + description: A required code to toggle the alarm control panel chime with. + example: 1234 diff --git a/homeassistant/components/alarmdotcom/alarm_control_panel.py b/homeassistant/components/alarmdotcom/alarm_control_panel.py index 07d69960e0b7..dd6b1272223c 100644 --- a/homeassistant/components/alarmdotcom/alarm_control_panel.py +++ b/homeassistant/components/alarmdotcom/alarm_control_panel.py @@ -7,6 +7,10 @@ import homeassistant.components.alarm_control_panel as alarm from homeassistant.components.alarm_control_panel import PLATFORM_SCHEMA +from homeassistant.components.alarm_control_panel.const import ( + SUPPORT_ALARM_ARM_AWAY, + SUPPORT_ALARM_ARM_HOME, +) from homeassistant.const import ( CONF_CODE, CONF_NAME, @@ -95,6 +99,11 @@ def state(self): return STATE_ALARM_ARMED_AWAY return None + @property + def supported_features(self) -> int: + """Return the list of supported features.""" + return SUPPORT_ALARM_ARM_HOME | SUPPORT_ALARM_ARM_AWAY + @property def device_state_attributes(self): """Return the state attributes.""" diff --git a/homeassistant/components/alexa/capabilities.py b/homeassistant/components/alexa/capabilities.py index 49b5c5141b68..1f18cb7a5909 100644 --- a/homeassistant/components/alexa/capabilities.py +++ b/homeassistant/components/alexa/capabilities.py @@ -9,9 +9,11 @@ STATE_ALARM_ARMED_CUSTOM_BYPASS, STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT, + STATE_CLOSED, STATE_LOCKED, STATE_OFF, STATE_ON, + STATE_OPEN, STATE_PAUSED, STATE_PLAYING, STATE_UNAVAILABLE, @@ -33,6 +35,7 @@ DATE_FORMAT, PERCENTAGE_FAN_MAP, RANGE_FAN_MAP, + Inputs, ) from .errors import UnsupportedProperty @@ -113,6 +116,11 @@ def configuration(): """Return the Configuration object.""" return [] + @staticmethod + def inputs(): + """Applicable only to media players.""" + return [] + @staticmethod def supported_operations(): """Return the supportedOperations object.""" @@ -162,6 +170,10 @@ def serialize_discovery(self): if supported_operations: result["supportedOperations"] = supported_operations + inputs = self.inputs() + if inputs: + result["inputs"] = inputs + return result def serialize_properties(self): @@ -215,6 +227,20 @@ def serialize_friendly_names(resources): return friendly_names +class Alexa(AlexaCapability): + """Implements Alexa Interface. + + Although endpoints implement this interface implicitly, + The API suggests you should explicitly include this interface. + + https://developer.amazon.com/docs/device-apis/alexa-interface.html + """ + + def name(self): + """Return the Alexa API name of this interface.""" + return "Alexa" + + class AlexaEndpointHealth(AlexaCapability): """Implements Alexa.EndpointHealth. @@ -529,6 +555,23 @@ def name(self): """Return the Alexa API name of this interface.""" return "Alexa.InputController" + def inputs(self): + """Return the list of valid supported inputs.""" + source_list = self.entity.attributes.get( + media_player.ATTR_INPUT_SOURCE_LIST, [] + ) + input_list = [] + for source in source_list: + formatted_source = ( + source.lower().replace("-", "").replace("_", "").replace(" ", "") + ) + if formatted_source in Inputs.VALID_SOURCE_NAME_MAP.keys(): + input_list.append( + {"name": Inputs.VALID_SOURCE_NAME_MAP[formatted_source]} + ) + + return input_list + class AlexaTemperatureSensor(AlexaCapability): """Implements Alexa.TemperatureSensor. @@ -888,6 +931,9 @@ def get_property(self, name): if self.instance == f"{fan.DOMAIN}.{fan.ATTR_DIRECTION}": return self.entity.attributes.get(fan.ATTR_DIRECTION) + if self.instance == f"{cover.DOMAIN}.{cover.ATTR_POSITION}": + return self.entity.attributes.get(cover.ATTR_POSITION) + return None def configuration(self): @@ -903,6 +949,12 @@ def capability_resources(self): {"type": Catalog.LABEL_ASSET, "value": Catalog.SETTING_DIRECTION} ] + if self.instance == f"{cover.DOMAIN}.{cover.ATTR_POSITION}": + capability_resources = [ + {"type": Catalog.LABEL_ASSET, "value": Catalog.SETTING_MODE}, + {"type": Catalog.LABEL_ASSET, "value": Catalog.SETTING_PRESET}, + ] + return capability_resources def mode_resources(self): @@ -927,6 +979,32 @@ def mode_resources(self): ], } + if self.instance == f"{cover.DOMAIN}.{cover.ATTR_POSITION}": + mode_resources = { + "ordered": False, + "resources": [ + { + "value": f"{cover.ATTR_POSITION}.{STATE_OPEN}", + "friendly_names": [ + {"type": Catalog.LABEL_TEXT, "value": "open"}, + {"type": Catalog.LABEL_TEXT, "value": "opened"}, + {"type": Catalog.LABEL_TEXT, "value": "raise"}, + {"type": Catalog.LABEL_TEXT, "value": "raised"}, + ], + }, + { + "value": f"{cover.ATTR_POSITION}.{STATE_CLOSED}", + "friendly_names": [ + {"type": Catalog.LABEL_TEXT, "value": "close"}, + {"type": Catalog.LABEL_TEXT, "value": "closed"}, + {"type": Catalog.LABEL_TEXT, "value": "shut"}, + {"type": Catalog.LABEL_TEXT, "value": "lower"}, + {"type": Catalog.LABEL_TEXT, "value": "lowered"}, + ], + }, + ], + } + return mode_resources def serialize_mode_resources(self): diff --git a/homeassistant/components/alexa/const.py b/homeassistant/components/alexa/const.py index 2a5f9a512b3b..1aa9d4f2c1dc 100644 --- a/homeassistant/components/alexa/const.py +++ b/homeassistant/components/alexa/const.py @@ -272,3 +272,84 @@ class Unit: WEIGHT_OUNCES = "Alexa.Unit.Weight.Ounces" WEIGHT_POUNDS = "Alexa.Unit.Weight.Pounds" + + +class Inputs: + """Valid names for the InputController. + + https://developer.amazon.com/docs/device-apis/alexa-property-schemas.html#input + """ + + VALID_SOURCE_NAME_MAP = { + "aux": "AUX 1", + "aux1": "AUX 1", + "aux2": "AUX 2", + "aux3": "AUX 3", + "aux4": "AUX 4", + "aux5": "AUX 5", + "aux6": "AUX 6", + "aux7": "AUX 7", + "bluray": "BLURAY", + "cable": "CABLE", + "cd": "CD", + "coax": "COAX 1", + "coax1": "COAX 1", + "coax2": "COAX 2", + "composite": "COMPOSITE 1", + "composite1": "COMPOSITE 1", + "dvd": "DVD", + "game": "GAME", + "gameconsole": "GAME", + "hdradio": "HD RADIO", + "hdmi": "HDMI 1", + "hdmi1": "HDMI 1", + "hdmi2": "HDMI 2", + "hdmi3": "HDMI 3", + "hdmi4": "HDMI 4", + "hdmi5": "HDMI 5", + "hdmi6": "HDMI 6", + "hdmi7": "HDMI 7", + "hdmi8": "HDMI 8", + "hdmi9": "HDMI 9", + "hdmi10": "HDMI 10", + "hdmiarc": "HDMI ARC", + "input": "INPUT 1", + "input1": "INPUT 1", + "input2": "INPUT 2", + "input3": "INPUT 3", + "input4": "INPUT 4", + "input5": "INPUT 5", + "input6": "INPUT 6", + "input7": "INPUT 7", + "input8": "INPUT 8", + "input9": "INPUT 9", + "input10": "INPUT 10", + "ipod": "IPOD", + "line": "LINE 1", + "line1": "LINE 1", + "line2": "LINE 2", + "line3": "LINE 3", + "line4": "LINE 4", + "line5": "LINE 5", + "line6": "LINE 6", + "line7": "LINE 7", + "mediaplayer": "MEDIA PLAYER", + "optical": "OPTICAL 1", + "optical1": "OPTICAL 1", + "optical2": "OPTICAL 2", + "phono": "PHONO", + "playstation": "PLAYSTATION", + "playstation3": "PLAYSTATION 3", + "playstation4": "PLAYSTATION 4", + "satellite": "SATELLITE", + "satellitetv": "SATELLITE", + "smartcast": "SMARTCAST", + "tuner": "TUNER", + "tv": "TV", + "usbdac": "USB DAC", + "video": "VIDEO 1", + "video1": "VIDEO 1", + "video2": "VIDEO 2", + "video3": "VIDEO 3", + "xbox": "XBOX", + } diff --git a/homeassistant/components/alexa/entities.py b/homeassistant/components/alexa/entities.py index 6d2f9aef56aa..f9463949b586 100644 --- a/homeassistant/components/alexa/entities.py +++ b/homeassistant/components/alexa/entities.py @@ -33,6 +33,7 @@ from .const import CONF_DESCRIPTION, CONF_DISPLAY_CATEGORIES from .capabilities import ( + Alexa, AlexaBrightnessController, AlexaChannelController, AlexaColorController, @@ -261,6 +262,7 @@ def interfaces(self): return [ AlexaPowerController(self.entity), AlexaEndpointHealth(self.hass, self.entity), + Alexa(self.hass), ] @@ -270,6 +272,10 @@ class SwitchCapabilities(AlexaEntity): def default_display_categories(self): """Return the display categories for this entity.""" + device_class = self.entity.attributes.get(ATTR_DEVICE_CLASS) + if device_class == switch.DEVICE_CLASS_OUTLET: + return [DisplayCategory.SMARTPLUG] + return [DisplayCategory.SWITCH] def interfaces(self): @@ -277,6 +283,7 @@ def interfaces(self): return [ AlexaPowerController(self.entity), AlexaEndpointHealth(self.hass, self.entity), + Alexa(self.hass), ] @@ -299,6 +306,7 @@ def interfaces(self): yield AlexaThermostatController(self.hass, self.entity) yield AlexaTemperatureSensor(self.hass, self.entity) yield AlexaEndpointHealth(self.hass, self.entity) + yield Alexa(self.hass) @ENTITY_ADAPTERS.register(cover.DOMAIN) @@ -307,7 +315,10 @@ class CoverCapabilities(AlexaEntity): def default_display_categories(self): """Return the display categories for this entity.""" - return [DisplayCategory.DOOR] + device_class = self.entity.attributes.get(ATTR_DEVICE_CLASS) + if device_class in (cover.DEVICE_CLASS_GARAGE, cover.DEVICE_CLASS_DOOR): + return [DisplayCategory.DOOR] + return [DisplayCategory.OTHER] def interfaces(self): """Yield the supported interfaces.""" @@ -315,7 +326,12 @@ def interfaces(self): supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0) if supported & cover.SUPPORT_SET_POSITION: yield AlexaPercentageController(self.entity) + if supported & (cover.SUPPORT_CLOSE | cover.SUPPORT_OPEN): + yield AlexaModeController( + self.entity, instance=f"{cover.DOMAIN}.{cover.ATTR_POSITION}" + ) yield AlexaEndpointHealth(self.hass, self.entity) + yield Alexa(self.hass) @ENTITY_ADAPTERS.register(light.DOMAIN) @@ -338,6 +354,7 @@ def interfaces(self): if supported & light.SUPPORT_COLOR_TEMP: yield AlexaColorTemperatureController(self.entity) yield AlexaEndpointHealth(self.hass, self.entity) + yield Alexa(self.hass) @ENTITY_ADAPTERS.register(fan.DOMAIN) @@ -369,6 +386,7 @@ def interfaces(self): ) yield AlexaEndpointHealth(self.hass, self.entity) + yield Alexa(self.hass) @ENTITY_ADAPTERS.register(lock.DOMAIN) @@ -384,6 +402,7 @@ def interfaces(self): return [ AlexaLockController(self.entity), AlexaEndpointHealth(self.hass, self.entity), + Alexa(self.hass), ] @@ -401,7 +420,6 @@ def default_display_categories(self): def interfaces(self): """Yield the supported interfaces.""" - yield AlexaEndpointHealth(self.hass, self.entity) yield AlexaPowerController(self.entity) supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0) @@ -435,6 +453,9 @@ def interfaces(self): if supported & media_player.const.SUPPORT_PLAY_MEDIA: yield AlexaChannelController(self.entity) + yield AlexaEndpointHealth(self.hass, self.entity) + yield Alexa(self.hass) + @ENTITY_ADAPTERS.register(scene.DOMAIN) class SceneCapabilities(AlexaEntity): @@ -453,7 +474,10 @@ def default_display_categories(self): def interfaces(self): """Yield the supported interfaces.""" - return [AlexaSceneController(self.entity, supports_deactivation=False)] + return [ + AlexaSceneController(self.entity, supports_deactivation=False), + Alexa(self.hass), + ] @ENTITY_ADAPTERS.register(script.DOMAIN) @@ -467,7 +491,10 @@ def default_display_categories(self): def interfaces(self): """Yield the supported interfaces.""" can_cancel = bool(self.entity.attributes.get("can_cancel")) - return [AlexaSceneController(self.entity, supports_deactivation=can_cancel)] + return [ + AlexaSceneController(self.entity, supports_deactivation=can_cancel), + Alexa(self.hass), + ] @ENTITY_ADAPTERS.register(sensor.DOMAIN) @@ -486,6 +513,7 @@ def interfaces(self): if attrs.get(ATTR_UNIT_OF_MEASUREMENT) in (TEMP_FAHRENHEIT, TEMP_CELSIUS): yield AlexaTemperatureSensor(self.hass, self.entity) yield AlexaEndpointHealth(self.hass, self.entity) + yield Alexa(self.hass) @ENTITY_ADAPTERS.register(binary_sensor.DOMAIN) @@ -515,8 +543,13 @@ def interfaces(self): if CONF_DISPLAY_CATEGORIES in entity_conf: if entity_conf[CONF_DISPLAY_CATEGORIES] == DisplayCategory.DOORBELL: yield AlexaDoorbellEventSource(self.entity) + elif entity_conf[CONF_DISPLAY_CATEGORIES] == DisplayCategory.CONTACT_SENSOR: + yield AlexaContactSensor(self.hass, self.entity) + elif entity_conf[CONF_DISPLAY_CATEGORIES] == DisplayCategory.MOTION_SENSOR: + yield AlexaMotionSensor(self.hass, self.entity) yield AlexaEndpointHealth(self.hass, self.entity) + yield Alexa(self.hass) def get_type(self): """Return the type of binary sensor.""" @@ -540,3 +573,4 @@ def interfaces(self): if not self.entity.attributes.get("code_arm_required"): yield AlexaSecurityPanelController(self.hass, self.entity) yield AlexaEndpointHealth(self.hass, self.entity) + yield Alexa(self.hass) diff --git a/homeassistant/components/alexa/handlers.py b/homeassistant/components/alexa/handlers.py index c23e01f501fd..f1aa260e88ea 100644 --- a/homeassistant/components/alexa/handlers.py +++ b/homeassistant/components/alexa/handlers.py @@ -9,7 +9,6 @@ ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, ATTR_TEMPERATURE, - STATE_ALARM_DISARMED, SERVICE_ALARM_ARM_AWAY, SERVICE_ALARM_ARM_HOME, SERVICE_ALARM_ARM_NIGHT, @@ -28,6 +27,9 @@ SERVICE_VOLUME_MUTE, SERVICE_VOLUME_SET, SERVICE_VOLUME_UP, + STATE_ALARM_DISARMED, + STATE_CLOSED, + STATE_OPEN, TEMP_CELSIUS, TEMP_FAHRENHEIT, ) @@ -42,6 +44,7 @@ API_THERMOSTAT_MODES, API_THERMOSTAT_PRESETS, Cause, + Inputs, PERCENTAGE_FAN_MAP, RANGE_FAN_MAP, SPEED_FAN_MAP, @@ -459,13 +462,20 @@ async def async_api_select_input(hass, config, directive, context): media_input = directive.payload["input"] entity = directive.entity - # attempt to map the ALL UPPERCASE payload name to a source - source_list = entity.attributes[media_player.const.ATTR_INPUT_SOURCE_LIST] or [] + # Attempt to map the ALL UPPERCASE payload name to a source. + # Strips trailing 1 to match single input devices. + source_list = entity.attributes.get(media_player.const.ATTR_INPUT_SOURCE_LIST, []) for source in source_list: - # response will always be space separated, so format the source in the - # most likely way to find a match - formatted_source = source.lower().replace("-", " ").replace("_", " ") - if formatted_source in media_input.lower(): + formatted_source = ( + source.lower().replace("-", "").replace("_", "").replace(" ", "") + ) + media_input = media_input.lower().replace(" ", "") + if ( + formatted_source in Inputs.VALID_SOURCE_NAME_MAP.keys() + and formatted_source == media_input + ) or ( + media_input.endswith("1") and formatted_source == media_input.rstrip("1") + ): media_input = source break else: @@ -956,23 +966,42 @@ async def async_api_set_mode(hass, config, directive, context): domain = entity.domain service = None data = {ATTR_ENTITY_ID: entity.entity_id} - mode = directive.payload["mode"] + capability_mode = directive.payload["mode"] - if domain != fan.DOMAIN: + if domain not in (fan.DOMAIN, cover.DOMAIN): msg = "Entity does not support directive" raise AlexaInvalidDirectiveError(msg) if instance == f"{fan.DOMAIN}.{fan.ATTR_DIRECTION}": - mode, direction = mode.split(".") - if direction in [fan.DIRECTION_REVERSE, fan.DIRECTION_FORWARD]: + _, direction = capability_mode.split(".") + if direction in (fan.DIRECTION_REVERSE, fan.DIRECTION_FORWARD): service = fan.SERVICE_SET_DIRECTION data[fan.ATTR_DIRECTION] = direction + if instance == f"{cover.DOMAIN}.{cover.ATTR_POSITION}": + _, position = capability_mode.split(".") + + if position == STATE_CLOSED: + service = cover.SERVICE_CLOSE_COVER + + if position == STATE_OPEN: + service = cover.SERVICE_OPEN_COVER + await hass.services.async_call( domain, service, data, blocking=False, context=context ) - return directive.response() + response = directive.response() + response.add_context_property( + { + "namespace": "Alexa.ModeController", + "instance": instance, + "name": "mode", + "value": capability_mode, + } + ) + + return response @HANDLERS.register(("Alexa.ModeController", "AdjustMode")) @@ -1115,21 +1144,25 @@ async def async_api_changechannel(hass, config, directive, context): """Process a change channel request.""" channel = "0" entity = directive.entity - payload = directive.payload["channel"] + channel_payload = directive.payload["channel"] + metadata_payload = directive.payload["channelMetadata"] payload_name = "number" - if "number" in payload: - channel = payload["number"] + if "number" in channel_payload: + channel = channel_payload["number"] payload_name = "number" - elif "callSign" in payload: - channel = payload["callSign"] + elif "callSign" in channel_payload: + channel = channel_payload["callSign"] payload_name = "callSign" - elif "affiliateCallSign" in payload: - channel = payload["affiliateCallSign"] + elif "affiliateCallSign" in channel_payload: + channel = channel_payload["affiliateCallSign"] payload_name = "affiliateCallSign" - elif "uri" in payload: - channel = payload["uri"] + elif "uri" in channel_payload: + channel = channel_payload["uri"] payload_name = "uri" + elif "name" in metadata_payload: + channel = metadata_payload["name"] + payload_name = "callSign" data = { ATTR_ENTITY_ID: entity.entity_id, diff --git a/homeassistant/components/almond/.translations/bg.json b/homeassistant/components/almond/.translations/bg.json index da5571ad0294..3327e34e7658 100644 --- a/homeassistant/components/almond/.translations/bg.json +++ b/homeassistant/components/almond/.translations/bg.json @@ -2,7 +2,13 @@ "config": { "abort": { "already_setup": "\u041c\u043e\u0436\u0435\u0442\u0435 \u0434\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u0442\u0435 \u0441\u0430\u043c\u043e \u0435\u0434\u0438\u043d Almond \u0430\u043a\u0430\u0443\u043d\u0442.", - "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435 \u0441 Almond \u0441\u044a\u0440\u0432\u044a\u0440\u0430." + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435 \u0441 Almond \u0441\u044a\u0440\u0432\u044a\u0440\u0430.", + "missing_configuration": "\u041c\u043e\u043b\u044f, \u043f\u0440\u043e\u0432\u0435\u0440\u0435\u0442\u0435 \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u044f\u0442\u0430 \u043a\u0430\u043a \u0434\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u0435 Almond." + }, + "step": { + "pick_implementation": { + "title": "\u0418\u0437\u0431\u043e\u0440 \u043d\u0430 \u043c\u0435\u0442\u043e\u0434 \u0437\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f" + } }, "title": "Almond" } diff --git a/homeassistant/components/almond/.translations/ca.json b/homeassistant/components/almond/.translations/ca.json index cf4618d22721..c626e2795ea5 100644 --- a/homeassistant/components/almond/.translations/ca.json +++ b/homeassistant/components/almond/.translations/ca.json @@ -2,7 +2,13 @@ "config": { "abort": { "already_setup": "Nom\u00e9s pots configurar un \u00fanic compte amb Almond.", - "cannot_connect": "No es pot connectar amb el servidor d'Almond." + "cannot_connect": "No es pot connectar amb el servidor d'Almond.", + "missing_configuration": "Consulta la documentaci\u00f3 sobre com configurar Almond." + }, + "step": { + "pick_implementation": { + "title": "Selecci\u00f3 del m\u00e8tode d'autenticaci\u00f3" + } }, "title": "Almond" } diff --git a/homeassistant/components/almond/.translations/de.json b/homeassistant/components/almond/.translations/de.json index 4e2816cb0011..1495cabf9c91 100644 --- a/homeassistant/components/almond/.translations/de.json +++ b/homeassistant/components/almond/.translations/de.json @@ -2,7 +2,13 @@ "config": { "abort": { "already_setup": "Sie k\u00f6nnen nur ein Almond-Konto konfigurieren.", - "cannot_connect": "Verbindung zum Almond-Server nicht m\u00f6glich." + "cannot_connect": "Verbindung zum Almond-Server nicht m\u00f6glich.", + "missing_configuration": "Bitte \u00fcberpr\u00fcfen Sie die Dokumentation zur Einrichtung von Almond." + }, + "step": { + "pick_implementation": { + "title": "W\u00e4hle die Authentifizierungsmethode" + } }, "title": "Almond" } diff --git a/homeassistant/components/almond/.translations/fr.json b/homeassistant/components/almond/.translations/fr.json index 0208366cea12..9ae881d332cd 100644 --- a/homeassistant/components/almond/.translations/fr.json +++ b/homeassistant/components/almond/.translations/fr.json @@ -5,6 +5,11 @@ "cannot_connect": "Impossible de se connecter au serveur Almond", "missing_configuration": "Veuillez consulter la documentation pour savoir comment configurer Almond." }, + "step": { + "pick_implementation": { + "title": "S\u00e9lectionner une m\u00e9thode d'authentification" + } + }, "title": "Almond" } } \ No newline at end of file diff --git a/homeassistant/components/almond/.translations/it.json b/homeassistant/components/almond/.translations/it.json index 740535f4f460..9d529e5e5c85 100644 --- a/homeassistant/components/almond/.translations/it.json +++ b/homeassistant/components/almond/.translations/it.json @@ -5,6 +5,11 @@ "cannot_connect": "Impossibile connettersi al server Almond.", "missing_configuration": "Si prega di controllare la documentazione su come impostare Almond." }, + "step": { + "pick_implementation": { + "title": "Seleziona metodo di autenticazione" + } + }, "title": "Almond" } } \ No newline at end of file diff --git a/homeassistant/components/almond/.translations/ko.json b/homeassistant/components/almond/.translations/ko.json index 9440242ebbcc..2a2346aaf501 100644 --- a/homeassistant/components/almond/.translations/ko.json +++ b/homeassistant/components/almond/.translations/ko.json @@ -2,7 +2,8 @@ "config": { "abort": { "already_setup": "\ud558\ub098\uc758 Almond \uacc4\uc815\ub9cc \uad6c\uc131 \ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.", - "cannot_connect": "Almond \uc11c\ubc84\uc5d0 \uc5f0\uacb0\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4." + "cannot_connect": "Almond \uc11c\ubc84\uc5d0 \uc5f0\uacb0\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4.", + "missing_configuration": "Almond \uc124\uc815 \ubc29\ubc95\uc5d0 \ub300\ud55c \uc124\uba85\uc11c\ub97c \ud655\uc778\ud574\uc8fc\uc138\uc694." }, "title": "Almond" } diff --git a/homeassistant/components/almond/.translations/lb.json b/homeassistant/components/almond/.translations/lb.json index 30cb8b868914..ca836267d46c 100644 --- a/homeassistant/components/almond/.translations/lb.json +++ b/homeassistant/components/almond/.translations/lb.json @@ -5,6 +5,11 @@ "cannot_connect": "Kann sech net mam Almond Server verbannen.", "missing_configuration": "Kuckt w.e.g. Dokumentatioun iwwert d'ariichten vun Almond." }, + "step": { + "pick_implementation": { + "title": "Wielt Authentifikatiouns Method aus" + } + }, "title": "Almond" } } \ No newline at end of file diff --git a/homeassistant/components/almond/.translations/nl.json b/homeassistant/components/almond/.translations/nl.json index dfe9c238db7b..d77fe69f7fa8 100644 --- a/homeassistant/components/almond/.translations/nl.json +++ b/homeassistant/components/almond/.translations/nl.json @@ -5,6 +5,11 @@ "cannot_connect": "Kan geen verbinding maken met de Almond-server.", "missing_configuration": "Raadpleeg de documentatie over het instellen van Almond." }, + "step": { + "pick_implementation": { + "title": "Kies de authenticatie methode" + } + }, "title": "Almond" } } \ No newline at end of file diff --git a/homeassistant/components/almond/.translations/nn.json b/homeassistant/components/almond/.translations/nn.json new file mode 100644 index 000000000000..a25f5dc15745 --- /dev/null +++ b/homeassistant/components/almond/.translations/nn.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Almond" + } +} \ No newline at end of file diff --git a/homeassistant/components/almond/.translations/no.json b/homeassistant/components/almond/.translations/no.json index 6ea2de635b19..0272a120f21d 100644 --- a/homeassistant/components/almond/.translations/no.json +++ b/homeassistant/components/almond/.translations/no.json @@ -5,6 +5,11 @@ "cannot_connect": "Kan ikke koble til Almond-serveren.", "missing_configuration": "Vennligst sjekk dokumentasjonen om hvordan du setter opp Almond." }, + "step": { + "pick_implementation": { + "title": "Velg autentiseringsmetode" + } + }, "title": "Almond" } } \ No newline at end of file diff --git a/homeassistant/components/almond/.translations/pl.json b/homeassistant/components/almond/.translations/pl.json index b96d9c09bb24..56aa629e015b 100644 --- a/homeassistant/components/almond/.translations/pl.json +++ b/homeassistant/components/almond/.translations/pl.json @@ -2,7 +2,13 @@ "config": { "abort": { "already_setup": "Mo\u017cesz skonfigurowa\u0107 tylko jedno konto Almond.", - "cannot_connect": "Nie mo\u017cna po\u0142\u0105czy\u0107 si\u0119 z serwerem Almond." + "cannot_connect": "Nie mo\u017cna po\u0142\u0105czy\u0107 si\u0119 z serwerem Almond.", + "missing_configuration": "Prosz\u0119 zapozna\u0107 si\u0119 z dokumentacj\u0105 konfiguracji Almond." + }, + "step": { + "pick_implementation": { + "title": "Wybierz metod\u0119 uwierzytelniania" + } }, "title": "Almond" } diff --git a/homeassistant/components/almond/.translations/pt.json b/homeassistant/components/almond/.translations/pt.json new file mode 100644 index 000000000000..720400e72a5f --- /dev/null +++ b/homeassistant/components/almond/.translations/pt.json @@ -0,0 +1,10 @@ +{ + "config": { + "step": { + "pick_implementation": { + "title": "Escolha o m\u00e9todo de autentica\u00e7\u00e3o" + } + }, + "title": "" + } +} \ No newline at end of file diff --git a/homeassistant/components/almond/.translations/ru.json b/homeassistant/components/almond/.translations/ru.json index d15e9a1eeb47..39dc41a39952 100644 --- a/homeassistant/components/almond/.translations/ru.json +++ b/homeassistant/components/almond/.translations/ru.json @@ -5,6 +5,11 @@ "cannot_connect": "\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 Almond.", "missing_configuration": "\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 \u043f\u043e \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0435 Almond." }, + "step": { + "pick_implementation": { + "title": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u043c\u0435\u0442\u043e\u0434 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438" + } + }, "title": "Almond" } } \ No newline at end of file diff --git a/homeassistant/components/almond/.translations/sl.json b/homeassistant/components/almond/.translations/sl.json index c809b908b9f5..086190590ac8 100644 --- a/homeassistant/components/almond/.translations/sl.json +++ b/homeassistant/components/almond/.translations/sl.json @@ -5,6 +5,11 @@ "cannot_connect": "Ni mogo\u010de vzpostaviti povezave s stre\u017enikom Almond.", "missing_configuration": "Prosimo, preverite dokumentacijo o tem, kako nastaviti Almond." }, + "step": { + "pick_implementation": { + "title": "Izberite na\u010din preverjanja pristnosti" + } + }, "title": "Almond" } } \ No newline at end of file diff --git a/homeassistant/components/almond/.translations/zh-Hant.json b/homeassistant/components/almond/.translations/zh-Hant.json index 743835b10465..4db6e0c936ef 100644 --- a/homeassistant/components/almond/.translations/zh-Hant.json +++ b/homeassistant/components/almond/.translations/zh-Hant.json @@ -5,6 +5,11 @@ "cannot_connect": "\u7121\u6cd5\u9023\u7dda\u81f3 Almond \u4f3a\u670d\u5668\u3002", "missing_configuration": "\u8acb\u53c3\u8003\u76f8\u95dc\u6587\u4ef6\u4ee5\u4e86\u89e3\u5982\u4f55\u8a2d\u5b9a Almond\u3002" }, + "step": { + "pick_implementation": { + "title": "\u9078\u64c7\u9a57\u8b49\u6a21\u5f0f" + } + }, "title": "Almond" } } \ No newline at end of file diff --git a/homeassistant/components/almond/__init__.py b/homeassistant/components/almond/__init__.py index 7c1f65f3ac3e..66d4b2fc9afb 100644 --- a/homeassistant/components/almond/__init__.py +++ b/homeassistant/components/almond/__init__.py @@ -10,7 +10,7 @@ from pyalmond import AlmondLocalAuth, AbstractAlmondWebAuth, WebAlmondAPI import voluptuous as vol -from homeassistant.core import HomeAssistant, CoreState +from homeassistant.core import HomeAssistant, CoreState, Context from homeassistant.const import CONF_TYPE, CONF_HOST, EVENT_HOMEASSISTANT_START from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.auth.const import GROUP_ID_ADMIN @@ -277,7 +277,7 @@ async def async_set_onboarding(self, shown): return True async def async_process( - self, text: str, conversation_id: Optional[str] = None + self, text: str, context: Context, conversation_id: Optional[str] = None ) -> intent.IntentResponse: """Process a sentence.""" response = await self.api.async_converse_text(text, conversation_id) diff --git a/homeassistant/components/alpha_vantage/__init__.py b/homeassistant/components/alpha_vantage/__init__.py index f8220c2cb811..b8da9c190245 100644 --- a/homeassistant/components/alpha_vantage/__init__.py +++ b/homeassistant/components/alpha_vantage/__init__.py @@ -1 +1 @@ -"""The alpha_vantage component.""" +"""The Alpha Vantage component.""" diff --git a/homeassistant/components/alpha_vantage/manifest.json b/homeassistant/components/alpha_vantage/manifest.json index 1213bb12e74c..99498991be26 100644 --- a/homeassistant/components/alpha_vantage/manifest.json +++ b/homeassistant/components/alpha_vantage/manifest.json @@ -1,9 +1,9 @@ { "domain": "alpha_vantage", - "name": "Alpha vantage", + "name": "Alpha Vantage", "documentation": "https://www.home-assistant.io/integrations/alpha_vantage", "requirements": [ - "alpha_vantage==2.1.1" + "alpha_vantage==2.1.2" ], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/ambiclimate/.translations/pl.json b/homeassistant/components/ambiclimate/.translations/pl.json index 675c5e18776c..18f5d043dbce 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, aby m\u00f3c si\u0119 z nim uwierzytelni\u0107. Zapoznaj si\u0119 z [instrukcj\u0105](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 ba667ea7b9a7..2a99430e4369 100644 --- a/homeassistant/components/ambiclimate/.translations/ru.json +++ b/homeassistant/components/ambiclimate/.translations/ru.json @@ -2,7 +2,7 @@ "config": { "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 \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430.", + "already_setup": "\u0423\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430.", "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": { @@ -14,7 +14,7 @@ }, "step": { "auth": { - "description": "\u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043f\u043e [\u0441\u0441\u044b\u043b\u043a\u0435]({authorization_url}) \u0438 \u0420\u0430\u0437\u0440\u0435\u0448\u0438\u0442\u0435 \u0434\u043e\u0441\u0442\u0443\u043f \u043a \u0412\u0430\u0448\u0435\u0439 \u0443\u0447\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 Ambi Climate, \u0437\u0430\u0442\u0435\u043c \u0432\u0435\u0440\u043d\u0438\u0442\u0435\u0441\u044c \u0441\u044e\u0434\u0430 \u0438 \u043d\u0430\u0436\u043c\u0438\u0442\u0435 \u041f\u041e\u0414\u0422\u0412\u0415\u0420\u0414\u0418\u0422\u042c. \n(\u0423\u0431\u0435\u0434\u0438\u0442\u0435\u0441\u044c, \u0447\u0442\u043e \u0443\u043a\u0430\u0437\u0430\u043d\u043d\u044b\u0439 URL \u043e\u0431\u0440\u0430\u0442\u043d\u043e\u0433\u043e \u0432\u044b\u0437\u043e\u0432\u0430 \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u0435\u0442 {cb_url})", + "description": "\u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043f\u043e [\u0441\u0441\u044b\u043b\u043a\u0435]({authorization_url}) \u0438 \u0420\u0430\u0437\u0440\u0435\u0448\u0438\u0442\u0435 \u0434\u043e\u0441\u0442\u0443\u043f \u043a \u0412\u0430\u0448\u0435\u0439 \u0443\u0447\u0451\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 Ambi Climate, \u0437\u0430\u0442\u0435\u043c \u0432\u0435\u0440\u043d\u0438\u0442\u0435\u0441\u044c \u0441\u044e\u0434\u0430 \u0438 \u043d\u0430\u0436\u043c\u0438\u0442\u0435 \u041f\u041e\u0414\u0422\u0412\u0415\u0420\u0414\u0418\u0422\u042c. \n(\u0423\u0431\u0435\u0434\u0438\u0442\u0435\u0441\u044c, \u0447\u0442\u043e \u0443\u043a\u0430\u0437\u0430\u043d\u043d\u044b\u0439 URL \u043e\u0431\u0440\u0430\u0442\u043d\u043e\u0433\u043e \u0432\u044b\u0437\u043e\u0432\u0430 \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u0435\u0442 {cb_url})", "title": "Ambi Climate" } }, diff --git a/homeassistant/components/ambient_station/.translations/ru.json b/homeassistant/components/ambient_station/.translations/ru.json index 3a7c405ea4cd..438b1cf87a7f 100644 --- a/homeassistant/components/ambient_station/.translations/ru.json +++ b/homeassistant/components/ambient_station/.translations/ru.json @@ -3,7 +3,7 @@ "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.", "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." + "no_devices": "\u0412 \u0443\u0447\u0451\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." }, "step": { "user": { diff --git a/homeassistant/components/ambient_station/__init__.py b/homeassistant/components/ambient_station/__init__.py index bff03eb422b2..7a805d6b8676 100644 --- a/homeassistant/components/ambient_station/__init__.py +++ b/homeassistant/components/ambient_station/__init__.py @@ -1,4 +1,5 @@ """Support for Ambient Weather Station Service.""" +import asyncio import logging from aioambient import Client @@ -297,8 +298,12 @@ async def async_unload_entry(hass, config_entry): ambient = hass.data[DOMAIN][DATA_CLIENT].pop(config_entry.entry_id) hass.async_create_task(ambient.ws_disconnect()) - for component in ("binary_sensor", "sensor"): - await hass.config_entries.async_forward_entry_unload(config_entry, component) + tasks = [ + hass.config_entries.async_forward_entry_unload(config_entry, component) + for component in ("binary_sensor", "sensor") + ] + + await asyncio.gather(*tasks) return True diff --git a/homeassistant/components/androidtv/manifest.json b/homeassistant/components/androidtv/manifest.json index c734b525e550..8b68f089617b 100644 --- a/homeassistant/components/androidtv/manifest.json +++ b/homeassistant/components/androidtv/manifest.json @@ -4,7 +4,8 @@ "documentation": "https://www.home-assistant.io/integrations/androidtv", "requirements": [ "adb-shell==0.0.8", - "androidtv==0.0.32" + "androidtv==0.0.34", + "pure-python-adb==0.2.2.dev0" ], "dependencies": [], "codeowners": ["@JeffLIrion"] diff --git a/homeassistant/components/androidtv/media_player.py b/homeassistant/components/androidtv/media_player.py index 7540973ea199..b1cb86f7633e 100644 --- a/homeassistant/components/androidtv/media_player.py +++ b/homeassistant/components/androidtv/media_player.py @@ -252,14 +252,18 @@ def service_adb_command(service): def adb_decorator(override_available=False): - """Send an ADB command if the device is available and catch exceptions.""" + """Wrap ADB methods and catch exceptions. + + Allows for overriding the available status of the ADB connection via the + `override_available` parameter. + """ def _adb_decorator(func): - """Wait if previous ADB commands haven't finished.""" + """Wrap the provided ADB method and catch exceptions.""" @functools.wraps(func) def _adb_exception_catcher(self, *args, **kwargs): - # If the device is unavailable, don't do anything + """Call an ADB-related method and catch exceptions.""" if not self.available and not override_available: return None @@ -319,7 +323,7 @@ def __init__(self, aftv, name, apps, turn_on_command, turn_off_command): # Property attributes self._adb_response = None - self._available = self.aftv.available + self._available = True self._current_app = None self._state = None diff --git a/homeassistant/components/apache_kafka/__init__.py b/homeassistant/components/apache_kafka/__init__.py index e0c8b824913e..7bd23630bd08 100644 --- a/homeassistant/components/apache_kafka/__init__.py +++ b/homeassistant/components/apache_kafka/__init__.py @@ -64,7 +64,7 @@ class DateTimeJSONEncoder(json.JSONEncoder): Additionally add encoding for datetime objects as isoformat. """ - def default(self, o): # pylint: disable=E0202 + def default(self, o): # pylint: disable=method-hidden """Implement encoding logic.""" if isinstance(o, datetime): return o.isoformat() diff --git a/homeassistant/components/apns/const.py b/homeassistant/components/apns/const.py new file mode 100644 index 000000000000..a8dc1204aa19 --- /dev/null +++ b/homeassistant/components/apns/const.py @@ -0,0 +1,2 @@ +"""Constants for the apns component.""" +DOMAIN = "apns" diff --git a/homeassistant/components/apns/notify.py b/homeassistant/components/apns/notify.py index c24c9cc16052..ce761b502ac4 100644 --- a/homeassistant/components/apns/notify.py +++ b/homeassistant/components/apns/notify.py @@ -9,7 +9,6 @@ from homeassistant.components.notify import ( ATTR_DATA, ATTR_TARGET, - DOMAIN, PLATFORM_SCHEMA, BaseNotificationService, ) @@ -18,13 +17,14 @@ from homeassistant.helpers import template as template_helper import homeassistant.helpers.config_validation as cv from homeassistant.helpers.event import track_state_change +from homeassistant.components.device_tracker import DOMAIN as DEVICE_TRACKER_DOMAIN + +from .const import DOMAIN APNS_DEVICES = "apns.yaml" CONF_CERTFILE = "cert_file" CONF_TOPIC = "topic" CONF_SANDBOX = "sandbox" -DEVICE_TRACKER_DOMAIN = "device_tracker" -SERVICE_REGISTER = "apns_register" ATTR_PUSH_ID = "push_id" diff --git a/homeassistant/components/apprise/manifest.json b/homeassistant/components/apprise/manifest.json index 3e971a96e7ea..09d840e796ac 100644 --- a/homeassistant/components/apprise/manifest.json +++ b/homeassistant/components/apprise/manifest.json @@ -3,7 +3,7 @@ "name": "Apprise", "documentation": "https://www.home-assistant.io/components/apprise", "requirements": [ - "apprise==0.8.1" + "apprise==0.8.2" ], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/arlo/alarm_control_panel.py b/homeassistant/components/arlo/alarm_control_panel.py index a56b2a63372c..838f319abc14 100644 --- a/homeassistant/components/arlo/alarm_control_panel.py +++ b/homeassistant/components/arlo/alarm_control_panel.py @@ -7,6 +7,11 @@ PLATFORM_SCHEMA, AlarmControlPanel, ) +from homeassistant.components.alarm_control_panel.const import ( + SUPPORT_ALARM_ARM_AWAY, + SUPPORT_ALARM_ARM_HOME, + SUPPORT_ALARM_ARM_NIGHT, +) from homeassistant.const import ( ATTR_ATTRIBUTION, STATE_ALARM_ARMED_AWAY, @@ -91,6 +96,11 @@ def state(self): """Return the state of the device.""" return self._state + @property + def supported_features(self) -> int: + """Return the list of supported features.""" + return SUPPORT_ALARM_ARM_HOME | SUPPORT_ALARM_ARM_AWAY | SUPPORT_ALARM_ARM_NIGHT + def update(self): """Update the state of the device.""" _LOGGER.debug("Updating Arlo Alarm Control Panel %s", self.name) diff --git a/homeassistant/components/asuswrt/__init__.py b/homeassistant/components/asuswrt/__init__.py index e0c6830adfe9..64d2d7c7a4bf 100644 --- a/homeassistant/components/asuswrt/__init__.py +++ b/homeassistant/components/asuswrt/__init__.py @@ -1,15 +1,16 @@ """Support for ASUSWRT devices.""" import logging +from aioasuswrt.asuswrt import AsusWrt import voluptuous as vol from homeassistant.const import ( CONF_HOST, + CONF_MODE, CONF_PASSWORD, - CONF_USERNAME, CONF_PORT, - CONF_MODE, CONF_PROTOCOL, + CONF_USERNAME, ) from homeassistant.helpers import config_validation as cv from homeassistant.helpers.discovery import async_load_platform @@ -53,7 +54,6 @@ async def async_setup(hass, config): """Set up the asuswrt component.""" - from aioasuswrt.asuswrt import AsusWrt conf = config[DOMAIN] diff --git a/homeassistant/components/aten_pe/__init__.py b/homeassistant/components/aten_pe/__init__.py new file mode 100644 index 000000000000..2a0fb277a48c --- /dev/null +++ b/homeassistant/components/aten_pe/__init__.py @@ -0,0 +1 @@ +"""The ATEN PE component.""" diff --git a/homeassistant/components/aten_pe/manifest.json b/homeassistant/components/aten_pe/manifest.json new file mode 100644 index 000000000000..4f6416dd76c6 --- /dev/null +++ b/homeassistant/components/aten_pe/manifest.json @@ -0,0 +1,12 @@ +{ + "domain": "aten_pe", + "name": "ATEN eco PDUs", + "documentation": "https://www.home-assistant.io/integrations/aten_pe", + "requirements": [ + "atenpdu==0.3.0" + ], + "dependencies": [], + "codeowners": [ + "@mtdcr" + ] +} diff --git a/homeassistant/components/aten_pe/switch.py b/homeassistant/components/aten_pe/switch.py new file mode 100644 index 000000000000..2ec6ec4b83d5 --- /dev/null +++ b/homeassistant/components/aten_pe/switch.py @@ -0,0 +1,122 @@ +"""The ATEN PE switch component.""" + +import logging + +from atenpdu import AtenPE, AtenPEError +import voluptuous as vol + +from homeassistant.components.switch import ( + DEVICE_CLASS_OUTLET, + PLATFORM_SCHEMA, + SwitchDevice, +) +from homeassistant.const import CONF_HOST, CONF_PORT, CONF_USERNAME +from homeassistant.exceptions import PlatformNotReady +import homeassistant.helpers.config_validation as cv + +_LOGGER = logging.getLogger(__name__) + +CONF_AUTH_KEY = "auth_key" +CONF_COMMUNITY = "community" +CONF_PRIV_KEY = "priv_key" +DEFAULT_COMMUNITY = "private" +DEFAULT_PORT = "161" +DEFAULT_USERNAME = "administrator" + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Optional(CONF_COMMUNITY, default=DEFAULT_COMMUNITY): cv.string, + vol.Optional(CONF_USERNAME, default=DEFAULT_USERNAME): cv.string, + vol.Optional(CONF_AUTH_KEY): cv.string, + vol.Optional(CONF_PRIV_KEY): cv.string, + } +) + + +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Set up the ATEN PE switch.""" + node = config[CONF_HOST] + serv = config[CONF_PORT] + + dev = AtenPE( + node=node, + serv=serv, + community=config[CONF_COMMUNITY], + username=config[CONF_USERNAME], + authkey=config.get(CONF_AUTH_KEY), + privkey=config.get(CONF_PRIV_KEY), + ) + + try: + await hass.async_add_executor_job(dev.initialize) + mac = await dev.deviceMAC() + outlets = dev.outlets() + except AtenPEError as exc: + _LOGGER.error("Failed to initialize %s:%s: %s", node, serv, str(exc)) + raise PlatformNotReady + + switches = [] + async for outlet in outlets: + switches.append(AtenSwitch(dev, mac, outlet.id, outlet.name)) + + async_add_entities(switches) + + +class AtenSwitch(SwitchDevice): + """Represents an ATEN PE switch.""" + + def __init__(self, device, mac, outlet, name): + """Initialize an ATEN PE switch.""" + self._device = device + self._mac = mac + self._outlet = outlet + self._name = name or f"Outlet {outlet}" + self._enabled = False + self._outlet_power = 0.0 + + @property + def unique_id(self) -> str: + """Return a unique ID.""" + return f"{self._mac}-{self._outlet}" + + @property + def name(self) -> str: + """Return the name of the entity.""" + return self._name + + @property + def device_class(self) -> str: + """Return the class of this device, from component DEVICE_CLASSES.""" + return DEVICE_CLASS_OUTLET + + @property + def is_on(self) -> bool: + """Return True if entity is on.""" + return self._enabled + + @property + def current_power_w(self) -> float: + """Return the current power usage in W.""" + return self._outlet_power + + async def async_turn_on(self, **kwargs): + """Turn the switch on.""" + await self._device.setOutletStatus(self._outlet, "on") + self._enabled = True + + async def async_turn_off(self, **kwargs): + """Turn the switch off.""" + await self._device.setOutletStatus(self._outlet, "off") + self._enabled = False + + async def async_update(self): + """Process update from entity.""" + status = await self._device.displayOutletStatus(self._outlet) + if status == "on": + self._enabled = True + self._outlet_power = await self._device.outletPower(self._outlet) + elif status == "off": + self._enabled = False + self._outlet_power = 0.0 diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index 3409ce832ddb..3863ab0c88df 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -24,7 +24,7 @@ from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import condition, extract_domain_configs, script import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.config_validation import ENTITY_SERVICE_SCHEMA +from homeassistant.helpers.config_validation import make_entity_service_schema from homeassistant.helpers.entity import ToggleEntity from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.restore_state import RestoreEntity @@ -106,7 +106,7 @@ def _platform_validator(config): } ) -TRIGGER_SERVICE_SCHEMA = ENTITY_SERVICE_SCHEMA.extend( +TRIGGER_SERVICE_SCHEMA = make_entity_service_schema( {vol.Optional(ATTR_VARIABLES, default={}): dict} ) @@ -184,12 +184,18 @@ async def reload_service_handler(service_call): ) hass.services.async_register( - DOMAIN, SERVICE_TOGGLE, toggle_service_handler, schema=ENTITY_SERVICE_SCHEMA + DOMAIN, + SERVICE_TOGGLE, + toggle_service_handler, + schema=make_entity_service_schema({}), ) for service in (SERVICE_TURN_ON, SERVICE_TURN_OFF): hass.services.async_register( - DOMAIN, service, turn_onoff_service_handler, schema=ENTITY_SERVICE_SCHEMA + DOMAIN, + service, + turn_onoff_service_handler, + schema=make_entity_service_schema({}), ) return True diff --git a/homeassistant/components/aws/__init__.py b/homeassistant/components/aws/__init__.py index 780a65b2d473..b553b7eafd67 100644 --- a/homeassistant/components/aws/__init__.py +++ b/homeassistant/components/aws/__init__.py @@ -12,7 +12,7 @@ from homeassistant.helpers import config_validation as cv, discovery # Loading the config flow file will register the flow -from . import config_flow # noqa +from . import config_flow # noqa: F401 from .const import ( CONF_ACCESS_KEY_ID, CONF_CONTEXT, diff --git a/homeassistant/components/axis/.translations/ru.json b/homeassistant/components/axis/.translations/ru.json index 0345862b865f..24990bb0f1ad 100644 --- a/homeassistant/components/axis/.translations/ru.json +++ b/homeassistant/components/axis/.translations/ru.json @@ -10,7 +10,7 @@ "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", "already_in_progress": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \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." + "faulty_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0443\u0447\u0451\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435." }, "flow_title": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e Axis {name} ({host})", "step": { diff --git a/homeassistant/components/bayesian/binary_sensor.py b/homeassistant/components/bayesian/binary_sensor.py index ffa13a6288ca..1d3720f6723d 100644 --- a/homeassistant/components/bayesian/binary_sensor.py +++ b/homeassistant/components/bayesian/binary_sensor.py @@ -133,7 +133,7 @@ def __init__(self, name, prior, observations, probability_threshold, device_clas to_observe.update(set([obs.get("entity_id")])) if "value_template" in obs: to_observe.update(set(obs.get(CONF_VALUE_TEMPLATE).extract_entities())) - self.entity_obs = dict.fromkeys(to_observe, []) + self.entity_obs = {key: [] for key in to_observe} for ind, obs in enumerate(self._observations): obs["id"] = ind diff --git a/homeassistant/components/bbox/sensor.py b/homeassistant/components/bbox/sensor.py index ad6bcc39796e..7b795a8788e4 100644 --- a/homeassistant/components/bbox/sensor.py +++ b/homeassistant/components/bbox/sensor.py @@ -9,9 +9,15 @@ import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import CONF_NAME, CONF_MONITORED_VARIABLES, ATTR_ATTRIBUTION +from homeassistant.const import ( + CONF_NAME, + CONF_MONITORED_VARIABLES, + ATTR_ATTRIBUTION, + DEVICE_CLASS_TIMESTAMP, +) from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle +from homeassistant.util.dt import utcnow _LOGGER = logging.getLogger(__name__) @@ -45,6 +51,8 @@ BANDWIDTH_MEGABITS_SECONDS, "mdi:upload", ], + "uptime": ["Uptime", None, "mdi:clock"], + "number_of_reboots": ["Number of reboot", None, "mdi:restart"], } PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( @@ -72,11 +80,61 @@ def setup_platform(hass, config, add_entities, discovery_info=None): sensors = [] for variable in config[CONF_MONITORED_VARIABLES]: - sensors.append(BboxSensor(bbox_data, variable, name)) + if variable == "uptime": + sensors.append(BboxUptimeSensor(bbox_data, variable, name)) + else: + sensors.append(BboxSensor(bbox_data, variable, name)) add_entities(sensors, True) +class BboxUptimeSensor(Entity): + """Bbox uptime sensor.""" + + def __init__(self, bbox_data, sensor_type, name): + """Initialize the sensor.""" + self.client_name = name + self.type = sensor_type + self._name = SENSOR_TYPES[sensor_type][0] + self._unit_of_measurement = SENSOR_TYPES[sensor_type][1] + self._icon = SENSOR_TYPES[sensor_type][2] + self.bbox_data = bbox_data + self._state = None + + @property + def name(self): + """Return the name of the sensor.""" + return f"{self.client_name} {self._name}" + + @property + def state(self): + """Return the state of the sensor.""" + return self._state + + @property + def icon(self): + """Icon to use in the frontend, if any.""" + return self._icon + + @property + def device_state_attributes(self): + """Return the state attributes.""" + return {ATTR_ATTRIBUTION: ATTRIBUTION} + + @property + def device_class(self): + """Return the class of this sensor.""" + return DEVICE_CLASS_TIMESTAMP + + def update(self): + """Get the latest data from Bbox and update the state.""" + self.bbox_data.update() + uptime = utcnow() - timedelta( + seconds=self.bbox_data.router_infos["device"]["uptime"] + ) + self._state = uptime.replace(microsecond=0).isoformat() + + class BboxSensor(Entity): """Implementation of a Bbox sensor.""" @@ -126,6 +184,8 @@ def update(self): self._state = round(self.bbox_data.data["rx"]["bandwidth"] / 1000, 2) elif self.type == "current_up_bandwidth": self._state = round(self.bbox_data.data["tx"]["bandwidth"] / 1000, 2) + elif self.type == "number_of_reboots": + self._state = self.bbox_data.router_infos["device"]["numberofboots"] class BboxData: @@ -134,6 +194,7 @@ class BboxData: def __init__(self): """Initialize the data object.""" self.data = None + self.router_infos = None @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): @@ -142,7 +203,9 @@ def update(self): try: box = pybbox.Bbox() self.data = box.get_ip_stats() + self.router_infos = box.get_bbox_info() except requests.exceptions.HTTPError as error: _LOGGER.error(error) self.data = None + self.router_infos = None return False diff --git a/homeassistant/components/binary_sensor/__init__.py b/homeassistant/components/binary_sensor/__init__.py index 9af6a10c4252..e5f5dc94ff12 100644 --- a/homeassistant/components/binary_sensor/__init__.py +++ b/homeassistant/components/binary_sensor/__init__.py @@ -8,7 +8,7 @@ from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.entity import Entity from homeassistant.const import STATE_ON, STATE_OFF -from homeassistant.helpers.config_validation import ( # noqa +from homeassistant.helpers.config_validation import ( # noqa: F401 PLATFORM_SCHEMA, PLATFORM_SCHEMA_BASE, ) diff --git a/homeassistant/components/blackbird/const.py b/homeassistant/components/blackbird/const.py new file mode 100644 index 000000000000..aa8d7e7d514e --- /dev/null +++ b/homeassistant/components/blackbird/const.py @@ -0,0 +1,3 @@ +"""Constants for the Monoprice Blackbird Matrix Switch component.""" +DOMAIN = "blackbird" +SERVICE_SETALLZONES = "set_all_zones" diff --git a/homeassistant/components/blackbird/media_player.py b/homeassistant/components/blackbird/media_player.py index e1aa7200c073..08efc1e66471 100644 --- a/homeassistant/components/blackbird/media_player.py +++ b/homeassistant/components/blackbird/media_player.py @@ -8,7 +8,6 @@ from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice from homeassistant.components.media_player.const import ( - DOMAIN, SUPPORT_SELECT_SOURCE, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, @@ -23,6 +22,7 @@ STATE_ON, ) import homeassistant.helpers.config_validation as cv +from .const import DOMAIN, SERVICE_SETALLZONES _LOGGER = logging.getLogger(__name__) @@ -39,7 +39,6 @@ DATA_BLACKBIRD = "blackbird" -SERVICE_SETALLZONES = "blackbird_set_all_zones" ATTR_SOURCE = "source" BLACKBIRD_SETALLZONES_SCHEMA = MEDIA_PLAYER_SCHEMA.extend( diff --git a/homeassistant/components/blackbird/services.yaml b/homeassistant/components/blackbird/services.yaml index e69de29bb2d1..d541e21049da 100644 --- a/homeassistant/components/blackbird/services.yaml +++ b/homeassistant/components/blackbird/services.yaml @@ -0,0 +1,10 @@ +set_all_zones: + description: Set all Blackbird zones to a single source. + fields: + entity_id: + description: Name of any blackbird zone. + example: 'media_player.zone_1' + source: + description: Name of source to switch to. + example: 'Source 1' + diff --git a/homeassistant/components/blink/alarm_control_panel.py b/homeassistant/components/blink/alarm_control_panel.py index b1c9f6a7ec07..9b23c1606d43 100644 --- a/homeassistant/components/blink/alarm_control_panel.py +++ b/homeassistant/components/blink/alarm_control_panel.py @@ -2,6 +2,7 @@ import logging from homeassistant.components.alarm_control_panel import AlarmControlPanel +from homeassistant.components.alarm_control_panel.const import SUPPORT_ALARM_ARM_AWAY from homeassistant.const import ( ATTR_ATTRIBUTION, STATE_ALARM_ARMED_AWAY, @@ -52,6 +53,11 @@ def state(self): """Return the state of the device.""" return self._state + @property + def supported_features(self) -> int: + """Return the list of supported features.""" + return SUPPORT_ALARM_ARM_AWAY + @property def name(self): """Return the name of the panel.""" diff --git a/homeassistant/components/bluesound/const.py b/homeassistant/components/bluesound/const.py new file mode 100644 index 000000000000..af1a8e5187c2 --- /dev/null +++ b/homeassistant/components/bluesound/const.py @@ -0,0 +1,6 @@ +"""Constants for the Bluesound HiFi wireless speakers and audio integrations component.""" +DOMAIN = "bluesound" +SERVICE_CLEAR_TIMER = "clear_sleep_timer" +SERVICE_JOIN = "join" +SERVICE_SET_TIMER = "set_sleep_timer" +SERVICE_UNJOIN = "unjoin" diff --git a/homeassistant/components/bluesound/media_player.py b/homeassistant/components/bluesound/media_player.py index 7b2719c1e4e9..5a9f3561dc93 100644 --- a/homeassistant/components/bluesound/media_player.py +++ b/homeassistant/components/bluesound/media_player.py @@ -15,7 +15,6 @@ from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice from homeassistant.components.media_player.const import ( ATTR_MEDIA_ENQUEUE, - DOMAIN, MEDIA_TYPE_MUSIC, SUPPORT_CLEAR_PLAYLIST, SUPPORT_NEXT_TRACK, @@ -50,6 +49,13 @@ from homeassistant.helpers.event import async_track_time_interval from homeassistant.util import Throttle import homeassistant.util.dt as dt_util +from .const import ( + DOMAIN, + SERVICE_CLEAR_TIMER, + SERVICE_JOIN, + SERVICE_SET_TIMER, + SERVICE_UNJOIN, +) _LOGGER = logging.getLogger(__name__) @@ -62,10 +68,6 @@ NODE_OFFLINE_CHECK_TIMEOUT = 180 NODE_RETRY_INITIATION = timedelta(minutes=3) -SERVICE_CLEAR_TIMER = "bluesound_clear_sleep_timer" -SERVICE_JOIN = "bluesound_join" -SERVICE_SET_TIMER = "bluesound_set_sleep_timer" -SERVICE_UNJOIN = "bluesound_unjoin" STATE_GROUPED = "grouped" SYNC_STATUS_INTERVAL = timedelta(minutes=5) diff --git a/homeassistant/components/bluesound/services.yaml b/homeassistant/components/bluesound/services.yaml index e69de29bb2d1..6c85c77e961c 100644 --- a/homeassistant/components/bluesound/services.yaml +++ b/homeassistant/components/bluesound/services.yaml @@ -0,0 +1,30 @@ +join: + description: Group player together. + fields: + master: + description: Entity ID of the player that should become the master of the group. + example: 'media_player.bluesound_livingroom' + entity_id: + description: Name(s) of entities that will coordinate the grouping. Platform dependent. + example: 'media_player.bluesound_livingroom' + +unjoin: + description: Unjoin the player from a group. + fields: + entity_id: + description: Name(s) of entities that will be unjoined from their group. Platform dependent. + example: 'media_player.bluesound_livingroom' + +set_sleep_timer: + description: "Set a Bluesound timer. It will increase timer in steps: 15, 30, 45, 60, 90, 0" + fields: + entity_id: + description: Name(s) of entities that will have a timer set. + example: 'media_player.bluesound_livingroom' + +clear_sleep_timer: + description: Clear a Bluesound timer. + fields: + entity_id: + description: Name(s) of entities that will have the timer cleared. + example: 'media_player.bluesound_livingroom' \ No newline at end of file diff --git a/homeassistant/components/bluetooth_tracker/const.py b/homeassistant/components/bluetooth_tracker/const.py new file mode 100644 index 000000000000..b481efa296f7 --- /dev/null +++ b/homeassistant/components/bluetooth_tracker/const.py @@ -0,0 +1,3 @@ +"""Constants for the Bluetooth Tracker component.""" +DOMAIN = "bluetooth_tracker" +SERVICE_UPDATE = "update" diff --git a/homeassistant/components/bluetooth_tracker/device_tracker.py b/homeassistant/components/bluetooth_tracker/device_tracker.py index 6a26775b0a8a..102c8e494aa4 100644 --- a/homeassistant/components/bluetooth_tracker/device_tracker.py +++ b/homeassistant/components/bluetooth_tracker/device_tracker.py @@ -13,7 +13,6 @@ CONF_SCAN_INTERVAL, CONF_TRACK_NEW, DEFAULT_TRACK_NEW, - DOMAIN, SCAN_INTERVAL, SOURCE_TYPE_BLUETOOTH, ) @@ -25,6 +24,7 @@ from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.typing import HomeAssistantType +from .const import DOMAIN, SERVICE_UPDATE _LOGGER = logging.getLogger(__name__) @@ -184,8 +184,6 @@ async def handle_manual_update_bluetooth(call): 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 - ) + hass.services.async_register(DOMAIN, SERVICE_UPDATE, handle_manual_update_bluetooth) return True diff --git a/homeassistant/components/bluetooth_tracker/services.yaml b/homeassistant/components/bluetooth_tracker/services.yaml index e69de29bb2d1..b48c48a89680 100644 --- a/homeassistant/components/bluetooth_tracker/services.yaml +++ b/homeassistant/components/bluetooth_tracker/services.yaml @@ -0,0 +1,2 @@ +update: + description: Trigger manual tracker update \ No newline at end of file diff --git a/homeassistant/components/braviatv/media_player.py b/homeassistant/components/braviatv/media_player.py index 5072cb7b74c2..d0458541f7be 100644 --- a/homeassistant/components/braviatv/media_player.py +++ b/homeassistant/components/braviatv/media_player.py @@ -2,10 +2,11 @@ import ipaddress import logging +from braviarc.braviarc import BraviaRC from getmac import get_mac_address import voluptuous as vol -from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA +from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice from homeassistant.components.media_player.const import ( SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, @@ -128,12 +129,11 @@ def request_configuration(config, hass, add_entities): def bravia_configuration_callback(data): """Handle the entry of user PIN.""" - from braviarc import braviarc pin = data.get("pin") - braviarc = braviarc.BraviaRC(host) - braviarc.connect(pin, CLIENTID_PREFIX, NICKNAME) - if braviarc.is_connected(): + _braviarc = BraviaRC(host) + _braviarc.connect(pin, CLIENTID_PREFIX, NICKNAME) + if _braviarc.is_connected(): setup_bravia(config, pin, hass, add_entities) else: request_configuration(config, hass, add_entities) @@ -154,10 +154,9 @@ class BraviaTVDevice(MediaPlayerDevice): def __init__(self, host, mac, name, pin): """Initialize the Sony Bravia device.""" - from braviarc import braviarc self._pin = pin - self._braviarc = braviarc.BraviaRC(host, mac) + self._braviarc = BraviaRC(host, mac) self._name = name self._state = STATE_OFF self._muted = False diff --git a/homeassistant/components/broadlink/__init__.py b/homeassistant/components/broadlink/__init__.py index 589da62feaa1..521cd68780c6 100644 --- a/homeassistant/components/broadlink/__init__.py +++ b/homeassistant/components/broadlink/__init__.py @@ -1,7 +1,9 @@ """The broadlink component.""" import asyncio from base64 import b64decode, b64encode +from binascii import unhexlify import logging +import re import socket from datetime import timedelta @@ -27,6 +29,31 @@ def data_packet(value): return b64decode(value) +def hostname(value): + """Validate a hostname.""" + host = str(value).lower() + if len(host) > 253: + raise ValueError + if host[-1] == ".": + host = host[:-1] + allowed = re.compile(r"(?!-)[a-z\d-]{1,63}(? int: + """Return the list of supported features.""" + return SUPPORT_ALARM_ARM_HOME | SUPPORT_ALARM_ARM_AWAY | SUPPORT_ALARM_ARM_NIGHT + @property def device_state_attributes(self): """Return the state attributes.""" diff --git a/homeassistant/components/canary/sensor.py b/homeassistant/components/canary/sensor.py index 6bb01c9d1148..67654c99f3eb 100644 --- a/homeassistant/components/canary/sensor.py +++ b/homeassistant/components/canary/sensor.py @@ -1,4 +1,5 @@ """Support for Canary sensors.""" +from canary.api import SensorType from homeassistant.const import TEMP_CELSIUS from homeassistant.helpers.entity import Entity @@ -103,8 +104,6 @@ def update(self): """Get the latest state of the sensor.""" self._data.update() - from canary.api import SensorType - canary_sensor_type = None if self._sensor_type[0] == "air_quality": canary_sensor_type = SensorType.AIR_QUALITY diff --git a/homeassistant/components/cast/helpers.py b/homeassistant/components/cast/helpers.py index ea5c77ebc1af..e82f6c9e4ed1 100644 --- a/homeassistant/components/cast/helpers.py +++ b/homeassistant/components/cast/helpers.py @@ -150,7 +150,6 @@ def __init__(self, cast_device, chromecast, mz_mgr): chromecast.register_status_listener(self) chromecast.socket_client.media_controller.register_status_listener(self) chromecast.register_connection_listener(self) - # pylint: disable=protected-access if cast_device._cast_info.is_audio_group: self._mz_mgr.add_multizone(chromecast) else: diff --git a/homeassistant/components/channels/const.py b/homeassistant/components/channels/const.py new file mode 100644 index 000000000000..5ae7fdebb0b1 --- /dev/null +++ b/homeassistant/components/channels/const.py @@ -0,0 +1,5 @@ +"""Constants for the Channels component.""" +DOMAIN = "channels" +SERVICE_SEEK_FORWARD = "seek_forward" +SERVICE_SEEK_BACKWARD = "seek_backward" +SERVICE_SEEK_BY = "seek_by" diff --git a/homeassistant/components/channels/media_player.py b/homeassistant/components/channels/media_player.py index 6d978a5451e3..e4acc2f907c8 100644 --- a/homeassistant/components/channels/media_player.py +++ b/homeassistant/components/channels/media_player.py @@ -6,7 +6,6 @@ from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice from homeassistant.components.media_player.const import ( - DOMAIN, MEDIA_TYPE_CHANNEL, MEDIA_TYPE_EPISODE, MEDIA_TYPE_MOVIE, @@ -31,6 +30,8 @@ ) import homeassistant.helpers.config_validation as cv +from .const import DOMAIN, SERVICE_SEEK_BACKWARD, SERVICE_SEEK_BY, SERVICE_SEEK_FORWARD + _LOGGER = logging.getLogger(__name__) DATA_CHANNELS = "channels" @@ -56,9 +57,6 @@ } ) -SERVICE_SEEK_FORWARD = "channels_seek_forward" -SERVICE_SEEK_BACKWARD = "channels_seek_backward" -SERVICE_SEEK_BY = "channels_seek_by" # Service call validation schemas ATTR_SECONDS = "seconds" diff --git a/homeassistant/components/channels/services.yaml b/homeassistant/components/channels/services.yaml index e69de29bb2d1..cbb1dd201a67 100644 --- a/homeassistant/components/channels/services.yaml +++ b/homeassistant/components/channels/services.yaml @@ -0,0 +1,23 @@ +seek_forward: + description: Seek forward by a set number of seconds. + fields: + entity_id: + description: Name of entity for the instance of Channels to seek in. + example: 'media_player.family_room_channels' + +seek_backward: + description: Seek backward by a set number of seconds. + fields: + entity_id: + description: Name of entity for the instance of Channels to seek in. + example: 'media_player.family_room_channels' + +seek_by: + description: Seek by an inputted number of seconds. + fields: + entity_id: + description: Name of entity for the instance of Channels to seek in. + example: 'media_player.family_room_channels' + seconds: + description: Number of seconds to seek by. Negative numbers seek backwards. + example: 120 diff --git a/homeassistant/components/clementine/media_player.py b/homeassistant/components/clementine/media_player.py index 37ed97915c78..9e05b831359f 100644 --- a/homeassistant/components/clementine/media_player.py +++ b/homeassistant/components/clementine/media_player.py @@ -3,9 +3,10 @@ import logging import time +from clementineremote import ClementineRemote import voluptuous as vol -from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA +from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice from homeassistant.components.media_player.const import ( MEDIA_TYPE_MUSIC, SUPPORT_NEXT_TRACK, @@ -56,7 +57,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Clementine platform.""" - from clementineremote import ClementineRemote host = config.get(CONF_HOST) port = config.get(CONF_PORT) diff --git a/homeassistant/components/climate/.translations/bg.json b/homeassistant/components/climate/.translations/bg.json new file mode 100644 index 000000000000..d7901d298844 --- /dev/null +++ b/homeassistant/components/climate/.translations/bg.json @@ -0,0 +1,17 @@ +{ + "device_automation": { + "action_type": { + "set_hvac_mode": "\u041f\u0440\u043e\u043c\u044f\u043d\u0430 \u043d\u0430 \u0440\u0435\u0436\u0438\u043c \u043d\u0430 \u041e\u0412\u041a \u043d\u0430 {entity_name}", + "set_preset_mode": "\u041f\u0440\u043e\u043c\u0435\u043d\u0438 \u043f\u0440\u0435\u0434\u0432\u0430\u0440\u0438\u0442\u0435\u043b\u043d\u043e \u0437\u0430\u0434\u0430\u0434\u0435\u043d \u0440\u0435\u0436\u0438\u043c \u043d\u0430 {entity_name}" + }, + "condtion_type": { + "is_hvac_mode": "{entity_name} \u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d \u043d\u0430 \u0441\u043f\u0435\u0446\u0438\u0444\u0438\u0447\u0435\u043d \u041e\u0412\u041a \u0440\u0435\u0436\u0438\u043c", + "is_preset_mode": "{entity_name} \u0435 \u0432 \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0435\u043d \u043f\u0440\u0435\u0434\u0432\u0430\u0440\u0438\u0442\u0435\u043b\u043d\u043e \u0437\u0430\u0434\u0430\u0434\u0435\u043d \u0440\u0435\u0436\u0438\u043c" + }, + "trigger_type": { + "current_humidity_changed": "{entity_name} \u0438\u0437\u043c\u0435\u0440\u0435\u043d\u0430\u0442\u0430 \u0432\u043b\u0430\u0436\u043d\u043e\u0441\u0442 \u0441\u0435 \u043f\u0440\u043e\u043c\u0435\u043d\u0438", + "current_temperature_changed": "{entity_name} \u0438\u0437\u043c\u0435\u0440\u0435\u043d\u0430\u0442\u0430 \u0442\u0435\u043c\u043f\u0435\u0440\u0430\u0442\u0443\u0440\u0430 \u0441\u0435 \u043f\u0440\u043e\u043c\u0435\u043d\u0438", + "hvac_mode_changed": "{entity_name} \u0420\u0435\u0436\u0438\u043c \u043d\u0430 \u041e\u0412\u041a \u0441\u0435 \u043f\u0440\u043e\u043c\u0435\u043d\u0438" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/climate/.translations/ca.json b/homeassistant/components/climate/.translations/ca.json index 7aff7383a39d..743729041ab2 100644 --- a/homeassistant/components/climate/.translations/ca.json +++ b/homeassistant/components/climate/.translations/ca.json @@ -1,7 +1,8 @@ { "device_automation": { "action_type": { - "set_hvac_mode": "Canvia el mode HVAC de {entity_name}" + "set_hvac_mode": "Canvia el mode HVAC de {entity_name}", + "set_preset_mode": "Canvia la configuraci\u00f3 preestablerta de {entity_name}" }, "condtion_type": { "is_hvac_mode": "{entity_name} est\u00e0 configurat/ada en un mode HVAC espec\u00edfic", diff --git a/homeassistant/components/climate/.translations/de.json b/homeassistant/components/climate/.translations/de.json new file mode 100644 index 000000000000..75ffe328fc82 --- /dev/null +++ b/homeassistant/components/climate/.translations/de.json @@ -0,0 +1,8 @@ +{ + "device_automation": { + "trigger_type": { + "current_humidity_changed": "Gemessene Luftfeuchtigkeit von {entity_name} ge\u00e4ndert", + "current_temperature_changed": "Gemessene Temperatur von {entity_name} ge\u00e4ndert" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/climate/.translations/fr.json b/homeassistant/components/climate/.translations/fr.json index d82a3644493a..db29f8424d5f 100644 --- a/homeassistant/components/climate/.translations/fr.json +++ b/homeassistant/components/climate/.translations/fr.json @@ -1,8 +1,13 @@ { "device_automation": { "action_type": { + "set_hvac_mode": "Changer le mode HVAC sur {entity_name}.", "set_preset_mode": "Changer les pr\u00e9r\u00e9glages de {entity_name}" }, + "condtion_type": { + "is_hvac_mode": "{entity_name} est d\u00e9fini sur un mode HVAC sp\u00e9cifique", + "is_preset_mode": "{entity_name} est d\u00e9fini sur un mode pr\u00e9d\u00e9fini sp\u00e9cifique" + }, "trigger_type": { "current_humidity_changed": "Changement d'humidit\u00e9 mesur\u00e9e pour {entity_name}", "current_temperature_changed": "Changement de temp\u00e9rature mesur\u00e9e pour {entity_name}", diff --git a/homeassistant/components/climate/.translations/pl.json b/homeassistant/components/climate/.translations/pl.json new file mode 100644 index 000000000000..c5b0c483ca93 --- /dev/null +++ b/homeassistant/components/climate/.translations/pl.json @@ -0,0 +1,17 @@ +{ + "device_automation": { + "action_type": { + "set_hvac_mode": "zmie\u0144 tryb HVAC na {entity_name}", + "set_preset_mode": "zmie\u0144 ustawienia dla {entity_name}" + }, + "condtion_type": { + "is_hvac_mode": "na {entity_name} jest ustawiony okre\u015blony tryb HVAC", + "is_preset_mode": "na {entity_name} jest okre\u015blone ustawienie" + }, + "trigger_type": { + "current_humidity_changed": "zmieni si\u0119 zmierzona wilgotno\u015b\u0107 {entity_name}", + "current_temperature_changed": "zmieni si\u0119 zmierzona temperatura {entity_name}", + "hvac_mode_changed": "zmieni si\u0119 tryb HVAC {entity_name}" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/climate/__init__.py b/homeassistant/components/climate/__init__.py index 0c8d6103b123..6006b2a9a3b1 100644 --- a/homeassistant/components/climate/__init__.py +++ b/homeassistant/components/climate/__init__.py @@ -1,4 +1,5 @@ """Provides functionality to interact with climate devices.""" +from abc import abstractmethod from datetime import timedelta import functools as ft import logging @@ -17,8 +18,8 @@ TEMP_CELSIUS, ) import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.config_validation import ( # noqa - ENTITY_SERVICE_SCHEMA, +from homeassistant.helpers.config_validation import ( # noqa: F401 + make_entity_service_schema, PLATFORM_SCHEMA, PLATFORM_SCHEMA_BASE, ) @@ -83,38 +84,19 @@ _LOGGER = logging.getLogger(__name__) -SET_AUX_HEAT_SCHEMA = ENTITY_SERVICE_SCHEMA.extend( - {vol.Required(ATTR_AUX_HEAT): cv.boolean} -) -SET_TEMPERATURE_SCHEMA = vol.Schema( - vol.All( - cv.has_at_least_one_key( - ATTR_TEMPERATURE, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW - ), - ENTITY_SERVICE_SCHEMA.extend( - { - vol.Exclusive(ATTR_TEMPERATURE, "temperature"): vol.Coerce(float), - vol.Inclusive(ATTR_TARGET_TEMP_HIGH, "temperature"): vol.Coerce(float), - vol.Inclusive(ATTR_TARGET_TEMP_LOW, "temperature"): vol.Coerce(float), - vol.Optional(ATTR_HVAC_MODE): vol.In(HVAC_MODES), - } - ), - ) -) -SET_FAN_MODE_SCHEMA = ENTITY_SERVICE_SCHEMA.extend( - {vol.Required(ATTR_FAN_MODE): cv.string} -) -SET_PRESET_MODE_SCHEMA = ENTITY_SERVICE_SCHEMA.extend( - {vol.Required(ATTR_PRESET_MODE): cv.string} -) -SET_HVAC_MODE_SCHEMA = ENTITY_SERVICE_SCHEMA.extend( - {vol.Required(ATTR_HVAC_MODE): vol.In(HVAC_MODES)} -) -SET_HUMIDITY_SCHEMA = ENTITY_SERVICE_SCHEMA.extend( - {vol.Required(ATTR_HUMIDITY): vol.Coerce(float)} -) -SET_SWING_MODE_SCHEMA = ENTITY_SERVICE_SCHEMA.extend( - {vol.Required(ATTR_SWING_MODE): cv.string} + +SET_TEMPERATURE_SCHEMA = vol.All( + cv.has_at_least_one_key( + ATTR_TEMPERATURE, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW + ), + make_entity_service_schema( + { + vol.Exclusive(ATTR_TEMPERATURE, "temperature"): vol.Coerce(float), + vol.Inclusive(ATTR_TARGET_TEMP_HIGH, "temperature"): vol.Coerce(float), + vol.Inclusive(ATTR_TARGET_TEMP_LOW, "temperature"): vol.Coerce(float), + vol.Optional(ATTR_HVAC_MODE): vol.In(HVAC_MODES), + } + ), ) @@ -125,32 +107,40 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: ) await component.async_setup(config) + component.async_register_entity_service(SERVICE_TURN_ON, {}, "async_turn_on") + component.async_register_entity_service(SERVICE_TURN_OFF, {}, "async_turn_off") component.async_register_entity_service( - SERVICE_TURN_ON, ENTITY_SERVICE_SCHEMA, "async_turn_on" - ) - component.async_register_entity_service( - SERVICE_TURN_OFF, ENTITY_SERVICE_SCHEMA, "async_turn_off" + SERVICE_SET_HVAC_MODE, + {vol.Required(ATTR_HVAC_MODE): vol.In(HVAC_MODES)}, + "async_set_hvac_mode", ) component.async_register_entity_service( - SERVICE_SET_HVAC_MODE, SET_HVAC_MODE_SCHEMA, "async_set_hvac_mode" + SERVICE_SET_PRESET_MODE, + {vol.Required(ATTR_PRESET_MODE): cv.string}, + "async_set_preset_mode", ) component.async_register_entity_service( - SERVICE_SET_PRESET_MODE, SET_PRESET_MODE_SCHEMA, "async_set_preset_mode" + SERVICE_SET_AUX_HEAT, + {vol.Required(ATTR_AUX_HEAT): cv.boolean}, + async_service_aux_heat, ) component.async_register_entity_service( - SERVICE_SET_AUX_HEAT, SET_AUX_HEAT_SCHEMA, async_service_aux_heat + SERVICE_SET_TEMPERATURE, SET_TEMPERATURE_SCHEMA, async_service_temperature_set, ) component.async_register_entity_service( - SERVICE_SET_TEMPERATURE, SET_TEMPERATURE_SCHEMA, async_service_temperature_set + SERVICE_SET_HUMIDITY, + {vol.Required(ATTR_HUMIDITY): vol.Coerce(float)}, + "async_set_humidity", ) component.async_register_entity_service( - SERVICE_SET_HUMIDITY, SET_HUMIDITY_SCHEMA, "async_set_humidity" + SERVICE_SET_FAN_MODE, + {vol.Required(ATTR_FAN_MODE): cv.string}, + "async_set_fan_mode", ) component.async_register_entity_service( - SERVICE_SET_FAN_MODE, SET_FAN_MODE_SCHEMA, "async_set_fan_mode" - ) - component.async_register_entity_service( - SERVICE_SET_SWING_MODE, SET_SWING_MODE_SCHEMA, "async_set_swing_mode" + SERVICE_SET_SWING_MODE, + {vol.Required(ATTR_SWING_MODE): cv.string}, + "async_set_swing_mode", ) return True @@ -270,20 +260,20 @@ def target_humidity(self) -> Optional[int]: return None @property + @abstractmethod def hvac_mode(self) -> str: """Return hvac operation ie. heat, cool mode. Need to be one of HVAC_MODE_*. """ - raise NotImplementedError() @property + @abstractmethod def hvac_modes(self) -> List[str]: """Return the list of available hvac operation modes. Need to be a subset of HVAC_MODES. """ - raise NotImplementedError() @property def hvac_action(self) -> Optional[str]: diff --git a/homeassistant/components/climate/device_action.py b/homeassistant/components/climate/device_action.py index b53109f69cb7..836e2277461f 100644 --- a/homeassistant/components/climate/device_action.py +++ b/homeassistant/components/climate/device_action.py @@ -59,14 +59,15 @@ async def async_get_actions(hass: HomeAssistant, device_id: str) -> List[dict]: CONF_TYPE: "set_hvac_mode", } ) - actions.append( - { - CONF_DEVICE_ID: device_id, - CONF_DOMAIN: DOMAIN, - CONF_ENTITY_ID: entry.entity_id, - CONF_TYPE: "set_preset_mode", - } - ) + if state.attributes["supported_features"] & const.SUPPORT_PRESET_MODE: + actions.append( + { + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + CONF_TYPE: "set_preset_mode", + } + ) return actions diff --git a/homeassistant/components/climate/device_condition.py b/homeassistant/components/climate/device_condition.py index c923f3123f15..3a0752339429 100644 --- a/homeassistant/components/climate/device_condition.py +++ b/homeassistant/components/climate/device_condition.py @@ -61,7 +61,7 @@ async def async_get_conditions( } ) - if state and const.ATTR_PRESET_MODES in state.attributes: + if state and state.attributes["supported_features"] & const.SUPPORT_PRESET_MODE: conditions.append( { CONF_CONDITION: "device", diff --git a/homeassistant/components/climate/services.yaml b/homeassistant/components/climate/services.yaml index f10e1b4bd69d..34e89d573468 100644 --- a/homeassistant/components/climate/services.yaml +++ b/homeassistant/components/climate/services.yaml @@ -9,6 +9,7 @@ set_aux_heat: aux_heat: description: New value of axillary heater. example: true + set_preset_mode: description: Set preset mode for climate device. fields: @@ -18,6 +19,7 @@ set_preset_mode: preset_mode: description: New value of preset mode example: 'away' + set_temperature: description: Set target temperature of climate device. fields: @@ -36,6 +38,7 @@ set_temperature: hvac_mode: description: HVAC operation mode to set temperature to. example: 'heat' + set_humidity: description: Set target humidity of climate device. fields: @@ -45,6 +48,7 @@ set_humidity: humidity: description: New target humidity for climate device. example: 60 + set_fan_mode: description: Set fan operation for climate device. fields: @@ -54,6 +58,7 @@ set_fan_mode: fan_mode: description: New value of fan mode. example: On Low + set_hvac_mode: description: Set HVAC operation mode for climate device. fields: @@ -63,6 +68,7 @@ set_hvac_mode: hvac_mode: description: New value of operation mode. example: heat + set_swing_mode: description: Set swing operation for climate device. fields: @@ -72,29 +78,6 @@ set_swing_mode: swing_mode: description: New value of swing mode. -mill_set_room_temperature: - description: Set Mill room temperatures. - fields: - room_name: - description: Name of room to change. - example: 'kitchen' - away_temp: - description: Away temp. - example: 12 - comfort_temp: - description: Comfort temp. - example: 22 - sleep_temp: - description: Sleep temp. - example: 17 - -nuheat_resume_program: - description: Resume the programmed schedule. - fields: - entity_id: - description: Name(s) of entities to change. - example: 'climate.kitchen' - turn_on: description: Turn climate device on. fields: diff --git a/homeassistant/components/cloud/__init__.py b/homeassistant/components/cloud/__init__.py index 763f62141855..6d9b70051f5d 100644 --- a/homeassistant/components/cloud/__init__.py +++ b/homeassistant/components/cloud/__init__.py @@ -4,7 +4,6 @@ from hass_nabucasa import Cloud import voluptuous as vol -from homeassistant.auth.const import GROUP_ID_ADMIN from homeassistant.components.alexa import const as alexa_const from homeassistant.components.google_assistant import const as ga_c from homeassistant.const import ( @@ -186,19 +185,6 @@ async def async_setup(hass, config): prefs = CloudPreferences(hass) await prefs.async_initialize() - # Cloud user - user = None - if prefs.cloud_user: - # Fetch the user. It can happen that the user no longer exists if - # an image was restored without restoring the cloud prefs. - user = await hass.auth.async_get_user(prefs.cloud_user) - - if user is None: - user = await hass.auth.async_create_system_user( - "Home Assistant Cloud", [GROUP_ID_ADMIN] - ) - await prefs.async_update(cloud_user=user.id) - # Initialize Cloud websession = hass.helpers.aiohttp_client.async_get_clientsession() client = CloudClient(hass, prefs, websession, alexa_conf, google_conf) diff --git a/homeassistant/components/cloud/client.py b/homeassistant/components/cloud/client.py index c7626777943d..956d35caf2d7 100644 --- a/homeassistant/components/cloud/client.py +++ b/homeassistant/components/cloud/client.py @@ -7,7 +7,7 @@ import aiohttp from hass_nabucasa.client import CloudClient as Interface -from homeassistant.core import callback +from homeassistant.core import callback, Context from homeassistant.components.google_assistant import smart_home as ga from homeassistant.helpers.typing import HomeAssistantType from homeassistant.helpers.dispatcher import async_dispatcher_send @@ -44,7 +44,6 @@ def __init__( self.alexa_user_config = alexa_user_config self._alexa_config = None self._google_config = None - self.cloud = None @property def base_path(self) -> Path: @@ -92,23 +91,23 @@ def alexa_config(self) -> alexa_config.AlexaConfig: return self._alexa_config - @property - def google_config(self) -> google_config.CloudGoogleConfig: + async def get_google_config(self) -> google_config.CloudGoogleConfig: """Return Google config.""" if not self._google_config: assert self.cloud is not None + + cloud_user = await self._prefs.get_cloud_user() + self._google_config = google_config.CloudGoogleConfig( - self._hass, self.google_user_config, self._prefs, self.cloud + self._hass, self.google_user_config, cloud_user, self._prefs, self.cloud ) + await self._google_config.async_initialize() return self._google_config - async def async_initialize(self, cloud) -> None: - """Initialize the client.""" - self.cloud = cloud - - if not self.cloud.is_logged_in: - return + async def logged_in(self) -> None: + """When user logs in.""" + await self.prefs.async_set_username(self.cloud.username) if self.alexa_config.enabled and self.alexa_config.should_report_state: try: @@ -116,14 +115,18 @@ async def async_initialize(self, cloud) -> None: except alexa_errors.NoTokenAvailable: pass - if self.google_config.enabled: - self.google_config.async_enable_local_sdk() + if self._prefs.google_enabled: + gconf = await self.get_google_config() - if self.google_config.should_report_state: - self.google_config.async_enable_report_state() + gconf.async_enable_local_sdk() + + if gconf.should_report_state: + gconf.async_enable_report_state() async def cleanups(self) -> None: """Cleanup some stuff after logout.""" + await self.prefs.async_set_username(None) + self._google_config = None @callback @@ -141,8 +144,13 @@ def dispatcher_message(self, identifier: str, data: Any = None) -> None: async def async_alexa_message(self, payload: Dict[Any, Any]) -> Dict[Any, Any]: """Process cloud alexa message to client.""" + cloud_user = await self._prefs.get_cloud_user() return await alexa_sh.async_handle_message( - self._hass, self.alexa_config, payload, enabled=self._prefs.alexa_enabled + self._hass, + self.alexa_config, + payload, + context=Context(user_id=cloud_user), + enabled=self._prefs.alexa_enabled, ) async def async_google_message(self, payload: Dict[Any, Any]) -> Dict[Any, Any]: @@ -150,8 +158,10 @@ async def async_google_message(self, payload: Dict[Any, Any]) -> Dict[Any, Any]: if not self._prefs.google_enabled: return ga.turned_off_response(payload) + gconf = await self.get_google_config() + return await ga.async_handle_message( - self._hass, self.google_config, self.prefs.cloud_user, payload + self._hass, gconf, gconf.cloud_user, payload ) async def async_webhook_message(self, payload: Dict[Any, Any]) -> Dict[Any, Any]: diff --git a/homeassistant/components/cloud/const.py b/homeassistant/components/cloud/const.py index 9a2dccf8d7cb..406263c85f89 100644 --- a/homeassistant/components/cloud/const.py +++ b/homeassistant/components/cloud/const.py @@ -17,6 +17,7 @@ PREF_ALIASES = "aliases" PREF_SHOULD_EXPOSE = "should_expose" PREF_GOOGLE_LOCAL_WEBHOOK_ID = "google_local_webhook_id" +PREF_USERNAME = "username" DEFAULT_SHOULD_EXPOSE = True DEFAULT_DISABLE_2FA = False DEFAULT_ALEXA_REPORT_STATE = False diff --git a/homeassistant/components/cloud/google_config.py b/homeassistant/components/cloud/google_config.py index 582fa0075504..3df06c140a05 100644 --- a/homeassistant/components/cloud/google_config.py +++ b/homeassistant/components/cloud/google_config.py @@ -23,10 +23,11 @@ class CloudGoogleConfig(AbstractConfig): """HA Cloud Configuration for Google Assistant.""" - def __init__(self, hass, config, prefs, cloud): + def __init__(self, hass, config, cloud_user, prefs, cloud): """Initialize the Google config.""" super().__init__(hass) self._config = config + self._user = cloud_user self._prefs = prefs self._cloud = cloud self._cur_entity_prefs = self._prefs.google_entity_configs @@ -41,12 +42,7 @@ def __init__(self, hass, config, prefs, cloud): @property def enabled(self): """Return if Google is enabled.""" - return self._prefs.google_enabled - - @property - def agent_user_id(self): - """Return Agent User Id to use for query responses.""" - return self._cloud.claims["cognito:username"] + return self._cloud.is_logged_in and self._prefs.google_enabled @property def entity_config(self): @@ -61,7 +57,7 @@ def secure_devices_pin(self): @property def should_report_state(self): """Return if states should be proactively reported.""" - return self._prefs.google_report_state + return self._cloud.is_logged_in and self._prefs.google_report_state @property def local_sdk_webhook_id(self): @@ -74,7 +70,12 @@ def local_sdk_webhook_id(self): @property def local_sdk_user_id(self): """Return the user ID to be used for actions received via the local SDK.""" - return self._prefs.cloud_user + return self._user + + @property + def cloud_user(self): + """Return Cloud User account.""" + return self._user def should_expose(self, state): """If a state object should be exposed.""" @@ -98,14 +99,14 @@ def should_2fa(self, state): 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): + async def async_report_state(self, message, agent_user_id: str): """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): + async def _async_request_sync_devices(self, agent_user_id: str): """Trigger a sync with Google.""" if self._sync_entities_lock.locked(): return 200 @@ -126,13 +127,6 @@ async def _async_request_sync_devices(self): _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: @@ -143,7 +137,7 @@ async def _async_prefs_updated(self, prefs): # 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() + await self.async_sync_entities_all() # If entity prefs are the same or we have filter in config.yaml, # don't sync. @@ -151,7 +145,7 @@ async def _async_prefs_updated(self, prefs): self._cur_entity_prefs is not prefs.google_entity_configs and self._config["filter"].empty_filter ): - self.async_schedule_google_sync() + self.async_schedule_google_sync_all() if self.enabled and not self.is_local_sdk_active: self.async_enable_local_sdk() @@ -167,4 +161,4 @@ async def _handle_entity_registry_updated(self, event): # 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() + await self.async_sync_entities_all() diff --git a/homeassistant/components/cloud/http_api.py b/homeassistant/components/cloud/http_api.py index d969612ce8e6..c68f24172f03 100644 --- a/homeassistant/components/cloud/http_api.py +++ b/homeassistant/components/cloud/http_api.py @@ -174,7 +174,8 @@ async def post(self, request): """Trigger a Google Actions sync.""" hass = request.app["hass"] cloud: Cloud = hass.data[DOMAIN] - status = await cloud.client.google_config.async_sync_entities() + gconf = await cloud.client.get_google_config() + status = await gconf.async_sync_entities(gconf.cloud_user) return self.json({}, status_code=status) @@ -192,11 +193,7 @@ async def post(self, request, data): """Handle login request.""" hass = request.app["hass"] cloud = hass.data[DOMAIN] - - with async_timeout.timeout(REQUEST_TIMEOUT): - await hass.async_add_job(cloud.auth.login, data["email"], data["password"]) - - hass.async_add_job(cloud.iot.connect) + await cloud.login(data["email"], data["password"]) return self.json({"success": True}) @@ -477,7 +474,8 @@ async def websocket_remote_disconnect(hass, connection, msg): async def google_assistant_list(hass, connection, msg): """List all google assistant entities.""" cloud = hass.data[DOMAIN] - entities = google_helpers.async_get_entities(hass, cloud.client.google_config) + gconf = await cloud.client.get_google_config() + entities = google_helpers.async_get_entities(hass, gconf) result = [] diff --git a/homeassistant/components/cloud/manifest.json b/homeassistant/components/cloud/manifest.json index 2feef55835ed..ec9a556af0ac 100644 --- a/homeassistant/components/cloud/manifest.json +++ b/homeassistant/components/cloud/manifest.json @@ -2,7 +2,7 @@ "domain": "cloud", "name": "Cloud", "documentation": "https://www.home-assistant.io/integrations/cloud", - "requirements": ["hass-nabucasa==0.29"], + "requirements": ["hass-nabucasa==0.30"], "dependencies": ["http", "webhook"], "codeowners": ["@home-assistant/cloud"] } diff --git a/homeassistant/components/cloud/prefs.py b/homeassistant/components/cloud/prefs.py index 0599b00a8bd8..e96ee9527fb9 100644 --- a/homeassistant/components/cloud/prefs.py +++ b/homeassistant/components/cloud/prefs.py @@ -1,7 +1,10 @@ """Preference management for cloud.""" from ipaddress import ip_address +from typing import Optional from homeassistant.core import callback +from homeassistant.auth.models import User +from homeassistant.auth.const import GROUP_ID_ADMIN from homeassistant.util.logging import async_create_catching_coro from .const import ( @@ -19,6 +22,7 @@ PREF_SHOULD_EXPOSE, PREF_ALEXA_ENTITY_CONFIGS, PREF_ALEXA_REPORT_STATE, + PREF_USERNAME, DEFAULT_ALEXA_REPORT_STATE, PREF_GOOGLE_REPORT_STATE, PREF_GOOGLE_LOCAL_WEBHOOK_ID, @@ -47,16 +51,7 @@ async def async_initialize(self): prefs = await self._store.async_load() if prefs is None: - prefs = { - PREF_ENABLE_ALEXA: True, - PREF_ENABLE_GOOGLE: True, - PREF_ENABLE_REMOTE: False, - PREF_GOOGLE_SECURE_DEVICES_PIN: None, - PREF_GOOGLE_ENTITY_CONFIGS: {}, - PREF_ALEXA_ENTITY_CONFIGS: {}, - PREF_CLOUDHOOKS: {}, - PREF_CLOUD_USER: None, - } + prefs = self._empty_config("") self._prefs = prefs @@ -166,6 +161,27 @@ async def async_update_alexa_entity_config( updated_entities = {**entities, entity_id: updated_entity} await self.async_update(alexa_entity_configs=updated_entities) + async def async_set_username(self, username): + """Set the username that is logged in.""" + # Logging out. + if username is None: + user = await self._load_cloud_user() + + if user is not None: + await self._hass.auth.async_remove_user(user) + await self._save_prefs({**self._prefs, PREF_CLOUD_USER: None}) + return + + cur_username = self._prefs.get(PREF_USERNAME) + + if cur_username == username: + return + + if cur_username is None: + await self._save_prefs({**self._prefs, PREF_USERNAME: username}) + else: + await self._save_prefs(self._empty_config(username)) + def as_dict(self): """Return dictionary version.""" return { @@ -178,7 +194,6 @@ def as_dict(self): 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, } @property @@ -239,10 +254,29 @@ def cloudhooks(self): """Return the published cloud webhooks.""" return self._prefs.get(PREF_CLOUDHOOKS, {}) - @property - def cloud_user(self) -> str: + async def get_cloud_user(self) -> str: """Return ID from Home Assistant Cloud system user.""" - return self._prefs.get(PREF_CLOUD_USER) + user = await self._load_cloud_user() + + if user: + return user.id + + user = await self._hass.auth.async_create_system_user( + "Home Assistant Cloud", [GROUP_ID_ADMIN] + ) + await self.async_update(cloud_user=user.id) + return user.id + + async def _load_cloud_user(self) -> Optional[User]: + """Load cloud user if available.""" + user_id = self._prefs.get(PREF_CLOUD_USER) + + if user_id is None: + return None + + # Fetch the user. It can happen that the user no longer exists if + # an image was restored without restoring the cloud prefs. + return await self._hass.auth.async_get_user(user_id) @property def _has_local_trusted_network(self) -> bool: @@ -283,3 +317,19 @@ async def _save_prefs(self, prefs): for listener in self._listeners: self._hass.async_create_task(async_create_catching_coro(listener(self))) + + @callback + def _empty_config(self, username): + """Return an empty config.""" + return { + PREF_ENABLE_ALEXA: True, + PREF_ENABLE_GOOGLE: True, + PREF_ENABLE_REMOTE: False, + PREF_GOOGLE_SECURE_DEVICES_PIN: None, + PREF_GOOGLE_ENTITY_CONFIGS: {}, + PREF_ALEXA_ENTITY_CONFIGS: {}, + PREF_CLOUDHOOKS: {}, + PREF_CLOUD_USER: None, + PREF_USERNAME: username, + PREF_GOOGLE_LOCAL_WEBHOOK_ID: self._hass.components.webhook.async_generate_id(), + } diff --git a/homeassistant/components/cloudflare/services.yaml b/homeassistant/components/cloudflare/services.yaml index e69de29bb2d1..23ffdd14d5f0 100644 --- a/homeassistant/components/cloudflare/services.yaml +++ b/homeassistant/components/cloudflare/services.yaml @@ -0,0 +1,2 @@ +update_records: + description: Manually trigger update to Cloudflare records. diff --git a/homeassistant/components/comfoconnect/__init__.py b/homeassistant/components/comfoconnect/__init__.py index efdbf020f1ad..f1fd67cc4bbb 100644 --- a/homeassistant/components/comfoconnect/__init__.py +++ b/homeassistant/components/comfoconnect/__init__.py @@ -1,12 +1,7 @@ """Support to control a Zehnder ComfoAir Q350/450/600 ventilation unit.""" import logging -from pycomfoconnect import ( - SENSOR_TEMPERATURE_EXTRACT, - SENSOR_TEMPERATURE_OUTDOOR, - Bridge, - ComfoConnect, -) +from pycomfoconnect import Bridge, ComfoConnect import voluptuous as vol from homeassistant.const import ( @@ -24,14 +19,7 @@ DOMAIN = "comfoconnect" -SIGNAL_COMFOCONNECT_UPDATE_RECEIVED = "comfoconnect_update_received" - -ATTR_CURRENT_TEMPERATURE = "current_temperature" -ATTR_CURRENT_HUMIDITY = "current_humidity" -ATTR_OUTSIDE_TEMPERATURE = "outside_temperature" -ATTR_OUTSIDE_HUMIDITY = "outside_humidity" -ATTR_AIR_FLOW_SUPPLY = "air_flow_supply" -ATTR_AIR_FLOW_EXHAUST = "air_flow_exhaust" +SIGNAL_COMFOCONNECT_UPDATE_RECEIVED = "comfoconnect_update_received_{}" CONF_USER_AGENT = "user_agent" @@ -105,6 +93,7 @@ def __init__(self, hass, bridge, name, token, friendly_name, pin): self.data = {} self.name = name self.hass = hass + self.unique_id = bridge.uuid.hex() self.comfoconnect = ComfoConnect( bridge=bridge, @@ -125,13 +114,8 @@ def disconnect(self): self.comfoconnect.disconnect() def sensor_callback(self, var, value): - """Call function for sensor updates.""" - _LOGGER.debug("Got value from bridge: %d = %d", var, value) - - if var in [SENSOR_TEMPERATURE_EXTRACT, SENSOR_TEMPERATURE_OUTDOOR]: - self.data[var] = value / 10 - else: - self.data[var] = value - - # Notify listeners that we have received an update - dispatcher_send(self.hass, SIGNAL_COMFOCONNECT_UPDATE_RECEIVED, var) + """Notify listeners that we have received an update.""" + _LOGGER.debug("Received update for %s: %s", var, value) + dispatcher_send( + self.hass, SIGNAL_COMFOCONNECT_UPDATE_RECEIVED.format(var), value + ) diff --git a/homeassistant/components/comfoconnect/fan.py b/homeassistant/components/comfoconnect/fan.py index 34e784d61ebe..432b25ac602b 100644 --- a/homeassistant/components/comfoconnect/fan.py +++ b/homeassistant/components/comfoconnect/fan.py @@ -43,24 +43,34 @@ def __init__(self, name, ccb: ComfoConnectBridge) -> None: async def async_added_to_hass(self): """Register for sensor updates.""" + _LOGGER.debug("Registering for fan speed") + async_dispatcher_connect( + self.hass, + SIGNAL_COMFOCONNECT_UPDATE_RECEIVED.format(SENSOR_FAN_SPEED_MODE), + self._handle_update, + ) await self.hass.async_add_executor_job( self._ccb.comfoconnect.register_sensor, SENSOR_FAN_SPEED_MODE ) - async_dispatcher_connect( - self.hass, SIGNAL_COMFOCONNECT_UPDATE_RECEIVED, self._handle_update - ) - def _handle_update(self, var): + def _handle_update(self, value): """Handle update callbacks.""" - if var == SENSOR_FAN_SPEED_MODE: - _LOGGER.debug("Received update for %s", var) - self.schedule_update_ha_state() + _LOGGER.debug( + "Handle update for fan speed (%d): %s", SENSOR_FAN_SPEED_MODE, value + ) + self._ccb.data[SENSOR_FAN_SPEED_MODE] = value + self.schedule_update_ha_state() @property def should_poll(self) -> bool: """Do not poll.""" return False + @property + def unique_id(self): + """Return a unique_id for this entity.""" + return self._ccb.unique_id + @property def name(self): """Return the name of the fan.""" diff --git a/homeassistant/components/comfoconnect/sensor.py b/homeassistant/components/comfoconnect/sensor.py index a1f16ed96311..3e3507ea48dd 100644 --- a/homeassistant/components/comfoconnect/sensor.py +++ b/homeassistant/components/comfoconnect/sensor.py @@ -2,93 +2,214 @@ import logging from pycomfoconnect import ( + SENSOR_BYPASS_STATE, + SENSOR_DAYS_TO_REPLACE_FILTER, + SENSOR_FAN_EXHAUST_DUTY, SENSOR_FAN_EXHAUST_FLOW, + SENSOR_FAN_EXHAUST_SPEED, + SENSOR_FAN_SUPPLY_DUTY, SENSOR_FAN_SUPPLY_FLOW, + SENSOR_FAN_SUPPLY_SPEED, + SENSOR_HUMIDITY_EXHAUST, SENSOR_HUMIDITY_EXTRACT, SENSOR_HUMIDITY_OUTDOOR, + SENSOR_HUMIDITY_SUPPLY, + SENSOR_POWER_CURRENT, + SENSOR_TEMPERATURE_EXHAUST, SENSOR_TEMPERATURE_EXTRACT, SENSOR_TEMPERATURE_OUTDOOR, + SENSOR_TEMPERATURE_SUPPLY, ) +import voluptuous as vol -from homeassistant.const import CONF_RESOURCES, TEMP_CELSIUS +from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import ( + ATTR_DEVICE_CLASS, + CONF_RESOURCES, + DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_POWER, + DEVICE_CLASS_TEMPERATURE, + POWER_WATT, + TEMP_CELSIUS, +) +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import Entity -from . import ( - ATTR_AIR_FLOW_EXHAUST, - ATTR_AIR_FLOW_SUPPLY, - ATTR_CURRENT_HUMIDITY, - ATTR_CURRENT_TEMPERATURE, - ATTR_OUTSIDE_HUMIDITY, - ATTR_OUTSIDE_TEMPERATURE, - DOMAIN, - SIGNAL_COMFOCONNECT_UPDATE_RECEIVED, - ComfoConnectBridge, -) +from . import DOMAIN, SIGNAL_COMFOCONNECT_UPDATE_RECEIVED, ComfoConnectBridge -_LOGGER = logging.getLogger(__name__) +ATTR_AIR_FLOW_EXHAUST = "air_flow_exhaust" +ATTR_AIR_FLOW_SUPPLY = "air_flow_supply" +ATTR_BYPASS_STATE = "bypass_state" +ATTR_CURRENT_HUMIDITY = "current_humidity" +ATTR_CURRENT_TEMPERATURE = "current_temperature" +ATTR_DAYS_TO_REPLACE_FILTER = "days_to_replace_filter" +ATTR_EXHAUST_FAN_DUTY = "exhaust_fan_duty" +ATTR_EXHAUST_FAN_SPEED = "exhaust_fan_speed" +ATTR_EXHAUST_HUMIDITY = "exhaust_humidity" +ATTR_EXHAUST_TEMPERATURE = "exhaust_temperature" +ATTR_OUTSIDE_HUMIDITY = "outside_humidity" +ATTR_OUTSIDE_TEMPERATURE = "outside_temperature" +ATTR_POWER_CURRENT = "power_usage" +ATTR_SUPPLY_FAN_DUTY = "supply_fan_duty" +ATTR_SUPPLY_FAN_SPEED = "supply_fan_speed" +ATTR_SUPPLY_HUMIDITY = "supply_humidity" +ATTR_SUPPLY_TEMPERATURE = "supply_temperature" -SENSOR_TYPES = {} +_LOGGER = logging.getLogger(__name__) +ATTR_ICON = "icon" +ATTR_ID = "id" +ATTR_LABEL = "label" +ATTR_MULTIPLIER = "multiplier" +ATTR_UNIT = "unit" -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the ComfoConnect fan platform.""" +SENSOR_TYPES = { + ATTR_CURRENT_TEMPERATURE: { + ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, + ATTR_LABEL: "Inside Temperature", + ATTR_UNIT: TEMP_CELSIUS, + ATTR_ICON: "mdi:thermometer", + ATTR_ID: SENSOR_TEMPERATURE_EXTRACT, + ATTR_MULTIPLIER: 0.1, + }, + ATTR_CURRENT_HUMIDITY: { + ATTR_DEVICE_CLASS: DEVICE_CLASS_HUMIDITY, + ATTR_LABEL: "Inside Humidity", + ATTR_UNIT: "%", + ATTR_ICON: "mdi:water-percent", + ATTR_ID: SENSOR_HUMIDITY_EXTRACT, + }, + ATTR_OUTSIDE_TEMPERATURE: { + ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, + ATTR_LABEL: "Outside Temperature", + ATTR_UNIT: TEMP_CELSIUS, + ATTR_ICON: "mdi:thermometer", + ATTR_ID: SENSOR_TEMPERATURE_OUTDOOR, + ATTR_MULTIPLIER: 0.1, + }, + ATTR_OUTSIDE_HUMIDITY: { + ATTR_DEVICE_CLASS: DEVICE_CLASS_HUMIDITY, + ATTR_LABEL: "Outside Humidity", + ATTR_UNIT: "%", + ATTR_ICON: "mdi:water-percent", + ATTR_ID: SENSOR_HUMIDITY_OUTDOOR, + }, + ATTR_SUPPLY_TEMPERATURE: { + ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, + ATTR_LABEL: "Supply Temperature", + ATTR_UNIT: TEMP_CELSIUS, + ATTR_ICON: "mdi:thermometer", + ATTR_ID: SENSOR_TEMPERATURE_SUPPLY, + ATTR_MULTIPLIER: 0.1, + }, + ATTR_SUPPLY_HUMIDITY: { + ATTR_DEVICE_CLASS: DEVICE_CLASS_HUMIDITY, + ATTR_LABEL: "Supply Humidity", + ATTR_UNIT: "%", + ATTR_ICON: "mdi:water-percent", + ATTR_ID: SENSOR_HUMIDITY_SUPPLY, + }, + ATTR_SUPPLY_FAN_SPEED: { + ATTR_DEVICE_CLASS: None, + ATTR_LABEL: "Supply Fan Speed", + ATTR_UNIT: "rpm", + ATTR_ICON: "mdi:fan", + ATTR_ID: SENSOR_FAN_SUPPLY_SPEED, + }, + ATTR_SUPPLY_FAN_DUTY: { + ATTR_DEVICE_CLASS: None, + ATTR_LABEL: "Supply Fan Duty", + ATTR_UNIT: "%", + ATTR_ICON: "mdi:fan", + ATTR_ID: SENSOR_FAN_SUPPLY_DUTY, + }, + ATTR_EXHAUST_FAN_SPEED: { + ATTR_DEVICE_CLASS: None, + ATTR_LABEL: "Exhaust Fan Speed", + ATTR_UNIT: "rpm", + ATTR_ICON: "mdi:fan", + ATTR_ID: SENSOR_FAN_EXHAUST_SPEED, + }, + ATTR_EXHAUST_FAN_DUTY: { + ATTR_DEVICE_CLASS: None, + ATTR_LABEL: "Exhaust Fan Duty", + ATTR_UNIT: "%", + ATTR_ICON: "mdi:fan", + ATTR_ID: SENSOR_FAN_EXHAUST_DUTY, + }, + ATTR_EXHAUST_TEMPERATURE: { + ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, + ATTR_LABEL: "Exhaust Temperature", + ATTR_UNIT: TEMP_CELSIUS, + ATTR_ICON: "mdi:thermometer", + ATTR_ID: SENSOR_TEMPERATURE_EXHAUST, + ATTR_MULTIPLIER: 0.1, + }, + ATTR_EXHAUST_HUMIDITY: { + ATTR_DEVICE_CLASS: DEVICE_CLASS_HUMIDITY, + ATTR_LABEL: "Exhaust Humidity", + ATTR_UNIT: "%", + ATTR_ICON: "mdi:water-percent", + ATTR_ID: SENSOR_HUMIDITY_EXHAUST, + }, + ATTR_AIR_FLOW_SUPPLY: { + ATTR_DEVICE_CLASS: None, + ATTR_LABEL: "Supply airflow", + ATTR_UNIT: "m³/h", + ATTR_ICON: "mdi:fan", + ATTR_ID: SENSOR_FAN_SUPPLY_FLOW, + }, + ATTR_AIR_FLOW_EXHAUST: { + ATTR_DEVICE_CLASS: None, + ATTR_LABEL: "Exhaust airflow", + ATTR_UNIT: "m³/h", + ATTR_ICON: "mdi:fan", + ATTR_ID: SENSOR_FAN_EXHAUST_FLOW, + }, + ATTR_BYPASS_STATE: { + ATTR_DEVICE_CLASS: None, + ATTR_LABEL: "Bypass State", + ATTR_UNIT: "%", + ATTR_ICON: "mdi:camera-iris", + ATTR_ID: SENSOR_BYPASS_STATE, + }, + ATTR_DAYS_TO_REPLACE_FILTER: { + ATTR_DEVICE_CLASS: None, + ATTR_LABEL: "Days to replace filter", + ATTR_UNIT: "days", + ATTR_ICON: "mdi:calendar", + ATTR_ID: SENSOR_DAYS_TO_REPLACE_FILTER, + }, + ATTR_POWER_CURRENT: { + ATTR_DEVICE_CLASS: DEVICE_CLASS_POWER, + ATTR_LABEL: "Power usage", + ATTR_UNIT: POWER_WATT, + ATTR_ICON: "mdi:flash", + ATTR_ID: SENSOR_POWER_CURRENT, + }, +} - global SENSOR_TYPES - SENSOR_TYPES = { - ATTR_CURRENT_TEMPERATURE: [ - "Inside Temperature", - TEMP_CELSIUS, - "mdi:thermometer", - SENSOR_TEMPERATURE_EXTRACT, - ], - ATTR_CURRENT_HUMIDITY: [ - "Inside Humidity", - "%", - "mdi:water-percent", - SENSOR_HUMIDITY_EXTRACT, - ], - ATTR_OUTSIDE_TEMPERATURE: [ - "Outside Temperature", - TEMP_CELSIUS, - "mdi:thermometer", - SENSOR_TEMPERATURE_OUTDOOR, - ], - ATTR_OUTSIDE_HUMIDITY: [ - "Outside Humidity", - "%", - "mdi:water-percent", - SENSOR_HUMIDITY_OUTDOOR, - ], - ATTR_AIR_FLOW_SUPPLY: [ - "Supply airflow", - "m³/h", - "mdi:air-conditioner", - SENSOR_FAN_SUPPLY_FLOW, - ], - ATTR_AIR_FLOW_EXHAUST: [ - "Exhaust airflow", - "m³/h", - "mdi:air-conditioner", - SENSOR_FAN_EXHAUST_FLOW, - ], +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Optional(CONF_RESOURCES, default=[]): vol.All( + cv.ensure_list, [vol.In(SENSOR_TYPES)] + ), } +) + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the ComfoConnect fan platform.""" ccb = hass.data[DOMAIN] sensors = [] for resource in config[CONF_RESOURCES]: - sensor_type = resource.lower() - - if sensor_type not in SENSOR_TYPES: - _LOGGER.warning("Sensor type: %s is not a valid sensor", sensor_type) - continue - sensors.append( ComfoConnectSensor( - name=f"{ccb.name} {SENSOR_TYPES[sensor_type][0]}", + name=f"{ccb.name} {SENSOR_TYPES[resource][ATTR_LABEL]}", ccb=ccb, - sensor_type=sensor_type, + sensor_type=resource, ) ) @@ -102,23 +223,35 @@ def __init__(self, name, ccb: ComfoConnectBridge, sensor_type) -> None: """Initialize the ComfoConnect sensor.""" self._ccb = ccb self._sensor_type = sensor_type - self._sensor_id = SENSOR_TYPES[self._sensor_type][3] + self._sensor_id = SENSOR_TYPES[self._sensor_type][ATTR_ID] self._name = name async def async_added_to_hass(self): """Register for sensor updates.""" - await self.hass.async_add_executor_job( - self._ccb.comfoconnect.register_sensor, self._sensor_id + _LOGGER.debug( + "Registering for sensor %s (%d)", self._sensor_type, self._sensor_id, ) async_dispatcher_connect( - self.hass, SIGNAL_COMFOCONNECT_UPDATE_RECEIVED, self._handle_update + self.hass, + SIGNAL_COMFOCONNECT_UPDATE_RECEIVED.format(self._sensor_id), + self._handle_update, + ) + await self.hass.async_add_executor_job( + self._ccb.comfoconnect.register_sensor, self._sensor_id ) - def _handle_update(self, var): + def _handle_update(self, value): """Handle update callbacks.""" - if var == self._sensor_id: - _LOGGER.debug("Received update for %s", var) - self.schedule_update_ha_state() + _LOGGER.debug( + "Handle update for sensor %s (%d): %s", + self._sensor_type, + self._sensor_id, + value, + ) + self._ccb.data[self._sensor_id] = round( + value * SENSOR_TYPES[self._sensor_type].get(ATTR_MULTIPLIER, 1), 2 + ) + self.schedule_update_ha_state() @property def state(self): @@ -133,6 +266,11 @@ def should_poll(self) -> bool: """Do not poll.""" return False + @property + def unique_id(self): + """Return a unique_id for this entity.""" + return f"{self._ccb.unique_id}-{self._sensor_type}" + @property def name(self): """Return the name of the sensor.""" @@ -140,10 +278,15 @@ def name(self): @property def icon(self): - """Return the icon to use in the frontend, if any.""" - return SENSOR_TYPES[self._sensor_type][2] + """Return the icon to use in the frontend.""" + return SENSOR_TYPES[self._sensor_type][ATTR_ICON] @property def unit_of_measurement(self): - """Return the unit of measurement of this entity, if any.""" - return SENSOR_TYPES[self._sensor_type][1] + """Return the unit of measurement of this entity.""" + return SENSOR_TYPES[self._sensor_type][ATTR_UNIT] + + @property + def device_class(self): + """Return the device_class.""" + return SENSOR_TYPES[self._sensor_type][ATTR_DEVICE_CLASS] diff --git a/homeassistant/components/concord232/alarm_control_panel.py b/homeassistant/components/concord232/alarm_control_panel.py index 37bbf0528381..81a54a182d46 100644 --- a/homeassistant/components/concord232/alarm_control_panel.py +++ b/homeassistant/components/concord232/alarm_control_panel.py @@ -8,6 +8,10 @@ import homeassistant.components.alarm_control_panel as alarm from homeassistant.components.alarm_control_panel import PLATFORM_SCHEMA +from homeassistant.components.alarm_control_panel.const import ( + SUPPORT_ALARM_ARM_AWAY, + SUPPORT_ALARM_ARM_HOME, +) from homeassistant.const import ( CONF_CODE, CONF_HOST, @@ -85,6 +89,11 @@ def state(self): """Return the state of the device.""" return self._state + @property + def supported_features(self) -> int: + """Return the list of supported features.""" + return SUPPORT_ALARM_ARM_HOME | SUPPORT_ALARM_ARM_AWAY + def update(self): """Update values from API.""" try: diff --git a/homeassistant/components/conversation/__init__.py b/homeassistant/components/conversation/__init__.py index a82034a4237b..ec5868e86fea 100644 --- a/homeassistant/components/conversation/__init__.py +++ b/homeassistant/components/conversation/__init__.py @@ -50,26 +50,17 @@ def async_set_agent(hass: core.HomeAssistant, agent: AbstractConversationAgent): hass.data[DATA_AGENT] = agent -async def get_agent(hass: core.HomeAssistant) -> AbstractConversationAgent: - """Get agent.""" - agent = hass.data.get(DATA_AGENT) - if agent is None: - agent = hass.data[DATA_AGENT] = DefaultAgent(hass) - await agent.async_initialize(hass.data.get(DATA_CONFIG)) - return agent - - async def async_setup(hass, config): """Register the process service.""" - hass.data[DATA_CONFIG] = config async def handle_service(service): """Parse text into commands.""" text = service.data[ATTR_TEXT] _LOGGER.debug("Processing: <%s>", text) + agent = await _get_agent(hass) try: - await process(hass, text, service.context.id) + await agent.async_process(text, service.context) except intent.IntentHandleError as err: _LOGGER.error("Error processing %s: %s", text, err) @@ -84,27 +75,6 @@ async def handle_service(service): return True -async def process(hass: core.HomeAssistant, text: str, conversation_id: str): - """Process text and get intent.""" - agent = await get_agent(hass) - return await agent.async_process(text, conversation_id) - - -async def get_intent(hass: core.HomeAssistant, text: str, conversation_id: str): - """Process text and get intent.""" - try: - intent_result = await process(hass, text, conversation_id) - except intent.IntentHandleError as err: - intent_result = intent.IntentResponse() - intent_result.async_set_speech(str(err)) - - if intent_result is None: - intent_result = intent.IntentResponse() - intent_result.async_set_speech("Sorry, I didn't understand that") - - return intent_result - - @websocket_api.async_response @websocket_api.websocket_command( {"type": "conversation/process", "text": str, vol.Optional("conversation_id"): str} @@ -112,7 +82,10 @@ async def get_intent(hass: core.HomeAssistant, text: str, conversation_id: str): async def websocket_process(hass, connection, msg): """Process text.""" connection.send_result( - msg["id"], await get_intent(hass, msg["text"], msg.get("conversation_id")) + msg["id"], + await _async_converse( + hass, msg["text"], msg.get("conversation_id"), connection.context(msg) + ), ) @@ -120,7 +93,7 @@ async def websocket_process(hass, connection, msg): @websocket_api.websocket_command({"type": "conversation/agent/info"}) async def websocket_get_agent_info(hass, connection, msg): """Do we need onboarding.""" - agent = await get_agent(hass) + agent = await _get_agent(hass) connection.send_result( msg["id"], @@ -135,7 +108,7 @@ async def websocket_get_agent_info(hass, connection, msg): @websocket_api.websocket_command({"type": "conversation/onboarding/set", "shown": bool}) async def websocket_set_onboarding(hass, connection, msg): """Set onboarding status.""" - agent = await get_agent(hass) + agent = await _get_agent(hass) success = await agent.async_set_onboarding(msg["shown"]) @@ -157,8 +130,36 @@ class ConversationProcessView(http.HomeAssistantView): async def post(self, request, data): """Send a request for processing.""" hass = request.app["hass"] - intent_result = await get_intent( - hass, data["text"], data.get("conversation_id") + + intent_result = await _async_converse( + hass, data["text"], data.get("conversation_id"), self.context(request) ) return self.json(intent_result) + + +async def _get_agent(hass: core.HomeAssistant) -> AbstractConversationAgent: + """Get the active conversation agent.""" + agent = hass.data.get(DATA_AGENT) + if agent is None: + agent = hass.data[DATA_AGENT] = DefaultAgent(hass) + await agent.async_initialize(hass.data.get(DATA_CONFIG)) + return agent + + +async def _async_converse( + hass: core.HomeAssistant, text: str, conversation_id: str, context: core.Context +) -> intent.IntentResponse: + """Process text and get intent.""" + agent = await _get_agent(hass) + try: + intent_result = await agent.async_process(text, context, conversation_id) + except intent.IntentHandleError as err: + intent_result = intent.IntentResponse() + intent_result.async_set_speech(str(err)) + + if intent_result is None: + intent_result = intent.IntentResponse() + intent_result.async_set_speech("Sorry, I didn't understand that") + + return intent_result diff --git a/homeassistant/components/conversation/agent.py b/homeassistant/components/conversation/agent.py index 0c47d6156455..c9c2ab46cf9f 100644 --- a/homeassistant/components/conversation/agent.py +++ b/homeassistant/components/conversation/agent.py @@ -2,6 +2,7 @@ from abc import ABC, abstractmethod from typing import Optional +from homeassistant.core import Context from homeassistant.helpers import intent @@ -23,6 +24,6 @@ async def async_set_onboarding(self, shown): @abstractmethod async def async_process( - self, text: str, conversation_id: Optional[str] = None + self, text: str, context: Context, conversation_id: Optional[str] = None ) -> intent.IntentResponse: """Process a sentence.""" diff --git a/homeassistant/components/conversation/default_agent.py b/homeassistant/components/conversation/default_agent.py index c202cdf1e659..2f09cba2eb19 100644 --- a/homeassistant/components/conversation/default_agent.py +++ b/homeassistant/components/conversation/default_agent.py @@ -3,9 +3,12 @@ import re from typing import Optional -from homeassistant import core -from homeassistant.components.cover import INTENT_CLOSE_COVER, INTENT_OPEN_COVER -from homeassistant.components.shopping_list import INTENT_ADD_ITEM, INTENT_LAST_ITEMS +from homeassistant import core, setup +from homeassistant.components.cover.intent import INTENT_CLOSE_COVER, INTENT_OPEN_COVER +from homeassistant.components.shopping_list.intent import ( + INTENT_ADD_ITEM, + INTENT_LAST_ITEMS, +) from homeassistant.const import EVENT_COMPONENT_LOADED from homeassistant.core import callback from homeassistant.helpers import intent @@ -58,6 +61,9 @@ def __init__(self, hass: core.HomeAssistant): async def async_initialize(self, config): """Initialize the default agent.""" + if "intent" not in self.hass.config.components: + await setup.async_setup_component(self.hass, "intent", {}) + config = config.get(DOMAIN, {}) intents = self.hass.data.setdefault(DOMAIN, {}) @@ -109,7 +115,7 @@ def register_utterances(self, component): async_register(self.hass, intent_type, sentences) async def async_process( - self, text: str, conversation_id: Optional[str] = None + self, text: str, context: core.Context, conversation_id: Optional[str] = None ) -> intent.IntentResponse: """Process a sentence.""" intents = self.hass.data[DOMAIN] @@ -127,4 +133,5 @@ async def async_process( intent_type, {key: {"value": value} for key, value in match.groupdict().items()}, text, + context, ) diff --git a/homeassistant/components/coolmaster/.translations/nl.json b/homeassistant/components/coolmaster/.translations/nl.json index 79a1e9fe1e6f..02b65cdfff91 100644 --- a/homeassistant/components/coolmaster/.translations/nl.json +++ b/homeassistant/components/coolmaster/.translations/nl.json @@ -5,6 +5,9 @@ }, "step": { "user": { + "data": { + "off": "Kan uitgeschakeld worden" + }, "title": "Stel uw CoolMasterNet-verbindingsgegevens in." } }, diff --git a/homeassistant/components/counter/__init__.py b/homeassistant/components/counter/__init__.py index aca3461b4f7f..c2f61d0c1b43 100644 --- a/homeassistant/components/counter/__init__.py +++ b/homeassistant/components/counter/__init__.py @@ -6,7 +6,6 @@ from homeassistant.const import CONF_ICON, CONF_NAME, CONF_MAXIMUM, CONF_MINIMUM import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.config_validation import ENTITY_SERVICE_SCHEMA from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.restore_state import RestoreEntity @@ -33,15 +32,6 @@ SERVICE_RESET = "reset" SERVICE_CONFIGURE = "configure" -SERVICE_SCHEMA_CONFIGURE = ENTITY_SERVICE_SCHEMA.extend( - { - vol.Optional(ATTR_MINIMUM): vol.Any(None, vol.Coerce(int)), - vol.Optional(ATTR_MAXIMUM): vol.Any(None, vol.Coerce(int)), - vol.Optional(ATTR_STEP): cv.positive_int, - vol.Optional(ATTR_INITIAL): cv.positive_int, - vol.Optional(VALUE): cv.positive_int, - } -) CONFIG_SCHEMA = vol.Schema( { @@ -95,17 +85,19 @@ async def async_setup(hass, config): if not entities: return False + component.async_register_entity_service(SERVICE_INCREMENT, {}, "async_increment") + component.async_register_entity_service(SERVICE_DECREMENT, {}, "async_decrement") + component.async_register_entity_service(SERVICE_RESET, {}, "async_reset") component.async_register_entity_service( - SERVICE_INCREMENT, ENTITY_SERVICE_SCHEMA, "async_increment" - ) - component.async_register_entity_service( - SERVICE_DECREMENT, ENTITY_SERVICE_SCHEMA, "async_decrement" - ) - component.async_register_entity_service( - SERVICE_RESET, ENTITY_SERVICE_SCHEMA, "async_reset" - ) - component.async_register_entity_service( - SERVICE_CONFIGURE, SERVICE_SCHEMA_CONFIGURE, "async_configure" + SERVICE_CONFIGURE, + { + vol.Optional(ATTR_MINIMUM): vol.Any(None, vol.Coerce(int)), + vol.Optional(ATTR_MAXIMUM): vol.Any(None, vol.Coerce(int)), + vol.Optional(ATTR_STEP): cv.positive_int, + vol.Optional(ATTR_INITIAL): cv.positive_int, + vol.Optional(VALUE): cv.positive_int, + }, + "async_configure", ) await component.async_add_entities(entities) diff --git a/homeassistant/components/cover/.translations/pt.json b/homeassistant/components/cover/.translations/pt.json index cb9f85c4a939..6234d2685f4b 100644 --- a/homeassistant/components/cover/.translations/pt.json +++ b/homeassistant/components/cover/.translations/pt.json @@ -4,7 +4,15 @@ "is_closed": "{entity_name} est\u00e1 fechada", "is_closing": "{entity_name} est\u00e1 a fechar", "is_open": "{entity_name} est\u00e1 aberta", - "is_opening": "{entity_name} est\u00e1 a abrir" + "is_opening": "{entity_name} est\u00e1 a abrir", + "is_position": "A posi\u00e7\u00e3o atual de {entity_name} \u00e9", + "is_tilt_position": "A inclina\u00e7\u00e3o actual de {entity_name} \u00e9" + }, + "trigger_type": { + "closed": "{entity_name} fechou", + "closing": "{entity_name} est\u00e1 a fechar", + "opened": "{entity_name} abriu", + "opening": "{entity_name} est\u00e1 a abrir" } } } \ No newline at end of file diff --git a/homeassistant/components/cover/__init__.py b/homeassistant/components/cover/__init__.py index cfac143a5d80..0b8fbfa9dd2b 100644 --- a/homeassistant/components/cover/__init__.py +++ b/homeassistant/components/cover/__init__.py @@ -9,13 +9,11 @@ from homeassistant.loader import bind_hass from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.entity import Entity -from homeassistant.helpers.config_validation import ( # noqa +from homeassistant.helpers.config_validation import ( # noqa: F401 PLATFORM_SCHEMA, PLATFORM_SCHEMA_BASE, ) -from homeassistant.helpers.config_validation import ENTITY_SERVICE_SCHEMA from homeassistant.components import group -from homeassistant.helpers import intent from homeassistant.const import ( SERVICE_OPEN_COVER, SERVICE_CLOSE_COVER, @@ -83,21 +81,6 @@ ATTR_POSITION = "position" ATTR_TILT_POSITION = "tilt_position" -INTENT_OPEN_COVER = "HassOpenCover" -INTENT_CLOSE_COVER = "HassCloseCover" - -COVER_SET_COVER_POSITION_SCHEMA = ENTITY_SERVICE_SCHEMA.extend( - {vol.Required(ATTR_POSITION): vol.All(vol.Coerce(int), vol.Range(min=0, max=100))} -) - -COVER_SET_COVER_TILT_POSITION_SCHEMA = ENTITY_SERVICE_SCHEMA.extend( - { - vol.Required(ATTR_TILT_POSITION): vol.All( - vol.Coerce(int), vol.Range(min=0, max=100) - ) - } -) - @bind_hass def is_closed(hass, entity_id=None): @@ -114,59 +97,50 @@ async def async_setup(hass, config): await component.async_setup(config) - component.async_register_entity_service( - SERVICE_OPEN_COVER, ENTITY_SERVICE_SCHEMA, "async_open_cover" - ) + component.async_register_entity_service(SERVICE_OPEN_COVER, {}, "async_open_cover") component.async_register_entity_service( - SERVICE_CLOSE_COVER, ENTITY_SERVICE_SCHEMA, "async_close_cover" + SERVICE_CLOSE_COVER, {}, "async_close_cover" ) component.async_register_entity_service( SERVICE_SET_COVER_POSITION, - COVER_SET_COVER_POSITION_SCHEMA, + { + vol.Required(ATTR_POSITION): vol.All( + vol.Coerce(int), vol.Range(min=0, max=100) + ) + }, "async_set_cover_position", ) - component.async_register_entity_service( - SERVICE_STOP_COVER, ENTITY_SERVICE_SCHEMA, "async_stop_cover" - ) + component.async_register_entity_service(SERVICE_STOP_COVER, {}, "async_stop_cover") - component.async_register_entity_service( - SERVICE_TOGGLE, ENTITY_SERVICE_SCHEMA, "async_toggle" - ) + component.async_register_entity_service(SERVICE_TOGGLE, {}, "async_toggle") component.async_register_entity_service( - SERVICE_OPEN_COVER_TILT, ENTITY_SERVICE_SCHEMA, "async_open_cover_tilt" + SERVICE_OPEN_COVER_TILT, {}, "async_open_cover_tilt" ) component.async_register_entity_service( - SERVICE_CLOSE_COVER_TILT, ENTITY_SERVICE_SCHEMA, "async_close_cover_tilt" + SERVICE_CLOSE_COVER_TILT, {}, "async_close_cover_tilt" ) component.async_register_entity_service( - SERVICE_STOP_COVER_TILT, ENTITY_SERVICE_SCHEMA, "async_stop_cover_tilt" + SERVICE_STOP_COVER_TILT, {}, "async_stop_cover_tilt" ) component.async_register_entity_service( SERVICE_SET_COVER_TILT_POSITION, - COVER_SET_COVER_TILT_POSITION_SCHEMA, + { + vol.Required(ATTR_TILT_POSITION): vol.All( + vol.Coerce(int), vol.Range(min=0, max=100) + ) + }, "async_set_cover_tilt_position", ) component.async_register_entity_service( - SERVICE_TOGGLE_COVER_TILT, ENTITY_SERVICE_SCHEMA, "async_toggle_tilt" - ) - - hass.helpers.intent.async_register( - intent.ServiceIntentHandler( - INTENT_OPEN_COVER, DOMAIN, SERVICE_OPEN_COVER, "Opened {}" - ) - ) - hass.helpers.intent.async_register( - intent.ServiceIntentHandler( - INTENT_CLOSE_COVER, DOMAIN, SERVICE_CLOSE_COVER, "Closed {}" - ) + SERVICE_TOGGLE_COVER_TILT, {}, "async_toggle_tilt" ) return True diff --git a/homeassistant/components/cover/intent.py b/homeassistant/components/cover/intent.py new file mode 100644 index 000000000000..f8d13e6a90eb --- /dev/null +++ b/homeassistant/components/cover/intent.py @@ -0,0 +1,22 @@ +"""Intents for the cover integration.""" +from homeassistant.core import HomeAssistant +from homeassistant.helpers import intent + +from . import DOMAIN, SERVICE_OPEN_COVER, SERVICE_CLOSE_COVER + +INTENT_OPEN_COVER = "HassOpenCover" +INTENT_CLOSE_COVER = "HassCloseCover" + + +async def async_setup_intents(hass: HomeAssistant) -> None: + """Set up the cover intents.""" + hass.helpers.intent.async_register( + intent.ServiceIntentHandler( + INTENT_OPEN_COVER, DOMAIN, SERVICE_OPEN_COVER, "Opened {}" + ) + ) + hass.helpers.intent.async_register( + intent.ServiceIntentHandler( + INTENT_CLOSE_COVER, DOMAIN, SERVICE_CLOSE_COVER, "Closed {}" + ) + ) diff --git a/homeassistant/components/cups/sensor.py b/homeassistant/components/cups/sensor.py index 4af51e911a1c..17a246561a50 100644 --- a/homeassistant/components/cups/sensor.py +++ b/homeassistant/components/cups/sensor.py @@ -306,7 +306,6 @@ def update(self): self._attributes = self.data.attributes -# pylint: disable=no-name-in-module class CupsData: """Get the latest data from CUPS and update the state.""" diff --git a/homeassistant/components/daikin/__init__.py b/homeassistant/components/daikin/__init__.py index 390e80d0916b..209bf71e594f 100644 --- a/homeassistant/components/daikin/__init__.py +++ b/homeassistant/components/daikin/__init__.py @@ -5,6 +5,7 @@ from aiohttp import ClientConnectionError from async_timeout import timeout +from pydaikin.appliance import Appliance import voluptuous as vol from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry @@ -15,7 +16,7 @@ from homeassistant.helpers.typing import HomeAssistantType from homeassistant.util import Throttle -from . import config_flow # noqa pylint_disable=unused-import +from . import config_flow # noqa: F401 _LOGGER = logging.getLogger(__name__) @@ -87,7 +88,6 @@ async def async_unload_entry(hass, config_entry): async def daikin_api_setup(hass, host): """Create a Daikin instance only once.""" - from pydaikin.appliance import Appliance session = hass.helpers.aiohttp_client.async_get_clientsession() try: diff --git a/homeassistant/components/daikin/climate.py b/homeassistant/components/daikin/climate.py index ddc5353250ce..d46ea26d4870 100644 --- a/homeassistant/components/daikin/climate.py +++ b/homeassistant/components/daikin/climate.py @@ -1,6 +1,7 @@ """Support for the Daikin HVAC.""" import logging +from pydaikin import appliance import voluptuous as vol from homeassistant.components.climate import PLATFORM_SCHEMA, ClimateDevice @@ -91,7 +92,6 @@ class DaikinClimate(ClimateDevice): def __init__(self, api): """Initialize the climate device.""" - from pydaikin import appliance self._api = api self._list = { diff --git a/homeassistant/components/daikin/config_flow.py b/homeassistant/components/daikin/config_flow.py index 36d8ef0d3835..bd90a87db86e 100644 --- a/homeassistant/components/daikin/config_flow.py +++ b/homeassistant/components/daikin/config_flow.py @@ -4,6 +4,7 @@ from aiohttp import ClientError from async_timeout import timeout +from pydaikin.appliance import Appliance import voluptuous as vol from homeassistant import config_entries @@ -32,7 +33,6 @@ async def _create_entry(self, host, mac): async def _create_device(self, host): """Create device.""" - from pydaikin.appliance import Appliance try: device = Appliance( diff --git a/homeassistant/components/danfoss_air/__init__.py b/homeassistant/components/danfoss_air/__init__.py index adfb13e722c7..b1dbf890eb97 100644 --- a/homeassistant/components/danfoss_air/__init__.py +++ b/homeassistant/components/danfoss_air/__init__.py @@ -2,6 +2,8 @@ from datetime import timedelta import logging +from pydanfossair.commands import ReadCommand +from pydanfossair.danfossclient import DanfossClient import voluptuous as vol from homeassistant.const import CONF_HOST @@ -40,8 +42,6 @@ def __init__(self, host): """Initialize the Danfoss Air CCM connection.""" self._data = {} - from pydanfossair.danfossclient import DanfossClient - self._client = DanfossClient(host) def get_value(self, item): @@ -56,7 +56,6 @@ def update_state(self, command, state_command): def update(self): """Use the data from Danfoss Air API.""" _LOGGER.debug("Fetching data from Danfoss Air CCM module") - from pydanfossair.commands import ReadCommand self._data[ReadCommand.exhaustTemperature] = self._client.command( ReadCommand.exhaustTemperature diff --git a/homeassistant/components/datadog/__init__.py b/homeassistant/components/datadog/__init__.py index 5517e41d5c66..adb8bb1f95c7 100644 --- a/homeassistant/components/datadog/__init__.py +++ b/homeassistant/components/datadog/__init__.py @@ -1,6 +1,7 @@ """Support for sending data to Datadog.""" import logging +from datadog import initialize, statsd import voluptuous as vol from homeassistant.const import ( @@ -42,7 +43,6 @@ def setup(hass, config): """Set up the Datadog component.""" - from datadog import initialize, statsd conf = config[DOMAIN] host = conf.get(CONF_HOST) diff --git a/homeassistant/components/deconz/.translations/bg.json b/homeassistant/components/deconz/.translations/bg.json index e8dc5845c134..fb75fc81f5fe 100644 --- a/homeassistant/components/deconz/.translations/bg.json +++ b/homeassistant/components/deconz/.translations/bg.json @@ -24,12 +24,12 @@ "init": { "data": { "host": "\u0410\u0434\u0440\u0435\u0441", - "port": "\u041f\u043e\u0440\u0442 (\u0441\u0442\u043e\u0439\u043d\u043e\u0441\u0442 \u043f\u043e \u043f\u043e\u0434\u0440\u0430\u0437\u0431\u0438\u0440\u0430\u043d\u0435: '80')" + "port": "\u041f\u043e\u0440\u0442" }, "title": "\u0414\u0435\u0444\u0438\u043d\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 deCONZ \u0448\u043b\u044e\u0437" }, "link": { - "description": "\u041e\u0442\u043a\u043b\u044e\u0447\u0438 deCONZ \u0448\u043b\u044e\u0437\u0430 \u0437\u0430 \u0434\u0430 \u0441\u0435 \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u0430 \u0441 Home Assistant.\n\n1. \u041e\u0442\u0432\u043e\u0440\u0435\u0442\u0435 \u0441\u0438\u0441\u0442\u0435\u043c\u043d\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u043d\u0430 deCONZ\n2. \u041d\u0430\u0442\u0438\u0441\u043d\u0435\u0442\u0435 \u0431\u0443\u0442\u043e\u043d\u0430 \"Unlock Gateway\"", + "description": "\u041e\u0442\u043a\u043b\u044e\u0447\u0438 deCONZ \u0448\u043b\u044e\u0437\u0430 \u0437\u0430 \u0434\u0430 \u0441\u0435 \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u0430 \u0441 Home Assistant.\n\n1. \u041e\u0442\u0438\u0434\u0435\u0442\u0435 \u043d\u0430 deCONZ Settings -> Gateway -> Advanced\n2. \u041d\u0430\u0442\u0438\u0441\u043d\u0435\u0442\u0435 \u0431\u0443\u0442\u043e\u043d\u0430 \"Authenticate app\"", "title": "\u0412\u0440\u044a\u0437\u043a\u0430 \u0441 deCONZ" }, "options": { @@ -40,7 +40,7 @@ "title": "\u0414\u043e\u043f\u044a\u043b\u043d\u0438\u0442\u0435\u043b\u043d\u0438 \u043e\u043f\u0446\u0438\u0438 \u0437\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 deCONZ" } }, - "title": "deCONZ" + "title": "deCONZ Zigbee \u0448\u043b\u044e\u0437" }, "device_automation": { "trigger_subtype": { @@ -55,10 +55,17 @@ "left": "\u041b\u044f\u0432\u043e", "open": "\u041e\u0442\u0432\u0430\u0440\u044f\u043d\u0435", "right": "\u0414\u044f\u0441\u043d\u043e", + "side_1": "\u0421\u0442\u0440\u0430\u043d\u0430 1", + "side_2": "\u0421\u0442\u0440\u0430\u043d\u0430 2", + "side_3": "\u0421\u0442\u0440\u0430\u043d\u0430 3", + "side_4": "\u0421\u0442\u0440\u0430\u043d\u0430 4", + "side_5": "\u0421\u0442\u0440\u0430\u043d\u0430 5", + "side_6": "\u0421\u0442\u0440\u0430\u043d\u0430 6", "turn_off": "\u0418\u0437\u043a\u043b\u044e\u0447\u0438", "turn_on": "\u0412\u043a\u043b\u044e\u0447\u0438" }, "trigger_type": { + "remote_awakened": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0441\u0435 \u0441\u044a\u0431\u0443\u0434\u0438", "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", @@ -69,7 +76,16 @@ "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", - "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" + "remote_double_tap": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \"{subtype}\" \u0435 \u043f\u043e\u0447\u0443\u043a\u0430\u043d\u043e \u0434\u0432\u0430 \u043f\u044a\u0442\u0438", + "remote_falling": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u043f\u0430\u0434\u0430", + "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", + "remote_moved": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0435 \u043f\u0440\u0435\u043c\u0435\u0441\u0442\u0435\u043d\u043e \u0441 \"{subtype}\" \u043d\u0430\u0433\u043e\u0440\u0435", + "remote_rotate_from_side_1": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0435 \u0437\u0430\u0432\u044a\u0440\u0442\u044f\u043d\u043e \u043e\u0442 \"\u0441\u0442\u0440\u0430\u043d\u0430 1\" \u043a\u044a\u043c \" {subtype} \"", + "remote_rotate_from_side_2": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0435 \u0437\u0430\u0432\u044a\u0440\u0442\u044f\u043d\u043e \u043e\u0442 \"\u0441\u0442\u0440\u0430\u043d\u0430 2\" \u043a\u044a\u043c \" {subtype} \"", + "remote_rotate_from_side_3": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0435 \u0437\u0430\u0432\u044a\u0440\u0442\u044f\u043d\u043e \u043e\u0442 \"\u0441\u0442\u0440\u0430\u043d\u0430 3\" \u043a\u044a\u043c \" {subtype} \"", + "remote_rotate_from_side_4": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0435 \u0437\u0430\u0432\u044a\u0440\u0442\u044f\u043d\u043e \u043e\u0442 \"\u0441\u0442\u0440\u0430\u043d\u0430 4\" \u043a\u044a\u043c \" {subtype} \"", + "remote_rotate_from_side_5": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0435 \u0437\u0430\u0432\u044a\u0440\u0442\u044f\u043d\u043e \u043e\u0442 \"\u0441\u0442\u0440\u0430\u043d\u0430 5\" \u043a\u044a\u043c \" {subtype} \"", + "remote_rotate_from_side_6": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0435 \u0437\u0430\u0432\u044a\u0440\u0442\u044f\u043d\u043e \u043e\u0442 \"\u0441\u0442\u0440\u0430\u043d\u0430 6\" \u043a\u044a\u043c \" {subtype} \"" } }, "options": { diff --git a/homeassistant/components/deconz/.translations/ca.json b/homeassistant/components/deconz/.translations/ca.json index 676641d38c62..a51bfa056f64 100644 --- a/homeassistant/components/deconz/.translations/ca.json +++ b/homeassistant/components/deconz/.translations/ca.json @@ -55,12 +55,12 @@ "left": "Esquerra", "open": "Obert", "right": "Dreta", - "side_1": "Costat 1", - "side_2": "Costat 2", - "side_3": "Costat 3", - "side_4": "Costat 4", - "side_5": "Costat 5", - "side_6": "Costat 6", + "side_1": "cara 1", + "side_2": "cara 2", + "side_3": "cara 3", + "side_4": "cara 4", + "side_5": "cara 5", + "side_6": "cara 6", "turn_off": "Desactiva", "turn_on": "Activa" }, @@ -76,8 +76,16 @@ "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", + "remote_double_tap": "Dispositiu \"{subtype}\" tocat dues vegades", "remote_falling": "Dispositiu en caiguda lliure", - "remote_gyro_activated": "Dispositiu sacsejat" + "remote_gyro_activated": "Dispositiu sacsejat", + "remote_moved": "Dispositiu mogut amb la \"{subtype}\" amunt", + "remote_rotate_from_side_1": "Dispositiu rotat de la \"cara 1\" a la \"{subtype}\"", + "remote_rotate_from_side_2": "Dispositiu rotat de la \"cara 2\" a la \"{subtype}\"", + "remote_rotate_from_side_3": "Dispositiu rotat de la \"cara 3\" a la \"{subtype}\"", + "remote_rotate_from_side_4": "Dispositiu rotat de la \"cara 4\" a la \"{subtype}\"", + "remote_rotate_from_side_5": "Dispositiu rotat de la \"cara 5\" a la \"{subtype}\"", + "remote_rotate_from_side_6": "Dispositiu rotat de la \"cara 6\" a la \"{subtype}\"" } }, "options": { diff --git a/homeassistant/components/deconz/.translations/ko.json b/homeassistant/components/deconz/.translations/ko.json index 61725316b137..fede936b964d 100644 --- a/homeassistant/components/deconz/.translations/ko.json +++ b/homeassistant/components/deconz/.translations/ko.json @@ -55,10 +55,17 @@ "left": "\uc67c\ucabd", "open": "\uc5f4\uae30", "right": "\uc624\ub978\ucabd", + "side_1": "\uba74 1", + "side_2": "\uba74 2", + "side_3": "\uba74 3", + "side_4": "\uba74 4", + "side_5": "\uba74 5", + "side_6": "\uba74 6", "turn_off": "\ub044\uae30", "turn_on": "\ucf1c\uae30" }, "trigger_type": { + "remote_awakened": "\uae30\uae30 \uc808\uc804 \ubaa8\ub4dc \ud574\uc81c\ub428", "remote_button_double_press": "\"{subtype}\" \ubc84\ud2bc\uc744 \ub450 \ubc88 \ub204\ub984", "remote_button_long_press": "\"{subtype}\" \ubc84\ud2bc\uc744 \uacc4\uc18d \ub204\ub984", "remote_button_long_release": "\"{subtype}\" \ubc84\ud2bc\uc744 \uae38\uac8c \ub20c\ub800\ub2e4\uac00 \ub5cc", @@ -69,7 +76,16 @@ "remote_button_short_press": "\"{subtype}\" \ubc84\ud2bc\uc744 \ub204\ub984", "remote_button_short_release": "\"{subtype}\" \ubc84\ud2bc\uc744 \ub5cc", "remote_button_triple_press": "\"{subtype}\" \ubc84\ud2bc\uc744 \uc138 \ubc88 \ub204\ub984", - "remote_gyro_activated": "\uae30\uae30 \ud754\ub4e6" + "remote_double_tap": "\uae30\uae30\uc758 \"{subtype}\" \uac00 \ub354\ube14\ud0ed \ub428", + "remote_falling": "\uae30\uae30\uac00 \ub5a8\uc5b4\uc9d0", + "remote_gyro_activated": "\uae30\uae30 \ud754\ub4e6", + "remote_moved": "\uae30\uae30\uc758 \"{subtype}\" \uac00 \uc704\ub85c \ud5a5\ud55c\ucc44\ub85c \uc6c0\uc9c1\uc784", + "remote_rotate_from_side_1": "\"\uba74 1\" \uc5d0\uc11c \"{subtype}\" \ub85c \uae30\uae30\uac00 \ud68c\uc804\ub428", + "remote_rotate_from_side_2": "\"\uba74 2\" \uc5d0\uc11c \"{subtype}\" \ub85c \uae30\uae30\uac00 \ud68c\uc804\ub428", + "remote_rotate_from_side_3": "\"\uba74 3\" \uc5d0\uc11c \"{subtype}\" \ub85c \uae30\uae30\uac00 \ud68c\uc804\ub428", + "remote_rotate_from_side_4": "\"\uba74 4\" \uc5d0\uc11c \"{subtype}\" \ub85c \uae30\uae30\uac00 \ud68c\uc804\ub428", + "remote_rotate_from_side_5": "\"\uba74 5\" \uc5d0\uc11c \"{subtype}\" \ub85c \uae30\uae30\uac00 \ud68c\uc804\ub428", + "remote_rotate_from_side_6": "\"\uba74 6\" \uc5d0\uc11c \"{subtype}\" \ub85c \uae30\uae30\uac00 \ud68c\uc804\ub428" } }, "options": { diff --git a/homeassistant/components/deconz/.translations/pt.json b/homeassistant/components/deconz/.translations/pt.json index 63a66595ace4..f24d7692a555 100644 --- a/homeassistant/components/deconz/.translations/pt.json +++ b/homeassistant/components/deconz/.translations/pt.json @@ -32,7 +32,13 @@ }, "device_automation": { "trigger_subtype": { - "left": "Esquerda" + "left": "Esquerda", + "side_1": "Lado 1", + "side_2": "Lado 2", + "side_3": "Lado 3", + "side_4": "Lado 4", + "side_5": "Lado 5", + "side_6": "Lado 6" } } } \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/ro.json b/homeassistant/components/deconz/.translations/ro.json new file mode 100644 index 000000000000..2d6fc6a39fbc --- /dev/null +++ b/homeassistant/components/deconz/.translations/ro.json @@ -0,0 +1,14 @@ +{ + "config": { + "step": { + "init": { + "data": { + "port": "Port" + } + }, + "link": { + "description": "Debloca\u021bi gateway-ul DECONZ pentru a v\u0103 \u00eenregistra la Home Assistant. \n\n 1. Accesa\u021bi Set\u0103rile deCONZ - > Gateway - > Avansat \n 2. Ap\u0103sa\u021bi butonul \u201eAutentifica\u021bi aplica\u021bia\u201d" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/deconz/binary_sensor.py b/homeassistant/components/deconz/binary_sensor.py index 1a4d9680c1e0..0fdc5904c2db 100644 --- a/homeassistant/components/deconz/binary_sensor.py +++ b/homeassistant/components/deconz/binary_sensor.py @@ -8,7 +8,7 @@ from .const import ATTR_DARK, ATTR_ON, NEW_SENSOR from .deconz_device import DeconzDevice -from .gateway import get_gateway_from_config_entry, DeconzEntityHandler +from .gateway import DeconzEntityHandler, get_gateway_from_config_entry ATTR_ORIENTATION = "orientation" ATTR_TILTANGLE = "tiltangle" diff --git a/homeassistant/components/deconz/config_flow.py b/homeassistant/components/deconz/config_flow.py index b9a299230ad8..b757f1f4d030 100644 --- a/homeassistant/components/deconz/config_flow.py +++ b/homeassistant/components/deconz/config_flow.py @@ -2,10 +2,9 @@ import asyncio import async_timeout -import voluptuous as vol - -from pydeconz.errors import ResponseError, RequestError +from pydeconz.errors import RequestError, ResponseError from pydeconz.utils import async_discovery, async_get_api_key, async_get_gateway_config +import voluptuous as vol from homeassistant import config_entries from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_PORT @@ -170,6 +169,8 @@ async def _update_entry(self, entry, host): async def async_step_ssdp(self, discovery_info): """Handle a discovered deCONZ bridge.""" + # Import it here, because only now do we know ssdp integration loaded. + # pylint: disable=import-outside-toplevel from homeassistant.components.ssdp import ATTR_MANUFACTURERURL, ATTR_SERIAL if discovery_info[ATTR_MANUFACTURERURL] != DECONZ_MANUFACTURERURL: diff --git a/homeassistant/components/deconz/cover.py b/homeassistant/components/deconz/cover.py index bcd408c25a75..6e5e616fbb89 100644 --- a/homeassistant/components/deconz/cover.py +++ b/homeassistant/components/deconz/cover.py @@ -1,11 +1,11 @@ """Support for deCONZ covers.""" from homeassistant.components.cover import ( ATTR_POSITION, - CoverDevice, SUPPORT_CLOSE, SUPPORT_OPEN, - SUPPORT_STOP, SUPPORT_SET_POSITION, + SUPPORT_STOP, + CoverDevice, ) from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect diff --git a/homeassistant/components/deconz/device_trigger.py b/homeassistant/components/deconz/device_trigger.py index 30fbfdd05ae1..b6691548b877 100644 --- a/homeassistant/components/deconz/device_trigger.py +++ b/homeassistant/components/deconz/device_trigger.py @@ -2,7 +2,6 @@ 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, @@ -209,6 +208,13 @@ (CONF_DOUBLE_PRESS, CONF_BOTH_BUTTONS): 3004, } +AQARA_DOUBLE_WALL_SWITCH_WXKG02LM_MODEL = "lumi.sensor_86sw2" +AQARA_DOUBLE_WALL_SWITCH_WXKG02LM = { + (CONF_SHORT_PRESS, CONF_LEFT): 1002, + (CONF_SHORT_PRESS, CONF_RIGHT): 2002, + (CONF_SHORT_PRESS, CONF_BOTH_BUTTONS): 3002, +} + AQARA_MINI_SWITCH_MODEL = "lumi.remote.b1acn01" AQARA_MINI_SWITCH = { (CONF_SHORT_PRESS, CONF_TURN_ON): 1002, @@ -238,6 +244,14 @@ (CONF_SHAKE, ""): 1007, } +AQARA_SQUARE_SWITCH_WXKG11LM_2016_MODEL = "lumi.sensor_switch.aq2" +AQARA_SQUARE_SWITCH_WXKG11LM_2016 = { + (CONF_SHORT_PRESS, CONF_TURN_ON): 1002, + (CONF_DOUBLE_PRESS, CONF_TURN_ON): 1004, + (CONF_TRIPLE_PRESS, CONF_TURN_ON): 1005, + (CONF_QUADRUPLE_PRESS, CONF_TURN_ON): 1006, +} + REMOTES = { HUE_DIMMER_REMOTE_MODEL_GEN1: HUE_DIMMER_REMOTE, HUE_DIMMER_REMOTE_MODEL_GEN2: HUE_DIMMER_REMOTE, @@ -249,9 +263,11 @@ TRADFRI_WIRELESS_DIMMER_MODEL: TRADFRI_WIRELESS_DIMMER, AQARA_CUBE_MODEL: AQARA_CUBE, AQARA_DOUBLE_WALL_SWITCH_MODEL: AQARA_DOUBLE_WALL_SWITCH, + AQARA_DOUBLE_WALL_SWITCH_WXKG02LM_MODEL: AQARA_DOUBLE_WALL_SWITCH_WXKG02LM, AQARA_MINI_SWITCH_MODEL: AQARA_MINI_SWITCH, AQARA_ROUND_SWITCH_MODEL: AQARA_ROUND_SWITCH, AQARA_SQUARE_SWITCH_MODEL: AQARA_SQUARE_SWITCH, + AQARA_SQUARE_SWITCH_WXKG11LM_2016_MODEL: AQARA_SQUARE_SWITCH_WXKG11LM_2016, } TRIGGER_SCHEMA = TRIGGER_BASE_SCHEMA.extend( @@ -282,7 +298,11 @@ async def async_validate_trigger_config(hass, config): trigger = (config[CONF_TYPE], config[CONF_SUBTYPE]) - if device.model not in REMOTES or trigger not in REMOTES[device.model]: + if ( + not device + or device.model not in REMOTES + or trigger not in REMOTES[device.model] + ): raise InvalidDeviceAutomationConfig return config diff --git a/homeassistant/components/deconz/gateway.py b/homeassistant/components/deconz/gateway.py index 75898b0fdab8..0c77285a6fe5 100644 --- a/homeassistant/components/deconz/gateway.py +++ b/homeassistant/components/deconz/gateway.py @@ -1,12 +1,12 @@ """Representation of a deCONZ gateway.""" import asyncio -import async_timeout +import async_timeout from pydeconz import DeconzSession, errors -from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.const import CONF_HOST from homeassistant.core import callback +from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import aiohttp_client from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from homeassistant.helpers.dispatcher import ( @@ -14,8 +14,8 @@ async_dispatcher_send, ) from homeassistant.helpers.entity_registry import ( - async_get_registry, DISABLED_CONFIG_ENTRY, + async_get_registry, ) from .const import ( @@ -30,7 +30,6 @@ NEW_DEVICE, SUPPORTED_PLATFORMS, ) - from .errors import AuthenticationRequired, CannotConnect diff --git a/homeassistant/components/deconz/light.py b/homeassistant/components/deconz/light.py index eda2041b9231..af708a153917 100644 --- a/homeassistant/components/deconz/light.py +++ b/homeassistant/components/deconz/light.py @@ -29,7 +29,7 @@ SWITCH_TYPES, ) from .deconz_device import DeconzDevice -from .gateway import get_gateway_from_config_entry, DeconzEntityHandler +from .gateway import DeconzEntityHandler, get_gateway_from_config_entry async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): diff --git a/homeassistant/components/deconz/sensor.py b/homeassistant/components/deconz/sensor.py index 3a3dbceb46bd..4c854a0ec115 100644 --- a/homeassistant/components/deconz/sensor.py +++ b/homeassistant/components/deconz/sensor.py @@ -11,7 +11,7 @@ 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 +from .gateway import DeconzEntityHandler, get_gateway_from_config_entry ATTR_CURRENT = "current" ATTR_POWER = "power" diff --git a/homeassistant/components/deconz/services.py b/homeassistant/components/deconz/services.py index 3498b46d879c..8efdc2718bcc 100644 --- a/homeassistant/components/deconz/services.py +++ b/homeassistant/components/deconz/services.py @@ -4,7 +4,7 @@ from homeassistant.helpers import config_validation as cv from .config_flow import get_master_gateway -from .const import CONF_BRIDGEID, DOMAIN, _LOGGER +from .const import _LOGGER, CONF_BRIDGEID, DOMAIN DECONZ_SERVICES = "deconz_services" diff --git a/homeassistant/components/decora_wifi/light.py b/homeassistant/components/decora_wifi/light.py index 77f3a6387ed2..6171f65ef242 100644 --- a/homeassistant/components/decora_wifi/light.py +++ b/homeassistant/components/decora_wifi/light.py @@ -28,7 +28,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Decora WiFi platform.""" - # pylint: disable=import-error, no-name-in-module + # pylint: disable=import-error from decora_wifi import DecoraWiFiSession from decora_wifi.models.person import Person from decora_wifi.models.residential_account import ResidentialAccount diff --git a/homeassistant/components/deluge/sensor.py b/homeassistant/components/deluge/sensor.py index 098484cf7ae6..7df87490c609 100644 --- a/homeassistant/components/deluge/sensor.py +++ b/homeassistant/components/deluge/sensor.py @@ -1,21 +1,22 @@ """Support for monitoring the Deluge BitTorrent client API.""" import logging +from deluge_client import DelugeRPCClient, FailedToReconnectException import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( CONF_HOST, - CONF_PASSWORD, - CONF_USERNAME, + CONF_MONITORED_VARIABLES, CONF_NAME, + CONF_PASSWORD, CONF_PORT, - CONF_MONITORED_VARIABLES, + CONF_USERNAME, STATE_IDLE, ) -from homeassistant.helpers.entity import Entity from homeassistant.exceptions import PlatformNotReady +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) _THROTTLED_REFRESH = None @@ -46,7 +47,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Deluge sensors.""" - from deluge_client import DelugeRPCClient name = config.get(CONF_NAME) host = config.get(CONF_HOST) @@ -103,7 +103,6 @@ def unit_of_measurement(self): def update(self): """Get the latest data from Deluge and updates the state.""" - from deluge_client import FailedToReconnectException try: self.data = self.client.call( diff --git a/homeassistant/components/deluge/switch.py b/homeassistant/components/deluge/switch.py index 981ef129b473..7ac98f284c8a 100644 --- a/homeassistant/components/deluge/switch.py +++ b/homeassistant/components/deluge/switch.py @@ -1,21 +1,22 @@ """Support for setting the Deluge BitTorrent client in Pause.""" import logging +from deluge_client import DelugeRPCClient, FailedToReconnectException import voluptuous as vol from homeassistant.components.switch import PLATFORM_SCHEMA -from homeassistant.exceptions import PlatformNotReady from homeassistant.const import ( CONF_HOST, CONF_NAME, - CONF_PORT, CONF_PASSWORD, + CONF_PORT, CONF_USERNAME, STATE_OFF, STATE_ON, ) -from homeassistant.helpers.entity import ToggleEntity +from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import ToggleEntity _LOGGER = logging.getLogger(__name__) @@ -35,7 +36,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Deluge switch.""" - from deluge_client import DelugeRPCClient name = config.get(CONF_NAME) host = config.get(CONF_HOST) @@ -95,7 +95,6 @@ def turn_off(self, **kwargs): def update(self): """Get the latest data from deluge and updates the state.""" - from deluge_client import FailedToReconnectException try: torrent_list = self.deluge_client.call( diff --git a/homeassistant/components/demo/.translations/bg.json b/homeassistant/components/demo/.translations/bg.json new file mode 100644 index 000000000000..3b1f5f8a8d23 --- /dev/null +++ b/homeassistant/components/demo/.translations/bg.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "\u0414\u0435\u043c\u043e\u043d\u0441\u0442\u0440\u0430\u0446\u0438\u044f" + } +} \ No newline at end of file diff --git a/homeassistant/components/demo/.translations/ca.json b/homeassistant/components/demo/.translations/ca.json new file mode 100644 index 000000000000..944d358e7391 --- /dev/null +++ b/homeassistant/components/demo/.translations/ca.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Demostraci\u00f3" + } +} \ No newline at end of file diff --git a/homeassistant/components/demo/.translations/de.json b/homeassistant/components/demo/.translations/de.json new file mode 100644 index 000000000000..ef01fcb4f3c3 --- /dev/null +++ b/homeassistant/components/demo/.translations/de.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Demo" + } +} \ No newline at end of file diff --git a/homeassistant/components/demo/.translations/en.json b/homeassistant/components/demo/.translations/en.json new file mode 100644 index 000000000000..ef01fcb4f3c3 --- /dev/null +++ b/homeassistant/components/demo/.translations/en.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Demo" + } +} \ No newline at end of file diff --git a/homeassistant/components/demo/.translations/es.json b/homeassistant/components/demo/.translations/es.json new file mode 100644 index 000000000000..ef01fcb4f3c3 --- /dev/null +++ b/homeassistant/components/demo/.translations/es.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Demo" + } +} \ No newline at end of file diff --git a/homeassistant/components/demo/.translations/fr.json b/homeassistant/components/demo/.translations/fr.json new file mode 100644 index 000000000000..bc093330c26d --- /dev/null +++ b/homeassistant/components/demo/.translations/fr.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "D\u00e9mo" + } +} \ No newline at end of file diff --git a/homeassistant/components/demo/.translations/it.json b/homeassistant/components/demo/.translations/it.json new file mode 100644 index 000000000000..ef01fcb4f3c3 --- /dev/null +++ b/homeassistant/components/demo/.translations/it.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Demo" + } +} \ No newline at end of file diff --git a/homeassistant/components/demo/.translations/lb.json b/homeassistant/components/demo/.translations/lb.json new file mode 100644 index 000000000000..ef01fcb4f3c3 --- /dev/null +++ b/homeassistant/components/demo/.translations/lb.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Demo" + } +} \ No newline at end of file diff --git a/homeassistant/components/demo/.translations/nl.json b/homeassistant/components/demo/.translations/nl.json new file mode 100644 index 000000000000..ef01fcb4f3c3 --- /dev/null +++ b/homeassistant/components/demo/.translations/nl.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Demo" + } +} \ No newline at end of file diff --git a/homeassistant/components/demo/.translations/no.json b/homeassistant/components/demo/.translations/no.json new file mode 100644 index 000000000000..ef01fcb4f3c3 --- /dev/null +++ b/homeassistant/components/demo/.translations/no.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Demo" + } +} \ No newline at end of file diff --git a/homeassistant/components/demo/.translations/pl.json b/homeassistant/components/demo/.translations/pl.json new file mode 100644 index 000000000000..ef01fcb4f3c3 --- /dev/null +++ b/homeassistant/components/demo/.translations/pl.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Demo" + } +} \ No newline at end of file diff --git a/homeassistant/components/demo/.translations/pt.json b/homeassistant/components/demo/.translations/pt.json new file mode 100644 index 000000000000..8183f28aed3f --- /dev/null +++ b/homeassistant/components/demo/.translations/pt.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Demonstra\u00e7\u00e3o" + } +} \ No newline at end of file diff --git a/homeassistant/components/demo/.translations/ru.json b/homeassistant/components/demo/.translations/ru.json new file mode 100644 index 000000000000..0438252a4299 --- /dev/null +++ b/homeassistant/components/demo/.translations/ru.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "\u0414\u0435\u043c\u043e" + } +} \ No newline at end of file diff --git a/homeassistant/components/demo/.translations/sl.json b/homeassistant/components/demo/.translations/sl.json new file mode 100644 index 000000000000..ef01fcb4f3c3 --- /dev/null +++ b/homeassistant/components/demo/.translations/sl.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Demo" + } +} \ No newline at end of file diff --git a/homeassistant/components/demo/.translations/zh-Hant.json b/homeassistant/components/demo/.translations/zh-Hant.json new file mode 100644 index 000000000000..cfb0fced0c2d --- /dev/null +++ b/homeassistant/components/demo/.translations/zh-Hant.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "\u5c55\u793a" + } +} \ No newline at end of file diff --git a/homeassistant/components/demo/__init__.py b/homeassistant/components/demo/__init__.py index 93a34794366d..05febfad6033 100644 --- a/homeassistant/components/demo/__init__.py +++ b/homeassistant/components/demo/__init__.py @@ -3,33 +3,37 @@ import logging import time -from homeassistant import bootstrap +from homeassistant import bootstrap, config_entries import homeassistant.core as ha from homeassistant.const import ATTR_ENTITY_ID, EVENT_HOMEASSISTANT_START DOMAIN = "demo" _LOGGER = logging.getLogger(__name__) -COMPONENTS_WITH_DEMO_PLATFORM = [ + +COMPONENTS_WITH_CONFIG_ENTRY_DEMO_PLATFORM = [ "air_quality", "alarm_control_panel", "binary_sensor", - "calendar", "camera", "climate", "cover", - "device_tracker", "fan", - "image_processing", "light", "lock", "media_player", - "notify", "sensor", - "stt", "switch", + "water_heater", +] + +COMPONENTS_WITH_DEMO_PLATFORM = [ "tts", + "stt", "mailbox", - "water_heater", + "notify", + "image_processing", + "calendar", + "device_tracker", ] @@ -38,8 +42,12 @@ async def async_setup(hass, config): if DOMAIN not in config: return True - config.setdefault(ha.DOMAIN, {}) - config.setdefault(DOMAIN, {}) + if not hass.config_entries.async_entries(DOMAIN): + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data={} + ) + ) # Set up demo platforms for component in COMPONENTS_WITH_DEMO_PLATFORM: @@ -47,6 +55,9 @@ async def async_setup(hass, config): hass.helpers.discovery.async_load_platform(component, DOMAIN, {}, config) ) + config.setdefault(ha.DOMAIN, {}) + config.setdefault(DOMAIN, {}) + # Set up sun if not hass.config.latitude: hass.config.latitude = 32.87336 @@ -176,6 +187,16 @@ async def demo_start_listener(_event): return True +async def async_setup_entry(hass, config_entry): + """Set the config entry up.""" + # Set up demo platforms with config entry + for component in COMPONENTS_WITH_CONFIG_ENTRY_DEMO_PLATFORM: + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(config_entry, component) + ) + return True + + async def finish_setup(hass, config): """Finish set up once demo platforms are set up.""" switches = None diff --git a/homeassistant/components/demo/air_quality.py b/homeassistant/components/demo/air_quality.py index 0b41d9c87e9a..9fe0f675d9d0 100644 --- a/homeassistant/components/demo/air_quality.py +++ b/homeassistant/components/demo/air_quality.py @@ -2,13 +2,18 @@ from homeassistant.components.air_quality import AirQualityEntity -def setup_platform(hass, config, add_entities, discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the Air Quality.""" - add_entities( + async_add_entities( [DemoAirQuality("Home", 14, 23, 100), DemoAirQuality("Office", 4, 16, None)] ) +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the Demo config entry.""" + await async_setup_platform(hass, {}, async_add_entities) + + class DemoAirQuality(AirQualityEntity): """Representation of Air Quality data.""" diff --git a/homeassistant/components/demo/alarm_control_panel.py b/homeassistant/components/demo/alarm_control_panel.py index 378ba3b18dd6..0323b68b1b04 100644 --- a/homeassistant/components/demo/alarm_control_panel.py +++ b/homeassistant/components/demo/alarm_control_panel.py @@ -1,16 +1,17 @@ """Demo platform that has two fake alarm control panels.""" import datetime + from homeassistant.components.manual.alarm_control_panel import ManualAlarm from homeassistant.const import ( + CONF_DELAY_TIME, + CONF_PENDING_TIME, + CONF_TRIGGER_TIME, STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_CUSTOM_BYPASS, STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT, STATE_ALARM_DISARMED, STATE_ALARM_TRIGGERED, - CONF_DELAY_TIME, - CONF_PENDING_TIME, - CONF_TRIGGER_TIME, ) @@ -57,3 +58,8 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= ) ] ) + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the Demo config entry.""" + await async_setup_platform(hass, {}, async_add_entities) diff --git a/homeassistant/components/demo/binary_sensor.py b/homeassistant/components/demo/binary_sensor.py index 96c2b66fa5be..c1e42807f6d3 100644 --- a/homeassistant/components/demo/binary_sensor.py +++ b/homeassistant/components/demo/binary_sensor.py @@ -1,26 +1,49 @@ """Demo platform that has two fake binary sensors.""" from homeassistant.components.binary_sensor import BinarySensorDevice +from . import DOMAIN -def setup_platform(hass, config, add_entities, discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the Demo binary sensor platform.""" - add_entities( + async_add_entities( [ - DemoBinarySensor("Basement Floor Wet", False, "moisture"), - DemoBinarySensor("Movement Backyard", True, "motion"), + DemoBinarySensor("binary_1", "Basement Floor Wet", False, "moisture"), + DemoBinarySensor("binary_2", "Movement Backyard", True, "motion"), ] ) +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the Demo config entry.""" + await async_setup_platform(hass, {}, async_add_entities) + + class DemoBinarySensor(BinarySensorDevice): """representation of a Demo binary sensor.""" - def __init__(self, name, state, device_class): + def __init__(self, unique_id, name, state, device_class): """Initialize the demo sensor.""" + self._unique_id = unique_id self._name = name self._state = state self._sensor_type = device_class + @property + def device_info(self): + """Return device info.""" + return { + "identifiers": { + # Serial numbers are unique identifiers within a specific domain + (DOMAIN, self.unique_id) + }, + "name": self.name, + } + + @property + def unique_id(self): + """Return the unique id.""" + return self._unique_id + @property def device_class(self): """Return the class of this sensor.""" diff --git a/homeassistant/components/demo/camera.py b/homeassistant/components/demo/camera.py index 0cd77b6112ec..f639dae9757c 100644 --- a/homeassistant/components/demo/camera.py +++ b/homeassistant/components/demo/camera.py @@ -12,6 +12,11 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async_add_entities([DemoCamera("Demo camera")]) +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the Demo config entry.""" + await async_setup_platform(hass, {}, async_add_entities) + + class DemoCamera(Camera): """The representation of a Demo camera.""" diff --git a/homeassistant/components/demo/climate.py b/homeassistant/components/demo/climate.py index f5117b7986cc..f4affed7ced1 100644 --- a/homeassistant/components/demo/climate.py +++ b/homeassistant/components/demo/climate.py @@ -1,4 +1,5 @@ """Demo platform that offers a fake climate device.""" +import logging from homeassistant.components.climate import ClimateDevice from homeassistant.components.climate.const import ( ATTR_TARGET_TEMP_HIGH, @@ -20,15 +21,18 @@ HVAC_MODE_AUTO, ) from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT +from . import DOMAIN SUPPORT_FLAGS = 0 +_LOGGER = logging.getLogger(__name__) -def setup_platform(hass, config, add_entities, discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the Demo climate devices.""" - add_entities( + async_add_entities( [ DemoClimate( + unique_id="climate_1", name="HeatPump", target_temperature=68, unit_of_measurement=TEMP_FAHRENHEIT, @@ -46,6 +50,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): hvac_modes=[HVAC_MODE_HEAT, HVAC_MODE_OFF], ), DemoClimate( + unique_id="climate_2", name="Hvac", target_temperature=21, unit_of_measurement=TEMP_CELSIUS, @@ -63,6 +68,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): hvac_modes=[mode for mode in HVAC_MODES if mode != HVAC_MODE_HEAT_COOL], ), DemoClimate( + unique_id="climate_3", name="Ecobee", target_temperature=None, unit_of_measurement=TEMP_CELSIUS, @@ -84,11 +90,17 @@ def setup_platform(hass, config, add_entities, discovery_info=None): ) +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the Demo climate devices config entry.""" + await async_setup_platform(hass, {}, async_add_entities) + + class DemoClimate(ClimateDevice): """Representation of a demo climate device.""" def __init__( self, + unique_id, name, target_temperature, unit_of_measurement, @@ -107,6 +119,7 @@ def __init__( preset_modes=None, ): """Initialize the climate device.""" + self._unique_id = unique_id self._name = name self._support_flags = SUPPORT_FLAGS if target_temperature is not None: @@ -143,6 +156,22 @@ def __init__( self._target_temperature_high = target_temp_high self._target_temperature_low = target_temp_low + @property + def device_info(self): + """Return device info.""" + return { + "identifiers": { + # Serial numbers are unique identifiers within a specific domain + (DOMAIN, self.unique_id) + }, + "name": self.name, + } + + @property + def unique_id(self): + """Return the unique id.""" + return self._unique_id + @property def supported_features(self): """Return the list of supported features.""" diff --git a/homeassistant/components/demo/config_flow.py b/homeassistant/components/demo/config_flow.py new file mode 100644 index 000000000000..e6b275920c8c --- /dev/null +++ b/homeassistant/components/demo/config_flow.py @@ -0,0 +1,16 @@ +"""Config flow to configure demo component.""" + +from homeassistant import config_entries + +# pylint: disable=unused-import +from . import DOMAIN + + +class DemoConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Demo configuration flow.""" + + VERSION = 1 + + async def async_step_import(self, import_info): + """Set the config entry up from yaml.""" + return self.async_create_entry(title="Demo", data={}) diff --git a/homeassistant/components/demo/const.py b/homeassistant/components/demo/const.py new file mode 100644 index 000000000000..e11b0b0731a3 --- /dev/null +++ b/homeassistant/components/demo/const.py @@ -0,0 +1,3 @@ +"""Constants for the Demo component.""" +DOMAIN = "demo" +SERVICE_RANDOMIZE_DEVICE_TRACKER_DATA = "randomize_device_tracker_data" diff --git a/homeassistant/components/demo/cover.py b/homeassistant/components/demo/cover.py index 180312eefa39..20a8747aaa5b 100644 --- a/homeassistant/components/demo/cover.py +++ b/homeassistant/components/demo/cover.py @@ -8,17 +8,19 @@ SUPPORT_OPEN, CoverDevice, ) +from . import DOMAIN -def setup_platform(hass, config, add_entities, discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the Demo covers.""" - add_entities( + async_add_entities( [ - DemoCover(hass, "Kitchen Window"), - DemoCover(hass, "Hall Window", 10), - DemoCover(hass, "Living Room Window", 70, 50), + DemoCover(hass, "cover_1", "Kitchen Window"), + DemoCover(hass, "cover_2", "Hall Window", 10), + DemoCover(hass, "cover_3", "Living Room Window", 70, 50), DemoCover( hass, + "cover_4", "Garage Door", device_class="garage", supported_features=(SUPPORT_OPEN | SUPPORT_CLOSE), @@ -27,12 +29,18 @@ def setup_platform(hass, config, add_entities, discovery_info=None): ) +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the Demo config entry.""" + await async_setup_platform(hass, {}, async_add_entities) + + class DemoCover(CoverDevice): """Representation of a demo cover.""" def __init__( self, hass, + unique_id, name, position=None, tilt_position=None, @@ -41,6 +49,7 @@ def __init__( ): """Initialize the cover.""" self.hass = hass + self._unique_id = unique_id self._name = name self._position = position self._device_class = device_class @@ -59,6 +68,22 @@ def __init__( else: self._closed = self.current_cover_position <= 0 + @property + def device_info(self): + """Return device info.""" + return { + "identifiers": { + # Serial numbers are unique identifiers within a specific domain + (DOMAIN, self.unique_id) + }, + "name": self.name, + } + + @property + def unique_id(self): + """Return unique ID for cover.""" + return self._unique_id + @property def name(self): """Return the name of the cover.""" diff --git a/homeassistant/components/demo/device_tracker.py b/homeassistant/components/demo/device_tracker.py index fba8095efd64..02864111527b 100644 --- a/homeassistant/components/demo/device_tracker.py +++ b/homeassistant/components/demo/device_tracker.py @@ -1,7 +1,7 @@ """Demo platform for the Device tracker component.""" import random -from homeassistant.components.device_tracker import DOMAIN +from .const import DOMAIN, SERVICE_RANDOMIZE_DEVICE_TRACKER_DATA def setup_scanner(hass, config, see, discovery_info=None): @@ -36,6 +36,6 @@ def observe(call=None): battery=53, ) - hass.services.register(DOMAIN, "demo", observe) + hass.services.register(DOMAIN, SERVICE_RANDOMIZE_DEVICE_TRACKER_DATA, observe) return True diff --git a/homeassistant/components/demo/fan.py b/homeassistant/components/demo/fan.py index ab8a6f3fae9d..500d5f6a5ce5 100644 --- a/homeassistant/components/demo/fan.py +++ b/homeassistant/components/demo/fan.py @@ -15,9 +15,9 @@ LIMITED_SUPPORT = SUPPORT_SET_SPEED -def setup_platform(hass, config, add_entities_callback, discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the demo fan platform.""" - add_entities_callback( + async_add_entities( [ DemoFan(hass, "Living Room Fan", FULL_SUPPORT), DemoFan(hass, "Ceiling Fan", LIMITED_SUPPORT), @@ -25,6 +25,11 @@ def setup_platform(hass, config, add_entities_callback, discovery_info=None): ) +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the Demo config entry.""" + await async_setup_platform(hass, {}, async_add_entities) + + class DemoFan(FanEntity): """A demonstration fan component.""" diff --git a/homeassistant/components/demo/light.py b/homeassistant/components/demo/light.py index f8b1b5115115..a2c06b72986e 100644 --- a/homeassistant/components/demo/light.py +++ b/homeassistant/components/demo/light.py @@ -15,6 +15,8 @@ Light, ) +from . import DOMAIN + LIGHT_COLORS = [(56, 86), (345, 75)] LIGHT_EFFECT_LIST = ["rainbow", "none"] @@ -30,24 +32,33 @@ ) -def setup_platform(hass, config, add_entities_callback, discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the demo light platform.""" - add_entities_callback( + async_add_entities( [ DemoLight( - 1, + "light_1", "Bed Light", False, True, effect_list=LIGHT_EFFECT_LIST, effect=LIGHT_EFFECT_LIST[0], ), - DemoLight(2, "Ceiling Lights", True, True, LIGHT_COLORS[0], LIGHT_TEMPS[1]), - DemoLight(3, "Kitchen Lights", True, True, LIGHT_COLORS[1], LIGHT_TEMPS[0]), + DemoLight( + "light_2", "Ceiling Lights", True, True, LIGHT_COLORS[0], LIGHT_TEMPS[1] + ), + DemoLight( + "light_3", "Kitchen Lights", True, True, LIGHT_COLORS[1], LIGHT_TEMPS[0] + ), ] ) +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the Demo config entry.""" + await async_setup_platform(hass, {}, async_add_entities) + + class DemoLight(Light): """Representation of a demo light.""" @@ -76,6 +87,17 @@ def __init__( self._effect = effect self._available = True + @property + def device_info(self): + """Return device info.""" + return { + "identifiers": { + # Serial numbers are unique identifiers within a specific domain + (DOMAIN, self.unique_id) + }, + "name": self.name, + } + @property def should_poll(self) -> bool: """No polling needed for a demo light.""" diff --git a/homeassistant/components/demo/lock.py b/homeassistant/components/demo/lock.py index d8fb244b9bcb..923469f045c4 100644 --- a/homeassistant/components/demo/lock.py +++ b/homeassistant/components/demo/lock.py @@ -4,9 +4,9 @@ from homeassistant.components.lock import SUPPORT_OPEN, LockDevice -def setup_platform(hass, config, add_entities, discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the Demo lock platform.""" - add_entities( + async_add_entities( [ DemoLock("Front Door", STATE_LOCKED), DemoLock("Kitchen Door", STATE_UNLOCKED), @@ -15,6 +15,11 @@ def setup_platform(hass, config, add_entities, discovery_info=None): ) +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the Demo config entry.""" + await async_setup_platform(hass, {}, async_add_entities) + + class DemoLock(LockDevice): """Representation of a Demo lock.""" diff --git a/homeassistant/components/demo/media_player.py b/homeassistant/components/demo/media_player.py index fb64f8015c0f..9d7c3892af85 100644 --- a/homeassistant/components/demo/media_player.py +++ b/homeassistant/components/demo/media_player.py @@ -24,9 +24,9 @@ import homeassistant.util.dt as dt_util -def setup_platform(hass, config, add_entities, discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the media player demo platform.""" - add_entities( + async_add_entities( [ DemoYoutubePlayer( "Living Room", @@ -43,6 +43,11 @@ def setup_platform(hass, config, add_entities, discovery_info=None): ) +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the Demo config entry.""" + await async_setup_platform(hass, {}, async_add_entities) + + YOUTUBE_COVER_URL_FORMAT = "https://img.youtube.com/vi/{}/hqdefault.jpg" SOUND_MODE_LIST = ["Dummy Music", "Dummy Movie"] DEFAULT_SOUND_MODE = "Dummy Music" diff --git a/homeassistant/components/demo/remote.py b/homeassistant/components/demo/remote.py index 4a363781e2ef..70e0d3c8b6e1 100644 --- a/homeassistant/components/demo/remote.py +++ b/homeassistant/components/demo/remote.py @@ -3,6 +3,11 @@ from homeassistant.const import DEVICE_DEFAULT_NAME +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the Demo config entry.""" + setup_platform(hass, {}, async_add_entities) + + def setup_platform(hass, config, add_entities_callback, discovery_info=None): """Set up the demo remotes.""" add_entities_callback( diff --git a/homeassistant/components/demo/sensor.py b/homeassistant/components/demo/sensor.py index 78bd88b42cf5..bf5df94e74c3 100644 --- a/homeassistant/components/demo/sensor.py +++ b/homeassistant/components/demo/sensor.py @@ -6,31 +6,63 @@ DEVICE_CLASS_TEMPERATURE, ) from homeassistant.helpers.entity import Entity +from . import DOMAIN -def setup_platform(hass, config, add_entities, discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the Demo sensors.""" - add_entities( + async_add_entities( [ DemoSensor( - "Outside Temperature", 15.6, DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, 12 + "sensor_1", + "Outside Temperature", + 15.6, + DEVICE_CLASS_TEMPERATURE, + TEMP_CELSIUS, + 12, + ), + DemoSensor( + "sensor_2", "Outside Humidity", 54, DEVICE_CLASS_HUMIDITY, "%", None ), - DemoSensor("Outside Humidity", 54, DEVICE_CLASS_HUMIDITY, "%", None), ] ) +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the Demo config entry.""" + await async_setup_platform(hass, {}, async_add_entities) + + class DemoSensor(Entity): """Representation of a Demo sensor.""" - def __init__(self, name, state, device_class, unit_of_measurement, battery): + def __init__( + self, unique_id, name, state, device_class, unit_of_measurement, battery + ): """Initialize the sensor.""" + self._unique_id = unique_id self._name = name self._state = state self._device_class = device_class self._unit_of_measurement = unit_of_measurement self._battery = battery + @property + def device_info(self): + """Return device info.""" + return { + "identifiers": { + # Serial numbers are unique identifiers within a specific domain + (DOMAIN, self.unique_id) + }, + "name": self.name, + } + + @property + def unique_id(self): + """Return the unique id.""" + return self._unique_id + @property def should_poll(self): """No polling needed for a demo sensor.""" diff --git a/homeassistant/components/demo/services.yaml b/homeassistant/components/demo/services.yaml index e69de29bb2d1..a8a96b21c192 100644 --- a/homeassistant/components/demo/services.yaml +++ b/homeassistant/components/demo/services.yaml @@ -0,0 +1,2 @@ +randomize_device_tracker_data: + description: Demonstrates using a device tracker to see where devices are located \ No newline at end of file diff --git a/homeassistant/components/demo/strings.json b/homeassistant/components/demo/strings.json new file mode 100644 index 000000000000..a2c0103280be --- /dev/null +++ b/homeassistant/components/demo/strings.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Demo" + } +} diff --git a/homeassistant/components/demo/switch.py b/homeassistant/components/demo/switch.py index 65860867ed63..23006cff875a 100644 --- a/homeassistant/components/demo/switch.py +++ b/homeassistant/components/demo/switch.py @@ -1,29 +1,52 @@ """Demo platform that has two fake switches.""" from homeassistant.components.switch import SwitchDevice from homeassistant.const import DEVICE_DEFAULT_NAME +from . import DOMAIN -def setup_platform(hass, config, add_entities_callback, discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the demo switches.""" - add_entities_callback( + async_add_entities( [ - DemoSwitch("Decorative Lights", True, None, True), - DemoSwitch("AC", False, "mdi:air-conditioner", False), + DemoSwitch("swith1", "Decorative Lights", True, None, True), + DemoSwitch("swith2", "AC", False, "mdi:air-conditioner", False), ] ) +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the Demo config entry.""" + await async_setup_platform(hass, {}, async_add_entities) + + class DemoSwitch(SwitchDevice): """Representation of a demo switch.""" - def __init__(self, name, state, icon, assumed, device_class=None): + def __init__(self, unique_id, name, state, icon, assumed, device_class=None): """Initialize the Demo switch.""" + self._unique_id = unique_id self._name = name or DEVICE_DEFAULT_NAME self._state = state self._icon = icon self._assumed = assumed self._device_class = device_class + @property + def device_info(self): + """Return device info.""" + return { + "identifiers": { + # Serial numbers are unique identifiers within a specific domain + (DOMAIN, self.unique_id) + }, + "name": self.name, + } + + @property + def unique_id(self): + """Return the unique id.""" + return self._unique_id + @property def should_poll(self): """No polling needed for a demo switch.""" diff --git a/homeassistant/components/demo/vacuum.py b/homeassistant/components/demo/vacuum.py index 2ba704d39252..fb64f17a4521 100644 --- a/homeassistant/components/demo/vacuum.py +++ b/homeassistant/components/demo/vacuum.py @@ -76,6 +76,11 @@ DEMO_VACUUM_STATE = "5_Fifth_floor" +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the Demo config entry.""" + setup_platform(hass, {}, async_add_entities) + + def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Demo vacuums.""" add_entities( diff --git a/homeassistant/components/demo/water_heater.py b/homeassistant/components/demo/water_heater.py index d1d53e058c66..c3fff26c9922 100644 --- a/homeassistant/components/demo/water_heater.py +++ b/homeassistant/components/demo/water_heater.py @@ -13,9 +13,9 @@ ) -def setup_platform(hass, config, add_entities, discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the Demo water_heater devices.""" - add_entities( + async_add_entities( [ DemoWaterHeater("Demo Water Heater", 119, TEMP_FAHRENHEIT, False, "eco"), DemoWaterHeater("Demo Water Heater Celsius", 45, TEMP_CELSIUS, True, "eco"), @@ -23,6 +23,11 @@ def setup_platform(hass, config, add_entities, discovery_info=None): ) +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the Demo config entry.""" + await async_setup_platform(hass, {}, async_add_entities) + + class DemoWaterHeater(WaterHeaterDevice): """Representation of a demo water_heater device.""" diff --git a/homeassistant/components/demo/weather.py b/homeassistant/components/demo/weather.py index 2253f261ad2c..8cc1b2f95fdf 100644 --- a/homeassistant/components/demo/weather.py +++ b/homeassistant/components/demo/weather.py @@ -30,6 +30,11 @@ } +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the Demo config entry.""" + setup_platform(hass, {}, async_add_entities) + + def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Demo weather.""" add_entities( diff --git a/homeassistant/components/device_tracker/.translations/pt.json b/homeassistant/components/device_tracker/.translations/pt.json new file mode 100644 index 000000000000..952eb4b1475d --- /dev/null +++ b/homeassistant/components/device_tracker/.translations/pt.json @@ -0,0 +1,8 @@ +{ + "device_automation": { + "condtion_type": { + "is_home": "{entity_name} est\u00e1 em casa", + "is_not_home": "{entity_name} n\u00e3o est\u00e1 em casa" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/device_tracker/__init__.py b/homeassistant/components/device_tracker/__init__.py index 84bc76e0b047..25e33d2a2db2 100644 --- a/homeassistant/components/device_tracker/__init__.py +++ b/homeassistant/components/device_tracker/__init__.py @@ -13,11 +13,11 @@ from homeassistant.const import ATTR_GPS_ACCURACY, STATE_HOME from . import legacy, setup -from .config_entry import ( # noqa # pylint: disable=unused-import +from .config_entry import ( # noqa: F401 pylint: disable=unused-import async_setup_entry, async_unload_entry, ) -from .legacy import DeviceScanner # noqa # pylint: disable=unused-import +from .legacy import DeviceScanner # noqa: F401 pylint: disable=unused-import from .const import ( ATTR_ATTRIBUTES, ATTR_BATTERY, diff --git a/homeassistant/components/device_tracker/services.yaml b/homeassistant/components/device_tracker/services.yaml index 938e9c8e3249..51865034b00f 100644 --- a/homeassistant/components/device_tracker/services.yaml +++ b/homeassistant/components/device_tracker/services.yaml @@ -24,40 +24,3 @@ see: battery: description: Battery level of device. example: '100' - -icloud_lost_iphone: - description: Service to play the lost iphone sound on an iDevice. - fields: - account_name: - description: Name of the account in the config that will be used to look for the device. This is optional, if it isn't given it will use all accounts. - example: 'bart' - device_name: - description: Name of the device that will play the sound. This is optional, if it isn't given it will play on all devices for the given account. - example: 'iphonebart' -icloud_set_interval: - description: Service to set the interval of an iDevice. - fields: - account_name: - description: Name of the account in the config that will be used to look for the device. This is optional, if it isn't given it will use all accounts. - example: 'bart' - device_name: - description: Name of the device that will get a new interval. This is optional, if it isn't given it will change the interval for all devices for the given account. - example: 'iphonebart' - interval: - description: The interval (in minutes) that the iDevice will have until the according device_tracker entity changes from zone or until this service is used again. This is optional, if it isn't given the interval of the device will revert back to the original interval based on the current state. - example: 1 -icloud_update: - description: Service to ask for an update of an iDevice. - fields: - account_name: - description: Name of the account in the config that will be used to look for the device. This is optional, if it isn't given it will use all accounts. - example: 'bart' - device_name: - description: Name of the device that will be updated. This is optional, if it isn't given it will update all devices for the given account. - example: 'iphonebart' -icloud_reset_account: - description: Service to restart an iCloud account. Helpful when not all devices are found after initializing or when you add a new device. - fields: - account_name: - description: Name of the account in the config that will be restarted. This is optional, if it isn't given it will restart all accounts. - example: 'bart' diff --git a/homeassistant/components/directv/media_player.py b/homeassistant/components/directv/media_player.py index 2be2544cec1c..5dd673ca93f9 100644 --- a/homeassistant/components/directv/media_player.py +++ b/homeassistant/components/directv/media_player.py @@ -1,9 +1,11 @@ """Support for the DirecTV receivers.""" import logging + +from DirectPy import DIRECTV import requests import voluptuous as vol -from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA +from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice from homeassistant.components.media_player.const import ( MEDIA_TYPE_CHANNEL, MEDIA_TYPE_MOVIE, @@ -99,8 +101,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): # Attempt to discover additional RVU units _LOGGER.debug("Doing discovery of DirecTV devices on %s", host) - from DirectPy import DIRECTV - dtv = DIRECTV(host, DEFAULT_PORT) try: resp = dtv.get_locations() @@ -156,7 +156,6 @@ class DirecTvDevice(MediaPlayerDevice): def __init__(self, name, host, port, device): """Initialize the device.""" - from DirectPy import DIRECTV self.dtv = DIRECTV(host, port, device) self._name = name diff --git a/homeassistant/components/discord/manifest.json b/homeassistant/components/discord/manifest.json index d00d26d2b5ed..b5bf7eab6ccd 100644 --- a/homeassistant/components/discord/manifest.json +++ b/homeassistant/components/discord/manifest.json @@ -3,7 +3,7 @@ "name": "Discord", "documentation": "https://www.home-assistant.io/integrations/discord", "requirements": [ - "discord.py==1.2.4" + "discord.py==1.2.5" ], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/discovery/__init__.py b/homeassistant/components/discovery/__init__.py index 15fcfc15338d..884f6680d7cd 100644 --- a/homeassistant/components/discovery/__init__.py +++ b/homeassistant/components/discovery/__init__.py @@ -10,6 +10,7 @@ from datetime import timedelta import logging +from netdisco.discovery import NetworkDiscovery import voluptuous as vol from homeassistant import config_entries @@ -129,7 +130,6 @@ async def async_setup(hass, config): """Start a discovery service.""" - from netdisco.discovery import NetworkDiscovery logger = logging.getLogger(__name__) netdisco = NetworkDiscovery() diff --git a/homeassistant/components/dlib_face_detect/image_processing.py b/homeassistant/components/dlib_face_detect/image_processing.py index cdd8bc101b5d..6e5c49e7aba4 100644 --- a/homeassistant/components/dlib_face_detect/image_processing.py +++ b/homeassistant/components/dlib_face_detect/image_processing.py @@ -12,7 +12,7 @@ ) # pylint: disable=unused-import -from homeassistant.components.image_processing import PLATFORM_SCHEMA # noqa +from homeassistant.components.image_processing import PLATFORM_SCHEMA # noqa: F401 from homeassistant.core import split_entity_id _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/dlink/switch.py b/homeassistant/components/dlink/switch.py index 25091e14dbd3..7fa391e80606 100644 --- a/homeassistant/components/dlink/switch.py +++ b/homeassistant/components/dlink/switch.py @@ -3,6 +3,7 @@ import logging import urllib +from pyW215.pyW215 import SmartPlug import voluptuous as vol from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchDevice @@ -42,7 +43,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up a D-Link Smart Plug.""" - from pyW215.pyW215 import SmartPlug host = config.get(CONF_HOST) username = config.get(CONF_USERNAME) diff --git a/homeassistant/components/dlna_dmr/media_player.py b/homeassistant/components/dlna_dmr/media_player.py index 5dd7ab7a88a7..a9ea9b21d596 100644 --- a/homeassistant/components/dlna_dmr/media_player.py +++ b/homeassistant/components/dlna_dmr/media_player.py @@ -6,9 +6,12 @@ from typing import Optional import aiohttp +from async_upnp_client import UpnpFactory +from async_upnp_client.aiohttp import AiohttpNotifyServer, AiohttpSessionRequester +from async_upnp_client.profiles.dlna import DeviceState, DmrDevice import voluptuous as vol -from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA +from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice from homeassistant.components.media_player.const import ( MEDIA_TYPE_CHANNEL, MEDIA_TYPE_EPISODE, @@ -40,10 +43,10 @@ ) 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.helpers.typing import HomeAssistantType from homeassistant.util import get_local_ip +import homeassistant.util.dt as dt_util _LOGGER = logging.getLogger(__name__) @@ -121,8 +124,6 @@ async def async_start_event_handler( return hass_data["event_handler"] # start event handler - from async_upnp_client.aiohttp import AiohttpNotifyServer - server = AiohttpNotifyServer( requester, listen_port=server_port, @@ -163,8 +164,6 @@ async def async_setup_platform( hass.data[DLNA_DMR_DATA]["lock"] = asyncio.Lock() # build upnp/aiohttp requester - from async_upnp_client.aiohttp import AiohttpSessionRequester - session = async_get_clientsession(hass) requester = AiohttpSessionRequester(session, True) @@ -180,8 +179,6 @@ async def async_setup_platform( ) # create upnp device - from async_upnp_client import UpnpFactory - factory = UpnpFactory(requester, disable_state_variable_validation=True) try: upnp_device = await factory.async_create_device(url) @@ -189,8 +186,6 @@ async def async_setup_platform( raise PlatformNotReady() # wrap with DmrDevice - from async_upnp_client.profiles.dlna import DmrDevice - dlna_device = DmrDevice(upnp_device, event_handler) # create our own device @@ -361,8 +356,6 @@ async def async_play_media(self, media_type, media_id, **kwargs): await self._device.async_wait_for_can_play() # If already playing, no need to call Play - from async_upnp_client.profiles.dlna import DeviceState - if self._device.state == DeviceState.PLAYING: return @@ -403,8 +396,6 @@ def state(self): if not self._available: return STATE_OFF - from async_upnp_client.profiles.dlna import DeviceState - if self._device.state is None: return STATE_ON if self._device.state == DeviceState.PLAYING: diff --git a/homeassistant/components/dominos/__init__.py b/homeassistant/components/dominos/__init__.py index 59869ed0a977..78852fa2699a 100644 --- a/homeassistant/components/dominos/__init__.py +++ b/homeassistant/components/dominos/__init__.py @@ -2,6 +2,8 @@ from datetime import timedelta import logging +from pizzapi import Address, Customer, Order +from pizzapi.address import StoreException import voluptuous as vol from homeassistant.components import http @@ -91,8 +93,6 @@ class Dominos: def __init__(self, hass, config): """Set up main service.""" conf = config[DOMAIN] - from pizzapi import Address, Customer - from pizzapi.address import StoreException self.hass = hass self.customer = Customer( @@ -127,8 +127,6 @@ def handle_order(self, call): @Throttle(MIN_TIME_BETWEEN_STORE_UPDATES) def update_closest_store(self): """Update the shared closest store (if open).""" - from pizzapi.address import StoreException - try: self.closest_store = self.address.closest_store() return True @@ -209,8 +207,6 @@ def state(self): @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): """Update the order state and refreshes the store.""" - from pizzapi.address import StoreException - try: self.dominos.update_closest_store() except StoreException: @@ -226,9 +222,6 @@ def update(self): def order(self): """Create the order object.""" - from pizzapi import Order - from pizzapi.address import StoreException - if self.dominos.closest_store is None: raise StoreException @@ -246,8 +239,6 @@ def order(self): def place(self): """Place the order.""" - from pizzapi.address import StoreException - try: order = self.order() order.place() diff --git a/homeassistant/components/doorbird/__init__.py b/homeassistant/components/doorbird/__init__.py index ff0bbd711940..d92ff3d36924 100644 --- a/homeassistant/components/doorbird/__init__.py +++ b/homeassistant/components/doorbird/__init__.py @@ -2,6 +2,7 @@ import logging from urllib.error import HTTPError +from doorbirdpy import DoorBird import voluptuous as vol from homeassistant.components.http import HomeAssistantView @@ -51,7 +52,6 @@ def setup(hass, config): """Set up the DoorBird component.""" - from doorbirdpy import DoorBird # Provide an endpoint for the doorstations to call to trigger events hass.http.register_view(DoorBirdRequestView) @@ -264,7 +264,6 @@ class DoorBirdRequestView(HomeAssistantView): name = API_URL[1:].replace("/", ":") extra_urls = [API_URL + "/{event}"] - # pylint: disable=no-self-use async def get(self, request, event): """Respond to requests from the device.""" from aiohttp import web diff --git a/homeassistant/components/doorbird/camera.py b/homeassistant/components/doorbird/camera.py index 457c319d9e16..d9a802f071f5 100644 --- a/homeassistant/components/doorbird/camera.py +++ b/homeassistant/components/doorbird/camera.py @@ -6,7 +6,7 @@ import aiohttp import async_timeout -from homeassistant.components.camera import Camera, SUPPORT_STREAM +from homeassistant.components.camera import SUPPORT_STREAM, Camera from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.util.dt as dt_util diff --git a/homeassistant/components/dovado/__init__.py b/homeassistant/components/dovado/__init__.py index a13c49cc61a1..03f12314c5af 100644 --- a/homeassistant/components/dovado/__init__.py +++ b/homeassistant/components/dovado/__init__.py @@ -21,11 +21,16 @@ CONFIG_SCHEMA = vol.Schema( { - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Optional(CONF_HOST): cv.string, - vol.Optional(CONF_PORT): cv.port, - } + DOMAIN: vol.Schema( + { + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Optional(CONF_HOST): cv.string, + vol.Optional(CONF_PORT): cv.port, + } + ) + }, + extra=vol.ALLOW_EXTRA, ) MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=30) @@ -36,10 +41,10 @@ def setup(hass, config): hass.data[DOMAIN] = DovadoData( dovado.Dovado( - config[CONF_USERNAME], - config[CONF_PASSWORD], - config.get(CONF_HOST), - config.get(CONF_PORT), + config[DOMAIN].get(CONF_USERNAME), + config[DOMAIN].get(CONF_PASSWORD), + config[DOMAIN].get(CONF_HOST), + config[DOMAIN].get(CONF_PORT), ) ) return True diff --git a/homeassistant/components/dsmr_reader/__init__.py b/homeassistant/components/dsmr_reader/__init__.py new file mode 100755 index 000000000000..946be91d1a56 --- /dev/null +++ b/homeassistant/components/dsmr_reader/__init__.py @@ -0,0 +1 @@ +"""The DSMR Reader component.""" diff --git a/homeassistant/components/dsmr_reader/definitions.py b/homeassistant/components/dsmr_reader/definitions.py new file mode 100644 index 000000000000..f4a36ebc3468 --- /dev/null +++ b/homeassistant/components/dsmr_reader/definitions.py @@ -0,0 +1,243 @@ +"""Definitions for DSMR Reader sensors added to MQTT.""" + + +def dsmr_transform(value): + """Transform DSMR version value to right format.""" + return float(value) / 10 + + +def tariff_transform(value): + """Transform tariff from number to description.""" + if value == "1": + return "low" + return "high" + + +DEFINITIONS = { + "dsmr/reading/electricity_delivered_1": { + "name": "Low tariff usage", + "icon": "mdi:flash", + "unit": "kWh", + }, + "dsmr/reading/electricity_returned_1": { + "name": "Low tariff returned", + "icon": "mdi:flash-outline", + "unit": "kWh", + }, + "dsmr/reading/electricity_delivered_2": { + "name": "High tariff usage", + "icon": "mdi:flash", + "unit": "kWh", + }, + "dsmr/reading/electricity_returned_2": { + "name": "High tariff returned", + "icon": "mdi:flash-outline", + "unit": "kWh", + }, + "dsmr/reading/electricity_currently_delivered": { + "name": "Current power usage", + "icon": "mdi:flash", + "unit": "kW", + }, + "dsmr/reading/electricity_currently_returned": { + "name": "Current power return", + "icon": "mdi:flash-outline", + "unit": "kW", + }, + "dsmr/reading/phase_currently_delivered_l1": { + "name": "Current power usage L1", + "icon": "mdi:flash", + "unit": "kW", + }, + "dsmr/reading/phase_currently_delivered_l2": { + "name": "Current power usage L2", + "icon": "mdi:flash", + "unit": "kW", + }, + "dsmr/reading/phase_currently_delivered_l3": { + "name": "Current power usage L3", + "icon": "mdi:flash", + "unit": "kW", + }, + "dsmr/reading/phase_currently_returned_l1": { + "name": "Current power return L1", + "icon": "mdi:flash-outline", + "unit": "kW", + }, + "dsmr/reading/phase_currently_returned_l2": { + "name": "Current power return L2", + "icon": "mdi:flash-outline", + "unit": "kW", + }, + "dsmr/reading/phase_currently_returned_l3": { + "name": "Current power return L3", + "icon": "mdi:flash-outline", + "unit": "kW", + }, + "dsmr/reading/extra_device_delivered": { + "name": "Gas meter usage", + "icon": "mdi:fire", + "unit": "m3", + }, + "dsmr/reading/phase_voltage_l1": { + "name": "Current voltage L1", + "icon": "mdi:flash", + "unit": "V", + }, + "dsmr/reading/phase_voltage_l2": { + "name": "Current voltage L2", + "icon": "mdi:flash", + "unit": "V", + }, + "dsmr/reading/phase_voltage_l3": { + "name": "Current voltage L3", + "icon": "mdi:flash", + "unit": "V", + }, + "dsmr/consumption/gas/delivered": { + "name": "Gas usage", + "icon": "mdi:fire", + "unit": "m3", + }, + "dsmr/consumption/gas/currently_delivered": { + "name": "Current gas usage", + "icon": "mdi:fire", + "unit": "m3", + }, + "dsmr/consumption/gas/read_at": { + "name": "Gas meter read", + "icon": "mdi:clock", + "unit": "", + }, + "dsmr/day-consumption/electricity1": { + "name": "Low tariff usage", + "icon": "mdi:counter", + "unit": "kWh", + }, + "dsmr/day-consumption/electricity2": { + "name": "High tariff usage", + "icon": "mdi:counter", + "unit": "kWh", + }, + "dsmr/day-consumption/electricity1_returned": { + "name": "Low tariff return", + "icon": "mdi:counter", + "unit": "kWh", + }, + "dsmr/day-consumption/electricity2_returned": { + "name": "High tariff return", + "icon": "mdi:counter", + "unit": "kWh", + }, + "dsmr/day-consumption/electricity_merged": { + "name": "Power usage total", + "icon": "mdi:counter", + "unit": "kWh", + }, + "dsmr/day-consumption/electricity_returned_merged": { + "name": "Power return total", + "icon": "mdi:counter", + "unit": "kWh", + }, + "dsmr/day-consumption/electricity1_cost": { + "name": "Low tariff cost", + "icon": "mdi:currency-eur", + "unit": "€", + }, + "dsmr/day-consumption/electricity2_cost": { + "name": "High tariff cost", + "icon": "mdi:currency-eur", + "unit": "€", + }, + "dsmr/day-consumption/electricity_cost_merged": { + "name": "Power total cost", + "icon": "mdi:currency-eur", + "unit": "€", + }, + "dsmr/day-consumption/gas": { + "name": "Gas usage", + "icon": "mdi:counter", + "unit": "m3", + }, + "dsmr/day-consumption/gas_cost": { + "name": "Gas cost", + "icon": "mdi:currency-eur", + "unit": "€", + }, + "dsmr/day-consumption/total_cost": { + "name": "Total cost", + "icon": "mdi:currency-eur", + "unit": "€", + }, + "dsmr/day-consumption/energy_supplier_price_electricity_delivered_1": { + "name": "Low tariff delivered price", + "icon": "mdi:currency-eur", + "unit": "€", + }, + "dsmr/day-consumption/energy_supplier_price_electricity_delivered_2": { + "name": "High tariff delivered price", + "icon": "mdi:currency-eur", + "unit": "€", + }, + "dsmr/day-consumption/energy_supplier_price_electricity_returned_1": { + "name": "Low tariff returned price", + "icon": "mdi:currency-eur", + "unit": "€", + }, + "dsmr/day-consumption/energy_supplier_price_electricity_returned_2": { + "name": "High tariff returned price", + "icon": "mdi:currency-eur", + "unit": "€", + }, + "dsmr/day-consumption/energy_supplier_price_gas": { + "name": "Gas price", + "icon": "mdi:currency-eur", + "unit": "€", + }, + "dsmr/meter-stats/dsmr_version": { + "name": "DSMR version", + "icon": "mdi:alert-circle", + "transform": dsmr_transform, + }, + "dsmr/meter-stats/electricity_tariff": { + "name": "Electricity tariff", + "icon": "mdi:flash", + "transform": tariff_transform, + }, + "dsmr/meter-stats/power_failure_count": { + "name": "Power failure count", + "icon": "mdi:flash", + }, + "dsmr/meter-stats/long_power_failure_count": { + "name": "Long power failure count", + "icon": "mdi:flash", + }, + "dsmr/meter-stats/voltage_sag_count_l1": { + "name": "Voltage sag L1", + "icon": "mdi:flash", + }, + "dsmr/meter-stats/voltage_sag_count_l2": { + "name": "Voltage sag L2", + "icon": "mdi:flash", + }, + "dsmr/meter-stats/voltage_sag_count_l3": { + "name": "Voltage sag L3", + "icon": "mdi:flash", + }, + "dsmr/meter-stats/voltage_swell_count_l1": { + "name": "Voltage swell L1", + "icon": "mdi:flash", + }, + "dsmr/meter-stats/voltage_swell_count_l2": { + "name": "Voltage swell L2", + "icon": "mdi:flash", + }, + "dsmr/meter-stats/voltage_swell_count_l3": { + "name": "Voltage swell L3", + "icon": "mdi:flash", + }, + "dsmr/meter-stats/rejected_telegrams": { + "name": "Rejected telegrams", + "icon": "mdi:flash", + }, +} diff --git a/homeassistant/components/dsmr_reader/manifest.json b/homeassistant/components/dsmr_reader/manifest.json new file mode 100755 index 000000000000..f1c52e02c830 --- /dev/null +++ b/homeassistant/components/dsmr_reader/manifest.json @@ -0,0 +1,12 @@ +{ + "domain": "dsmr_reader", + "name": "DSMR Reader", + "documentation": "https://www.home-assistant.io/integrations/dsmr_reader", + "requirements": [], + "dependencies": [ + "mqtt" + ], + "codeowners": [ + "@depl0y" + ] +} diff --git a/homeassistant/components/dsmr_reader/sensor.py b/homeassistant/components/dsmr_reader/sensor.py new file mode 100755 index 000000000000..01c010c4971a --- /dev/null +++ b/homeassistant/components/dsmr_reader/sensor.py @@ -0,0 +1,78 @@ +"""Support for DSMR Reader through MQTT.""" +from homeassistant.components import mqtt +from homeassistant.core import callback +from homeassistant.helpers.entity import Entity +from homeassistant.util import slugify + +from .definitions import DEFINITIONS + +DOMAIN = "dsmr_reader" + + +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Set up DSMR Reader sensors.""" + + sensors = [] + for topic in DEFINITIONS: + sensors.append(DSMRSensor(topic)) + + async_add_entities(sensors) + + +class DSMRSensor(Entity): + """Representation of a DSMR sensor that is updated via MQTT.""" + + def __init__(self, topic): + """Initialize the sensor.""" + + self._definition = DEFINITIONS[topic] + + self._entity_id = slugify(topic.replace("/", "_")) + self._topic = topic + + self._name = self._definition.get("name", topic.split("/")[-1]) + self._unit_of_measurement = self._definition.get("unit") + self._icon = self._definition.get("icon") + self._transform = self._definition.get("transform") + self._state = None + + async def async_added_to_hass(self): + """Subscribe to MQTT events.""" + + @callback + def message_received(message): + """Handle new MQTT messages.""" + + if self._transform is not None: + self._state = self._transform(message.payload) + else: + self._state = message.payload + + self.async_schedule_update_ha_state() + + await mqtt.async_subscribe(self.hass, self._topic, message_received, 1) + + @property + def name(self): + """Return the name of the sensor supplied in constructor.""" + return self._name + + @property + def entity_id(self): + """Return the entity ID for this sensor.""" + return f"sensor.{self._entity_id}" + + @property + def state(self): + """Return the current state of the entity.""" + return self._state + + @property + def unit_of_measurement(self): + """Return the unit_of_measurement of this sensor.""" + return self._unit_of_measurement + + @property + def icon(self): + """Return the icon of this sensor.""" + return self._icon diff --git a/homeassistant/components/duke_energy/sensor.py b/homeassistant/components/duke_energy/sensor.py index 998809decc02..cd30ae96caf3 100644 --- a/homeassistant/components/duke_energy/sensor.py +++ b/homeassistant/components/duke_energy/sensor.py @@ -1,12 +1,13 @@ """Support for Duke Energy Gas and Electric meters.""" import logging +from pydukeenergy.api import DukeEnergy, DukeEnergyException import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import CONF_USERNAME, CONF_PASSWORD -from homeassistant.helpers.entity import Entity +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -21,7 +22,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up all Duke Energy meters.""" - from pydukeenergy.api import DukeEnergy, DukeEnergyException try: duke = DukeEnergy( diff --git a/homeassistant/components/dunehd/media_player.py b/homeassistant/components/dunehd/media_player.py index 95e8cac3dbda..bb32bff2a15a 100644 --- a/homeassistant/components/dunehd/media_player.py +++ b/homeassistant/components/dunehd/media_player.py @@ -1,7 +1,8 @@ """DuneHD implementation of the media player.""" +from pdunehd import DuneHDPlayer import voluptuous as vol -from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA +from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice from homeassistant.components.media_player.const import ( SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, @@ -46,7 +47,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the DuneHD media player platform.""" - from pdunehd import DuneHDPlayer sources = config.get(CONF_SOURCES, {}) host = config.get(CONF_HOST) diff --git a/homeassistant/components/dwd_weather_warnings/sensor.py b/homeassistant/components/dwd_weather_warnings/sensor.py index 4d7ad04e3825..2484ba70074f 100644 --- a/homeassistant/components/dwd_weather_warnings/sensor.py +++ b/homeassistant/components/dwd_weather_warnings/sensor.py @@ -19,6 +19,7 @@ import voluptuous as vol import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.aiohttp_client import SERVER_SOFTWARE as HA_USER_AGENT from homeassistant.helpers.entity import Entity from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ATTR_ATTRIBUTION, CONF_NAME, CONF_MONITORED_CONDITIONS @@ -184,7 +185,10 @@ def __init__(self, region_name): "jsonp=loadWarnings", ) - self._rest = RestData("GET", resource, None, None, None, True) + # a User-Agent is necessary for this rest api endpoint (#29496) + headers = {"User-Agent": HA_USER_AGENT} + + self._rest = RestData("GET", resource, None, headers, None, True) self.region_name = region_name self.region_id = None self.region_state = None diff --git a/homeassistant/components/dyson/__init__.py b/homeassistant/components/dyson/__init__.py index 7f247be6bccd..a5dde58d30ff 100644 --- a/homeassistant/components/dyson/__init__.py +++ b/homeassistant/components/dyson/__init__.py @@ -1,11 +1,12 @@ """Support for Dyson Pure Cool Link devices.""" import logging +from libpurecool.dyson import DysonAccount import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.const import CONF_DEVICES, CONF_PASSWORD, CONF_TIMEOUT, CONF_USERNAME from homeassistant.helpers import discovery +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -43,8 +44,6 @@ def setup(hass, config): if DYSON_DEVICES not in hass.data: hass.data[DYSON_DEVICES] = [] - from libpurecool.dyson import DysonAccount - dyson_account = DysonAccount( config[DOMAIN].get(CONF_USERNAME), config[DOMAIN].get(CONF_PASSWORD), diff --git a/homeassistant/components/dyson/air_quality.py b/homeassistant/components/dyson/air_quality.py index 0276e47ed611..647fb2367074 100644 --- a/homeassistant/components/dyson/air_quality.py +++ b/homeassistant/components/dyson/air_quality.py @@ -1,7 +1,11 @@ """Support for Dyson Pure Cool Air Quality Sensors.""" import logging -from homeassistant.components.air_quality import AirQualityEntity, DOMAIN +from libpurecool.dyson_pure_cool import DysonPureCool +from libpurecool.dyson_pure_state_v2 import DysonEnvironmentalSensorV2State + +from homeassistant.components.air_quality import DOMAIN, AirQualityEntity + from . import DYSON_DEVICES ATTRIBUTION = "Dyson purifier air quality sensor" @@ -15,7 +19,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Dyson Sensors.""" - from libpurecool.dyson_pure_cool import DysonPureCool if discovery_info is None: return @@ -47,8 +50,6 @@ async def async_added_to_hass(self): def on_message(self, message): """Handle new messages which are received from the fan.""" - from libpurecool.dyson_pure_state_v2 import DysonEnvironmentalSensorV2State - _LOGGER.debug( "%s: Message received for %s device: %s", DOMAIN, self.name, message ) diff --git a/homeassistant/components/dyson/climate.py b/homeassistant/components/dyson/climate.py index 90c19e9de889..df97358d5500 100644 --- a/homeassistant/components/dyson/climate.py +++ b/homeassistant/components/dyson/climate.py @@ -1,20 +1,20 @@ """Support for Dyson Pure Hot+Cool link fan.""" import logging -from libpurecool.const import HeatMode, HeatState, FocusMode, HeatTarget -from libpurecool.dyson_pure_state import DysonPureHotCoolState +from libpurecool.const import FocusMode, HeatMode, HeatState, HeatTarget from libpurecool.dyson_pure_hotcool_link import DysonPureHotCoolLink +from libpurecool.dyson_pure_state import DysonPureHotCoolState from homeassistant.components.climate import ClimateDevice from homeassistant.components.climate.const import ( CURRENT_HVAC_COOL, CURRENT_HVAC_HEAT, CURRENT_HVAC_IDLE, + FAN_DIFFUSE, + FAN_FOCUS, HVAC_MODE_COOL, HVAC_MODE_HEAT, SUPPORT_FAN_MODE, - FAN_FOCUS, - FAN_DIFFUSE, SUPPORT_TARGET_TEMPERATURE, ) from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS diff --git a/homeassistant/components/dyson/fan.py b/homeassistant/components/dyson/fan.py index 341919935d0e..1fdbed0d2045 100644 --- a/homeassistant/components/dyson/fan.py +++ b/homeassistant/components/dyson/fan.py @@ -5,18 +5,24 @@ """ import logging +from libpurecool.const import FanMode, FanSpeed, NightMode, Oscillation +from libpurecool.dyson_pure_cool import DysonPureCool +from libpurecool.dyson_pure_cool_link import DysonPureCoolLink +from libpurecool.dyson_pure_state import DysonPureCoolState +from libpurecool.dyson_pure_state_v2 import DysonPureCoolV2State import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.fan import ( + SPEED_HIGH, + SPEED_LOW, + SPEED_MEDIUM, SUPPORT_OSCILLATE, SUPPORT_SET_SPEED, FanEntity, - SPEED_LOW, - SPEED_MEDIUM, - SPEED_HIGH, ) from homeassistant.const import ATTR_ENTITY_ID +import homeassistant.helpers.config_validation as cv + from . import DYSON_DEVICES _LOGGER = logging.getLogger(__name__) @@ -88,8 +94,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Dyson fan components.""" - from libpurecool.dyson_pure_cool_link import DysonPureCoolLink - from libpurecool.dyson_pure_cool import DysonPureCool if discovery_info is None: return @@ -197,7 +201,6 @@ async def async_added_to_hass(self): def on_message(self, message): """Call when new messages received from the fan.""" - from libpurecool.dyson_pure_state import DysonPureCoolState if isinstance(message, DysonPureCoolState): _LOGGER.debug("Message received for fan device %s: %s", self.name, message) @@ -215,8 +218,6 @@ def name(self): def set_speed(self, speed: str) -> None: """Set the speed of the fan. Never called ??.""" - from libpurecool.const import FanSpeed, FanMode - _LOGGER.debug("Set fan speed to: %s", speed) if speed == FanSpeed.FAN_SPEED_AUTO.value: @@ -227,8 +228,6 @@ def set_speed(self, speed: str) -> None: def turn_on(self, speed: str = None, **kwargs) -> None: """Turn on the fan.""" - from libpurecool.const import FanSpeed, FanMode - _LOGGER.debug("Turn on fan %s with speed %s", self.name, speed) if speed: if speed == FanSpeed.FAN_SPEED_AUTO.value: @@ -244,15 +243,11 @@ def turn_on(self, speed: str = None, **kwargs) -> None: def turn_off(self, **kwargs) -> None: """Turn off the fan.""" - from libpurecool.const import FanMode - _LOGGER.debug("Turn off fan %s", self.name) self._device.set_configuration(fan_mode=FanMode.OFF) def oscillate(self, oscillating: bool) -> None: """Turn on/off oscillating.""" - from libpurecool.const import Oscillation - _LOGGER.debug("Turn oscillation %s for device %s", oscillating, self.name) if oscillating: @@ -275,8 +270,6 @@ def is_on(self): @property def speed(self) -> str: """Return the current speed.""" - from libpurecool.const import FanSpeed - if self._device.state: if self._device.state.speed == FanSpeed.FAN_SPEED_AUTO.value: return self._device.state.speed @@ -295,8 +288,6 @@ def night_mode(self): def set_night_mode(self, night_mode: bool) -> None: """Turn fan in night mode.""" - from libpurecool.const import NightMode - _LOGGER.debug("Set %s night mode %s", self.name, night_mode) if night_mode: self._device.set_configuration(night_mode=NightMode.NIGHT_MODE_ON) @@ -310,8 +301,6 @@ def auto_mode(self): def set_auto_mode(self, auto_mode: bool) -> None: """Turn fan in auto mode.""" - from libpurecool.const import FanMode - _LOGGER.debug("Set %s auto mode %s", self.name, auto_mode) if auto_mode: self._device.set_configuration(fan_mode=FanMode.AUTO) @@ -321,8 +310,6 @@ def set_auto_mode(self, auto_mode: bool) -> None: @property def speed_list(self) -> list: """Get the list of available speeds.""" - from libpurecool.const import FanSpeed - supported_speeds = [ FanSpeed.FAN_SPEED_AUTO.value, int(FanSpeed.FAN_SPEED_1.value), @@ -365,8 +352,6 @@ async def async_added_to_hass(self): def on_message(self, message): """Call when new messages received from the fan.""" - from libpurecool.dyson_pure_state_v2 import DysonPureCoolV2State - if isinstance(message, DysonPureCoolV2State): _LOGGER.debug("Message received for fan device %s: %s", self.name, message) self.schedule_update_ha_state() @@ -392,8 +377,6 @@ def turn_on(self, speed: str = None, **kwargs) -> None: def set_speed(self, speed: str) -> None: """Set the speed of the fan.""" - from libpurecool.const import FanSpeed - if speed == SPEED_LOW: self._device.set_fan_speed(FanSpeed.FAN_SPEED_4) elif speed == SPEED_MEDIUM: @@ -408,8 +391,6 @@ def turn_off(self, **kwargs): def set_dyson_speed(self, speed: str = None) -> None: """Set the exact speed of the purecool fan.""" - from libpurecool.const import FanSpeed - _LOGGER.debug("Set exact speed for fan %s", self.name) fan_speed = FanSpeed("{0:04d}".format(int(speed))) @@ -487,8 +468,6 @@ def is_on(self): @property def speed(self): """Return the current speed.""" - from libpurecool.const import FanSpeed - speed_map = { FanSpeed.FAN_SPEED_1.value: SPEED_LOW, FanSpeed.FAN_SPEED_2.value: SPEED_LOW, @@ -508,8 +487,6 @@ def speed(self): @property def dyson_speed(self): """Return the current speed.""" - from libpurecool.const import FanSpeed - if self._device.state: if self._device.state.speed == FanSpeed.FAN_SPEED_AUTO.value: return self._device.state.speed @@ -563,8 +540,6 @@ def speed_list(self) -> list: @property def dyson_speed_list(self) -> list: """Get the list of available dyson speeds.""" - from libpurecool.const import FanSpeed - return [ int(FanSpeed.FAN_SPEED_1.value), int(FanSpeed.FAN_SPEED_2.value), diff --git a/homeassistant/components/dyson/sensor.py b/homeassistant/components/dyson/sensor.py index 1eb2b79c0735..c51f46c77901 100644 --- a/homeassistant/components/dyson/sensor.py +++ b/homeassistant/components/dyson/sensor.py @@ -1,8 +1,12 @@ """Support for Dyson Pure Cool Link Sensors.""" import logging +from libpurecool.dyson_pure_cool import DysonPureCool +from libpurecool.dyson_pure_cool_link import DysonPureCoolLink + from homeassistant.const import STATE_OFF, TEMP_CELSIUS from homeassistant.helpers.entity import Entity + from . import DYSON_DEVICES SENSOR_UNITS = { @@ -27,8 +31,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Dyson Sensors.""" - from libpurecool.dyson_pure_cool_link import DysonPureCoolLink - from libpurecool.dyson_pure_cool import DysonPureCool if discovery_info is None: return diff --git a/homeassistant/components/dyson/vacuum.py b/homeassistant/components/dyson/vacuum.py index cef5f0c99611..6203b65c9db5 100644 --- a/homeassistant/components/dyson/vacuum.py +++ b/homeassistant/components/dyson/vacuum.py @@ -1,6 +1,9 @@ """Support for the Dyson 360 eye vacuum cleaner robot.""" import logging +from libpurecool.const import Dyson360EyeMode, PowerMode +from libpurecool.dyson_360_eye import Dyson360Eye + from homeassistant.components.vacuum import ( SUPPORT_BATTERY, SUPPORT_FAN_SPEED, @@ -38,8 +41,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Dyson 360 Eye robot vacuum platform.""" - from libpurecool.dyson_360_eye import Dyson360Eye - _LOGGER.debug("Creating new Dyson 360 Eye robot vacuum") if DYSON_360_EYE_DEVICES not in hass.data: hass.data[DYSON_360_EYE_DEVICES] = [] @@ -86,8 +87,6 @@ def name(self): @property def status(self): """Return the status of the vacuum cleaner.""" - from libpurecool.const import Dyson360EyeMode - dyson_labels = { Dyson360EyeMode.INACTIVE_CHARGING: "Stopped - Charging", Dyson360EyeMode.INACTIVE_CHARGED: "Stopped - Charged", @@ -110,8 +109,6 @@ def battery_level(self): @property def fan_speed(self): """Return the fan speed of the vacuum cleaner.""" - from libpurecool.const import PowerMode - speed_labels = {PowerMode.MAX: "Max", PowerMode.QUIET: "Quiet"} return speed_labels[self._device.state.power_mode] @@ -128,8 +125,6 @@ def device_state_attributes(self): @property def is_on(self) -> bool: """Return True if entity is on.""" - from libpurecool.const import Dyson360EyeMode - return self._device.state.state in [ Dyson360EyeMode.FULL_CLEAN_INITIATED, Dyson360EyeMode.FULL_CLEAN_ABORTED, @@ -149,8 +144,6 @@ def supported_features(self): @property def battery_icon(self): """Return the battery icon for the vacuum cleaner.""" - from libpurecool.const import Dyson360EyeMode - charging = self._device.state.state in [Dyson360EyeMode.INACTIVE_CHARGING] return icon_for_battery_level( battery_level=self.battery_level, charging=charging @@ -158,8 +151,6 @@ def battery_icon(self): def turn_on(self, **kwargs): """Turn the vacuum on.""" - from libpurecool.const import Dyson360EyeMode - _LOGGER.debug("Turn on device %s", self.name) if self._device.state.state in [Dyson360EyeMode.FULL_CLEAN_PAUSED]: self._device.resume() @@ -178,16 +169,12 @@ def stop(self, **kwargs): def set_fan_speed(self, fan_speed, **kwargs): """Set fan speed.""" - from libpurecool.const import PowerMode - _LOGGER.debug("Set fan speed %s on device %s", fan_speed, self.name) power_modes = {"Quiet": PowerMode.QUIET, "Max": PowerMode.MAX} self._device.set_power_mode(power_modes[fan_speed]) def start_pause(self, **kwargs): """Start, pause or resume the cleaning task.""" - from libpurecool.const import Dyson360EyeMode - if self._device.state.state in [Dyson360EyeMode.FULL_CLEAN_PAUSED]: _LOGGER.debug("Resume device %s", self.name) self._device.resume() diff --git a/homeassistant/components/ebox/sensor.py b/homeassistant/components/ebox/sensor.py index 95c5513ecaf9..55504e8edf7e 100644 --- a/homeassistant/components/ebox/sensor.py +++ b/homeassistant/components/ebox/sensor.py @@ -6,23 +6,24 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.ebox/ """ -import logging from datetime import timedelta +import logging +from pyebox import EboxClient +from pyebox.client import PyEboxError import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( - CONF_USERNAME, - CONF_PASSWORD, - CONF_NAME, CONF_MONITORED_VARIABLES, + CONF_NAME, + CONF_PASSWORD, + CONF_USERNAME, ) +from homeassistant.exceptions import PlatformNotReady +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle -from homeassistant.exceptions import PlatformNotReady - _LOGGER = logging.getLogger(__name__) @@ -75,8 +76,6 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= name = config.get(CONF_NAME) - from pyebox.client import PyEboxError - try: await ebox_data.async_update() except PyEboxError as exp: @@ -135,16 +134,12 @@ class EBoxData: def __init__(self, username, password, httpsession): """Initialize the data object.""" - from pyebox import EboxClient - self.client = EboxClient(username, password, REQUESTS_TIMEOUT, httpsession) self.data = {} @Throttle(MIN_TIME_BETWEEN_UPDATES) async def async_update(self): """Get the latest data from Ebox.""" - from pyebox.client import PyEboxError - try: await self.client.fetch_data() except PyEboxError as exp: diff --git a/homeassistant/components/ecoal_boiler/__init__.py b/homeassistant/components/ecoal_boiler/__init__.py index 40769c9990ae..ed8e315bfedc 100644 --- a/homeassistant/components/ecoal_boiler/__init__.py +++ b/homeassistant/components/ecoal_boiler/__init__.py @@ -1,15 +1,16 @@ """Support to control ecoal/esterownik.pl coal/wood boiler controller.""" import logging +from ecoaliface.simple import ECoalController import voluptuous as vol from homeassistant.const import ( CONF_HOST, - CONF_PASSWORD, - CONF_USERNAME, CONF_MONITORED_CONDITIONS, + CONF_PASSWORD, CONF_SENSORS, CONF_SWITCHES, + CONF_USERNAME, ) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.discovery import load_platform @@ -80,7 +81,6 @@ def setup(hass, hass_config): """Set up global ECoalController instance same for sensors and switches.""" - from ecoaliface.simple import ECoalController conf = hass_config[DOMAIN] host = conf[CONF_HOST] diff --git a/homeassistant/components/econet/const.py b/homeassistant/components/econet/const.py new file mode 100644 index 000000000000..88b1b851aa6d --- /dev/null +++ b/homeassistant/components/econet/const.py @@ -0,0 +1,5 @@ +"""Constants for Econet integration.""" + +DOMAIN = "econet" +SERVICE_ADD_VACATION = "add_vacation" +SERVICE_DELETE_VACATION = "delete_vacation" diff --git a/homeassistant/components/econet/services.yaml b/homeassistant/components/econet/services.yaml index e69de29bb2d1..9f489165c22e 100644 --- a/homeassistant/components/econet/services.yaml +++ b/homeassistant/components/econet/services.yaml @@ -0,0 +1,19 @@ +add_vacation: + description: Add a vacation to your water heater. + fields: + entity_id: + description: Name(s) of entities to change. + example: 'water_heater.econet' + start_date: + description: The timestamp of when the vacation should start. (Optional, defaults to now) + example: 1513186320 + end_date: + description: The timestamp of when the vacation should end. + example: 1513445520 + +delete_vacation: + description: Delete your existing vacation from your water heater. + fields: + entity_id: + description: Name(s) of entities to change. + example: 'water_heater.econet' \ No newline at end of file diff --git a/homeassistant/components/econet/water_heater.py b/homeassistant/components/econet/water_heater.py index 1c8deae5b99b..26ee7cb8bd42 100644 --- a/homeassistant/components/econet/water_heater.py +++ b/homeassistant/components/econet/water_heater.py @@ -2,10 +2,10 @@ import datetime import logging +from pyeconet.api import PyEcoNet import voluptuous as vol from homeassistant.components.water_heater import ( - DOMAIN, PLATFORM_SCHEMA, STATE_ECO, STATE_ELECTRIC, @@ -27,6 +27,8 @@ ) import homeassistant.helpers.config_validation as cv +from .const import DOMAIN, SERVICE_ADD_VACATION, SERVICE_DELETE_VACATION + _LOGGER = logging.getLogger(__name__) ATTR_VACATION_START = "next_vacation_start_date" @@ -40,9 +42,6 @@ SUPPORT_FLAGS_HEATER = SUPPORT_TARGET_TEMPERATURE | SUPPORT_OPERATION_MODE -SERVICE_ADD_VACATION = "econet_add_vacation" -SERVICE_DELETE_VACATION = "econet_delete_vacation" - ADD_VACATION_SCHEMA = vol.Schema( { vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, @@ -74,7 +73,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the EcoNet water heaters.""" - from pyeconet.api import PyEcoNet hass.data[ECONET_DATA] = {} hass.data[ECONET_DATA]["water_heaters"] = [] diff --git a/homeassistant/components/ecovacs/__init__.py b/homeassistant/components/ecovacs/__init__.py index 76566912d12c..964dd7a3f2ac 100644 --- a/homeassistant/components/ecovacs/__init__.py +++ b/homeassistant/components/ecovacs/__init__.py @@ -3,6 +3,7 @@ import random import string +from sucks import EcoVacsAPI, VacBot import voluptuous as vol from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, EVENT_HOMEASSISTANT_STOP @@ -44,8 +45,6 @@ def setup(hass, config): hass.data[ECOVACS_DEVICES] = [] - from sucks import EcoVacsAPI, VacBot - ecovacs_api = EcoVacsAPI( ECOVACS_API_DEVICEID, config[DOMAIN].get(CONF_USERNAME), diff --git a/homeassistant/components/ecovacs/vacuum.py b/homeassistant/components/ecovacs/vacuum.py index fdaf6291be51..16a9d67bffcd 100644 --- a/homeassistant/components/ecovacs/vacuum.py +++ b/homeassistant/components/ecovacs/vacuum.py @@ -1,6 +1,8 @@ """Support for Ecovacs Ecovacs Vaccums.""" import logging +import sucks + from homeassistant.components.vacuum import ( SUPPORT_BATTERY, SUPPORT_CLEAN_SPOT, @@ -123,9 +125,8 @@ def status(self): def return_to_base(self, **kwargs): """Set the vacuum cleaner to return to the dock.""" - from sucks import Charge - self.device.run(Charge()) + self.device.run(sucks.Charge()) @property def battery_icon(self): @@ -150,15 +151,13 @@ def fan_speed(self): @property def fan_speed_list(self): """Get the list of available fan speed steps of the vacuum cleaner.""" - from sucks import FAN_SPEED_NORMAL, FAN_SPEED_HIGH - return [FAN_SPEED_NORMAL, FAN_SPEED_HIGH] + return [sucks.FAN_SPEED_NORMAL, sucks.FAN_SPEED_HIGH] def turn_on(self, **kwargs): """Turn the vacuum on and start cleaning.""" - from sucks import Clean - self.device.run(Clean()) + self.device.run(sucks.Clean()) def turn_off(self, **kwargs): """Turn the vacuum off stopping the cleaning and returning home.""" @@ -166,34 +165,28 @@ def turn_off(self, **kwargs): def stop(self, **kwargs): """Stop the vacuum cleaner.""" - from sucks import Stop - self.device.run(Stop()) + self.device.run(sucks.Stop()) def clean_spot(self, **kwargs): """Perform a spot clean-up.""" - from sucks import Spot - self.device.run(Spot()) + self.device.run(sucks.Spot()) def locate(self, **kwargs): """Locate the vacuum cleaner.""" - from sucks import PlaySound - self.device.run(PlaySound()) + self.device.run(sucks.PlaySound()) def set_fan_speed(self, fan_speed, **kwargs): """Set fan speed.""" if self.is_on: - from sucks import Clean - self.device.run(Clean(mode=self.device.clean_status, speed=fan_speed)) + self.device.run(sucks.Clean(mode=self.device.clean_status, speed=fan_speed)) def send_command(self, command, params=None, **kwargs): """Send a command to a vacuum cleaner.""" - from sucks import VacBotCommand - - self.device.run(VacBotCommand(command, params)) + self.device.run(sucks.VacBotCommand(command, params)) @property def device_state_attributes(self): diff --git a/homeassistant/components/eddystone_temperature/sensor.py b/homeassistant/components/eddystone_temperature/sensor.py index 67724e9fcf34..2084e3070294 100644 --- a/homeassistant/components/eddystone_temperature/sensor.py +++ b/homeassistant/components/eddystone_temperature/sensor.py @@ -9,6 +9,12 @@ """ import logging +# pylint: disable=import-error +from beacontools import ( + BeaconScanner, + EddystoneFilter, + EddystoneTLMFrame, +) import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -150,12 +156,6 @@ def callback(bt_addr, _, packet, additional_info): packet.temperature, ) - from beacontools import ( # pylint: disable=import-error - BeaconScanner, - EddystoneFilter, - EddystoneTLMFrame, - ) - device_filters = [EddystoneFilter(d.namespace, d.instance) for d in devices] self.scanner = BeaconScanner( diff --git a/homeassistant/components/edimax/switch.py b/homeassistant/components/edimax/switch.py index f1d8f8046efe..3d558f6c7708 100644 --- a/homeassistant/components/edimax/switch.py +++ b/homeassistant/components/edimax/switch.py @@ -1,9 +1,10 @@ """Support for Edimax switches.""" import logging +from pyedimax.smartplug import SmartPlug import voluptuous as vol -from homeassistant.components.switch import SwitchDevice, PLATFORM_SCHEMA +from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchDevice from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_USERNAME import homeassistant.helpers.config_validation as cv @@ -25,8 +26,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Find and return Edimax Smart Plugs.""" - from pyedimax.smartplug import SmartPlug - host = config.get(CONF_HOST) auth = (config.get(CONF_USERNAME), config.get(CONF_PASSWORD)) name = config.get(CONF_NAME) diff --git a/homeassistant/components/ee_brightbox/device_tracker.py b/homeassistant/components/ee_brightbox/device_tracker.py index 81dbf9eab1f5..845d557e029f 100644 --- a/homeassistant/components/ee_brightbox/device_tracker.py +++ b/homeassistant/components/ee_brightbox/device_tracker.py @@ -1,6 +1,7 @@ """Support for EE Brightbox router.""" import logging +from eebrightbox import EEBrightBox, EEBrightBoxException import voluptuous as vol from homeassistant.components.device_tracker import ( @@ -46,8 +47,6 @@ def __init__(self, config): def check_config(self): """Check if provided configuration and credentials are correct.""" - from eebrightbox import EEBrightBox, EEBrightBoxException - try: with EEBrightBox(self.config) as ee_brightbox: return bool(ee_brightbox.get_devices()) @@ -57,8 +56,6 @@ def check_config(self): def scan_devices(self): """Scan for devices.""" - from eebrightbox import EEBrightBox - with EEBrightBox(self.config) as ee_brightbox: self.devices = {d["mac"]: d for d in ee_brightbox.get_devices()} diff --git a/homeassistant/components/egardia/__init__.py b/homeassistant/components/egardia/__init__.py index 9e11f522dd53..efe477364791 100644 --- a/homeassistant/components/egardia/__init__.py +++ b/homeassistant/components/egardia/__init__.py @@ -1,6 +1,7 @@ """Interfaces with Egardia/Woonveilig alarm control panel.""" import logging +from pythonegardia import egardiadevice, egardiaserver import requests import voluptuous as vol @@ -78,8 +79,6 @@ def setup(hass, config): """Set up the Egardia platform.""" - from pythonegardia import egardiadevice - from pythonegardia import egardiaserver conf = config[DOMAIN] username = conf.get(CONF_USERNAME) diff --git a/homeassistant/components/egardia/alarm_control_panel.py b/homeassistant/components/egardia/alarm_control_panel.py index 22a458ae9aab..2c18be47a1f8 100644 --- a/homeassistant/components/egardia/alarm_control_panel.py +++ b/homeassistant/components/egardia/alarm_control_panel.py @@ -4,6 +4,10 @@ import requests import homeassistant.components.alarm_control_panel as alarm +from homeassistant.components.alarm_control_panel.const import ( + SUPPORT_ALARM_ARM_AWAY, + SUPPORT_ALARM_ARM_HOME, +) from homeassistant.const import ( STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, @@ -79,6 +83,11 @@ def state(self): """Return the state of the device.""" return self._status + @property + def supported_features(self) -> int: + """Return the list of supported features.""" + return SUPPORT_ALARM_ARM_HOME | SUPPORT_ALARM_ARM_AWAY + @property def should_poll(self): """Poll if no report server is enabled.""" diff --git a/homeassistant/components/eight_sleep/__init__.py b/homeassistant/components/eight_sleep/__init__.py index 923c3f7d3091..a8a5a6e1fccd 100644 --- a/homeassistant/components/eight_sleep/__init__.py +++ b/homeassistant/components/eight_sleep/__init__.py @@ -1,23 +1,24 @@ """Support for Eight smart mattress covers and mattresses.""" -import logging from datetime import timedelta +import logging +from pyeight.eight import EightSleep import voluptuous as vol -from homeassistant.core import callback from homeassistant.const import ( - CONF_USERNAME, + ATTR_ENTITY_ID, + CONF_BINARY_SENSORS, CONF_PASSWORD, CONF_SENSORS, - CONF_BINARY_SENSORS, - ATTR_ENTITY_ID, + CONF_USERNAME, EVENT_HOMEASSISTANT_STOP, ) +from homeassistant.core import callback from homeassistant.helpers import discovery import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import ( - async_dispatcher_send, async_dispatcher_connect, + async_dispatcher_send, ) from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import async_track_point_in_utc_time @@ -90,7 +91,6 @@ async def async_setup(hass, config): """Set up the Eight Sleep component.""" - from pyeight.eight import EightSleep conf = config.get(DOMAIN) user = conf.get(CONF_USERNAME) diff --git a/homeassistant/components/elkm1/__init__.py b/homeassistant/components/elkm1/__init__.py index d257c46839cc..67b84c4f3bf7 100644 --- a/homeassistant/components/elkm1/__init__.py +++ b/homeassistant/components/elkm1/__init__.py @@ -14,10 +14,10 @@ CONF_TEMPERATURE_UNIT, CONF_USERNAME, ) -from homeassistant.core import HomeAssistant, callback # noqa +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import config_validation as cv, discovery from homeassistant.helpers.entity import Entity -from homeassistant.helpers.typing import ConfigType # noqa +from homeassistant.helpers.typing import ConfigType DOMAIN = "elkm1" @@ -35,6 +35,11 @@ _LOGGER = logging.getLogger(__name__) +SERVICE_ALARM_DISPLAY_MESSAGE = "alarm_display_message" +SERVICE_ALARM_ARM_VACATION = "alarm_arm_vacation" +SERVICE_ALARM_ARM_HOME_INSTANT = "alarm_arm_home_instant" +SERVICE_ALARM_ARM_NIGHT_INSTANT = "alarm_arm_night_instant" + SUPPORTED_DOMAINS = [ "alarm_control_panel", "climate", diff --git a/homeassistant/components/elkm1/alarm_control_panel.py b/homeassistant/components/elkm1/alarm_control_panel.py index 38519ab5b3f7..1e1a8eba9e0e 100644 --- a/homeassistant/components/elkm1/alarm_control_panel.py +++ b/homeassistant/components/elkm1/alarm_control_panel.py @@ -2,7 +2,15 @@ from elkm1_lib.const import AlarmState, ArmedStatus, ArmLevel, ArmUpState import voluptuous as vol -import homeassistant.components.alarm_control_panel as alarm +from homeassistant.components.alarm_control_panel import ( + AlarmControlPanel, + FORMAT_NUMBER, +) +from homeassistant.components.alarm_control_panel.const import ( + SUPPORT_ALARM_ARM_AWAY, + SUPPORT_ALARM_ARM_HOME, + SUPPORT_ALARM_ARM_NIGHT, +) from homeassistant.const import ( ATTR_CODE, ATTR_ENTITY_ID, @@ -20,7 +28,15 @@ async_dispatcher_send, ) -from . import DOMAIN as ELK_DOMAIN, ElkEntity, create_elk_entities +from . import ( + create_elk_entities, + DOMAIN, + ElkEntity, + SERVICE_ALARM_ARM_HOME_INSTANT, + SERVICE_ALARM_ARM_NIGHT_INSTANT, + SERVICE_ALARM_ARM_VACATION, + SERVICE_ALARM_DISPLAY_MESSAGE, +) SIGNAL_ARM_ENTITY = "elkm1_arm" SIGNAL_DISPLAY_MESSAGE = "elkm1_display_message" @@ -51,7 +67,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= if discovery_info is None: return - elk_datas = hass.data[ELK_DOMAIN] + elk_datas = hass.data[DOMAIN] entities = [] for elk_data in elk_datas.values(): elk = elk_data["elk"] @@ -70,7 +86,7 @@ def _arm_service(service): for service in _arm_services(): hass.services.async_register( - alarm.DOMAIN, service, _arm_service, ELK_ALARM_SERVICE_SCHEMA + DOMAIN, service, _arm_service, ELK_ALARM_SERVICE_SCHEMA ) def _display_message_service(service): @@ -86,8 +102,8 @@ def _display_message_service(service): _dispatch(SIGNAL_DISPLAY_MESSAGE, entity_ids, *args) hass.services.async_register( - alarm.DOMAIN, - "elkm1_alarm_display_message", + DOMAIN, + SERVICE_ALARM_DISPLAY_MESSAGE, _display_message_service, DISPLAY_MESSAGE_SERVICE_SCHEMA, ) @@ -95,13 +111,13 @@ def _display_message_service(service): def _arm_services(): return { - "elkm1_alarm_arm_vacation": ArmLevel.ARMED_VACATION.value, - "elkm1_alarm_arm_home_instant": ArmLevel.ARMED_STAY_INSTANT.value, - "elkm1_alarm_arm_night_instant": ArmLevel.ARMED_NIGHT_INSTANT.value, + SERVICE_ALARM_ARM_VACATION: ArmLevel.ARMED_VACATION.value, + SERVICE_ALARM_ARM_HOME_INSTANT: ArmLevel.ARMED_STAY_INSTANT.value, + SERVICE_ALARM_ARM_NIGHT_INSTANT: ArmLevel.ARMED_NIGHT_INSTANT.value, } -class ElkArea(ElkEntity, alarm.AlarmControlPanel): +class ElkArea(ElkEntity, AlarmControlPanel): """Representation of an Area / Partition within the ElkM1 alarm panel.""" def __init__(self, element, elk, elk_data): @@ -128,7 +144,7 @@ def _watch_keypad(self, keypad, changeset): if keypad.area != self._element.index: return if changeset.get("last_user") is not None: - self._changed_by_entity_id = self.hass.data[ELK_DOMAIN][self._prefix][ + self._changed_by_entity_id = self.hass.data[DOMAIN][self._prefix][ "keypads" ].get(keypad.index, "") self.async_schedule_update_ha_state(True) @@ -136,13 +152,18 @@ def _watch_keypad(self, keypad, changeset): @property def code_format(self): """Return the alarm code format.""" - return alarm.FORMAT_NUMBER + return FORMAT_NUMBER @property def state(self): """Return the state of the element.""" return self._state + @property + def supported_features(self) -> int: + """Return the list of supported features.""" + return SUPPORT_ALARM_ARM_HOME | SUPPORT_ALARM_ARM_AWAY | SUPPORT_ALARM_ARM_NIGHT + @property def device_state_attributes(self): """Attributes of the area.""" diff --git a/homeassistant/components/elkm1/services.yaml b/homeassistant/components/elkm1/services.yaml index 405716569630..fbcbf7edc6dd 100644 --- a/homeassistant/components/elkm1/services.yaml +++ b/homeassistant/components/elkm1/services.yaml @@ -1,12 +1,65 @@ -speak_word: - description: Speak a word. See list of words in ElkM1 ASCII Protocol documentation. +alarm_arm_home_instant: + description: Arm the ElkM1 in home instant mode. fields: - number: - description: Word number to speak. - example: 142 + entity_id: + description: Name of alarm control panel to arm. + example: 'alarm_control_panel.main' + code: + description: An code to arm the alarm control panel. + example: 1234 + +alarm_arm_night_instant: + description: Arm the ElkM1 in night instant mode. + fields: + entity_id: + description: Name of alarm control panel to arm. + example: 'alarm_control_panel.main' + code: + description: An code to arm the alarm control panel. + example: 1234 + +alarm_arm_vacation: + description: Arm the ElkM1 in vacation mode. + fields: + entity_id: + description: Name of alarm control panel to arm. + example: 'alarm_control_panel.main' + code: + description: An code to arm the alarm control panel. + example: 1234 + +alarm_display_message: + description: Display a message on all of the ElkM1 keypads for an area. + fields: + entity_id: + description: Name of alarm control panel to display messages on. + example: 'alarm_control_panel.main' + clear: + description: 0=clear message, 1=clear message with * key, 2=Display until timeout; default 2 + example: 1 + beep: + description: 0=no beep, 1=beep; default 0 + example: 1 + timeout: + description: Time to display message, 0=forever, max 65535, default 0 + example: 4242 + line1: + description: Up to 16 characters of text (truncated if too long). Default blank. + example: The answer to life, + line2: + description: Up to 16 characters of text (truncated if too long). Default blank. + example: the universe, and everything. + speak_phrase: description: Speak a phrase. See list of phrases in ElkM1 ASCII Protocol documentation. fields: number: description: Phrase number to speak. example: 42 + +speak_word: + description: Speak a word. See list of words in ElkM1 ASCII Protocol documentation. + fields: + number: + description: Word number to speak. + example: 142 diff --git a/homeassistant/components/elv/manifest.json b/homeassistant/components/elv/manifest.json index b4871a805d2d..8390fc597f08 100644 --- a/homeassistant/components/elv/manifest.json +++ b/homeassistant/components/elv/manifest.json @@ -4,5 +4,5 @@ "documentation": "https://www.home-assistant.io/integrations/pca", "dependencies": [], "codeowners": ["@majuss"], - "requirements": ["pypca==0.0.5"] + "requirements": ["pypca==0.0.7"] } diff --git a/homeassistant/components/emby/media_player.py b/homeassistant/components/emby/media_player.py index d8a98a965859..57f781deceb7 100644 --- a/homeassistant/components/emby/media_player.py +++ b/homeassistant/components/emby/media_player.py @@ -1,9 +1,10 @@ """Support to interface with the Emby API.""" import logging +from pyemby import EmbyServer import voluptuous as vol -from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA +from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice from homeassistant.components.media_player.const import ( MEDIA_TYPE_CHANNEL, MEDIA_TYPE_MOVIE, @@ -70,7 +71,6 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the Emby platform.""" - from pyemby import EmbyServer host = config.get(CONF_HOST) key = config.get(CONF_API_KEY) diff --git a/homeassistant/components/emulated_hue/hue_api.py b/homeassistant/components/emulated_hue/hue_api.py index 5d08af6c5eeb..e7f15e7fc535 100644 --- a/homeassistant/components/emulated_hue/hue_api.py +++ b/homeassistant/components/emulated_hue/hue_api.py @@ -1,7 +1,6 @@ """Support for a Hue API to control Home Assistant.""" import logging - -from aiohttp import web +import hashlib from homeassistant import core from homeassistant.components import ( @@ -36,8 +35,10 @@ from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_HS_COLOR, + ATTR_COLOR_TEMP, SUPPORT_BRIGHTNESS, SUPPORT_COLOR, + SUPPORT_COLOR_TEMP, ) from homeassistant.components.media_player.const import ( ATTR_MEDIA_VOLUME_LEVEL, @@ -48,6 +49,7 @@ ATTR_SUPPORTED_FEATURES, ATTR_TEMPERATURE, HTTP_BAD_REQUEST, + HTTP_UNAUTHORIZED, HTTP_NOT_FOUND, SERVICE_CLOSE_COVER, SERVICE_OPEN_COVER, @@ -56,23 +58,36 @@ SERVICE_VOLUME_SET, STATE_OFF, STATE_ON, + STATE_UNAVAILABLE, ) from homeassistant.util.network import is_local _LOGGER = logging.getLogger(__name__) +STATE_BRIGHTNESS = "bri" +STATE_COLORMODE = "colormode" +STATE_HUE = "hue" +STATE_SATURATION = "sat" +STATE_COLOR_TEMP = "ct" + +# Hue API states, defined separately in case they change HUE_API_STATE_ON = "on" HUE_API_STATE_BRI = "bri" +HUE_API_STATE_COLORMODE = "colormode" HUE_API_STATE_HUE = "hue" HUE_API_STATE_SAT = "sat" +HUE_API_STATE_CT = "ct" +HUE_API_STATE_EFFECT = "effect" -HUE_API_STATE_HUE_MAX = 65535.0 -HUE_API_STATE_SAT_MAX = 254.0 -HUE_API_STATE_BRI_MAX = 255.0 - -STATE_BRIGHTNESS = HUE_API_STATE_BRI -STATE_HUE = HUE_API_STATE_HUE -STATE_SATURATION = HUE_API_STATE_SAT +# Hue API min/max values - https://developers.meethue.com/develop/hue-api/lights-api/ +HUE_API_STATE_BRI_MIN = 1 # Brightness +HUE_API_STATE_BRI_MAX = 254 +HUE_API_STATE_HUE_MIN = 0 # Hue +HUE_API_STATE_HUE_MAX = 65535 +HUE_API_STATE_SAT_MIN = 0 # Saturation +HUE_API_STATE_SAT_MAX = 254 +HUE_API_STATE_CT_MIN = 153 # Color temp +HUE_API_STATE_CT_MAX = 500 class HueUsernameView(HomeAssistantView): @@ -85,6 +100,9 @@ class HueUsernameView(HomeAssistantView): async def post(self, request): """Handle a POST request.""" + if not is_local(request[KEY_REAL_IP]): + return self.json_message("Only local IPs allowed", HTTP_UNAUTHORIZED) + try: data = await request.json() except ValueError: @@ -93,14 +111,11 @@ async def post(self, request): if "devicetype" not in data: return self.json_message("devicetype not specified", HTTP_BAD_REQUEST) - if not is_local(request[KEY_REAL_IP]): - return self.json_message("only local IPs allowed", HTTP_BAD_REQUEST) - return self.json([{"success": {"username": "12345678901234567890"}}]) class HueAllGroupsStateView(HomeAssistantView): - """Group handler.""" + """Handle requests for getting info about entity groups.""" url = "/api/{username}/groups" name = "emulated_hue:all_groups:state" @@ -114,7 +129,7 @@ def __init__(self, config): def get(self, request, username): """Process a request to make the Brilliant Lightpad work.""" if not is_local(request[KEY_REAL_IP]): - return self.json_message("only local IPs allowed", HTTP_BAD_REQUEST) + return self.json_message("Only local IPs allowed", HTTP_UNAUTHORIZED) return self.json({}) @@ -134,7 +149,7 @@ def __init__(self, config): def put(self, request, username): """Process a request to make the Logitech Pop working.""" if not is_local(request[KEY_REAL_IP]): - return self.json_message("only local IPs allowed", HTTP_BAD_REQUEST) + return self.json_message("Only local IPs allowed", HTTP_UNAUTHORIZED) return self.json( [ @@ -150,7 +165,7 @@ def put(self, request, username): class HueAllLightsStateView(HomeAssistantView): - """Handle requests for getting and setting info about entities.""" + """Handle requests for getting info about all entities.""" url = "/api/{username}/lights" name = "emulated_hue:lights:state" @@ -164,23 +179,21 @@ def __init__(self, config): def get(self, request, username): """Process a request to get the list of available lights.""" if not is_local(request[KEY_REAL_IP]): - return self.json_message("only local IPs allowed", HTTP_BAD_REQUEST) + return self.json_message("Only local IPs allowed", HTTP_UNAUTHORIZED) hass = request.app["hass"] json_response = {} for entity in hass.states.async_all(): if self.config.is_entity_exposed(entity): - state = get_entity_state(self.config, entity) - number = self.config.entity_id_to_number(entity.entity_id) - json_response[number] = entity_to_json(self.config, entity, state) + json_response[number] = entity_to_json(self.config, entity) return self.json(json_response) class HueOneLightStateView(HomeAssistantView): - """Handle requests for getting and setting info about entities.""" + """Handle requests for getting info about a single entity.""" url = "/api/{username}/lights/{entity_id}" name = "emulated_hue:light:state" @@ -194,7 +207,7 @@ def __init__(self, config): def get(self, request, username, entity_id): """Process a request to get the state of an individual light.""" if not is_local(request[KEY_REAL_IP]): - return self.json_message("only local IPs allowed", HTTP_BAD_REQUEST) + return self.json_message("Only local IPs allowed", HTTP_UNAUTHORIZED) hass = request.app["hass"] hass_entity_id = self.config.number_to_entity_id(entity_id) @@ -204,27 +217,25 @@ def get(self, request, username, entity_id): "Unknown entity number: %s not found in emulated_hue_ids.json", entity_id, ) - return web.Response(text="Entity not found", status=404) + return self.json_message("Entity not found", HTTP_NOT_FOUND) entity = hass.states.get(hass_entity_id) if entity is None: _LOGGER.error("Entity not found: %s", hass_entity_id) - return web.Response(text="Entity not found", status=404) + return self.json_message("Entity not found", HTTP_NOT_FOUND) if not self.config.is_entity_exposed(entity): _LOGGER.error("Entity not exposed: %s", entity_id) - return web.Response(text="Entity not exposed", status=404) - - state = get_entity_state(self.config, entity) + return self.json_message("Entity not exposed", HTTP_UNAUTHORIZED) - json_response = entity_to_json(self.config, entity, state) + json_response = entity_to_json(self.config, entity) return self.json(json_response) class HueOneLightChangeView(HomeAssistantView): - """Handle requests for getting and setting info about entities.""" + """Handle requests for setting info about entities.""" url = "/api/{username}/lights/{entity_number}/state" name = "emulated_hue:light:state" @@ -237,7 +248,7 @@ def __init__(self, config): async def put(self, request, username, entity_number): """Process a request to set the state of an individual light.""" if not is_local(request[KEY_REAL_IP]): - return self.json_message("only local IPs allowed", HTTP_BAD_REQUEST) + return self.json_message("Only local IPs allowed", HTTP_UNAUTHORIZED) config = self.config hass = request.app["hass"] @@ -255,7 +266,7 @@ async def put(self, request, username, entity_number): if not config.is_entity_exposed(entity): _LOGGER.error("Entity not exposed: %s", entity_id) - return web.Response(text="Entity not exposed", status=404) + return self.json_message("Entity not exposed", HTTP_UNAUTHORIZED) try: request_json = await request.json() @@ -263,12 +274,60 @@ async def put(self, request, username, entity_number): _LOGGER.error("Received invalid json") return self.json_message("Invalid JSON", HTTP_BAD_REQUEST) - # Parse the request into requested "on" status and brightness - parsed = parse_hue_api_put_light_body(request_json, entity) + # Get the entity's supported features + entity_features = entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0) + + # Parse the request + parsed = { + STATE_ON: False, + STATE_BRIGHTNESS: None, + STATE_HUE: None, + STATE_SATURATION: None, + STATE_COLOR_TEMP: None, + } - if parsed is None: - _LOGGER.error("Unable to parse data: %s", request_json) - return web.Response(text="Bad request", status=400) + if HUE_API_STATE_ON in request_json: + if not isinstance(request_json[HUE_API_STATE_ON], bool): + _LOGGER.error("Unable to parse data: %s", request_json) + return self.json_message("Bad request", HTTP_BAD_REQUEST) + parsed[STATE_ON] = request_json[HUE_API_STATE_ON] + else: + parsed[STATE_ON] = entity.state != STATE_OFF + + for (key, attr) in ( + (HUE_API_STATE_BRI, STATE_BRIGHTNESS), + (HUE_API_STATE_HUE, STATE_HUE), + (HUE_API_STATE_SAT, STATE_SATURATION), + (HUE_API_STATE_CT, STATE_COLOR_TEMP), + ): + if key in request_json: + try: + parsed[attr] = int(request_json[key]) + except ValueError: + _LOGGER.error("Unable to parse data (2): %s", request_json) + return self.json_message("Bad request", HTTP_BAD_REQUEST) + + if HUE_API_STATE_BRI in request_json: + if entity.domain == light.DOMAIN: + parsed[STATE_ON] = parsed[STATE_BRIGHTNESS] > 0 + if not entity_features & SUPPORT_BRIGHTNESS: + parsed[STATE_BRIGHTNESS] = None + + elif entity.domain == scene.DOMAIN: + parsed[STATE_BRIGHTNESS] = None + parsed[STATE_ON] = True + + elif entity.domain in [ + script.DOMAIN, + media_player.DOMAIN, + fan.DOMAIN, + cover.DOMAIN, + climate.DOMAIN, + ]: + # Convert 0-255 to 0-100 + level = (parsed[STATE_BRIGHTNESS] / HUE_API_STATE_BRI_MAX) * 100 + parsed[STATE_BRIGHTNESS] = round(level) + parsed[STATE_ON] = True # Choose general HA domain domain = core.DOMAIN @@ -282,29 +341,37 @@ async def put(self, request, username, entity_number): # Construct what we need to send to the service data = {ATTR_ENTITY_ID: entity_id} - # Make sure the entity actually supports brightness - entity_features = entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0) - + # If the requested entity is a light, set the brightness, hue, + # saturation and color temp if entity.domain == light.DOMAIN: if parsed[STATE_ON]: if entity_features & SUPPORT_BRIGHTNESS: if parsed[STATE_BRIGHTNESS] is not None: data[ATTR_BRIGHTNESS] = parsed[STATE_BRIGHTNESS] + if entity_features & SUPPORT_COLOR: - if parsed[STATE_HUE] is not None: - if parsed[STATE_SATURATION]: + if any((parsed[STATE_HUE], parsed[STATE_SATURATION])): + if parsed[STATE_HUE] is not None: + hue = parsed[STATE_HUE] + else: + hue = 0 + + if parsed[STATE_SATURATION] is not None: sat = parsed[STATE_SATURATION] else: sat = 0 - hue = parsed[STATE_HUE] # Convert hs values to hass hs values - sat = int((sat / HUE_API_STATE_SAT_MAX) * 100) hue = int((hue / HUE_API_STATE_HUE_MAX) * 360) + sat = int((sat / HUE_API_STATE_SAT_MAX) * 100) data[ATTR_HS_COLOR] = (hue, sat) - # If the requested entity is a script add some variables + if entity_features & SUPPORT_COLOR_TEMP: + if parsed[STATE_COLOR_TEMP] is not None: + data[ATTR_COLOR_TEMP] = parsed[STATE_COLOR_TEMP] + + # If the requested entity is a script, add some variables elif entity.domain == script.DOMAIN: data["variables"] = { "requested_state": STATE_ON if parsed[STATE_ON] else STATE_OFF @@ -365,8 +432,8 @@ async def put(self, request, username, entity_number): elif 66.6 < brightness <= 100: data[ATTR_SPEED] = SPEED_HIGH + # Map the off command to on if entity.domain in config.off_maps_to_on_domains: - # Map the off command to on service = SERVICE_TURN_ON # Caching is required because things like scripts and scenes won't @@ -392,141 +459,65 @@ async def put(self, request, username, entity_number): hass.services.async_call(domain, service, data, blocking=True) ) + # Create success responses for all received keys json_response = [ create_hue_success_response(entity_id, HUE_API_STATE_ON, parsed[STATE_ON]) ] - if parsed[STATE_BRIGHTNESS] is not None: - json_response.append( - create_hue_success_response( - entity_id, HUE_API_STATE_BRI, parsed[STATE_BRIGHTNESS] - ) - ) - if parsed[STATE_HUE] is not None: - json_response.append( - create_hue_success_response( - entity_id, HUE_API_STATE_HUE, parsed[STATE_HUE] + for (key, val) in ( + (STATE_BRIGHTNESS, HUE_API_STATE_BRI), + (STATE_HUE, HUE_API_STATE_HUE), + (STATE_SATURATION, HUE_API_STATE_SAT), + (STATE_COLOR_TEMP, HUE_API_STATE_CT), + ): + if parsed[key] is not None: + json_response.append( + create_hue_success_response(entity_id, val, parsed[key]) ) - ) - if parsed[STATE_SATURATION] is not None: - json_response.append( - create_hue_success_response( - entity_id, HUE_API_STATE_SAT, parsed[STATE_SATURATION] - ) - ) return self.json(json_response) -def parse_hue_api_put_light_body(request_json, entity): - """Parse the body of a request to change the state of a light.""" - data = { - STATE_BRIGHTNESS: None, - STATE_HUE: None, - STATE_ON: False, - STATE_SATURATION: None, - } - - # Make sure the entity actually supports brightness - entity_features = entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0) - - if HUE_API_STATE_ON in request_json: - if not isinstance(request_json[HUE_API_STATE_ON], bool): - return None - - if request_json[HUE_API_STATE_ON]: - # Echo requested device be turned on - data[STATE_BRIGHTNESS] = None - data[STATE_ON] = True - else: - # Echo requested device be turned off - data[STATE_BRIGHTNESS] = None - data[STATE_ON] = False - - if HUE_API_STATE_HUE in request_json: - try: - # Clamp brightness from 0 to 65535 - data[STATE_HUE] = max( - 0, min(int(request_json[HUE_API_STATE_HUE]), HUE_API_STATE_HUE_MAX) - ) - except ValueError: - return None - - if HUE_API_STATE_SAT in request_json: - try: - # Clamp saturation from 0 to 254 - data[STATE_SATURATION] = max( - 0, min(int(request_json[HUE_API_STATE_SAT]), HUE_API_STATE_SAT_MAX) - ) - except ValueError: - return None - - if HUE_API_STATE_BRI in request_json: - try: - # Clamp brightness from 0 to 255 - data[STATE_BRIGHTNESS] = max( - 0, min(int(request_json[HUE_API_STATE_BRI]), HUE_API_STATE_BRI_MAX) - ) - except ValueError: - return None - - if entity.domain == light.DOMAIN: - data[STATE_ON] = data[STATE_BRIGHTNESS] > 0 - if not entity_features & SUPPORT_BRIGHTNESS: - data[STATE_BRIGHTNESS] = None - - elif entity.domain == scene.DOMAIN: - data[STATE_BRIGHTNESS] = None - data[STATE_ON] = True - - elif entity.domain in [ - script.DOMAIN, - media_player.DOMAIN, - fan.DOMAIN, - cover.DOMAIN, - climate.DOMAIN, - ]: - # Convert 0-255 to 0-100 - level = (data[STATE_BRIGHTNESS] / HUE_API_STATE_BRI_MAX) * 100 - data[STATE_BRIGHTNESS] = round(level) - data[STATE_ON] = True - - return data - - def get_entity_state(config, entity): """Retrieve and convert state and brightness values for an entity.""" cached_state = config.cached_states.get(entity.entity_id, None) data = { + STATE_ON: False, STATE_BRIGHTNESS: None, STATE_HUE: None, - STATE_ON: False, STATE_SATURATION: None, + STATE_COLOR_TEMP: None, } if cached_state is None: data[STATE_ON] = entity.state != STATE_OFF + if data[STATE_ON]: data[STATE_BRIGHTNESS] = entity.attributes.get(ATTR_BRIGHTNESS, 0) hue_sat = entity.attributes.get(ATTR_HS_COLOR, None) if hue_sat is not None: hue = hue_sat[0] sat = hue_sat[1] - # convert hass hs values back to hue hs values + # Convert hass hs values back to hue hs values data[STATE_HUE] = int((hue / 360.0) * HUE_API_STATE_HUE_MAX) data[STATE_SATURATION] = int((sat / 100.0) * HUE_API_STATE_SAT_MAX) + else: + data[STATE_HUE] = HUE_API_STATE_HUE_MIN + data[STATE_SATURATION] = HUE_API_STATE_SAT_MIN + data[STATE_COLOR_TEMP] = entity.attributes.get(ATTR_COLOR_TEMP, 0) + else: data[STATE_BRIGHTNESS] = 0 data[STATE_HUE] = 0 data[STATE_SATURATION] = 0 + data[STATE_COLOR_TEMP] = 0 - # Make sure the entity actually supports brightness + # Get the entity's supported features entity_features = entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0) if entity.domain == light.DOMAIN: if entity_features & SUPPORT_BRIGHTNESS: pass - elif entity.domain == climate.DOMAIN: temperature = entity.attributes.get(ATTR_TEMPERATURE, 0) # Convert 0-100 to 0-255 @@ -536,7 +527,7 @@ def get_entity_state(config, entity): ATTR_MEDIA_VOLUME_LEVEL, 1.0 if data[STATE_ON] else 0.0 ) # Convert 0.0-1.0 to 0-255 - data[STATE_BRIGHTNESS] = round(min(1.0, level) * HUE_API_STATE_BRI_MAX) + data[STATE_BRIGHTNESS] = round(min(1.0, level) * 255) elif entity.domain == fan.DOMAIN: speed = entity.attributes.get(ATTR_SPEED, 0) # Convert 0.0-1.0 to 0-255 @@ -549,12 +540,13 @@ def get_entity_state(config, entity): data[STATE_BRIGHTNESS] = 255 elif entity.domain == cover.DOMAIN: level = entity.attributes.get(ATTR_CURRENT_POSITION, 0) - data[STATE_BRIGHTNESS] = round(level / 100 * HUE_API_STATE_BRI_MAX) + data[STATE_BRIGHTNESS] = round(level / 100 * 255) else: data = cached_state # Make sure brightness is valid if data[STATE_BRIGHTNESS] is None: data[STATE_BRIGHTNESS] = 255 if data[STATE_ON] else 0 + # Make sure hue/saturation are valid if (data[STATE_HUE] is None) or (data[STATE_SATURATION] is None): data[STATE_HUE] = 0 @@ -565,36 +557,117 @@ def get_entity_state(config, entity): data[STATE_HUE] = 0 data[STATE_SATURATION] = 0 + # Clamp brightness, hue, saturation, and color temp to valid values + for (key, v_min, v_max) in ( + (STATE_BRIGHTNESS, HUE_API_STATE_BRI_MIN, HUE_API_STATE_BRI_MAX), + (STATE_HUE, HUE_API_STATE_HUE_MIN, HUE_API_STATE_HUE_MAX), + (STATE_SATURATION, HUE_API_STATE_SAT_MIN, HUE_API_STATE_SAT_MAX), + (STATE_COLOR_TEMP, HUE_API_STATE_CT_MIN, HUE_API_STATE_CT_MAX), + ): + if data[key] is not None: + data[key] = max(v_min, min(data[key], v_max)) + return data -def entity_to_json(config, entity, state): +def entity_to_json(config, entity): """Convert an entity to its Hue bridge JSON representation.""" entity_features = entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0) - if (entity_features & SUPPORT_BRIGHTNESS) or entity.domain != light.DOMAIN: - return { - "state": { - HUE_API_STATE_ON: state[STATE_ON], - HUE_API_STATE_BRI: state[STATE_BRIGHTNESS], - HUE_API_STATE_HUE: state[STATE_HUE], - HUE_API_STATE_SAT: state[STATE_SATURATION], - "reachable": True, - }, - "type": "Dimmable light", - "name": config.get_entity_name(entity), - "modelid": "HASS123", - "uniqueid": entity.entity_id, - "swversion": "123", - } - return { - "state": {HUE_API_STATE_ON: state[STATE_ON], "reachable": True}, - "type": "On/off light", + unique_id = hashlib.md5(entity.entity_id.encode()).hexdigest() + unique_id = "00:{}:{}:{}:{}:{}:{}:{}-{}".format( + unique_id[0:2], + unique_id[2:4], + unique_id[4:6], + unique_id[6:8], + unique_id[8:10], + unique_id[10:12], + unique_id[12:14], + unique_id[14:16], + ) + + state = get_entity_state(config, entity) + + retval = { + "state": { + HUE_API_STATE_ON: state[STATE_ON], + "reachable": entity.state != STATE_UNAVAILABLE, + "mode": "homeautomation", + }, "name": config.get_entity_name(entity), - "modelid": "HASS321", - "uniqueid": entity.entity_id, + "uniqueid": unique_id, + "manufacturername": "Home Assistant", "swversion": "123", } + if ( + (entity_features & SUPPORT_BRIGHTNESS) + and (entity_features & SUPPORT_COLOR) + and (entity_features & SUPPORT_COLOR_TEMP) + ): + # Extended Color light (ZigBee Device ID: 0x0210) + # Same as Color light, but which supports additional setting of color temperature + retval["type"] = "Extended color light" + retval["modelid"] = "HASS231" + retval["state"].update( + { + HUE_API_STATE_BRI: state[STATE_BRIGHTNESS], + HUE_API_STATE_HUE: state[STATE_HUE], + HUE_API_STATE_SAT: state[STATE_SATURATION], + HUE_API_STATE_CT: state[STATE_COLOR_TEMP], + HUE_API_STATE_EFFECT: "none", + } + ) + if state[STATE_HUE] > 0 or state[STATE_SATURATION] > 0: + retval["state"][HUE_API_STATE_COLORMODE] = "hs" + else: + retval["state"][HUE_API_STATE_COLORMODE] = "ct" + elif (entity_features & SUPPORT_BRIGHTNESS) and (entity_features & SUPPORT_COLOR): + # Color light (ZigBee Device ID: 0x0200) + # Supports on/off, dimming and color control (hue/saturation, enhanced hue, color loop and XY) + retval["type"] = "Color light" + retval["modelid"] = "HASS213" + retval["state"].update( + { + HUE_API_STATE_BRI: state[STATE_BRIGHTNESS], + HUE_API_STATE_COLORMODE: "hs", + HUE_API_STATE_HUE: state[STATE_HUE], + HUE_API_STATE_SAT: state[STATE_SATURATION], + HUE_API_STATE_EFFECT: "none", + } + ) + elif (entity_features & SUPPORT_BRIGHTNESS) and ( + entity_features & SUPPORT_COLOR_TEMP + ): + # Color temperature light (ZigBee Device ID: 0x0220) + # Supports groups, scenes, on/off, dimming, and setting of a color temperature + retval["type"] = "Color temperature light" + retval["modelid"] = "HASS312" + retval["state"].update( + {HUE_API_STATE_COLORMODE: "ct", HUE_API_STATE_CT: state[STATE_COLOR_TEMP]} + ) + elif ( + entity_features + & ( + SUPPORT_BRIGHTNESS + | SUPPORT_SET_POSITION + | SUPPORT_SET_SPEED + | SUPPORT_VOLUME_SET + | SUPPORT_TARGET_TEMPERATURE + ) + ) or entity.domain == script.DOMAIN: + # Dimmable light (ZigBee Device ID: 0x0100) + # Supports groups, scenes, on/off and dimming + retval["type"] = "Dimmable light" + retval["modelid"] = "HASS123" + retval["state"].update({HUE_API_STATE_BRI: state[STATE_BRIGHTNESS]}) + else: + # On/off light (ZigBee Device ID: 0x0000) + # Supports groups, scenes and on/off control + retval["type"] = "On/off light" + retval["modelid"] = "HASS321" + + return retval + def create_hue_success_response(entity_id, attr, value): """Create a success response for an attribute set on a light.""" diff --git a/homeassistant/components/emulated_hue/manifest.json b/homeassistant/components/emulated_hue/manifest.json index 9b3b00d20b21..ddd39443886a 100644 --- a/homeassistant/components/emulated_hue/manifest.json +++ b/homeassistant/components/emulated_hue/manifest.json @@ -6,5 +6,7 @@ "aiohttp_cors==0.7.0" ], "dependencies": [], - "codeowners": [] + "codeowners": [ + "@NobleKangaroo" + ] } diff --git a/homeassistant/components/emulated_roku/binding.py b/homeassistant/components/emulated_roku/binding.py index 4c98af69848b..a44effff55a0 100644 --- a/homeassistant/components/emulated_roku/binding.py +++ b/homeassistant/components/emulated_roku/binding.py @@ -1,6 +1,8 @@ """Bridge between emulated_roku and Home Assistant.""" import logging +from emulated_roku import EmulatedRokuCommandHandler, EmulatedRokuServer + from homeassistant.const import EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP from homeassistant.core import CoreState, EventOrigin @@ -51,7 +53,6 @@ def __init__( async def setup(self): """Start the emulated_roku server.""" - from emulated_roku import EmulatedRokuServer, EmulatedRokuCommandHandler class EventCommandHandler(EmulatedRokuCommandHandler): """emulated_roku command handler to turn commands into events.""" diff --git a/homeassistant/components/enigma2/media_player.py b/homeassistant/components/enigma2/media_player.py index 5b0e705f3929..85dec4abd94f 100644 --- a/homeassistant/components/enigma2/media_player.py +++ b/homeassistant/components/enigma2/media_player.py @@ -1,35 +1,36 @@ """Support for Enigma2 media players.""" import logging +from openwebif.api import CreateDevice import voluptuous as vol from homeassistant.components.media_player import MediaPlayerDevice -from homeassistant.helpers.config_validation import PLATFORM_SCHEMA from homeassistant.components.media_player.const import ( + MEDIA_TYPE_TVSHOW, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PREVIOUS_TRACK, - SUPPORT_TURN_ON, + SUPPORT_SELECT_SOURCE, + SUPPORT_STOP, SUPPORT_TURN_OFF, + SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, - SUPPORT_STOP, - SUPPORT_SELECT_SOURCE, SUPPORT_VOLUME_STEP, - MEDIA_TYPE_TVSHOW, ) from homeassistant.const import ( CONF_HOST, CONF_NAME, - CONF_USERNAME, CONF_PASSWORD, + CONF_PORT, CONF_SSL, + CONF_USERNAME, STATE_OFF, STATE_ON, STATE_PLAYING, - CONF_PORT, ) import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.config_validation import PLATFORM_SCHEMA _LOGGER = logging.getLogger(__name__) @@ -101,8 +102,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): config[CONF_DEEP_STANDBY] = DEFAULT_DEEP_STANDBY config[CONF_SOURCE_BOUQUET] = DEFAULT_SOURCE_BOUQUET - from openwebif.api import CreateDevice - device = CreateDevice( host=config[CONF_HOST], port=config.get(CONF_PORT), diff --git a/homeassistant/components/enocean/__init__.py b/homeassistant/components/enocean/__init__.py index b75c8f001c0f..876c7a1f05ba 100644 --- a/homeassistant/components/enocean/__init__.py +++ b/homeassistant/components/enocean/__init__.py @@ -1,11 +1,14 @@ """Support for EnOcean devices.""" import logging +from enocean.communicators.serialcommunicator import SerialCommunicator +from enocean.protocol.packet import Packet, RadioPacket +from enocean.utils import combine_hex import voluptuous as vol from homeassistant.const import CONF_DEVICE -from homeassistant.helpers.entity import Entity import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -34,7 +37,6 @@ class EnOceanDongle: def __init__(self, hass, ser): """Initialize the EnOcean dongle.""" - from enocean.communicators.serialcommunicator import SerialCommunicator self.__communicator = SerialCommunicator(port=ser, callback=self.callback) self.__communicator.start() @@ -53,7 +55,6 @@ def callback(self, packet): This is the callback function called by python-enocan whenever there is an incoming packet. """ - from enocean.protocol.packet import RadioPacket if isinstance(packet, RadioPacket): _LOGGER.debug("Received radio packet: %s", packet) @@ -76,7 +77,6 @@ async def async_added_to_hass(self): def _message_received_callback(self, packet): """Handle incoming packets.""" - from enocean.utils import combine_hex if packet.sender_int == combine_hex(self.dev_id): self.value_changed(packet) @@ -84,10 +84,8 @@ def _message_received_callback(self, packet): def value_changed(self, packet): """Update the internal state of the device when a packet arrives.""" - # pylint: disable=no-self-use def send_command(self, data, optional, packet_type): """Send a command via the EnOcean dongle.""" - from enocean.protocol.packet import Packet packet = Packet(packet_type, data=data, optional=optional) self.hass.helpers.dispatcher.dispatcher_send(SIGNAL_SEND_MESSAGE, packet) diff --git a/homeassistant/components/enocean/sensor.py b/homeassistant/components/enocean/sensor.py index 2e6b5bdb986d..cfab52b36659 100644 --- a/homeassistant/components/enocean/sensor.py +++ b/homeassistant/components/enocean/sensor.py @@ -11,8 +11,8 @@ CONF_NAME, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_TEMPERATURE, - TEMP_CELSIUS, POWER_WATT, + TEMP_CELSIUS, ) import homeassistant.helpers.config_validation as cv diff --git a/homeassistant/components/enphase_envoy/sensor.py b/homeassistant/components/enphase_envoy/sensor.py index 13784e24d77f..3977326c06dd 100644 --- a/homeassistant/components/enphase_envoy/sensor.py +++ b/homeassistant/components/enphase_envoy/sensor.py @@ -1,19 +1,19 @@ """Support for Enphase Envoy solar energy monitor.""" import logging +from envoy_reader.envoy_reader import EnvoyReader import voluptuous as vol -from homeassistant.helpers.entity import Entity from homeassistant.components.sensor import PLATFORM_SCHEMA -import homeassistant.helpers.config_validation as cv from homeassistant.const import ( CONF_IP_ADDRESS, CONF_MONITORED_CONDITIONS, CONF_NAME, - POWER_WATT, ENERGY_WATT_HOUR, + POWER_WATT, ) - +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -52,7 +52,6 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the Enphase Envoy sensor.""" - from envoy_reader.envoy_reader import EnvoyReader ip_address = config[CONF_IP_ADDRESS] monitored_conditions = config[CONF_MONITORED_CONDITIONS] @@ -118,7 +117,6 @@ def icon(self): async def async_update(self): """Get the energy production data from the Enphase Envoy.""" - from envoy_reader.envoy_reader import EnvoyReader if self._type != "inverters": _state = await getattr(EnvoyReader(self._ip_address), self._type)() diff --git a/homeassistant/components/entur_public_transport/manifest.json b/homeassistant/components/entur_public_transport/manifest.json index b0910f165363..6396ff8e678d 100644 --- a/homeassistant/components/entur_public_transport/manifest.json +++ b/homeassistant/components/entur_public_transport/manifest.json @@ -3,8 +3,10 @@ "name": "Entur public transport", "documentation": "https://www.home-assistant.io/integrations/entur_public_transport", "requirements": [ - "enturclient==0.2.0" + "enturclient==0.2.1" ], "dependencies": [], - "codeowners": [] -} + "codeowners": [ + "@hfurubotten" + ] +} \ No newline at end of file diff --git a/homeassistant/components/entur_public_transport/sensor.py b/homeassistant/components/entur_public_transport/sensor.py index 0f8324ded9e0..2ecae21824ee 100644 --- a/homeassistant/components/entur_public_transport/sensor.py +++ b/homeassistant/components/entur_public_transport/sensor.py @@ -2,6 +2,7 @@ from datetime import datetime, timedelta import logging +from enturclient import EnturPublicTransportData import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -87,7 +88,6 @@ def due_in_minutes(timestamp: datetime) -> int: async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the Entur public transport sensor.""" - from enturclient import EnturPublicTransportData expand = config.get(CONF_EXPAND_PLATFORMS) line_whitelist = config.get(CONF_WHITELIST_LINES) diff --git a/homeassistant/components/environment_canada/camera.py b/homeassistant/components/environment_canada/camera.py index 2a23fb95a18f..4ef3e17fc468 100644 --- a/homeassistant/components/environment_canada/camera.py +++ b/homeassistant/components/environment_canada/camera.py @@ -7,17 +7,18 @@ import datetime import logging +from env_canada import ECRadar import voluptuous as vol from homeassistant.components.camera import PLATFORM_SCHEMA, Camera from homeassistant.const import ( - CONF_NAME, + ATTR_ATTRIBUTION, CONF_LATITUDE, CONF_LONGITUDE, - ATTR_ATTRIBUTION, + CONF_NAME, ) -from homeassistant.util import Throttle import homeassistant.helpers.config_validation as cv +from homeassistant.util import Throttle _LOGGER = logging.getLogger(__name__) @@ -46,7 +47,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Environment Canada camera.""" - from env_canada import ECRadar if config.get(CONF_STATION): radar_object = ECRadar( diff --git a/homeassistant/components/environment_canada/sensor.py b/homeassistant/components/environment_canada/sensor.py index 244fda61656f..1568ba19d6b7 100644 --- a/homeassistant/components/environment_canada/sensor.py +++ b/homeassistant/components/environment_canada/sensor.py @@ -8,18 +8,19 @@ import logging import re +from env_canada import ECData import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( - TEMP_CELSIUS, - CONF_LATITUDE, - CONF_LONGITUDE, ATTR_ATTRIBUTION, ATTR_LOCATION, + CONF_LATITUDE, + CONF_LONGITUDE, + TEMP_CELSIUS, ) -from homeassistant.helpers.entity import Entity import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -56,7 +57,6 @@ def validate_station(station): def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Environment Canada sensor.""" - from env_canada import ECData if config.get(CONF_STATION): ec_data = ECData( @@ -125,7 +125,7 @@ def update(self): value = sensor_data.get("value") if isinstance(value, list): - self._state = " | ".join([str(s.get("title")) for s in value]) + self._state = " | ".join([str(s.get("title")) for s in value])[:255] self._attr.update( { ATTR_DETAIL: " | ".join([str(s.get("detail")) for s in value]), @@ -134,6 +134,9 @@ def update(self): ) elif self.sensor_type == "tendency": self._state = str(value).capitalize() + elif value is not None and len(value) > 255: + self._state = value[:255] + _LOGGER.info("Value for %s truncated to 255 characters", self._unique_id) else: self._state = value diff --git a/homeassistant/components/environment_canada/weather.py b/homeassistant/components/environment_canada/weather.py index a4fad083d2a6..572543e39c4c 100644 --- a/homeassistant/components/environment_canada/weather.py +++ b/homeassistant/components/environment_canada/weather.py @@ -20,8 +20,8 @@ WeatherEntity, ) from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME, TEMP_CELSIUS -import homeassistant.util.dt as dt import homeassistant.helpers.config_validation as cv +import homeassistant.util.dt as dt _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/envisalink/__init__.py b/homeassistant/components/envisalink/__init__.py index 6cdedf897446..14113537de6e 100644 --- a/homeassistant/components/envisalink/__init__.py +++ b/homeassistant/components/envisalink/__init__.py @@ -2,14 +2,15 @@ import asyncio import logging +from pyenvisalink import EnvisalinkAlarmPanel import voluptuous as vol +from homeassistant.const import CONF_HOST, CONF_TIMEOUT, EVENT_HOMEASSISTANT_STOP from homeassistant.core import callback import homeassistant.helpers.config_validation as cv -from homeassistant.const import EVENT_HOMEASSISTANT_STOP, CONF_TIMEOUT, CONF_HOST -from homeassistant.helpers.entity import Entity from homeassistant.helpers.discovery import async_load_platform from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -98,7 +99,6 @@ async def async_setup(hass, config): """Set up for Envisalink devices.""" - from pyenvisalink import EnvisalinkAlarmPanel conf = config.get(DOMAIN) diff --git a/homeassistant/components/envisalink/alarm_control_panel.py b/homeassistant/components/envisalink/alarm_control_panel.py index 663f19c8ed58..7630169dcada 100644 --- a/homeassistant/components/envisalink/alarm_control_panel.py +++ b/homeassistant/components/envisalink/alarm_control_panel.py @@ -3,7 +3,16 @@ import voluptuous as vol -import homeassistant.components.alarm_control_panel as alarm +from homeassistant.components.alarm_control_panel import ( + FORMAT_NUMBER, + AlarmControlPanel, +) +from homeassistant.components.alarm_control_panel.const import ( + SUPPORT_ALARM_ARM_AWAY, + SUPPORT_ALARM_ARM_HOME, + SUPPORT_ALARM_ARM_NIGHT, + SUPPORT_ALARM_TRIGGER, +) from homeassistant.const import ( ATTR_ENTITY_ID, STATE_ALARM_ARMED_AWAY, @@ -23,6 +32,7 @@ CONF_PANIC, CONF_PARTITIONNAME, DATA_EVL, + DOMAIN, PARTITION_SCHEMA, SIGNAL_KEYPAD_UPDATE, SIGNAL_PARTITION_UPDATE, @@ -31,7 +41,7 @@ _LOGGER = logging.getLogger(__name__) -SERVICE_ALARM_KEYPRESS = "envisalink_alarm_keypress" +SERVICE_ALARM_KEYPRESS = "alarm_keypress" ATTR_KEYPRESS = "keypress" ALARM_KEYPRESS_SCHEMA = vol.Schema( { @@ -77,7 +87,7 @@ def alarm_keypress_handler(service): device.async_alarm_keypress(keypress) hass.services.async_register( - alarm.DOMAIN, + DOMAIN, SERVICE_ALARM_KEYPRESS, alarm_keypress_handler, schema=ALARM_KEYPRESS_SCHEMA, @@ -86,7 +96,7 @@ def alarm_keypress_handler(service): return True -class EnvisalinkAlarm(EnvisalinkDevice, alarm.AlarmControlPanel): +class EnvisalinkAlarm(EnvisalinkDevice, AlarmControlPanel): """Representation of an Envisalink-based alarm panel.""" def __init__( @@ -118,7 +128,7 @@ def code_format(self): """Regex for code format or None if no code is required.""" if self._code: return None - return alarm.FORMAT_NUMBER + return FORMAT_NUMBER @property def state(self): @@ -141,6 +151,16 @@ def state(self): state = STATE_ALARM_DISARMED return state + @property + def supported_features(self) -> int: + """Return the list of supported features.""" + return ( + SUPPORT_ALARM_ARM_HOME + | SUPPORT_ALARM_ARM_AWAY + | SUPPORT_ALARM_ARM_NIGHT + | SUPPORT_ALARM_TRIGGER + ) + async def async_alarm_disarm(self, code=None): """Send disarm command.""" if code: diff --git a/homeassistant/components/envisalink/services.yaml b/homeassistant/components/envisalink/services.yaml index e31aa804059d..2a5f91791df4 100644 --- a/homeassistant/components/envisalink/services.yaml +++ b/homeassistant/components/envisalink/services.yaml @@ -1,5 +1,15 @@ # Describes the format for available Envisalink services. +alarm_keypress: + description: Send custom keypresses to the alarm. + fields: + entity_id: + description: Name of the alarm control panel to trigger. + example: 'alarm_control_panel.downstairs' + keypress: + description: 'String to send to the alarm panel (1-6 characters).' + example: '*71' + invoke_custom_function: description: > Allows users with DSC panels to trigger a PGM output (1-4). diff --git a/homeassistant/components/epson/const.py b/homeassistant/components/epson/const.py new file mode 100644 index 000000000000..23f3b081d013 --- /dev/null +++ b/homeassistant/components/epson/const.py @@ -0,0 +1,10 @@ +"""Constants for the Epson projector component.""" +DOMAIN = "epson" +SERVICE_SELECT_CMODE = "select_cmode" + +ATTR_CMODE = "cmode" + +DATA_EPSON = "epson" +DEFAULT_NAME = "EPSON Projector" + +SUPPORT_CMODE = 33001 diff --git a/homeassistant/components/epson/media_player.py b/homeassistant/components/epson/media_player.py index 638f012ac7ab..f3428602fadd 100644 --- a/homeassistant/components/epson/media_player.py +++ b/homeassistant/components/epson/media_player.py @@ -29,7 +29,6 @@ from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA from homeassistant.components.media_player.const import ( - DOMAIN, SUPPORT_NEXT_TRACK, SUPPORT_PREVIOUS_TRACK, SUPPORT_SELECT_SOURCE, @@ -49,17 +48,17 @@ ) from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv +from .const import ( + ATTR_CMODE, + DATA_EPSON, + DEFAULT_NAME, + DOMAIN, + SERVICE_SELECT_CMODE, + SUPPORT_CMODE, +) _LOGGER = logging.getLogger(__name__) -ATTR_CMODE = "cmode" - -DATA_EPSON = "epson" -DEFAULT_NAME = "EPSON Projector" - -SERVICE_SELECT_CMODE = "epson_select_cmode" -SUPPORT_CMODE = 33001 - SUPPORT_EPSON = ( SUPPORT_TURN_ON | SUPPORT_TURN_OFF diff --git a/homeassistant/components/epson/services.yaml b/homeassistant/components/epson/services.yaml index e69de29bb2d1..6e9724c95f72 100644 --- a/homeassistant/components/epson/services.yaml +++ b/homeassistant/components/epson/services.yaml @@ -0,0 +1,9 @@ +select_cmode: + description: Select Color mode of Epson projector + fields: + entity_id: + description: Name of projector + example: 'media_player.epson_projector' + cmode: + description: Name of Cmode + example: 'cinema' diff --git a/homeassistant/components/epsonworkforce/sensor.py b/homeassistant/components/epsonworkforce/sensor.py index b310376e5cc5..3bb90eb06449 100644 --- a/homeassistant/components/epsonworkforce/sensor.py +++ b/homeassistant/components/epsonworkforce/sensor.py @@ -2,6 +2,7 @@ from datetime import timedelta import logging +from epsonprinter_pkg.epsonprinterapi import EpsonPrinterAPI import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -34,8 +35,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the cartridge sensor.""" host = config.get(CONF_HOST) - from epsonprinter_pkg.epsonprinterapi import EpsonPrinterAPI - api = EpsonPrinterAPI(host) if not api.available: raise PlatformNotReady() diff --git a/homeassistant/components/eq3btsmart/climate.py b/homeassistant/components/eq3btsmart/climate.py index 8499a0de5a09..d0b60c74443d 100644 --- a/homeassistant/components/eq3btsmart/climate.py +++ b/homeassistant/components/eq3btsmart/climate.py @@ -1,6 +1,8 @@ """Support for eQ-3 Bluetooth Smart thermostats.""" import logging +# pylint: disable=import-error +from bluepy.btle import BTLEException import eq3bt as eq3 # pylint: disable=import-error import voluptuous as vol @@ -11,9 +13,9 @@ HVAC_MODE_OFF, PRESET_AWAY, PRESET_BOOST, + PRESET_NONE, SUPPORT_PRESET_MODE, SUPPORT_TARGET_TEMPERATURE, - PRESET_NONE, ) from homeassistant.const import ( ATTR_TEMPERATURE, @@ -190,8 +192,6 @@ def set_preset_mode(self, preset_mode): def update(self): """Update the data from the thermostat.""" - # pylint: disable=import-error,no-name-in-module - from bluepy.btle import BTLEException try: self._thermostat.update() diff --git a/homeassistant/components/esphome/__init__.py b/homeassistant/components/esphome/__init__.py index a669726ca389..2ad24e6f75ec 100644 --- a/homeassistant/components/esphome/__init__.py +++ b/homeassistant/components/esphome/__init__.py @@ -38,7 +38,7 @@ from homeassistant.helpers.typing import ConfigType, HomeAssistantType # Import config flow so that it's added to the registry -from .config_flow import EsphomeFlowHandler # noqa +from .config_flow import EsphomeFlowHandler # noqa: F401 from .entry_data import ( DATA_KEY, DISPATCHER_ON_DEVICE_UPDATE, diff --git a/homeassistant/components/esphome/binary_sensor.py b/homeassistant/components/esphome/binary_sensor.py index 64506f69283c..fe41bb2f7bb6 100644 --- a/homeassistant/components/esphome/binary_sensor.py +++ b/homeassistant/components/esphome/binary_sensor.py @@ -1,5 +1,4 @@ """Support for ESPHome binary sensors.""" -import logging from typing import Optional from aioesphomeapi import BinarySensorInfo, BinarySensorState @@ -8,8 +7,6 @@ from . import EsphomeEntity, platform_async_setup_entry -_LOGGER = logging.getLogger(__name__) - async def async_setup_entry(hass, entry, async_add_entities): """Set up ESPHome binary sensors based on a config entry.""" diff --git a/homeassistant/components/esphome/climate.py b/homeassistant/components/esphome/climate.py index 5fed8da76ef6..960366a8332e 100644 --- a/homeassistant/components/esphome/climate.py +++ b/homeassistant/components/esphome/climate.py @@ -2,22 +2,52 @@ import logging from typing import List, Optional -from aioesphomeapi import ClimateInfo, ClimateMode, ClimateState +from aioesphomeapi import ( + ClimateAction, + ClimateFanMode, + ClimateInfo, + ClimateMode, + ClimateState, + ClimateSwingMode, +) from homeassistant.components.climate import ClimateDevice from homeassistant.components.climate.const import ( ATTR_HVAC_MODE, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, - HVAC_MODE_HEAT_COOL, + CURRENT_HVAC_COOL, + CURRENT_HVAC_DRY, + CURRENT_HVAC_FAN, + CURRENT_HVAC_HEAT, + CURRENT_HVAC_IDLE, + CURRENT_HVAC_OFF, + FAN_AUTO, + FAN_DIFFUSE, + FAN_FOCUS, + FAN_HIGH, + FAN_LOW, + FAN_MEDIUM, + FAN_MIDDLE, + FAN_OFF, + FAN_ON, HVAC_MODE_COOL, + HVAC_MODE_DRY, + HVAC_MODE_FAN_ONLY, HVAC_MODE_HEAT, - SUPPORT_TARGET_TEMPERATURE, - SUPPORT_PRESET_MODE, - SUPPORT_TARGET_TEMPERATURE_RANGE, - PRESET_AWAY, + HVAC_MODE_HEAT_COOL, HVAC_MODE_OFF, + PRESET_AWAY, PRESET_HOME, + SUPPORT_FAN_MODE, + SUPPORT_PRESET_MODE, + SUPPORT_SWING_MODE, + SUPPORT_TARGET_TEMPERATURE, + SUPPORT_TARGET_TEMPERATURE_RANGE, + SWING_BOTH, + SWING_HORIZONTAL, + SWING_OFF, + SWING_VERTICAL, ) from homeassistant.const import ( ATTR_TEMPERATURE, @@ -57,6 +87,45 @@ def _climate_modes(): ClimateMode.AUTO: HVAC_MODE_HEAT_COOL, ClimateMode.COOL: HVAC_MODE_COOL, ClimateMode.HEAT: HVAC_MODE_HEAT, + ClimateMode.FAN_ONLY: HVAC_MODE_FAN_ONLY, + ClimateMode.DRY: HVAC_MODE_DRY, + } + + +@esphome_map_enum +def _climate_actions(): + return { + ClimateAction.OFF: CURRENT_HVAC_OFF, + ClimateAction.COOLING: CURRENT_HVAC_COOL, + ClimateAction.HEATING: CURRENT_HVAC_HEAT, + ClimateAction.IDLE: CURRENT_HVAC_IDLE, + ClimateAction.DRYING: CURRENT_HVAC_DRY, + ClimateAction.FAN: CURRENT_HVAC_FAN, + } + + +@esphome_map_enum +def _fan_modes(): + return { + ClimateFanMode.ON: FAN_ON, + ClimateFanMode.OFF: FAN_OFF, + ClimateFanMode.AUTO: FAN_AUTO, + ClimateFanMode.LOW: FAN_LOW, + ClimateFanMode.MEDIUM: FAN_MEDIUM, + ClimateFanMode.HIGH: FAN_HIGH, + ClimateFanMode.MIDDLE: FAN_MIDDLE, + ClimateFanMode.FOCUS: FAN_FOCUS, + ClimateFanMode.DIFFUSE: FAN_DIFFUSE, + } + + +@esphome_map_enum +def _swing_modes(): + return { + ClimateSwingMode.OFF: SWING_OFF, + ClimateSwingMode.BOTH: SWING_BOTH, + ClimateSwingMode.VERTICAL: SWING_VERTICAL, + ClimateSwingMode.HORIZONTAL: SWING_HORIZONTAL, } @@ -94,11 +163,27 @@ def hvac_modes(self) -> List[str]: for mode in self._static_info.supported_modes ] + @property + def fan_modes(self): + """Return the list of available fan modes.""" + return [ + _fan_modes.from_esphome(mode) + for mode in self._static_info.supported_fan_modes + ] + @property def preset_modes(self): """Return preset modes.""" return [PRESET_AWAY, PRESET_HOME] if self._static_info.supports_away else [] + @property + def swing_modes(self): + """Return the list of available swing modes.""" + return [ + _swing_modes.from_esphome(mode) + for mode in self._static_info.supported_swing_modes + ] + @property def target_temperature_step(self) -> float: """Return the supported step of target temperature.""" @@ -125,6 +210,10 @@ def supported_features(self) -> int: features |= SUPPORT_TARGET_TEMPERATURE if self._static_info.supports_away: features |= SUPPORT_PRESET_MODE + if self._static_info.supported_fan_modes: + features |= SUPPORT_FAN_MODE + if self._static_info.supported_swing_modes: + features |= SUPPORT_SWING_MODE return features # https://github.com/PyCQA/pylint/issues/3150 for all @esphome_state_property @@ -135,11 +224,29 @@ def hvac_mode(self) -> Optional[str]: """Return current operation ie. heat, cool, idle.""" return _climate_modes.from_esphome(self._state.mode) + @esphome_state_property + def hvac_action(self) -> Optional[str]: + """Return current action.""" + # HA has no support feature field for hvac_action + if not self._static_info.supports_action: + return None + return _climate_actions.from_esphome(self._state.action) + + @esphome_state_property + def fan_mode(self): + """Return current fan setting.""" + return _fan_modes.from_esphome(self._state.fan_mode) + @esphome_state_property def preset_mode(self): """Return current preset mode.""" return PRESET_AWAY if self._state.away else PRESET_HOME + @esphome_state_property + def swing_mode(self): + """Return current swing mode.""" + return _swing_modes.from_esphome(self._state.swing_mode) + @esphome_state_property def current_temperature(self) -> Optional[float]: """Return the current temperature.""" @@ -183,3 +290,15 @@ async def async_set_preset_mode(self, preset_mode): """Set preset mode.""" away = preset_mode == PRESET_AWAY await self._client.climate_command(key=self._static_info.key, away=away) + + async def async_set_fan_mode(self, fan_mode: str) -> None: + """Set new fan mode.""" + await self._client.climate_command( + key=self._static_info.key, fan_mode=_fan_modes.from_hass(fan_mode) + ) + + async def async_set_swing_mode(self, swing_mode: str) -> None: + """Set new swing mode.""" + await self._client.climate_command( + key=self._static_info.key, swing_mode=_swing_modes.from_hass(swing_mode) + ) diff --git a/homeassistant/components/esphome/config_flow.py b/homeassistant/components/esphome/config_flow.py index 47c00f434635..53289799b439 100644 --- a/homeassistant/components/esphome/config_flow.py +++ b/homeassistant/components/esphome/config_flow.py @@ -2,6 +2,7 @@ from collections import OrderedDict from typing import Optional +from aioesphomeapi import APIClient, APIConnectionError import voluptuous as vol from homeassistant import config_entries @@ -147,8 +148,6 @@ async def async_step_authenticate(self, user_input=None, error=None): async def fetch_device_info(self): """Fetch device info from API and return any errors.""" - from aioesphomeapi import APIClient, APIConnectionError - cli = APIClient(self.hass.loop, self._host, self._port, "") try: @@ -165,8 +164,6 @@ async def fetch_device_info(self): async def try_login(self): """Try logging in to device and return any errors.""" - from aioesphomeapi import APIClient, APIConnectionError - cli = APIClient(self.hass.loop, self._host, self._port, self._password) try: diff --git a/homeassistant/components/esphome/cover.py b/homeassistant/components/esphome/cover.py index 980fc9369406..53014991de80 100644 --- a/homeassistant/components/esphome/cover.py +++ b/homeassistant/components/esphome/cover.py @@ -2,7 +2,7 @@ import logging from typing import Optional -from aioesphomeapi import CoverInfo, CoverState +from aioesphomeapi import CoverInfo, CoverOperation, CoverState from homeassistant.components.cover import ( ATTR_POSITION, @@ -82,15 +82,11 @@ def is_closed(self) -> Optional[bool]: @esphome_state_property def is_opening(self) -> bool: """Return if the cover is opening or not.""" - from aioesphomeapi import CoverOperation - return self._state.current_operation == CoverOperation.IS_OPENING @esphome_state_property def is_closing(self) -> bool: """Return if the cover is closing or not.""" - from aioesphomeapi import CoverOperation - return self._state.current_operation == CoverOperation.IS_CLOSING @esphome_state_property diff --git a/homeassistant/components/esphome/entry_data.py b/homeassistant/components/esphome/entry_data.py index d916e1a90c87..48f1aea2c2dd 100644 --- a/homeassistant/components/esphome/entry_data.py +++ b/homeassistant/components/esphome/entry_data.py @@ -1,22 +1,22 @@ """Runtime entry data for ESPHome stored in hass.data.""" import asyncio -from typing import Any, Callable, Dict, List, Optional, Tuple, Set +from typing import Any, Callable, Dict, List, Optional, Set, Tuple from aioesphomeapi import ( COMPONENT_TYPE_TO_INFO, - DeviceInfo, - EntityInfo, - EntityState, - UserService, BinarySensorInfo, CameraInfo, ClimateInfo, CoverInfo, + DeviceInfo, + EntityInfo, + EntityState, FanInfo, LightInfo, SensorInfo, SwitchInfo, TextSensorInfo, + UserService, ) import attr diff --git a/homeassistant/components/esphome/fan.py b/homeassistant/components/esphome/fan.py index cddb75b41bfa..8b9b4b4922cf 100644 --- a/homeassistant/components/esphome/fan.py +++ b/homeassistant/components/esphome/fan.py @@ -81,7 +81,6 @@ async def async_turn_on(self, speed: Optional[str] = None, **kwargs) -> None: data["speed"] = _fan_speeds.from_hass(speed) await self._client.fan_command(**data) - # pylint: disable=arguments-differ async def async_turn_off(self, **kwargs) -> None: """Turn off the fan.""" await self._client.fan_command(key=self._static_info.key, state=False) diff --git a/homeassistant/components/esphome/manifest.json b/homeassistant/components/esphome/manifest.json index 724946e69841..549a063528f3 100644 --- a/homeassistant/components/esphome/manifest.json +++ b/homeassistant/components/esphome/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/esphome", "requirements": [ - "aioesphomeapi==2.5.0" + "aioesphomeapi==2.6.1" ], "dependencies": [], "zeroconf": ["_esphomelib._tcp.local."], diff --git a/homeassistant/components/etherscan/sensor.py b/homeassistant/components/etherscan/sensor.py index 9cabb2762b03..1c14ce578c16 100644 --- a/homeassistant/components/etherscan/sensor.py +++ b/homeassistant/components/etherscan/sensor.py @@ -1,6 +1,7 @@ """Support for Etherscan sensors.""" from datetime import timedelta +from pyetherscan import get_balance import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -75,7 +76,6 @@ def device_state_attributes(self): def update(self): """Get the latest state of the sensor.""" - from pyetherscan import get_balance if self._token_address: self._state = get_balance(self._address, self._token_address) diff --git a/homeassistant/components/evohome/__init__.py b/homeassistant/components/evohome/__init__.py index 29f89dc08d60..3d903e86e306 100644 --- a/homeassistant/components/evohome/__init__.py +++ b/homeassistant/components/evohome/__init__.py @@ -237,11 +237,7 @@ def __init__(self, hass, client, client_v1, store, params) -> None: loc_idx = params[CONF_LOCATION_IDX] self.config = client.installation_info[loc_idx][GWS][0][TCS][0] - self.tcs = ( - client.locations[loc_idx] # pylint: disable=protected-access - ._gateways[0] - ._control_systems[0] - ) + self.tcs = client.locations[loc_idx]._gateways[0]._control_systems[0] self.temps = None async def save_auth_tokens(self) -> None: diff --git a/homeassistant/components/facebox/const.py b/homeassistant/components/facebox/const.py new file mode 100644 index 000000000000..991ec925a981 --- /dev/null +++ b/homeassistant/components/facebox/const.py @@ -0,0 +1,4 @@ +"""Constants for the Facebox component.""" + +DOMAIN = "facebox" +SERVICE_TEACH_FACE = "teach_face" diff --git a/homeassistant/components/facebox/image_processing.py b/homeassistant/components/facebox/image_processing.py index 228cae2f19d5..ba53ac1ac7d0 100644 --- a/homeassistant/components/facebox/image_processing.py +++ b/homeassistant/components/facebox/image_processing.py @@ -15,7 +15,6 @@ CONF_SOURCE, CONF_ENTITY_ID, CONF_NAME, - DOMAIN, ) from homeassistant.const import ( CONF_IP_ADDRESS, @@ -27,6 +26,8 @@ HTTP_UNAUTHORIZED, ) +from .const import DOMAIN, SERVICE_TEACH_FACE + _LOGGER = logging.getLogger(__name__) ATTR_BOUNDING_BOX = "bounding_box" @@ -38,7 +39,6 @@ CLASSIFIER = "facebox" DATA_FACEBOX = "facebox_classifiers" FILE_PATH = "file_path" -SERVICE_TEACH_FACE = "facebox_teach_face" PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( diff --git a/homeassistant/components/facebox/services.yaml b/homeassistant/components/facebox/services.yaml index e69de29bb2d1..c6b686efb851 100644 --- a/homeassistant/components/facebox/services.yaml +++ b/homeassistant/components/facebox/services.yaml @@ -0,0 +1,12 @@ +teach_face: + description: Teach facebox a face using a file. + fields: + entity_id: + description: The facebox entity to teach. + example: 'image_processing.facebox' + name: + description: The name of the face to teach. + example: 'my_name' + file_path: + description: The path to the image file. + example: '/images/my_image.jpg' diff --git a/homeassistant/components/familyhub/camera.py b/homeassistant/components/familyhub/camera.py index 546d95f24d12..2e4e7085927f 100644 --- a/homeassistant/components/familyhub/camera.py +++ b/homeassistant/components/familyhub/camera.py @@ -1,9 +1,10 @@ """Family Hub camera for Samsung Refrigerators.""" import logging +from pyfamilyhublocal import FamilyHubCam import voluptuous as vol -from homeassistant.components.camera import Camera, PLATFORM_SCHEMA +from homeassistant.components.camera import PLATFORM_SCHEMA, Camera from homeassistant.const import CONF_IP_ADDRESS, CONF_NAME from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv @@ -22,7 +23,6 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the Family Hub Camera.""" - from pyfamilyhublocal import FamilyHubCam address = config.get(CONF_IP_ADDRESS) name = config.get(CONF_NAME) diff --git a/homeassistant/components/fan/.translations/bg.json b/homeassistant/components/fan/.translations/bg.json new file mode 100644 index 000000000000..62452e67179f --- /dev/null +++ b/homeassistant/components/fan/.translations/bg.json @@ -0,0 +1,16 @@ +{ + "device_automation": { + "action_type": { + "turn_off": "\u0418\u0437\u043a\u043b\u044e\u0447\u0438 {entity_name}", + "turn_on": "\u0412\u043a\u043b\u044e\u0447\u0438 {entity_name}" + }, + "condtion_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" + }, + "trigger_type": { + "turned_off": "{entity_name} \u0431\u044a\u0434\u0435 \u0438\u0437\u043a\u043b\u044e\u0447\u0435\u043d", + "turned_on": "{entity_name} \u0431\u044a\u0434\u0435 \u0432\u043a\u043b\u044e\u0447\u0435\u043d" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fan/.translations/ca.json b/homeassistant/components/fan/.translations/ca.json index 1002f8d7dd6f..0530ccf5a85a 100644 --- a/homeassistant/components/fan/.translations/ca.json +++ b/homeassistant/components/fan/.translations/ca.json @@ -4,6 +4,10 @@ "turn_off": "Apaga {entity_name}", "turn_on": "Enc\u00e9n {entity_name}" }, + "condtion_type": { + "is_off": "{entity_name} est\u00e0 apagat", + "is_on": "{entity_name} est\u00e0 enc\u00e8s" + }, "trigger_type": { "turned_off": "{entity_name} s'ha apagat", "turned_on": "{entity_name} s'ha enc\u00e8s" diff --git a/homeassistant/components/fan/.translations/nl.json b/homeassistant/components/fan/.translations/nl.json new file mode 100644 index 000000000000..706c2c92b191 --- /dev/null +++ b/homeassistant/components/fan/.translations/nl.json @@ -0,0 +1,16 @@ +{ + "device_automation": { + "action_type": { + "turn_off": "Schakel {entity_name} uit", + "turn_on": "Schakel {entity_name} in" + }, + "condtion_type": { + "is_off": "{entity_name} is uitgeschakeld", + "is_on": "{entity_name} is ingeschakeld" + }, + "trigger_type": { + "turned_off": "{entity_name} uitgeschakeld", + "turned_on": "{entity_name} ingeschakeld" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fan/.translations/pl.json b/homeassistant/components/fan/.translations/pl.json new file mode 100644 index 000000000000..424794a5b64a --- /dev/null +++ b/homeassistant/components/fan/.translations/pl.json @@ -0,0 +1,16 @@ +{ + "device_automation": { + "action_type": { + "turn_off": "wy\u0142\u0105cz {entity_name}", + "turn_on": "w\u0142\u0105cz {entity_name}" + }, + "condtion_type": { + "is_off": "wentylator (entity_name} jest wy\u0142\u0105czony", + "is_on": "wentylator (entity_name} jest w\u0142\u0105czony" + }, + "trigger_type": { + "turned_off": "nast\u0105pi wy\u0142\u0105czenie {entity_name}", + "turned_on": "nast\u0105pi w\u0142\u0105czenie {entity_name}" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fan/.translations/pt.json b/homeassistant/components/fan/.translations/pt.json new file mode 100644 index 000000000000..a76550cbedde --- /dev/null +++ b/homeassistant/components/fan/.translations/pt.json @@ -0,0 +1,16 @@ +{ + "device_automation": { + "action_type": { + "turn_off": "Desligar {entity_name}", + "turn_on": "Ligar {entity_name}" + }, + "condtion_type": { + "is_off": "{entity_name} est\u00e1 desligada", + "is_on": "{entity_name} est\u00e1 ligada" + }, + "trigger_type": { + "turned_off": "{entity_name} desligou-se", + "turned_on": "{entity_name} ligou-se" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fan/__init__.py b/homeassistant/components/fan/__init__.py index 82f4d37938c7..51aecc3e7c20 100644 --- a/homeassistant/components/fan/__init__.py +++ b/homeassistant/components/fan/__init__.py @@ -11,8 +11,7 @@ from homeassistant.loader import bind_hass from homeassistant.helpers.entity import ToggleEntity from homeassistant.helpers.entity_component import EntityComponent -from homeassistant.helpers.config_validation import ( # noqa - ENTITY_SERVICE_SCHEMA, +from homeassistant.helpers.config_validation import ( # noqa: F401 PLATFORM_SCHEMA, PLATFORM_SCHEMA_BASE, ) @@ -57,20 +56,6 @@ "current_direction": ATTR_DIRECTION, } -FAN_SET_SPEED_SCHEMA = ENTITY_SERVICE_SCHEMA.extend( - {vol.Required(ATTR_SPEED): cv.string} -) - -FAN_TURN_ON_SCHEMA = ENTITY_SERVICE_SCHEMA.extend({vol.Optional(ATTR_SPEED): cv.string}) - -FAN_OSCILLATE_SCHEMA = ENTITY_SERVICE_SCHEMA.extend( - {vol.Required(ATTR_OSCILLATING): cv.boolean} -) - -FAN_SET_DIRECTION_SCHEMA = ENTITY_SERVICE_SCHEMA.extend( - {vol.Optional(ATTR_DIRECTION): cv.string} -) - @bind_hass def is_on(hass, entity_id: Optional[str] = None) -> bool: @@ -89,22 +74,22 @@ async def async_setup(hass, config: dict): await component.async_setup(config) component.async_register_entity_service( - SERVICE_TURN_ON, FAN_TURN_ON_SCHEMA, "async_turn_on" - ) - component.async_register_entity_service( - SERVICE_TURN_OFF, ENTITY_SERVICE_SCHEMA, "async_turn_off" - ) - component.async_register_entity_service( - SERVICE_TOGGLE, ENTITY_SERVICE_SCHEMA, "async_toggle" + SERVICE_TURN_ON, {vol.Optional(ATTR_SPEED): cv.string}, "async_turn_on" ) + component.async_register_entity_service(SERVICE_TURN_OFF, {}, "async_turn_off") + component.async_register_entity_service(SERVICE_TOGGLE, {}, "async_toggle") component.async_register_entity_service( - SERVICE_SET_SPEED, FAN_SET_SPEED_SCHEMA, "async_set_speed" + SERVICE_SET_SPEED, {vol.Required(ATTR_SPEED): cv.string}, "async_set_speed" ) component.async_register_entity_service( - SERVICE_OSCILLATE, FAN_OSCILLATE_SCHEMA, "async_oscillate" + SERVICE_OSCILLATE, + {vol.Required(ATTR_OSCILLATING): cv.boolean}, + "async_oscillate", ) component.async_register_entity_service( - SERVICE_SET_DIRECTION, FAN_SET_DIRECTION_SCHEMA, "async_set_direction" + SERVICE_SET_DIRECTION, + {vol.Optional(ATTR_DIRECTION): cv.string}, + "async_set_direction", ) return True diff --git a/homeassistant/components/fan/services.yaml b/homeassistant/components/fan/services.yaml index 0e3978690e69..ee478950095b 100644 --- a/homeassistant/components/fan/services.yaml +++ b/homeassistant/components/fan/services.yaml @@ -53,161 +53,3 @@ set_direction: direction: description: The direction to rotate. Either 'forward' or 'reverse' example: 'forward' - -xiaomi_miio_set_buzzer_on: - description: Turn the buzzer on. - fields: - entity_id: - description: Name of the xiaomi miio entity. - example: 'fan.xiaomi_miio_device' - -xiaomi_miio_set_buzzer_off: - description: Turn the buzzer off. - fields: - entity_id: - description: Name of the xiaomi miio entity. - example: 'fan.xiaomi_miio_device' - -xiaomi_miio_set_led_on: - description: Turn the led on. - fields: - entity_id: - description: Name of the xiaomi miio entity. - example: 'fan.xiaomi_miio_device' - -xiaomi_miio_set_led_off: - description: Turn the led off. - fields: - entity_id: - description: Name of the xiaomi miio entity. - example: 'fan.xiaomi_miio_device' - -xiaomi_miio_set_child_lock_on: - description: Turn the child lock on. - fields: - entity_id: - description: Name of the xiaomi miio entity. - example: 'fan.xiaomi_miio_device' - -xiaomi_miio_set_child_lock_off: - description: Turn the child lock off. - fields: - entity_id: - description: Name of the xiaomi miio entity. - example: 'fan.xiaomi_miio_device' - -xiaomi_miio_set_favorite_level: - description: Set the favorite level. - fields: - entity_id: - description: Name of the xiaomi miio entity. - example: 'fan.xiaomi_miio_device' - level: - description: Level, between 0 and 16. - example: 1 - -xiaomi_miio_set_led_brightness: - description: Set the led brightness. - fields: - entity_id: - description: Name of the xiaomi miio entity. - example: 'fan.xiaomi_miio_device' - brightness: - description: Brightness (0 = Bright, 1 = Dim, 2 = Off) - example: 1 - -xiaomi_miio_set_auto_detect_on: - description: Turn the auto detect on. - fields: - entity_id: - description: Name of the xiaomi miio entity. - example: 'fan.xiaomi_miio_device' - -xiaomi_miio_set_auto_detect_off: - description: Turn the auto detect off. - fields: - entity_id: - description: Name of the xiaomi miio entity. - example: 'fan.xiaomi_miio_device' - -xiaomi_miio_set_learn_mode_on: - description: Turn the learn mode on. - fields: - entity_id: - description: Name of the xiaomi miio entity. - example: 'fan.xiaomi_miio_device' - -xiaomi_miio_set_learn_mode_off: - description: Turn the learn mode off. - fields: - entity_id: - description: Name of the xiaomi miio entity. - example: 'fan.xiaomi_miio_device' - -xiaomi_miio_set_volume: - description: Set the sound volume. - fields: - entity_id: - description: Name of the xiaomi miio entity. - example: 'fan.xiaomi_miio_device' - volume: - description: Volume, between 0 and 100. - example: 50 - -xiaomi_miio_reset_filter: - description: Reset the filter lifetime and usage. - fields: - entity_id: - description: Name of the xiaomi miio entity. - example: 'fan.xiaomi_miio_device' - -xiaomi_miio_set_extra_features: - description: Manipulates a storage register which advertises extra features. The Mi Home app evaluates the value. A feature called "turbo mode" is unlocked in the app on value 1. - fields: - entity_id: - description: Name of the xiaomi miio entity. - example: 'fan.xiaomi_miio_device' - features: - description: Integer, known values are 0 (default) and 1 (turbo mode). - example: 1 - -xiaomi_miio_set_target_humidity: - description: Set the target humidity. - fields: - entity_id: - description: Name of the xiaomi miio entity. - example: 'fan.xiaomi_miio_device' - humidity: - description: Target humidity. Allowed values are 30, 40, 50, 60, 70 and 80. - example: 50 - -xiaomi_miio_set_dry_on: - description: Turn the dry mode on. - fields: - entity_id: - description: Name of the xiaomi miio entity. - example: 'fan.xiaomi_miio_device' - -xiaomi_miio_set_dry_off: - description: Turn the dry mode off. - fields: - entity_id: - description: Name of the xiaomi miio entity. - example: 'fan.xiaomi_miio_device' - -wemo_set_humidity: - description: Set the target humidity of WeMo humidifier devices. - fields: - entity_id: - description: Names of the WeMo humidifier entities (1 or more entity_ids are required). - example: 'fan.wemo_humidifier' - target_humidity: - description: Target humidity. This is a float value between 0 and 100, but will be mapped to the humidity levels that WeMo humidifiers support (45, 50, 55, 60, and 100/Max) by rounding the value down to the nearest supported value. - example: 56.5 - -wemo_reset_filter_life: - description: Reset the WeMo Humidifier's filter life to 100%. - fields: - entity_id: - description: Names of the WeMo humidifier entities (1 or more entity_ids are required). - example: 'fan.wemo_humidifier' diff --git a/homeassistant/components/fastdotcom/__init__.py b/homeassistant/components/fastdotcom/__init__.py index b070eef03101..e0a4782493e4 100644 --- a/homeassistant/components/fastdotcom/__init__.py +++ b/homeassistant/components/fastdotcom/__init__.py @@ -1,11 +1,12 @@ """Support for testing internet speed via Fast.com.""" -import logging from datetime import timedelta +import logging +from fastdotcom import fast_com import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.const import CONF_SCAN_INTERVAL +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.discovery import async_load_platform from homeassistant.helpers.dispatcher import dispatcher_send from homeassistant.helpers.event import async_track_time_interval @@ -63,7 +64,6 @@ def __init__(self, hass): def update(self, now=None): """Get the latest data from fast.com.""" - from fastdotcom import fast_com _LOGGER.debug("Executing fast.com speedtest") self.data = {"download": fast_com()} diff --git a/homeassistant/components/fastdotcom/manifest.json b/homeassistant/components/fastdotcom/manifest.json index 3655ce22ba70..2e47248d778f 100644 --- a/homeassistant/components/fastdotcom/manifest.json +++ b/homeassistant/components/fastdotcom/manifest.json @@ -6,5 +6,7 @@ "fastdotcom==0.0.3" ], "dependencies": [], - "codeowners": [] + "codeowners": [ + "@rohankapoorcom" + ] } diff --git a/homeassistant/components/ffmpeg_motion/binary_sensor.py b/homeassistant/components/ffmpeg_motion/binary_sensor.py index 235a9e4b009f..54f3981f48a0 100644 --- a/homeassistant/components/ffmpeg_motion/binary_sensor.py +++ b/homeassistant/components/ffmpeg_motion/binary_sensor.py @@ -1,6 +1,7 @@ """Provides a binary sensor which is a collection of ffmpeg tools.""" import logging +import haffmpeg.sensor as ffmpeg_sensor import voluptuous as vol from homeassistant.core import callback @@ -87,10 +88,11 @@ class FFmpegMotion(FFmpegBinarySensor): def __init__(self, hass, manager, config): """Initialize FFmpeg motion binary sensor.""" - from haffmpeg.sensor import SensorMotion super().__init__(config) - self.ffmpeg = SensorMotion(manager.binary, hass.loop, self._async_callback) + self.ffmpeg = ffmpeg_sensor.SensorMotion( + manager.binary, hass.loop, self._async_callback + ) async def _async_start_ffmpeg(self, entity_ids): """Start a FFmpeg instance. diff --git a/homeassistant/components/ffmpeg_noise/binary_sensor.py b/homeassistant/components/ffmpeg_noise/binary_sensor.py index 00e5dbb682f0..7c5f8656410f 100644 --- a/homeassistant/components/ffmpeg_noise/binary_sensor.py +++ b/homeassistant/components/ffmpeg_noise/binary_sensor.py @@ -1,6 +1,7 @@ """Provides a binary sensor which is a collection of ffmpeg tools.""" import logging +import haffmpeg.sensor as ffmpeg_sensor import voluptuous as vol import homeassistant.helpers.config_validation as cv @@ -54,10 +55,11 @@ class FFmpegNoise(FFmpegBinarySensor): def __init__(self, hass, manager, config): """Initialize FFmpeg noise binary sensor.""" - from haffmpeg.sensor import SensorNoise super().__init__(config) - self.ffmpeg = SensorNoise(manager.binary, hass.loop, self._async_callback) + self.ffmpeg = ffmpeg_sensor.SensorNoise( + manager.binary, hass.loop, self._async_callback + ) async def _async_start_ffmpeg(self, entity_ids): """Start a FFmpeg instance. diff --git a/homeassistant/components/fibaro/__init__.py b/homeassistant/components/fibaro/__init__.py index f500b3866430..d44819e758b5 100644 --- a/homeassistant/components/fibaro/__init__.py +++ b/homeassistant/components/fibaro/__init__.py @@ -1,7 +1,9 @@ """Support for the Fibaro devices.""" -import logging from collections import defaultdict +import logging from typing import Optional + +from fiblary3.client.v4.client import Client as FibaroClient, StateHandler import voluptuous as vol from homeassistant.const import ( @@ -109,7 +111,6 @@ class FibaroController: def __init__(self, config): """Initialize the Fibaro controller.""" - from fiblary3.client.v4.client import Client as FibaroClient self._client = FibaroClient( config[CONF_URL], config[CONF_USERNAME], config[CONF_PASSWORD] @@ -148,8 +149,6 @@ def connect(self): def enable_state_handler(self): """Start StateHandler thread for monitoring updates.""" - from fiblary3.client.v4.client import StateHandler - self._state_handler = StateHandler(self._client, self._on_state_change) def disable_state_handler(self): diff --git a/homeassistant/components/fints/sensor.py b/homeassistant/components/fints/sensor.py index 376ea2c0f9d4..d81f353c222f 100644 --- a/homeassistant/components/fints/sensor.py +++ b/homeassistant/components/fints/sensor.py @@ -3,10 +3,13 @@ from collections import namedtuple from datetime import timedelta import logging + +from fints.client import FinTS3PinTanClient +from fints.dialog import FinTSDialogError import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import CONF_USERNAME, CONF_PIN, CONF_URL, CONF_NAME +from homeassistant.const import CONF_NAME, CONF_PIN, CONF_URL, CONF_USERNAME import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity @@ -118,7 +121,6 @@ def client(self): the client objects. If that ever changes, consider caching the client object and also think about potential concurrency problems. """ - from fints.client import FinTS3PinTanClient return FinTS3PinTanClient( self._credentials.blz, @@ -129,7 +131,6 @@ def client(self): def detect_accounts(self): """Identify the accounts of the bank.""" - from fints.dialog import FinTSDialogError balance_accounts = [] holdings_accounts = [] diff --git a/homeassistant/components/fixer/sensor.py b/homeassistant/components/fixer/sensor.py index a97f77138dbb..e3dfd432a416 100644 --- a/homeassistant/components/fixer/sensor.py +++ b/homeassistant/components/fixer/sensor.py @@ -2,6 +2,8 @@ from datetime import timedelta import logging +from fixerio import Fixerio +from fixerio.exceptions import FixerioException import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -35,7 +37,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Fixer.io sensor.""" - from fixerio import Fixerio, exceptions api_key = config.get(CONF_API_KEY) name = config.get(CONF_NAME) @@ -43,7 +44,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): try: Fixerio(symbols=[target], access_key=api_key).latest() - except exceptions.FixerioException: + except FixerioException: _LOGGER.error("One of the given currencies is not supported") return @@ -102,7 +103,6 @@ class ExchangeData: def __init__(self, target_currency, api_key): """Initialize the data object.""" - from fixerio import Fixerio self.api_key = api_key self.rate = None diff --git a/homeassistant/components/fleetgo/device_tracker.py b/homeassistant/components/fleetgo/device_tracker.py index 0561530345c7..5a922ed4b924 100644 --- a/homeassistant/components/fleetgo/device_tracker.py +++ b/homeassistant/components/fleetgo/device_tracker.py @@ -2,11 +2,12 @@ import logging import requests +from ritassist import API import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.device_tracker import PLATFORM_SCHEMA -from homeassistant.const import CONF_USERNAME, CONF_PASSWORD +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.event import track_utc_time_change _LOGGER = logging.getLogger(__name__) @@ -40,7 +41,6 @@ class FleetGoDeviceScanner: def __init__(self, config, see): """Initialize FleetGoDeviceScanner.""" - from ritassist import API self._include = config.get(CONF_INCLUDE) self._see = see diff --git a/homeassistant/components/flexit/climate.py b/homeassistant/components/flexit/climate.py index 951033849b6e..34ddd9a8ffa6 100644 --- a/homeassistant/components/flexit/climate.py +++ b/homeassistant/components/flexit/climate.py @@ -13,26 +13,28 @@ """ import logging from typing import List + +from pyflexit.pyflexit import pyflexit import voluptuous as vol -from homeassistant.const import ( - CONF_NAME, - CONF_SLAVE, - TEMP_CELSIUS, - ATTR_TEMPERATURE, - DEVICE_DEFAULT_NAME, -) -from homeassistant.components.climate import ClimateDevice, PLATFORM_SCHEMA +from homeassistant.components.climate import PLATFORM_SCHEMA, ClimateDevice from homeassistant.components.climate.const import ( - SUPPORT_TARGET_TEMPERATURE, - SUPPORT_FAN_MODE, HVAC_MODE_COOL, + SUPPORT_FAN_MODE, + SUPPORT_TARGET_TEMPERATURE, ) from homeassistant.components.modbus import ( CONF_HUB, DEFAULT_HUB, DOMAIN as MODBUS_DOMAIN, ) +from homeassistant.const import ( + ATTR_TEMPERATURE, + CONF_NAME, + CONF_SLAVE, + DEVICE_DEFAULT_NAME, + TEMP_CELSIUS, +) import homeassistant.helpers.config_validation as cv PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( @@ -61,8 +63,6 @@ class Flexit(ClimateDevice): def __init__(self, hub, modbus_slave, name): """Initialize the unit.""" - from pyflexit import pyflexit - self._hub = hub self._name = name self._slave = modbus_slave @@ -79,7 +79,7 @@ def __init__(self, hub, modbus_slave, name): self._heating = None self._cooling = None self._alarm = False - self.unit = pyflexit.pyflexit(hub, modbus_slave) + self.unit = pyflexit(hub, modbus_slave) @property def supported_features(self): diff --git a/homeassistant/components/flume/__init__.py b/homeassistant/components/flume/__init__.py new file mode 100644 index 000000000000..ab626e1f1560 --- /dev/null +++ b/homeassistant/components/flume/__init__.py @@ -0,0 +1 @@ +"""The Flume component.""" diff --git a/homeassistant/components/flume/manifest.json b/homeassistant/components/flume/manifest.json new file mode 100644 index 000000000000..800751e80ef2 --- /dev/null +++ b/homeassistant/components/flume/manifest.json @@ -0,0 +1,11 @@ +{ + "domain": "flume", + "name": "Flume", + "documentation": "https://www.home-assistant.io/integrations/flume/", + "requirements": [ + "pyflume==0.2.1" + ], + "dependencies": [], + "codeowners": ["@ChrisMandich"] + } + \ No newline at end of file diff --git a/homeassistant/components/flume/sensor.py b/homeassistant/components/flume/sensor.py new file mode 100644 index 000000000000..5fee408e0dcc --- /dev/null +++ b/homeassistant/components/flume/sensor.py @@ -0,0 +1,90 @@ +"""Sensor for displaying the number of result from Flume.""" +from datetime import timedelta +import logging + +from pyflume import FlumeData, FlumeDeviceList +import voluptuous as vol + +from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import CONF_NAME, CONF_PASSWORD, CONF_USERNAME +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity + +LOGGER = logging.getLogger(__name__) + +DEFAULT_NAME = "Flume Sensor" + +CONF_CLIENT_ID = "client_id" +CONF_CLIENT_SECRET = "client_secret" +FLUME_TYPE_SENSOR = 2 + +SCAN_INTERVAL = timedelta(minutes=1) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Required(CONF_CLIENT_ID): cv.string, + vol.Required(CONF_CLIENT_SECRET): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + } +) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the Flume sensor.""" + username = config[CONF_USERNAME] + password = config[CONF_PASSWORD] + client_id = config[CONF_CLIENT_ID] + client_secret = config[CONF_CLIENT_SECRET] + time_zone = str(hass.config.time_zone) + name = config[CONF_NAME] + flume_entity_list = [] + + flume_devices = FlumeDeviceList(username, password, client_id, client_secret) + + for device in flume_devices.device_list: + if device["type"] == FLUME_TYPE_SENSOR: + flume = FlumeData( + username, + password, + client_id, + client_secret, + device["id"], + time_zone, + SCAN_INTERVAL, + ) + flume_entity_list.append(FlumeSensor(flume, f"{name} {device['id']}")) + + if flume_entity_list: + add_entities(flume_entity_list, True) + + +class FlumeSensor(Entity): + """Representation of the Flume sensor.""" + + def __init__(self, flume, name): + """Initialize the Flume sensor.""" + self.flume = flume + self._name = name + self._state = None + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @property + def state(self): + """Return the state of the sensor.""" + return self._state + + @property + def unit_of_measurement(self): + """Return the unit the value is expressed in.""" + return "gal" + + def update(self): + """Get the latest data and updates the states.""" + self.flume.update() + self._state = self.flume.value diff --git a/homeassistant/components/flunearyou/sensor.py b/homeassistant/components/flunearyou/sensor.py index 0df61fd24e11..86a97cce8c74 100644 --- a/homeassistant/components/flunearyou/sensor.py +++ b/homeassistant/components/flunearyou/sensor.py @@ -1,19 +1,21 @@ """Support for user- and CDC-based flu info sensors from Flu Near You.""" -import logging from datetime import timedelta +import logging +from pyflunearyou import Client +from pyflunearyou.errors import FluNearYouError import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( ATTR_ATTRIBUTION, ATTR_STATE, CONF_LATITUDE, - CONF_MONITORED_CONDITIONS, CONF_LONGITUDE, + CONF_MONITORED_CONDITIONS, ) from homeassistant.helpers import aiohttp_client +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle @@ -80,8 +82,6 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Configure the platform and add the sensors.""" - from pyflunearyou import Client - websession = aiohttp_client.async_get_clientsession(hass) latitude = config.get(CONF_LATITUDE, hass.config.latitude) @@ -219,8 +219,6 @@ def __init__(self, client, latitude, longitude, sensor_types): @Throttle(MIN_TIME_BETWEEN_UPDATES) async def async_update(self): """Update Flu Near You data.""" - from pyflunearyou.errors import FluNearYouError - for key, method in [ (CATEGORY_CDC_REPORT, self._client.cdc_reports.status_by_coordinates), (CATEGORY_USER_REPORT, self._client.user_reports.status_by_coordinates), diff --git a/homeassistant/components/folder_watcher/__init__.py b/homeassistant/components/folder_watcher/__init__.py index b328744aaba7..d99e4928cc5d 100644 --- a/homeassistant/components/folder_watcher/__init__.py +++ b/homeassistant/components/folder_watcher/__init__.py @@ -3,6 +3,8 @@ import os import voluptuous as vol +from watchdog.events import PatternMatchingEventHandler +from watchdog.observers import Observer from homeassistant.const import EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP import homeassistant.helpers.config_validation as cv @@ -50,7 +52,6 @@ def setup(hass, config): def create_event_handler(patterns, hass): """Return the Watchdog EventHandler object.""" - from watchdog.events import PatternMatchingEventHandler class EventHandler(PatternMatchingEventHandler): """Class for handling Watcher events.""" @@ -99,8 +100,6 @@ class Watcher: def __init__(self, path, patterns, hass): """Initialise the watchdog observer.""" - from watchdog.observers import Observer - self._observer = Observer() self._observer.schedule( create_event_handler(patterns, hass), path, recursive=True diff --git a/homeassistant/components/foobot/sensor.py b/homeassistant/components/foobot/sensor.py index 8d3cf6de27de..efb74e2cc9a1 100644 --- a/homeassistant/components/foobot/sensor.py +++ b/homeassistant/components/foobot/sensor.py @@ -1,26 +1,26 @@ """Support for the Foobot indoor air quality monitor.""" import asyncio -import logging from datetime import timedelta +import logging import aiohttp +from foobot_async import FoobotClient import voluptuous as vol -import homeassistant.helpers.config_validation as cv -from homeassistant.exceptions import PlatformNotReady from homeassistant.const import ( - ATTR_TIME, ATTR_TEMPERATURE, + ATTR_TIME, CONF_TOKEN, CONF_USERNAME, TEMP_CELSIUS, ) +from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.config_validation import PLATFORM_SCHEMA from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle - _LOGGER = logging.getLogger(__name__) ATTR_HUMIDITY = "humidity" @@ -51,8 +51,6 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the devices associated with the account.""" - from foobot_async import FoobotClient - token = config.get(CONF_TOKEN) username = config.get(CONF_USERNAME) diff --git a/homeassistant/components/fortigate/__init__.py b/homeassistant/components/fortigate/__init__.py index d1f6eb52333d..6de55ae3d657 100644 --- a/homeassistant/components/fortigate/__init__.py +++ b/homeassistant/components/fortigate/__init__.py @@ -1,12 +1,13 @@ """Fortigate integration.""" import logging +from pyFGT.fortigate import FGTConnectionError, FortiGate import voluptuous as vol from homeassistant.const import ( + CONF_API_KEY, CONF_DEVICES, CONF_HOST, - CONF_API_KEY, CONF_USERNAME, EVENT_HOMEASSISTANT_STOP, ) @@ -52,8 +53,6 @@ async def async_setup(hass, config): async def async_setup_fortigate(hass, config, host, user, api_key, devices): """Start up the Fortigate component platforms.""" - from pyFGT.fortigate import FGTConnectionError, FortiGate - fgt = FortiGate(host, user, apikey=api_key, disable_request_warnings=True) try: diff --git a/homeassistant/components/free_mobile/notify.py b/homeassistant/components/free_mobile/notify.py index 5733e3c19c03..8b5273f39d11 100644 --- a/homeassistant/components/free_mobile/notify.py +++ b/homeassistant/components/free_mobile/notify.py @@ -1,13 +1,13 @@ -"""Support for thr Free Mobile SMS platform.""" +"""Support for Free Mobile SMS platform.""" import logging +from freesms import FreeClient import voluptuous as vol +from homeassistant.components.notify import PLATFORM_SCHEMA, BaseNotificationService from homeassistant.const import CONF_ACCESS_TOKEN, CONF_USERNAME import homeassistant.helpers.config_validation as cv -from homeassistant.components.notify import PLATFORM_SCHEMA, BaseNotificationService - _LOGGER = logging.getLogger(__name__) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( @@ -25,8 +25,6 @@ class FreeSMSNotificationService(BaseNotificationService): def __init__(self, username, access_token): """Initialize the service.""" - from freesms import FreeClient - self.free_client = FreeClient(username, access_token) def send_message(self, message="", **kwargs): diff --git a/homeassistant/components/freebox/__init__.py b/homeassistant/components/freebox/__init__.py index 0bffedd46dc3..64c59c3ef2a8 100644 --- a/homeassistant/components/freebox/__init__.py +++ b/homeassistant/components/freebox/__init__.py @@ -2,6 +2,8 @@ import logging import socket +from aiofreepybox import Freepybox +from aiofreepybox.exceptions import HttpRequestError import voluptuous as vol from homeassistant.components.discovery import SERVICE_FREEBOX @@ -49,8 +51,6 @@ async def discovery_dispatch(service, discovery_info): async def async_setup_freebox(hass, config, host, port): """Start up the Freebox component platforms.""" - from aiofreepybox import Freepybox - from aiofreepybox.exceptions import HttpRequestError app_desc = { "app_id": "hass", diff --git a/homeassistant/components/fritzbox_callmonitor/sensor.py b/homeassistant/components/fritzbox_callmonitor/sensor.py index b1d601ce3822..600420db8591 100644 --- a/homeassistant/components/fritzbox_callmonitor/sensor.py +++ b/homeassistant/components/fritzbox_callmonitor/sensor.py @@ -60,6 +60,12 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up Fritz!Box call monitor sensor platform.""" name = config.get(CONF_NAME) host = config.get(CONF_HOST) + # Try to resolve a hostname; if it is already an IP, it will be returned as-is + try: + host = socket.gethostbyname(host) + except socket.error: + _LOGGER.error("Could not resolve hostname %s", host) + return port = config.get(CONF_PORT) username = config.get(CONF_USERNAME) password = config.get(CONF_PASSWORD) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index b196239dfb1f..75d02baaeeb2 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -3,7 +3,7 @@ "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", "requirements": [ - "home-assistant-frontend==20191119.6" + "home-assistant-frontend==20191204.1" ], "dependencies": [ "api", diff --git a/homeassistant/components/gearbest/sensor.py b/homeassistant/components/gearbest/sensor.py index bad9b335e738..b9b2a35b89d8 100644 --- a/homeassistant/components/gearbest/sensor.py +++ b/homeassistant/components/gearbest/sensor.py @@ -1,15 +1,16 @@ """Parse prices of an item from gearbest.""" -import logging from datetime import timedelta +import logging +from gearbest_parser import CurrencyConverter, GearbestParser import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import CONF_CURRENCY, CONF_ID, CONF_NAME, CONF_URL import homeassistant.helpers.config_validation as cv -from homeassistant.util import Throttle from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import track_time_interval -from homeassistant.const import CONF_NAME, CONF_ID, CONF_URL, CONF_CURRENCY +from homeassistant.util import Throttle _LOGGER = logging.getLogger(__name__) @@ -41,7 +42,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Gearbest sensor.""" - from gearbest_parser import CurrencyConverter currency = config.get(CONF_CURRENCY) @@ -71,7 +71,6 @@ class GearbestSensor(Entity): def __init__(self, converter, item, currency): """Initialize the sensor.""" - from gearbest_parser import GearbestParser self._name = item.get(CONF_NAME) self._parser = GearbestParser() diff --git a/homeassistant/components/geizhals/sensor.py b/homeassistant/components/geizhals/sensor.py index 28fe10ec5f58..f04e943964c7 100644 --- a/homeassistant/components/geizhals/sensor.py +++ b/homeassistant/components/geizhals/sensor.py @@ -2,6 +2,7 @@ import logging from datetime import timedelta +from geizhals import Device, Geizhals import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -45,7 +46,6 @@ class Geizwatch(Entity): def __init__(self, name, description, product_id, domain): """Initialize the sensor.""" - from geizhals import Device, Geizhals # internal self._name = name diff --git a/homeassistant/components/generic_thermostat/climate.py b/homeassistant/components/generic_thermostat/climate.py index b765dbbfda42..5cb4c21c5779 100644 --- a/homeassistant/components/generic_thermostat/climate.py +++ b/homeassistant/components/generic_thermostat/climate.py @@ -298,16 +298,12 @@ def hvac_modes(self): @property def preset_mode(self): """Return the current preset mode, e.g., home, away, temp.""" - if self._is_away: - return PRESET_AWAY - return None + return PRESET_AWAY if self._is_away else PRESET_NONE @property def preset_modes(self): - """Return a list of available preset modes.""" - if self._away_temp: - return [PRESET_NONE, PRESET_AWAY] - return None + """Return a list of available preset modes or PRESET_NONE if _away_temp is undefined.""" + return [PRESET_NONE, PRESET_AWAY] if self._away_temp else PRESET_NONE async def async_set_hvac_mode(self, hvac_mode): """Set hvac mode.""" diff --git a/homeassistant/components/geo_json_events/geo_location.py b/homeassistant/components/geo_json_events/geo_location.py index 2bf309e24501..2f881232495d 100644 --- a/homeassistant/components/geo_json_events/geo_location.py +++ b/homeassistant/components/geo_json_events/geo_location.py @@ -3,6 +3,7 @@ import logging from typing import Optional +from geojson_client.generic_feed import GenericFeedManager import voluptuous as vol from homeassistant.components.geo_location import PLATFORM_SCHEMA, GeolocationEvent @@ -71,7 +72,6 @@ def __init__( self, hass, add_entities, scan_interval, coordinates, url, radius_in_km ): """Initialize the GeoJSON Feed Manager.""" - from geojson_client.generic_feed import GenericFeedManager self._hass = hass self._feed_manager = GenericFeedManager( diff --git a/homeassistant/components/geo_location/__init__.py b/homeassistant/components/geo_location/__init__.py index 3c270f2c5217..e5c587f93ede 100644 --- a/homeassistant/components/geo_location/__init__.py +++ b/homeassistant/components/geo_location/__init__.py @@ -4,7 +4,7 @@ from typing import Optional from homeassistant.const import ATTR_LATITUDE, ATTR_LONGITUDE -from homeassistant.helpers.config_validation import ( # noqa +from homeassistant.helpers.config_validation import ( # noqa: F401 PLATFORM_SCHEMA, PLATFORM_SCHEMA_BASE, ) diff --git a/homeassistant/components/geo_rss_events/manifest.json b/homeassistant/components/geo_rss_events/manifest.json index c681807ad011..b8949286dea6 100644 --- a/homeassistant/components/geo_rss_events/manifest.json +++ b/homeassistant/components/geo_rss_events/manifest.json @@ -3,7 +3,7 @@ "name": "Geo RSS events", "documentation": "https://www.home-assistant.io/integrations/geo_rss_events", "requirements": [ - "georss_generic_client==0.2" + "georss_generic_client==0.3" ], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/geonetnz_quakes/.translations/pl.json b/homeassistant/components/geonetnz_quakes/.translations/pl.json index 427c753f6c1a..fd82bba43b57 100644 --- a/homeassistant/components/geonetnz_quakes/.translations/pl.json +++ b/homeassistant/components/geonetnz_quakes/.translations/pl.json @@ -9,7 +9,7 @@ "mmi": "MMI", "radius": "Promie\u0144" }, - "title": "Wype\u0142nij szczeg\u00f3\u0142y dotycz\u0105ce filtra." + "title": "Wprowad\u017a szczeg\u00f3\u0142owe dane filtra." } }, "title": "GeoNet NZ Quakes" diff --git a/homeassistant/components/geonetnz_quakes/.translations/ru.json b/homeassistant/components/geonetnz_quakes/.translations/ru.json index d6763d17e2d0..dddb5c47bb96 100644 --- a/homeassistant/components/geonetnz_quakes/.translations/ru.json +++ b/homeassistant/components/geonetnz_quakes/.translations/ru.json @@ -9,9 +9,9 @@ "mmi": "MMI", "radius": "\u0420\u0430\u0434\u0438\u0443\u0441" }, - "title": "GeoNet" + "title": "GeoNet NZ Quakes" } }, - "title": "\u0417\u0435\u043c\u043b\u0435\u0442\u0440\u044f\u0441\u0435\u043d\u0438\u044f \u0432 \u041d\u043e\u0432\u043e\u0439 \u0417\u0435\u043b\u0430\u043d\u0434\u0438\u0438 (GeoNet)" + "title": "GeoNet NZ Quakes" } } \ No newline at end of file diff --git a/homeassistant/components/geonetnz_quakes/.translations/zh-Hant.json b/homeassistant/components/geonetnz_quakes/.translations/zh-Hant.json index 59b4abf259a1..487ac9ea8c0d 100644 --- a/homeassistant/components/geonetnz_quakes/.translations/zh-Hant.json +++ b/homeassistant/components/geonetnz_quakes/.translations/zh-Hant.json @@ -12,6 +12,6 @@ "title": "\u586b\u5beb\u904e\u6ffe\u5668\u8cc7\u8a0a\u3002" } }, - "title": "GeoNet NZ Quakes" + "title": "\u7d10\u897f\u862d GeoNet \u5730\u9707\u9810\u8b66" } } \ No newline at end of file diff --git a/homeassistant/components/geonetnz_volcano/.translations/bg.json b/homeassistant/components/geonetnz_volcano/.translations/bg.json new file mode 100644 index 000000000000..f895d2829025 --- /dev/null +++ b/homeassistant/components/geonetnz_volcano/.translations/bg.json @@ -0,0 +1,16 @@ +{ + "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": { + "radius": "\u0420\u0430\u0434\u0438\u0443\u0441" + }, + "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 Volcano" + } +} \ No newline at end of file diff --git a/homeassistant/components/geonetnz_volcano/.translations/ca.json b/homeassistant/components/geonetnz_volcano/.translations/ca.json new file mode 100644 index 000000000000..2e595b730404 --- /dev/null +++ b/homeassistant/components/geonetnz_volcano/.translations/ca.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "identifier_exists": "Ubicaci\u00f3 ja registrada" + }, + "step": { + "user": { + "data": { + "radius": "Radi" + }, + "title": "Introdueix els detalls del filtre." + } + }, + "title": "GeoNet NZ Volcano" + } +} \ No newline at end of file diff --git a/homeassistant/components/geonetnz_volcano/.translations/de.json b/homeassistant/components/geonetnz_volcano/.translations/de.json new file mode 100644 index 000000000000..1a51f1fb4909 --- /dev/null +++ b/homeassistant/components/geonetnz_volcano/.translations/de.json @@ -0,0 +1,14 @@ +{ + "config": { + "error": { + "identifier_exists": "Standort bereits registriert" + }, + "step": { + "user": { + "data": { + "radius": "Radius" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/geonetnz_volcano/.translations/en.json b/homeassistant/components/geonetnz_volcano/.translations/en.json new file mode 100644 index 000000000000..1175597908e9 --- /dev/null +++ b/homeassistant/components/geonetnz_volcano/.translations/en.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "identifier_exists": "Location already registered" + }, + "step": { + "user": { + "data": { + "radius": "Radius" + }, + "title": "Fill in your filter details." + } + }, + "title": "GeoNet NZ Volcano" + } +} \ No newline at end of file diff --git a/homeassistant/components/geonetnz_volcano/.translations/es.json b/homeassistant/components/geonetnz_volcano/.translations/es.json new file mode 100644 index 000000000000..c6b92e830898 --- /dev/null +++ b/homeassistant/components/geonetnz_volcano/.translations/es.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "identifier_exists": "Lugar ya registrado" + }, + "step": { + "user": { + "data": { + "radius": "Radio" + }, + "title": "Complete los detalles de su filtro." + } + }, + "title": "GeoNet NZ Volc\u00e1n" + } +} \ No newline at end of file diff --git a/homeassistant/components/geonetnz_volcano/.translations/fr.json b/homeassistant/components/geonetnz_volcano/.translations/fr.json new file mode 100644 index 000000000000..c93ae906a466 --- /dev/null +++ b/homeassistant/components/geonetnz_volcano/.translations/fr.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "identifier_exists": "Emplacement d\u00e9j\u00e0 enregistr\u00e9" + }, + "step": { + "user": { + "data": { + "radius": "Rayon" + }, + "title": "Remplissez les d\u00e9tails de votre filtre." + } + }, + "title": "GeoNet NZ Volcano" + } +} \ No newline at end of file diff --git a/homeassistant/components/geonetnz_volcano/.translations/it.json b/homeassistant/components/geonetnz_volcano/.translations/it.json new file mode 100644 index 000000000000..85bfc7297ee2 --- /dev/null +++ b/homeassistant/components/geonetnz_volcano/.translations/it.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "identifier_exists": "Localit\u00e0 gi\u00e0 registrata" + }, + "step": { + "user": { + "data": { + "radius": "Raggio" + }, + "title": "Inserisci i tuoi dettagli del filtro." + } + }, + "title": "GeoNet NZ Vulcano" + } +} \ No newline at end of file diff --git a/homeassistant/components/geonetnz_volcano/.translations/lb.json b/homeassistant/components/geonetnz_volcano/.translations/lb.json new file mode 100644 index 000000000000..a7ad17e6bd57 --- /dev/null +++ b/homeassistant/components/geonetnz_volcano/.translations/lb.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "identifier_exists": "Standuert ass scho registr\u00e9iert" + }, + "step": { + "user": { + "data": { + "radius": "Radius" + }, + "title": "F\u00ebllt \u00e4r Filter D\u00e9tailer aus." + } + }, + "title": "GeoNet NZ Vulkan" + } +} \ No newline at end of file diff --git a/homeassistant/components/geonetnz_volcano/.translations/nl.json b/homeassistant/components/geonetnz_volcano/.translations/nl.json new file mode 100644 index 000000000000..73c7c1eaab35 --- /dev/null +++ b/homeassistant/components/geonetnz_volcano/.translations/nl.json @@ -0,0 +1,15 @@ +{ + "config": { + "error": { + "identifier_exists": "Locatie al geregistreerd" + }, + "step": { + "user": { + "data": { + "radius": "Straal" + }, + "title": "Vul uw filtergegevens in." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/geonetnz_volcano/.translations/no.json b/homeassistant/components/geonetnz_volcano/.translations/no.json new file mode 100644 index 000000000000..d66e0eb6d7d8 --- /dev/null +++ b/homeassistant/components/geonetnz_volcano/.translations/no.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "identifier_exists": "Beliggenhet er allerede registrert" + }, + "step": { + "user": { + "data": { + "radius": "Radius" + }, + "title": "Fyll ut filterdetaljene." + } + }, + "title": "GeoNet NZ Volcano" + } +} \ No newline at end of file diff --git a/homeassistant/components/geonetnz_volcano/.translations/pl.json b/homeassistant/components/geonetnz_volcano/.translations/pl.json new file mode 100644 index 000000000000..7d329815f3fd --- /dev/null +++ b/homeassistant/components/geonetnz_volcano/.translations/pl.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "identifier_exists": "Lokalizacja ju\u017c zarejestrowana" + }, + "step": { + "user": { + "data": { + "radius": "Promie\u0144" + }, + "title": "Wprowad\u017a szczeg\u00f3\u0142owe dane filtra." + } + }, + "title": "GeoNet NZ Volcano" + } +} \ No newline at end of file diff --git a/homeassistant/components/geonetnz_volcano/.translations/ro.json b/homeassistant/components/geonetnz_volcano/.translations/ro.json new file mode 100644 index 000000000000..4c0cd317d48b --- /dev/null +++ b/homeassistant/components/geonetnz_volcano/.translations/ro.json @@ -0,0 +1,13 @@ +{ + "config": { + "step": { + "user": { + "data": { + "radius": "Raz\u0103" + }, + "title": "Completa\u021bi detaliile filtrului." + } + }, + "title": "Vulcanul GeoNet NZ" + } +} \ No newline at end of file diff --git a/homeassistant/components/geonetnz_volcano/.translations/ru.json b/homeassistant/components/geonetnz_volcano/.translations/ru.json new file mode 100644 index 000000000000..6e7411f28b97 --- /dev/null +++ b/homeassistant/components/geonetnz_volcano/.translations/ru.json @@ -0,0 +1,16 @@ +{ + "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." + }, + "step": { + "user": { + "data": { + "radius": "\u0420\u0430\u0434\u0438\u0443\u0441" + }, + "title": "GeoNet NZ Volcano" + } + }, + "title": "GeoNet NZ Volcano" + } +} \ No newline at end of file diff --git a/homeassistant/components/geonetnz_volcano/.translations/sl.json b/homeassistant/components/geonetnz_volcano/.translations/sl.json new file mode 100644 index 000000000000..e31f473c26f2 --- /dev/null +++ b/homeassistant/components/geonetnz_volcano/.translations/sl.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "identifier_exists": "Lokacija je \u017ee registrirana" + }, + "step": { + "user": { + "data": { + "radius": "Radij" + }, + "title": "Izpolnite podrobnosti filtra." + } + }, + "title": "GeoNet NZ vulkan" + } +} \ No newline at end of file diff --git a/homeassistant/components/geonetnz_volcano/.translations/zh-Hant.json b/homeassistant/components/geonetnz_volcano/.translations/zh-Hant.json new file mode 100644 index 000000000000..0f74841fd7b1 --- /dev/null +++ b/homeassistant/components/geonetnz_volcano/.translations/zh-Hant.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "identifier_exists": "\u5ea7\u6a19\u5df2\u8a3b\u518a" + }, + "step": { + "user": { + "data": { + "radius": "\u534a\u5f91" + }, + "title": "\u586b\u5beb\u904e\u6ffe\u5668\u8cc7\u8a0a\u3002" + } + }, + "title": "\u7d10\u897f\u862d GeoNet \u706b\u5c71\u9810\u8b66" + } +} \ No newline at end of file diff --git a/homeassistant/components/geonetnz_volcano/__init__.py b/homeassistant/components/geonetnz_volcano/__init__.py new file mode 100644 index 000000000000..f0887da9c06f --- /dev/null +++ b/homeassistant/components/geonetnz_volcano/__init__.py @@ -0,0 +1,205 @@ +"""The GeoNet NZ Volcano integration.""" +import asyncio +import logging +from datetime import timedelta, datetime +from typing import Optional + +import voluptuous as vol +from aio_geojson_geonetnz_volcano import GeonetnzVolcanoFeedManager + +from homeassistant.core import callback +from homeassistant.util.unit_system import METRIC_SYSTEM +from homeassistant.config_entries import SOURCE_IMPORT +from homeassistant.const import ( + CONF_LATITUDE, + CONF_LONGITUDE, + CONF_RADIUS, + CONF_SCAN_INTERVAL, + CONF_UNIT_SYSTEM_IMPERIAL, + CONF_UNIT_SYSTEM, + LENGTH_MILES, +) +from homeassistant.helpers import config_validation as cv, aiohttp_client +from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.helpers.event import async_track_time_interval + +from .config_flow import configured_instances +from .const import ( + DEFAULT_RADIUS, + DEFAULT_SCAN_INTERVAL, + DOMAIN, + FEED, + SIGNAL_NEW_SENSOR, + SIGNAL_UPDATE_ENTITY, +) + +_LOGGER = logging.getLogger(__name__) + +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: vol.Schema( + { + vol.Optional(CONF_LATITUDE): cv.latitude, + vol.Optional(CONF_LONGITUDE): cv.longitude, + vol.Optional(CONF_RADIUS, default=DEFAULT_RADIUS): vol.Coerce(float), + vol.Optional( + CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL + ): cv.time_period, + } + ) + }, + extra=vol.ALLOW_EXTRA, +) + + +async def async_setup(hass, config): + """Set up the GeoNet NZ Volcano component.""" + if DOMAIN not in config: + return True + + conf = config[DOMAIN] + + latitude = conf.get(CONF_LATITUDE, hass.config.latitude) + longitude = conf.get(CONF_LONGITUDE, hass.config.longitude) + scan_interval = conf[CONF_SCAN_INTERVAL] + + identifier = f"{latitude}, {longitude}" + if identifier in configured_instances(hass): + return True + + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data={ + CONF_LATITUDE: latitude, + CONF_LONGITUDE: longitude, + CONF_RADIUS: conf[CONF_RADIUS], + CONF_SCAN_INTERVAL: scan_interval, + }, + ) + ) + + return True + + +async def async_setup_entry(hass, config_entry): + """Set up the GeoNet NZ Volcano component as config entry.""" + hass.data.setdefault(DOMAIN, {}) + hass.data[DOMAIN].setdefault(FEED, {}) + + radius = config_entry.data[CONF_RADIUS] + unit_system = config_entry.data[CONF_UNIT_SYSTEM] + if unit_system == CONF_UNIT_SYSTEM_IMPERIAL: + radius = METRIC_SYSTEM.length(radius, LENGTH_MILES) + # Create feed entity manager for all platforms. + manager = GeonetnzVolcanoFeedEntityManager(hass, config_entry, radius, unit_system) + hass.data[DOMAIN][FEED][config_entry.entry_id] = manager + _LOGGER.debug("Feed entity manager added for %s", config_entry.entry_id) + await manager.async_init() + return True + + +async def async_unload_entry(hass, config_entry): + """Unload an GeoNet NZ Volcano component config entry.""" + manager = hass.data[DOMAIN][FEED].pop(config_entry.entry_id) + await manager.async_stop() + await asyncio.wait( + [hass.config_entries.async_forward_entry_unload(config_entry, "sensor")] + ) + return True + + +class GeonetnzVolcanoFeedEntityManager: + """Feed Entity Manager for GeoNet NZ Volcano feed.""" + + def __init__(self, hass, config_entry, radius_in_km, unit_system): + """Initialize the Feed Entity Manager.""" + self._hass = hass + self._config_entry = config_entry + coordinates = ( + config_entry.data[CONF_LATITUDE], + config_entry.data[CONF_LONGITUDE], + ) + websession = aiohttp_client.async_get_clientsession(hass) + self._feed_manager = GeonetnzVolcanoFeedManager( + websession, + self._generate_entity, + self._update_entity, + self._remove_entity, + coordinates, + filter_radius=radius_in_km, + ) + self._config_entry_id = config_entry.entry_id + self._scan_interval = timedelta(seconds=config_entry.data[CONF_SCAN_INTERVAL]) + self._unit_system = unit_system + self._track_time_remove_callback = None + self.listeners = [] + + async def async_init(self): + """Schedule initial and regular updates based on configured time interval.""" + + self._hass.async_create_task( + self._hass.config_entries.async_forward_entry_setup( + self._config_entry, "sensor" + ) + ) + + async def update(event_time): + """Update.""" + await self.async_update() + + # Trigger updates at regular intervals. + self._track_time_remove_callback = async_track_time_interval( + self._hass, update, self._scan_interval + ) + + _LOGGER.debug("Feed entity manager initialized") + + async def async_update(self): + """Refresh data.""" + await self._feed_manager.update() + _LOGGER.debug("Feed entity manager updated") + + async def async_stop(self): + """Stop this feed entity manager from refreshing.""" + for unsub_dispatcher in self.listeners: + unsub_dispatcher() + self.listeners = [] + if self._track_time_remove_callback: + self._track_time_remove_callback() + _LOGGER.debug("Feed entity manager stopped") + + @callback + def async_event_new_entity(self): + """Return manager specific event to signal new entity.""" + return SIGNAL_NEW_SENSOR.format(self._config_entry_id) + + def get_entry(self, external_id): + """Get feed entry by external id.""" + return self._feed_manager.feed_entries.get(external_id) + + def last_update(self) -> Optional[datetime]: + """Return the last update of this feed.""" + return self._feed_manager.last_update + + def last_update_successful(self) -> Optional[datetime]: + """Return the last successful update of this feed.""" + return self._feed_manager.last_update_successful + + async def _generate_entity(self, external_id): + """Generate new entity.""" + async_dispatcher_send( + self._hass, + self.async_event_new_entity(), + self, + external_id, + self._unit_system, + ) + + async def _update_entity(self, external_id): + """Update entity.""" + async_dispatcher_send(self._hass, SIGNAL_UPDATE_ENTITY.format(external_id)) + + async def _remove_entity(self, external_id): + """Ignore removing entity.""" diff --git a/homeassistant/components/geonetnz_volcano/config_flow.py b/homeassistant/components/geonetnz_volcano/config_flow.py new file mode 100644 index 000000000000..7c079c432ddd --- /dev/null +++ b/homeassistant/components/geonetnz_volcano/config_flow.py @@ -0,0 +1,74 @@ +"""Config flow to configure the GeoNet NZ Volcano integration.""" +import logging + +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import ( + CONF_LATITUDE, + CONF_LONGITUDE, + CONF_RADIUS, + CONF_SCAN_INTERVAL, + CONF_UNIT_SYSTEM, + CONF_UNIT_SYSTEM_IMPERIAL, + CONF_UNIT_SYSTEM_METRIC, +) +from homeassistant.core import callback +from homeassistant.helpers import config_validation as cv + +from .const import DEFAULT_RADIUS, DEFAULT_SCAN_INTERVAL, DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +@callback +def configured_instances(hass): + """Return a set of configured GeoNet NZ Volcano instances.""" + return set( + f"{entry.data[CONF_LATITUDE]}, {entry.data[CONF_LONGITUDE]}" + for entry in hass.config_entries.async_entries(DOMAIN) + ) + + +class GeonetnzVolcanoFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a GeoNet NZ Volcano config flow.""" + + CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL + + async def _show_form(self, errors=None): + """Show the form to the user.""" + data_schema = vol.Schema( + {vol.Optional(CONF_RADIUS, default=DEFAULT_RADIUS): cv.positive_int} + ) + + return self.async_show_form( + step_id="user", data_schema=data_schema, errors=errors or {} + ) + + async def async_step_import(self, import_config): + """Import a config entry from configuration.yaml.""" + return await self.async_step_user(import_config) + + async def async_step_user(self, user_input=None): + """Handle the start of the config flow.""" + if not user_input: + return await self._show_form() + + latitude = user_input.get(CONF_LATITUDE, self.hass.config.latitude) + user_input[CONF_LATITUDE] = latitude + longitude = user_input.get(CONF_LONGITUDE, self.hass.config.longitude) + user_input[CONF_LONGITUDE] = longitude + + identifier = f"{user_input[CONF_LATITUDE]}, {user_input[CONF_LONGITUDE]}" + if identifier in configured_instances(self.hass): + return await self._show_form({"base": "identifier_exists"}) + + if self.hass.config.units.name == CONF_UNIT_SYSTEM_IMPERIAL: + user_input[CONF_UNIT_SYSTEM] = CONF_UNIT_SYSTEM_IMPERIAL + else: + user_input[CONF_UNIT_SYSTEM] = CONF_UNIT_SYSTEM_METRIC + + scan_interval = user_input.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL) + user_input[CONF_SCAN_INTERVAL] = scan_interval.seconds + + return self.async_create_entry(title=identifier, data=user_input) diff --git a/homeassistant/components/geonetnz_volcano/const.py b/homeassistant/components/geonetnz_volcano/const.py new file mode 100644 index 000000000000..7bc15d3a6a1c --- /dev/null +++ b/homeassistant/components/geonetnz_volcano/const.py @@ -0,0 +1,19 @@ +"""Define constants for the GeoNet NZ Volcano integration.""" +from datetime import timedelta + +DOMAIN = "geonetnz_volcano" + +FEED = "feed" + +ATTR_ACTIVITY = "activity" +ATTR_DISTANCE = "distance" +ATTR_EXTERNAL_ID = "external_id" +ATTR_HAZARDS = "hazards" + +# Icon alias "mdi:mountain" not working. +DEFAULT_ICON = "mdi:image-filter-hdr" +DEFAULT_RADIUS = 50.0 +DEFAULT_SCAN_INTERVAL = timedelta(minutes=5) + +SIGNAL_NEW_SENSOR = "geonetnz_volcano_new_sensor_{}" +SIGNAL_UPDATE_ENTITY = "geonetnz_volcano_update_{}" diff --git a/homeassistant/components/geonetnz_volcano/manifest.json b/homeassistant/components/geonetnz_volcano/manifest.json new file mode 100644 index 000000000000..a80ebdcff655 --- /dev/null +++ b/homeassistant/components/geonetnz_volcano/manifest.json @@ -0,0 +1,13 @@ +{ + "domain": "geonetnz_volcano", + "name": "GeoNet NZ Volcano", + "config_flow": true, + "documentation": "https://www.home-assistant.io/components/geonetnz_volcano", + "requirements": [ + "aio_geojson_geonetnz_volcano==0.5" + ], + "dependencies": [], + "codeowners": [ + "@exxamalte" + ] +} \ No newline at end of file diff --git a/homeassistant/components/geonetnz_volcano/sensor.py b/homeassistant/components/geonetnz_volcano/sensor.py new file mode 100644 index 000000000000..364ee416be41 --- /dev/null +++ b/homeassistant/components/geonetnz_volcano/sensor.py @@ -0,0 +1,169 @@ +"""Feed Entity Manager Sensor support for GeoNet NZ Volcano Feeds.""" +import logging +from typing import Optional + +from homeassistant.const import ( + CONF_UNIT_SYSTEM_IMPERIAL, + LENGTH_KILOMETERS, + ATTR_ATTRIBUTION, + ATTR_LONGITUDE, + ATTR_LATITUDE, +) +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity import Entity +from homeassistant.util import dt +from homeassistant.util.unit_system import IMPERIAL_SYSTEM + +from .const import ( + ATTR_ACTIVITY, + ATTR_DISTANCE, + ATTR_EXTERNAL_ID, + ATTR_HAZARDS, + DEFAULT_ICON, + DOMAIN, + FEED, + SIGNAL_UPDATE_ENTITY, +) + +_LOGGER = logging.getLogger(__name__) + +ATTR_LAST_UPDATE = "feed_last_update" +ATTR_LAST_UPDATE_SUCCESSFUL = "feed_last_update_successful" + + +async def async_setup_entry(hass, entry, async_add_entities): + """Set up the GeoNet NZ Volcano Feed platform.""" + manager = hass.data[DOMAIN][FEED][entry.entry_id] + + @callback + def async_add_sensor(feed_manager, external_id, unit_system): + """Add sensor entity from feed.""" + new_entity = GeonetnzVolcanoSensor( + entry.entry_id, feed_manager, external_id, unit_system + ) + _LOGGER.debug("Adding sensor %s", new_entity) + async_add_entities([new_entity], True) + + manager.listeners.append( + async_dispatcher_connect( + hass, manager.async_event_new_entity(), async_add_sensor + ) + ) + hass.async_create_task(manager.async_update()) + _LOGGER.debug("Sensor setup done") + + +class GeonetnzVolcanoSensor(Entity): + """This represents an external event with GeoNet NZ Volcano feed data.""" + + def __init__(self, config_entry_id, feed_manager, external_id, unit_system): + """Initialize entity with data from feed entry.""" + self._config_entry_id = config_entry_id + self._feed_manager = feed_manager + self._external_id = external_id + self._unit_system = unit_system + self._title = None + self._distance = None + self._latitude = None + self._longitude = None + self._attribution = None + self._alert_level = None + self._activity = None + self._hazards = None + self._feed_last_update = None + self._feed_last_update_successful = None + self._remove_signal_update = None + + async def async_added_to_hass(self): + """Call when entity is added to hass.""" + self._remove_signal_update = async_dispatcher_connect( + self.hass, + SIGNAL_UPDATE_ENTITY.format(self._external_id), + self._update_callback, + ) + + async def async_will_remove_from_hass(self) -> None: + """Call when entity will be removed from hass.""" + if self._remove_signal_update: + self._remove_signal_update() + + @callback + def _update_callback(self): + """Call update method.""" + self.async_schedule_update_ha_state(True) + + @property + def should_poll(self): + """No polling needed for GeoNet NZ Volcano feed location events.""" + return False + + async def async_update(self): + """Update this entity from the data held in the feed manager.""" + _LOGGER.debug("Updating %s", self._external_id) + feed_entry = self._feed_manager.get_entry(self._external_id) + last_update = self._feed_manager.last_update() + last_update_successful = self._feed_manager.last_update_successful() + if feed_entry: + self._update_from_feed(feed_entry, last_update, last_update_successful) + + def _update_from_feed(self, feed_entry, last_update, last_update_successful): + """Update the internal state from the provided feed entry.""" + self._title = feed_entry.title + # Convert distance if not metric system. + if self._unit_system == CONF_UNIT_SYSTEM_IMPERIAL: + self._distance = round( + IMPERIAL_SYSTEM.length(feed_entry.distance_to_home, LENGTH_KILOMETERS), + 1, + ) + else: + self._distance = round(feed_entry.distance_to_home, 1) + self._latitude = round(feed_entry.coordinates[0], 5) + self._longitude = round(feed_entry.coordinates[1], 5) + self._attribution = feed_entry.attribution + self._alert_level = feed_entry.alert_level + self._activity = feed_entry.activity + self._hazards = feed_entry.hazards + self._feed_last_update = dt.as_utc(last_update) if last_update else None + self._feed_last_update_successful = ( + dt.as_utc(last_update_successful) if last_update_successful else None + ) + + @property + def state(self): + """Return the state of the sensor.""" + return self._alert_level + + @property + def icon(self): + """Return the icon to use in the frontend, if any.""" + return DEFAULT_ICON + + @property + def name(self) -> Optional[str]: + """Return the name of the entity.""" + return f"Volcano {self._title}" + + @property + def unit_of_measurement(self): + """Return the unit of measurement.""" + return "alert level" + + @property + def device_state_attributes(self): + """Return the device state attributes.""" + attributes = {} + for key, value in ( + (ATTR_EXTERNAL_ID, self._external_id), + (ATTR_ATTRIBUTION, self._attribution), + (ATTR_ACTIVITY, self._activity), + (ATTR_HAZARDS, self._hazards), + (ATTR_LONGITUDE, self._longitude), + (ATTR_LATITUDE, self._latitude), + (ATTR_DISTANCE, self._distance), + (ATTR_LAST_UPDATE, self._feed_last_update), + (ATTR_LAST_UPDATE_SUCCESSFUL, self._feed_last_update_successful), + ): + if value or isinstance(value, bool): + attributes[key] = value + return attributes diff --git a/homeassistant/components/geonetnz_volcano/strings.json b/homeassistant/components/geonetnz_volcano/strings.json new file mode 100644 index 000000000000..93ec8603d03b --- /dev/null +++ b/homeassistant/components/geonetnz_volcano/strings.json @@ -0,0 +1,16 @@ +{ + "config": { + "title": "GeoNet NZ Volcano", + "step": { + "user": { + "title": "Fill in your filter details.", + "data": { + "radius": "Radius" + } + } + }, + "error": { + "identifier_exists": "Location already registered" + } + } +} diff --git a/homeassistant/components/gitter/sensor.py b/homeassistant/components/gitter/sensor.py index f124849a1938..4f1eeca7d719 100644 --- a/homeassistant/components/gitter/sensor.py +++ b/homeassistant/components/gitter/sensor.py @@ -1,6 +1,8 @@ """Support for displaying details about a Gitter.im chat room.""" import logging +from gitterpy.client import GitterClient +from gitterpy.errors import GitterRoomError, GitterTokenError import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -30,8 +32,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Gitter sensor.""" - from gitterpy.client import GitterClient - from gitterpy.errors import GitterTokenError name = config.get(CONF_NAME) api_key = config.get(CONF_API_KEY) @@ -91,7 +91,6 @@ def icon(self): def update(self): """Get the latest data and updates the state.""" - from gitterpy.errors import GitterRoomError try: data = self._data.user.unread_items(self._room) diff --git a/homeassistant/components/glances/.translations/nn.json b/homeassistant/components/glances/.translations/nn.json new file mode 100644 index 000000000000..2c9acc227bd9 --- /dev/null +++ b/homeassistant/components/glances/.translations/nn.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Glances" + } +} \ No newline at end of file diff --git a/homeassistant/components/glances/.translations/pt.json b/homeassistant/components/glances/.translations/pt.json new file mode 100644 index 000000000000..b46423599731 --- /dev/null +++ b/homeassistant/components/glances/.translations/pt.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "Palavra-passe", + "username": "Nome de Utilizador" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/gogogate2/cover.py b/homeassistant/components/gogogate2/cover.py index 26aecee25049..fcb0182ec0e4 100644 --- a/homeassistant/components/gogogate2/cover.py +++ b/homeassistant/components/gogogate2/cover.py @@ -1,15 +1,16 @@ """Support for Gogogate2 garage Doors.""" import logging +from pygogogate2 import Gogogate2API as pygogogate2 import voluptuous as vol -from homeassistant.components.cover import CoverDevice, SUPPORT_OPEN, SUPPORT_CLOSE +from homeassistant.components.cover import SUPPORT_CLOSE, SUPPORT_OPEN, CoverDevice from homeassistant.const import ( - CONF_USERNAME, - CONF_PASSWORD, - STATE_CLOSED, CONF_IP_ADDRESS, CONF_NAME, + CONF_PASSWORD, + CONF_USERNAME, + STATE_CLOSED, ) import homeassistant.helpers.config_validation as cv @@ -32,7 +33,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Gogogate2 component.""" - from pygogogate2 import Gogogate2API as pygogogate2 ip_address = config.get(CONF_IP_ADDRESS) name = config.get(CONF_NAME) diff --git a/homeassistant/components/google_assistant/__init__.py b/homeassistant/components/google_assistant/__init__.py index ebf906b6f2ac..ecb6d7678171 100644 --- a/homeassistant/components/google_assistant/__init__.py +++ b/homeassistant/components/google_assistant/__init__.py @@ -1,11 +1,7 @@ """Support for Actions on Google Assistant Smart Home Control.""" -import asyncio import logging from typing import Dict, Any -import aiohttp -import async_timeout - import voluptuous as vol # Typing imports @@ -13,7 +9,6 @@ from homeassistant.const import CONF_NAME from homeassistant.helpers import config_validation as cv -from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import ( DOMAIN, @@ -24,7 +19,6 @@ DEFAULT_EXPOSED_DOMAINS, CONF_API_KEY, SERVICE_REQUEST_SYNC, - REQUEST_SYNC_BASE_URL, CONF_ENTITY_CONFIG, CONF_EXPOSE, CONF_ALIASES, @@ -38,7 +32,7 @@ ) from .const import EVENT_COMMAND_RECEIVED, EVENT_SYNC_RECEIVED # noqa: F401 from .const import EVENT_QUERY_RECEIVED # noqa: F401 -from .http import async_register_http +from .http import GoogleAssistantView, GoogleConfig _LOGGER = logging.getLogger(__name__) @@ -99,37 +93,29 @@ def _check_report_state(data): async def async_setup(hass: HomeAssistant, yaml_config: Dict[str, Any]): """Activate Google Actions component.""" config = yaml_config.get(DOMAIN, {}) - api_key = config.get(CONF_API_KEY) - async_register_http(hass, config) + + google_config = GoogleConfig(hass, config) + await google_config.async_initialize() + + hass.http.register_view(GoogleAssistantView(google_config)) + + if google_config.should_report_state: + google_config.async_enable_report_state() async def request_sync_service_handler(call: ServiceCall): """Handle request sync service calls.""" - websession = async_get_clientsession(hass) - 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}, - json={"agent_user_id": agent_user_id}, - ) - _LOGGER.info("Submitted request_sync request to Google") - res.raise_for_status() - except aiohttp.ClientResponseError: - body = await res.read() - _LOGGER.error("request_sync request failed: %d %s", res.status, body) - except (asyncio.TimeoutError, aiohttp.ClientError): - _LOGGER.error("Could not contact Google for request_sync") - - # Register service only if api key is provided - if api_key is not None: + 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 + + await google_config.async_sync_entities(agent_user_id) + + # Register service only if key is provided + if CONF_API_KEY in config or CONF_SERVICE_ACCOUNT in config: hass.services.async_register( DOMAIN, SERVICE_REQUEST_SYNC, request_sync_service_handler ) diff --git a/homeassistant/components/google_assistant/const.py b/homeassistant/components/google_assistant/const.py index 03253e244fed..35a04e0e08ec 100644 --- a/homeassistant/components/google_assistant/const.py +++ b/homeassistant/components/google_assistant/const.py @@ -135,8 +135,11 @@ (media_player.DOMAIN, media_player.DEVICE_CLASS_TV): TYPE_TV, (media_player.DOMAIN, media_player.DEVICE_CLASS_SPEAKER): TYPE_SPEAKER, (sensor.DOMAIN, sensor.DEVICE_CLASS_TEMPERATURE): TYPE_SENSOR, + (sensor.DOMAIN, sensor.DEVICE_CLASS_HUMIDITY): TYPE_SENSOR, } CHALLENGE_ACK_NEEDED = "ackNeeded" CHALLENGE_PIN_NEEDED = "pinNeeded" CHALLENGE_FAILED_PIN_NEEDED = "challengeFailedPinNeeded" + +STORE_AGENT_USER_IDS = "agent_user_ids" diff --git a/homeassistant/components/google_assistant/helpers.py b/homeassistant/components/google_assistant/helpers.py index 96b9b93d70a4..09859c5d3d0b 100644 --- a/homeassistant/components/google_assistant/helpers.py +++ b/homeassistant/components/google_assistant/helpers.py @@ -10,6 +10,7 @@ from homeassistant.core import Context, callback, HomeAssistant, State from homeassistant.helpers.event import async_call_later from homeassistant.components import webhook +from homeassistant.helpers.storage import Store from homeassistant.const import ( CONF_NAME, STATE_UNAVAILABLE, @@ -26,6 +27,7 @@ ERR_FUNCTION_NOT_SUPPORTED, DEVICE_CLASS_TO_GOOGLE_TYPES, CONF_ROOM_HINT, + STORE_AGENT_USER_IDS, ) from .error import SmartHomeError @@ -41,19 +43,20 @@ class AbstractConfig: def __init__(self, hass): """Initialize abstract config.""" self.hass = hass - self._google_sync_unsub = None + self._store = None + self._google_sync_unsub = {} self._local_sdk_active = False + async def async_initialize(self): + """Perform async initialization of config.""" + self._store = GoogleConfigStore(self.hass) + await self._store.async_load() + @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.""" - return None - @property def entity_config(self): """Return entity config.""" @@ -77,7 +80,6 @@ def is_local_sdk_active(self): @property def should_report_state(self): """Return if states should be proactively reported.""" - # pylint: disable=no-self-use return False @property @@ -102,10 +104,18 @@ def should_2fa(self, state): # pylint: disable=no-self-use return True - async def async_report_state(self, message): + async def async_report_state(self, message, agent_user_id: str): """Send a state report to Google.""" raise NotImplementedError + async def async_report_state_all(self, message): + """Send a state report to Google for all previously synced users.""" + jobs = [ + self.async_report_state(message, agent_user_id) + for agent_user_id in self._store.agent_user_ids + ] + await gather(*jobs) + def async_enable_report_state(self): """Enable proactive mode.""" # Circular dep @@ -120,42 +130,63 @@ def async_disable_report_state(self): self._unsub_report_state() self._unsub_report_state = None - async def async_sync_entities(self): + async def async_sync_entities(self, agent_user_id: str): """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() + self._google_sync_unsub.pop(agent_user_id, lambda: None)() + return await self._async_request_sync_devices(agent_user_id) + + async def async_sync_entities_all(self): + """Sync all entities to Google for all registered agents.""" + res = await gather( + *[ + self.async_sync_entities(agent_user_id) + for agent_user_id in self._store.agent_user_ids + ] + ) + return max(res, default=204) @callback - def async_schedule_google_sync(self): + def async_schedule_google_sync(self, agent_user_id: str): """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 _schedule_callback(_now): + """Handle a scheduled sync callback.""" + self._google_sync_unsub.pop(agent_user_id, None) + await self.async_sync_entities(agent_user_id) + + self._google_sync_unsub.pop(agent_user_id, lambda: None)() + + self._google_sync_unsub[agent_user_id] = async_call_later( + self.hass, SYNC_DELAY, _schedule_callback ) - async def _async_request_sync_devices(self) -> int: + @callback + def async_schedule_google_sync_all(self): + """Schedule a sync for all registered agents.""" + for agent_user_id in self._store.agent_user_ids: + self.async_schedule_google_sync(agent_user_id) + + async def _async_request_sync_devices(self, agent_user_id: str) -> 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): + async def async_connect_agent_user(self, agent_user_id: str): + """Add an synced and known agent_user_id. + + Called when a completed sync response have been sent to Google. + """ + self._store.add_agent_user_id(agent_user_id) + + async def async_disconnect_agent_user(self, agent_user_id: str): """Turn off report state and disable further state reporting. Called when the user disconnects their account from Google. """ + self._store.pop_agent_user_id(agent_user_id) @callback def async_enable_local_sdk(self): @@ -166,7 +197,7 @@ def async_enable_local_sdk(self): return webhook.async_register( - self.hass, DOMAIN, "Local Support", webhook_id, self._handle_local_webhook + self.hass, DOMAIN, "Local Support", webhook_id, self._handle_local_webhook, ) self._local_sdk_active = True @@ -202,6 +233,44 @@ async def _handle_local_webhook(self, hass, webhook_id, request): return json_response(result) +class GoogleConfigStore: + """A configuration store for google assistant.""" + + _STORAGE_VERSION = 1 + _STORAGE_KEY = DOMAIN + + def __init__(self, hass): + """Initialize a configuration store.""" + self._hass = hass + self._store = Store(hass, self._STORAGE_VERSION, self._STORAGE_KEY) + self._data = {STORE_AGENT_USER_IDS: {}} + + @property + def agent_user_ids(self): + """Return a list of connected agent user_ids.""" + return self._data[STORE_AGENT_USER_IDS] + + @callback + def add_agent_user_id(self, agent_user_id): + """Add an agent user id to store.""" + if agent_user_id not in self._data[STORE_AGENT_USER_IDS]: + self._data[STORE_AGENT_USER_IDS][agent_user_id] = {} + self._store.async_delay_save(lambda: self._data, 1.0) + + @callback + def pop_agent_user_id(self, agent_user_id): + """Remove agent user id from store.""" + if agent_user_id in self._data[STORE_AGENT_USER_IDS]: + self._data[STORE_AGENT_USER_IDS].pop(agent_user_id, None) + self._store.async_delay_save(lambda: self._data, 1.0) + + async def async_load(self): + """Store current configuration to disk.""" + data = await self._store.async_load() + if data: + self._data = data + + class RequestData: """Hold data associated with a particular request.""" @@ -267,7 +336,7 @@ def should_expose(self): @callback def is_supported(self) -> bool: """Return if the entity is supported by Google.""" - return self.state.state != STATE_UNAVAILABLE and bool(self.traits()) + return bool(self.traits()) @callback def might_2fa(self) -> bool: @@ -281,7 +350,7 @@ def might_2fa(self) -> bool: trait.might_2fa(domain, features, device_class) for trait in self.traits() ) - async def sync_serialize(self): + async def sync_serialize(self, agent_user_id): """Serialize entity for a SYNC response. https://developers.google.com/actions/smarthome/create-app#actiondevicessync @@ -317,7 +386,7 @@ async def sync_serialize(self): "webhookId": self.config.local_sdk_webhook_id, "httpPort": self.hass.config.api.port, "httpSSL": self.hass.config.api.use_ssl, - "proxyDeviceId": self.config.agent_user_id, + "proxyDeviceId": agent_user_id, } for trt in traits: diff --git a/homeassistant/components/google_assistant/http.py b/homeassistant/components/google_assistant/http.py index 90fa1ced157a..c3d0dd493a81 100644 --- a/homeassistant/components/google_assistant/http.py +++ b/homeassistant/components/google_assistant/http.py @@ -10,7 +10,6 @@ # Typing imports from homeassistant.components.http import HomeAssistantView -from homeassistant.core import callback, ServiceCall from homeassistant.const import CLOUD_NEVER_EXPOSED_ENTITIES from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.util import dt as dt_util @@ -27,12 +26,10 @@ CONF_SERVICE_ACCOUNT, CONF_CLIENT_EMAIL, CONF_PRIVATE_KEY, - DOMAIN, HOMEGRAPH_TOKEN_URL, HOMEGRAPH_SCOPE, REPORT_STATE_BASE_URL, REQUEST_SYNC_BASE_URL, - SERVICE_REQUEST_SYNC, ) from .smart_home import async_handle_message from .helpers import AbstractConfig @@ -84,11 +81,6 @@ 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.""" - return None - @property def entity_config(self): """Return entity config.""" @@ -102,7 +94,6 @@ def secure_devices_pin(self): @property def should_report_state(self): """Return if states should be proactively reported.""" - # pylint: disable=no-self-use return self._config.get(CONF_REPORT_STATE) def should_expose(self, state) -> bool: @@ -134,6 +125,18 @@ def should_2fa(self, state): """If an entity should have 2FA checked.""" return True + async def _async_request_sync_devices(self, agent_user_id: str): + if CONF_API_KEY in self._config: + await self.async_call_homegraph_api_key( + REQUEST_SYNC_BASE_URL, {"agentUserId": agent_user_id} + ) + elif CONF_SERVICE_ACCOUNT in self._config: + await self.async_call_homegraph_api( + REQUEST_SYNC_BASE_URL, {"agentUserId": agent_user_id} + ) + else: + _LOGGER.error("No configuration for request_sync available") + async def _async_update_token(self, force=False): if CONF_SERVICE_ACCOUNT not in self._config: _LOGGER.error("Trying to get homegraph api token without service account") @@ -152,6 +155,25 @@ async def _async_update_token(self, force=False): self._access_token = token["access_token"] self._access_token_renew = now + timedelta(seconds=token["expires_in"]) + async def async_call_homegraph_api_key(self, url, data): + """Call a homegraph api with api key authentication.""" + websession = async_get_clientsession(self.hass) + try: + res = await websession.post( + url, params={"key": self._config.get(CONF_API_KEY)}, json=data + ) + _LOGGER.debug( + "Response on %s with data %s was %s", url, data, await res.text() + ) + res.raise_for_status() + return res.status + except ClientResponseError as error: + _LOGGER.error("Request for %s failed: %d", url, error.status) + return error.status + except (asyncio.TimeoutError, ClientError): + _LOGGER.error("Could not contact %s", url) + return 500 + async def async_call_homegraph_api(self, url, data): """Call a homegraph api with authenticaiton.""" session = async_get_clientsession(self.hass) @@ -166,63 +188,37 @@ async def _call(): "Response on %s with data %s was %s", url, data, await res.text() ) res.raise_for_status() + return res.status try: await self._async_update_token() try: - await _call() + return await _call() except ClientResponseError as error: if error.status == 401: _LOGGER.warning( "Request for %s unauthorized, renewing token and retrying", url ) await self._async_update_token(True) - await _call() - else: - raise + return await _call() + raise except ClientResponseError as error: _LOGGER.error("Request for %s failed: %d", url, error.status) + return error.status except (asyncio.TimeoutError, ClientError): _LOGGER.error("Could not contact %s", url) + return 500 - async def async_report_state(self, message): + async def async_report_state(self, message, agent_user_id: str): """Send a state report to Google.""" data = { "requestId": uuid4().hex, - "agentUserId": (await self.hass.auth.async_get_owner()).id, + "agentUserId": agent_user_id, "payload": message, } await self.async_call_homegraph_api(REPORT_STATE_BASE_URL, data) -@callback -def async_register_http(hass, cfg): - """Register HTTP views for Google Assistant.""" - config = GoogleConfig(hass, cfg) - hass.http.register_view(GoogleAssistantView(config)) - if config.should_report_state: - config.async_enable_report_state() - - async def request_sync_service_handler(call: ServiceCall): - """Handle request sync service calls.""" - 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 - await config.async_call_homegraph_api( - REQUEST_SYNC_BASE_URL, {"agentUserId": agent_user_id} - ) - - # Register service only if api key is provided - if CONF_API_KEY not in cfg and CONF_SERVICE_ACCOUNT in cfg: - hass.services.async_register( - DOMAIN, SERVICE_REQUEST_SYNC, request_sync_service_handler - ) - - class GoogleAssistantView(HomeAssistantView): """Handle Google Assistant requests.""" diff --git a/homeassistant/components/google_assistant/report_state.py b/homeassistant/components/google_assistant/report_state.py index aacb90e9d2bd..78a0f50e2773 100644 --- a/homeassistant/components/google_assistant/report_state.py +++ b/homeassistant/components/google_assistant/report_state.py @@ -45,7 +45,7 @@ async def async_entity_state_listener(changed_entity, old_state, new_state): if entity_data == old_entity.query_serialize(): return - await google_config.async_report_state( + await google_config.async_report_state_all( {"devices": {"states": {changed_entity: entity_data}}} ) @@ -62,7 +62,7 @@ async def inital_report(_now): except SmartHomeError: continue - await google_config.async_report_state({"devices": {"states": entities}}) + await google_config.async_report_state_all({"devices": {"states": entities}}) async_call_later(hass, INITIAL_REPORT_DELAY, inital_report) diff --git a/homeassistant/components/google_assistant/smart_home.py b/homeassistant/components/google_assistant/smart_home.py index 0944c9532eff..0e5037ce13a1 100644 --- a/homeassistant/components/google_assistant/smart_home.py +++ b/homeassistant/components/google_assistant/smart_home.py @@ -79,18 +79,19 @@ async def async_devices_sync(hass, data, payload): EVENT_SYNC_RECEIVED, {"request_id": data.request_id}, context=data.context ) + agent_user_id = data.context.user_id + devices = await asyncio.gather( *( - entity.sync_serialize() + entity.sync_serialize(agent_user_id) for entity in async_get_entities(hass, data.config) if entity.should_expose() ) ) - response = { - "agentUserId": data.config.agent_user_id or data.context.user_id, - "devices": devices, - } + response = {"agentUserId": agent_user_id, "devices": devices} + + await data.config.async_connect_agent_user(agent_user_id) return response @@ -197,7 +198,7 @@ async def async_devices_disconnect(hass, data: RequestData, payload): https://developers.google.com/assistant/smarthome/develop/process-intents#DISCONNECT """ - await data.config.async_deactivate_report_state() + await data.config.async_disconnect_agent_user(data.context.user_id) return None @@ -209,7 +210,7 @@ async def async_devices_identify(hass, data: RequestData, payload): """ return { "device": { - "id": data.config.agent_user_id, + "id": data.context.user_id, "isLocalOnly": True, "isProxy": True, "deviceInfo": { diff --git a/homeassistant/components/google_assistant/trait.py b/homeassistant/components/google_assistant/trait.py index 6b5530ab2cee..5b089459d83d 100644 --- a/homeassistant/components/google_assistant/trait.py +++ b/homeassistant/components/google_assistant/trait.py @@ -80,6 +80,7 @@ TRAIT_OPENCLOSE = PREFIX_TRAITS + "OpenClose" TRAIT_VOLUME = PREFIX_TRAITS + "Volume" TRAIT_ARMDISARM = PREFIX_TRAITS + "ArmDisarm" +TRAIT_HUMIDITY_SETTING = PREFIX_TRAITS + "HumiditySetting" PREFIX_COMMANDS = "action.devices.commands." COMMAND_ONOFF = PREFIX_COMMANDS + "OnOff" @@ -849,6 +850,56 @@ async def execute(self, command, data, params, challenge): ) +@register_trait +class HumiditySettingTrait(_Trait): + """Trait to offer humidity setting functionality. + + https://developers.google.com/actions/smarthome/traits/humiditysetting + """ + + name = TRAIT_HUMIDITY_SETTING + commands = [] + + @staticmethod + def supported(domain, features, device_class): + """Test if state is supported.""" + return domain == sensor.DOMAIN and device_class == sensor.DEVICE_CLASS_HUMIDITY + + def sync_attributes(self): + """Return humidity attributes for a sync request.""" + response = {} + attrs = self.state.attributes + domain = self.state.domain + if domain == sensor.DOMAIN: + device_class = attrs.get(ATTR_DEVICE_CLASS) + if device_class == sensor.DEVICE_CLASS_HUMIDITY: + response["queryOnlyHumiditySetting"] = True + + return response + + def query_attributes(self): + """Return humidity query attributes.""" + response = {} + attrs = self.state.attributes + domain = self.state.domain + if domain == sensor.DOMAIN: + device_class = attrs.get(ATTR_DEVICE_CLASS) + if device_class == sensor.DEVICE_CLASS_HUMIDITY: + current_humidity = self.state.state + if current_humidity is not None: + response["humidityAmbientPercent"] = round(float(current_humidity)) + + return response + + async def execute(self, command, data, params, challenge): + """Execute a humidity command.""" + domain = self.state.domain + if domain == sensor.DOMAIN: + raise SmartHomeError( + ERR_NOT_SUPPORTED, "Execute is not supported by sensor" + ) + + @register_trait class LockUnlockTrait(_Trait): """Trait to lock or unlock a lock. diff --git a/homeassistant/components/google_pubsub/__init__.py b/homeassistant/components/google_pubsub/__init__.py index e108d249797b..c4136c3b9cba 100644 --- a/homeassistant/components/google_pubsub/__init__.py +++ b/homeassistant/components/google_pubsub/__init__.py @@ -57,7 +57,9 @@ def setup(hass: HomeAssistant, yaml_config: Dict[str, Any]): service_principal_path ) - topic_path = publisher.topic_path(project_id, topic_name) # pylint: disable=E1101 + topic_path = publisher.topic_path( # pylint: disable=no-member + project_id, topic_name + ) encoder = DateTimeJSONEncoder() @@ -87,7 +89,7 @@ class DateTimeJSONEncoder(json.JSONEncoder): Additionally add encoding for datetime objects as isoformat. """ - def default(self, o): # pylint: disable=E0202 + def default(self, o): # pylint: disable=method-hidden """Implement encoding logic.""" if isinstance(o, datetime.datetime): return o.isoformat() diff --git a/homeassistant/components/gpmdp/media_player.py b/homeassistant/components/gpmdp/media_player.py index e6df8b0fe8b1..e7b18aacc15c 100644 --- a/homeassistant/components/gpmdp/media_player.py +++ b/homeassistant/components/gpmdp/media_player.py @@ -5,8 +5,9 @@ import time import voluptuous as vol +from websocket import _exceptions, create_connection -from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA +from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice from homeassistant.components.media_player.const import ( MEDIA_TYPE_MUSIC, SUPPORT_NEXT_TRACK, @@ -65,8 +66,6 @@ def request_configuration(hass, config, url, add_entities_callback): ) return - from websocket import create_connection - websocket = create_connection((url), timeout=1) websocket.send( json.dumps( @@ -81,7 +80,6 @@ def request_configuration(hass, config, url, add_entities_callback): def gpmdp_configuration_callback(callback_data): """Handle configuration changes.""" while True: - from websocket import _exceptions try: msg = json.loads(websocket.recv()) @@ -174,7 +172,6 @@ class GPMDP(MediaPlayerDevice): def __init__(self, name, url, code): """Initialize the media player.""" - from websocket import create_connection self._connection = create_connection self._url = url @@ -210,7 +207,6 @@ def get_ws(self): def send_gpmdp_msg(self, namespace, method, with_id=True): """Send ws messages to GPMDP and verify request id in response.""" - from websocket import _exceptions try: websocket = self.get_ws() diff --git a/homeassistant/components/greeneye_monitor/__init__.py b/homeassistant/components/greeneye_monitor/__init__.py index cb67ac7faa4c..4f5899f6a4a6 100644 --- a/homeassistant/components/greeneye_monitor/__init__.py +++ b/homeassistant/components/greeneye_monitor/__init__.py @@ -1,6 +1,7 @@ """Support for monitoring a GreenEye Monitor energy monitor.""" import logging +from greeneye import Monitors import voluptuous as vol from homeassistant.const import ( @@ -110,7 +111,6 @@ async def async_setup(hass, config): """Set up the GreenEye Monitor component.""" - from greeneye import Monitors monitors = Monitors() hass.data[DATA_GREENEYE_MONITOR] = monitors diff --git a/homeassistant/components/group/__init__.py b/homeassistant/components/group/__init__.py index 29126c82d44a..ba12e22b53e6 100644 --- a/homeassistant/components/group/__init__.py +++ b/homeassistant/components/group/__init__.py @@ -32,7 +32,7 @@ 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.helpers.config_validation import make_entity_service_schema from homeassistant.helpers.typing import HomeAssistantType @@ -63,28 +63,6 @@ CONTROL_TYPES = vol.In(["hidden", None]) -SET_VISIBILITY_SERVICE_SCHEMA = ENTITY_SERVICE_SCHEMA.extend( - {vol.Required(ATTR_VISIBLE): cv.boolean} -) - -RELOAD_SERVICE_SCHEMA = vol.Schema({}) - -SET_SERVICE_SCHEMA = vol.Schema( - { - vol.Required(ATTR_OBJECT_ID): cv.slug, - vol.Optional(ATTR_NAME): cv.string, - vol.Optional(ATTR_VIEW): cv.boolean, - vol.Optional(ATTR_ICON): cv.string, - vol.Optional(ATTR_CONTROL): CONTROL_TYPES, - vol.Optional(ATTR_VISIBLE): cv.boolean, - vol.Optional(ATTR_ALL): cv.boolean, - vol.Exclusive(ATTR_ENTITIES, "entities"): cv.entity_ids, - vol.Exclusive(ATTR_ADD_ENTITIES, "entities"): cv.entity_ids, - } -) - -REMOVE_SERVICE_SCHEMA = vol.Schema({vol.Required(ATTR_OBJECT_ID): cv.slug}) - _LOGGER = logging.getLogger(__name__) @@ -227,7 +205,7 @@ async def reload_service_handler(service): await component.async_add_entities(auto) hass.services.async_register( - DOMAIN, SERVICE_RELOAD, reload_service_handler, schema=RELOAD_SERVICE_SCHEMA + DOMAIN, SERVICE_RELOAD, reload_service_handler, schema=vol.Schema({}) ) service_lock = asyncio.Lock() @@ -319,11 +297,29 @@ async def groups_service_handler(service): await component.async_remove_entity(entity_id) hass.services.async_register( - DOMAIN, SERVICE_SET, locked_service_handler, schema=SET_SERVICE_SCHEMA + DOMAIN, + SERVICE_SET, + locked_service_handler, + schema=vol.Schema( + { + vol.Required(ATTR_OBJECT_ID): cv.slug, + vol.Optional(ATTR_NAME): cv.string, + vol.Optional(ATTR_VIEW): cv.boolean, + vol.Optional(ATTR_ICON): cv.string, + vol.Optional(ATTR_CONTROL): CONTROL_TYPES, + vol.Optional(ATTR_VISIBLE): cv.boolean, + vol.Optional(ATTR_ALL): cv.boolean, + vol.Exclusive(ATTR_ENTITIES, "entities"): cv.entity_ids, + vol.Exclusive(ATTR_ADD_ENTITIES, "entities"): cv.entity_ids, + } + ), ) hass.services.async_register( - DOMAIN, SERVICE_REMOVE, groups_service_handler, schema=REMOVE_SERVICE_SCHEMA + DOMAIN, + SERVICE_REMOVE, + groups_service_handler, + schema=vol.Schema({vol.Required(ATTR_OBJECT_ID): cv.slug}), ) async def visibility_service_handler(service): @@ -344,7 +340,7 @@ async def visibility_service_handler(service): DOMAIN, SERVICE_SET_VISIBILITY, visibility_service_handler, - schema=SET_VISIBILITY_SERVICE_SCHEMA, + schema=make_entity_service_schema({vol.Required(ATTR_VISIBLE): cv.boolean}), ) return True @@ -656,7 +652,6 @@ def _async_update_group_state(self, tr_state=None): if gr_on is None: return - # pylint: disable=too-many-boolean-expressions if tr_state is None or ( (gr_state == gr_on and tr_state.state == gr_off) or (gr_state == gr_off and tr_state.state == gr_on) diff --git a/homeassistant/components/gstreamer/media_player.py b/homeassistant/components/gstreamer/media_player.py index a213587bc0e6..9b371bfffcae 100644 --- a/homeassistant/components/gstreamer/media_player.py +++ b/homeassistant/components/gstreamer/media_player.py @@ -1,9 +1,10 @@ """Play media via gstreamer.""" import logging +from gsp import GstreamerPlayer import voluptuous as vol -from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA +from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice from homeassistant.components.media_player.const import ( MEDIA_TYPE_MUSIC, SUPPORT_NEXT_TRACK, @@ -36,7 +37,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Gstreamer platform.""" - from gsp import GstreamerPlayer name = config.get(CONF_NAME) pipeline = config.get(CONF_PIPELINE) diff --git a/homeassistant/components/habitica/__init__.py b/homeassistant/components/habitica/__init__.py index f94a09e4da52..52326555aab7 100644 --- a/homeassistant/components/habitica/__init__.py +++ b/homeassistant/components/habitica/__init__.py @@ -2,6 +2,7 @@ from collections import namedtuple import logging +from habitipy.aio import HabitipyAsync import voluptuous as vol from homeassistant.const import ( @@ -92,7 +93,6 @@ def has_all_unique_users_names(value): async def async_setup(hass, config): """Set up the Habitica service.""" - from habitipy.aio import HabitipyAsync conf = config[DOMAIN] data = hass.data[DOMAIN] = {} diff --git a/homeassistant/components/hangouts/__init__.py b/homeassistant/components/hangouts/__init__.py index 953994d6ac0c..d4892c668909 100644 --- a/homeassistant/components/hangouts/__init__.py +++ b/homeassistant/components/hangouts/__init__.py @@ -1,16 +1,16 @@ """Support for Hangouts.""" import logging +from hangups.auth import GoogleAuthError import voluptuous as vol from homeassistant import config_entries +from homeassistant.components.conversation.util import create_matcher from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.helpers import dispatcher, intent import homeassistant.helpers.config_validation as cv -from homeassistant.components.conversation.util import create_matcher # We need an import from .config_flow, without it .config_flow is never loaded. -from .intents import HelpIntent from .config_flow import HangoutsFlowHandler # noqa: F401 from .const import ( CONF_BOT, @@ -32,6 +32,8 @@ SERVICE_UPDATE, TARGETS_SCHEMA, ) +from .hangouts_bot import HangoutsBot +from .intents import HelpIntent _LOGGER = logging.getLogger(__name__) @@ -96,11 +98,7 @@ async def async_setup(hass, config): async def async_setup_entry(hass, config): """Set up a config entry.""" - from hangups.auth import GoogleAuthError - try: - from .hangouts_bot import HangoutsBot - bot = HangoutsBot( hass, config.data.get(CONF_REFRESH_TOKEN), diff --git a/homeassistant/components/hangouts/config_flow.py b/homeassistant/components/hangouts/config_flow.py index 8e262d8b40f7..f253df493419 100644 --- a/homeassistant/components/hangouts/config_flow.py +++ b/homeassistant/components/hangouts/config_flow.py @@ -1,7 +1,8 @@ """Config flow to configure Google Hangouts.""" import functools -import voluptuous as vol +from hangups import get_auth +import voluptuous as vol from homeassistant import config_entries from homeassistant.const import CONF_EMAIL, CONF_PASSWORD @@ -9,10 +10,16 @@ from .const import ( CONF_2FA, - CONF_REFRESH_TOKEN, CONF_AUTH_CODE, + CONF_REFRESH_TOKEN, DOMAIN as HANGOUTS_DOMAIN, ) +from .hangups_utils import ( + Google2FAError, + GoogleAuthError, + HangoutsCredentials, + HangoutsRefreshToken, +) @callback @@ -44,14 +51,6 @@ async def async_step_user(self, user_input=None): return self.async_abort(reason="already_configured") if user_input is not None: - from hangups import get_auth - from .hangups_utils import ( - HangoutsCredentials, - HangoutsRefreshToken, - GoogleAuthError, - Google2FAError, - ) - user_email = user_input[CONF_EMAIL] user_password = user_input[CONF_PASSWORD] user_auth_code = user_input.get(CONF_AUTH_CODE) @@ -99,9 +98,6 @@ async def async_step_2fa(self, user_input=None): errors = {} if user_input is not None: - from hangups import get_auth - from .hangups_utils import GoogleAuthError - self._credentials.set_verification_code(user_input[CONF_2FA]) try: await self.hass.async_add_executor_job( diff --git a/homeassistant/components/hangouts/hangouts_bot.py b/homeassistant/components/hangouts/hangouts_bot.py index 9eee2ca2be3e..8575a547a9c8 100644 --- a/homeassistant/components/hangouts/hangouts_bot.py +++ b/homeassistant/components/hangouts/hangouts_bot.py @@ -4,6 +4,8 @@ import logging import aiohttp +import hangups +from hangups import ChatMessageEvent, ChatMessageSegment, Client, get_auth, hangouts_pb2 from homeassistant.helpers import dispatcher, intent from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -24,6 +26,7 @@ EVENT_HANGOUTS_MESSAGE_RECEIVED, INTENT_HELP, ) +from .hangups_utils import HangoutsCredentials, HangoutsRefreshToken _LOGGER = logging.getLogger(__name__) @@ -126,8 +129,6 @@ def async_resolve_conversations(self, _): ) async def _async_handle_conversation_event(self, event): - from hangups import ChatMessageEvent - if isinstance(event, ChatMessageEvent): dispatcher.async_dispatcher_send( self.hass, @@ -196,11 +197,6 @@ async def _async_process(self, intents, text, conv_id): async def async_connect(self): """Login to the Google Hangouts.""" - from .hangups_utils import HangoutsRefreshToken, HangoutsCredentials - - from hangups import Client - from hangups import get_auth - session = await self.hass.async_add_executor_job( get_auth, HangoutsCredentials(None, None, None), @@ -252,8 +248,6 @@ async def _async_send_message(self, message, targets, data): if not conversations: return False - from hangups import ChatMessageSegment, hangouts_pb2 - messages = [] for segment in message: if messages: @@ -306,8 +300,6 @@ async def _async_send_message(self, message, targets, data): await conv.send_message(messages, image_file) async def _async_list_conversations(self): - import hangups - ( self._user_list, self._conversation_list, diff --git a/homeassistant/components/harmony/const.py b/homeassistant/components/harmony/const.py new file mode 100644 index 000000000000..12e710506657 --- /dev/null +++ b/homeassistant/components/harmony/const.py @@ -0,0 +1,4 @@ +"""Constants for the Harmony component.""" +DOMAIN = "harmony" +SERVICE_SYNC = "sync" +SERVICE_CHANGE_CHANNEL = "change_channel" diff --git a/homeassistant/components/harmony/remote.py b/homeassistant/components/harmony/remote.py index 118af7fe34ad..7f4d03ccbb08 100644 --- a/homeassistant/components/harmony/remote.py +++ b/homeassistant/components/harmony/remote.py @@ -19,7 +19,6 @@ ATTR_HOLD_SECS, ATTR_NUM_REPEATS, DEFAULT_DELAY_SECS, - DOMAIN, PLATFORM_SCHEMA, ) from homeassistant.const import ( @@ -33,6 +32,8 @@ import homeassistant.helpers.config_validation as cv from homeassistant.util import slugify +from .const import DOMAIN, SERVICE_CHANGE_CHANNEL, SERVICE_SYNC + _LOGGER = logging.getLogger(__name__) ATTR_CHANNEL = "channel" @@ -42,9 +43,6 @@ DEVICES = [] CONF_DEVICE_CACHE = "harmony_device_cache" -SERVICE_SYNC = "harmony_sync" -SERVICE_CHANGE_CHANNEL = "harmony_change_channel" - PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Optional(ATTR_ACTIVITY): cv.string, @@ -316,7 +314,6 @@ async def async_turn_off(self, **kwargs): except aioexc.TimeOut: _LOGGER.error("%s: Powering off timed-out", self.name) - # pylint: disable=arguments-differ async def async_send_command(self, command, **kwargs): """Send a list of commands to one device.""" _LOGGER.debug("%s: Send Command", self.name) diff --git a/homeassistant/components/harmony/services.yaml b/homeassistant/components/harmony/services.yaml index e69de29bb2d1..1b9ae225c7f8 100644 --- a/homeassistant/components/harmony/services.yaml +++ b/homeassistant/components/harmony/services.yaml @@ -0,0 +1,16 @@ +sync: + description: Syncs the remote's configuration. + fields: + entity_id: + description: Name(s) of entities to sync. + example: 'remote.family_room' + +change_channel: + description: Sends change channel command to the Harmony HUB + fields: + entity_id: + description: Name(s) of Harmony remote entities to send change channel command to + example: 'remote.family_room' + channel: + description: Channel number to change to + example: '200' \ No newline at end of file diff --git a/homeassistant/components/hdmi_cec/__init__.py b/homeassistant/components/hdmi_cec/__init__.py index d1637f96d95d..b460020546fa 100644 --- a/homeassistant/components/hdmi_cec/__init__.py +++ b/homeassistant/components/hdmi_cec/__init__.py @@ -4,6 +4,25 @@ import logging import multiprocessing +from pycec.cec import CecAdapter +from pycec.commands import CecCommand, KeyPressCommand, KeyReleaseCommand +from pycec.const import ( + ADDR_AUDIOSYSTEM, + ADDR_BROADCAST, + ADDR_UNREGISTERED, + KEY_MUTE_OFF, + KEY_MUTE_ON, + KEY_MUTE_TOGGLE, + KEY_VOLUME_DOWN, + KEY_VOLUME_UP, + POWER_OFF, + POWER_ON, + STATUS_PLAY, + STATUS_STILL, + STATUS_STOP, +) +from pycec.network import HDMINetwork, PhysicalAddress +from pycec.tcp import TcpAdapter import voluptuous as vol from homeassistant.components.media_player import DOMAIN as MEDIA_PLAYER @@ -155,8 +174,6 @@ def parse_mapping(mapping, parents=None): parents = [] for addr, val in mapping.items(): if isinstance(addr, (str,)) and isinstance(val, (str,)): - from pycec.network import PhysicalAddress - yield (addr, PhysicalAddress(val)) else: cur = parents + [addr] @@ -168,20 +185,6 @@ def parse_mapping(mapping, parents=None): def setup(hass: HomeAssistant, base_config): """Set up the CEC capability.""" - from pycec.network import HDMINetwork - from pycec.commands import CecCommand, KeyReleaseCommand, KeyPressCommand - from pycec.const import ( - KEY_VOLUME_UP, - KEY_VOLUME_DOWN, - KEY_MUTE_ON, - KEY_MUTE_OFF, - KEY_MUTE_TOGGLE, - ADDR_AUDIOSYSTEM, - ADDR_BROADCAST, - ADDR_UNREGISTERED, - ) - from pycec.cec import CecAdapter - from pycec.tcp import TcpAdapter # Parse configuration into a dict of device name to physical address # represented as a list of four elements. @@ -278,8 +281,6 @@ def _power_on(call): def _select_device(call): """Select the active device.""" - from pycec.network import PhysicalAddress - addr = call.data[ATTR_DEVICE] if not addr: _LOGGER.error("Device not found: %s", call.data[ATTR_DEVICE]) @@ -366,14 +367,6 @@ def __init__(self, device, logical) -> None: def update(self): """Update device status.""" device = self._device - from pycec.const import ( - STATUS_PLAY, - STATUS_STOP, - STATUS_STILL, - POWER_OFF, - POWER_ON, - ) - if device.power_status in [POWER_OFF, 3]: self._state = STATE_OFF elif device.status == STATUS_PLAY: diff --git a/homeassistant/components/hdmi_cec/media_player.py b/homeassistant/components/hdmi_cec/media_player.py index 379105430bc7..42c5f0b456c0 100644 --- a/homeassistant/components/hdmi_cec/media_player.py +++ b/homeassistant/components/hdmi_cec/media_player.py @@ -1,6 +1,27 @@ """Support for HDMI CEC devices as media players.""" import logging +from pycec.commands import CecCommand, KeyPressCommand, KeyReleaseCommand +from pycec.const import ( + KEY_BACKWARD, + KEY_FORWARD, + KEY_MUTE_TOGGLE, + KEY_PAUSE, + KEY_PLAY, + KEY_STOP, + KEY_VOLUME_DOWN, + KEY_VOLUME_UP, + POWER_OFF, + POWER_ON, + STATUS_PLAY, + STATUS_STILL, + STATUS_STOP, + TYPE_AUDIO, + TYPE_PLAYBACK, + TYPE_RECORDER, + TYPE_TUNER, +) + from homeassistant.components.media_player import MediaPlayerDevice from homeassistant.components.media_player.const import ( DOMAIN, @@ -50,8 +71,6 @@ def __init__(self, device, logical) -> None: def send_keypress(self, key): """Send keypress to CEC adapter.""" - from pycec.commands import KeyPressCommand, KeyReleaseCommand - _LOGGER.debug( "Sending keypress %s to device %s", hex(key), hex(self._logical_address) ) @@ -60,20 +79,14 @@ def send_keypress(self, key): def send_playback(self, key): """Send playback status to CEC adapter.""" - from pycec.commands import CecCommand - self._device.async_send_command(CecCommand(key, dst=self._logical_address)) def mute_volume(self, mute): """Mute volume.""" - from pycec.const import KEY_MUTE_TOGGLE - self.send_keypress(KEY_MUTE_TOGGLE) def media_previous_track(self): """Go to previous track.""" - from pycec.const import KEY_BACKWARD - self.send_keypress(KEY_BACKWARD) def turn_on(self): @@ -92,8 +105,6 @@ def turn_off(self): def media_stop(self): """Stop playback.""" - from pycec.const import KEY_STOP - self.send_keypress(KEY_STOP) self._state = STATE_IDLE @@ -103,8 +114,6 @@ def play_media(self, media_type, media_id, **kwargs): def media_next_track(self): """Skip to next track.""" - from pycec.const import KEY_FORWARD - self.send_keypress(KEY_FORWARD) def media_seek(self, position): @@ -117,8 +126,6 @@ def set_volume_level(self, volume): def media_pause(self): """Pause playback.""" - from pycec.const import KEY_PAUSE - self.send_keypress(KEY_PAUSE) self._state = STATE_PAUSED @@ -128,22 +135,16 @@ def select_source(self, source): def media_play(self): """Start playback.""" - from pycec.const import KEY_PLAY - self.send_keypress(KEY_PLAY) self._state = STATE_PLAYING def volume_up(self): """Increase volume.""" - from pycec.const import KEY_VOLUME_UP - _LOGGER.debug("%s: volume up", self._logical_address) self.send_keypress(KEY_VOLUME_UP) def volume_down(self): """Decrease volume.""" - from pycec.const import KEY_VOLUME_DOWN - _LOGGER.debug("%s: volume down", self._logical_address) self.send_keypress(KEY_VOLUME_DOWN) @@ -155,14 +156,6 @@ def state(self) -> str: def update(self): """Update device status.""" device = self._device - from pycec.const import ( - STATUS_PLAY, - STATUS_STOP, - STATUS_STILL, - POWER_OFF, - POWER_ON, - ) - if device.power_status in [POWER_OFF, 3]: self._state = STATE_OFF elif not self.support_pause: @@ -180,8 +173,6 @@ def update(self): @property def supported_features(self): """Flag media player features that are supported.""" - from pycec.const import TYPE_RECORDER, TYPE_PLAYBACK, TYPE_TUNER, TYPE_AUDIO - if self.type_id == TYPE_RECORDER or self.type == TYPE_PLAYBACK: return ( SUPPORT_TURN_ON diff --git a/homeassistant/components/heatmiser/climate.py b/homeassistant/components/heatmiser/climate.py index c9bed1e9d34f..1954749c21bf 100644 --- a/homeassistant/components/heatmiser/climate.py +++ b/homeassistant/components/heatmiser/climate.py @@ -1,54 +1,65 @@ """Support for the PRT Heatmiser themostats using the V3 protocol.""" import logging +from typing import List import voluptuous as vol -from homeassistant.components.climate import ClimateDevice, PLATFORM_SCHEMA +from heatmiserV3 import heatmiser, connection +from homeassistant.components.climate import ( + ClimateDevice, + PLATFORM_SCHEMA, + HVAC_MODE_HEAT, + HVAC_MODE_OFF, +) from homeassistant.components.climate.const import SUPPORT_TARGET_TEMPERATURE from homeassistant.const import ( TEMP_CELSIUS, + TEMP_FAHRENHEIT, ATTR_TEMPERATURE, + CONF_HOST, CONF_PORT, - CONF_NAME, CONF_ID, + CONF_NAME, ) import homeassistant.helpers.config_validation as cv + _LOGGER = logging.getLogger(__name__) -CONF_IPADDRESS = "ipaddress" -CONF_TSTATS = "tstats" +CONF_THERMOSTATS = "tstats" TSTATS_SCHEMA = vol.Schema( - {vol.Required(CONF_ID): cv.string, vol.Required(CONF_NAME): cv.string} + vol.All( + cv.ensure_list, + [{vol.Required(CONF_ID): cv.positive_int, vol.Required(CONF_NAME): cv.string}], + ) ) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { - vol.Required(CONF_IPADDRESS): cv.string, - vol.Required(CONF_PORT): cv.port, - vol.Required(CONF_TSTATS, default={}): vol.Schema({cv.string: TSTATS_SCHEMA}), + vol.Required(CONF_HOST): cv.string, + vol.Required(CONF_PORT): cv.string, + vol.Optional(CONF_THERMOSTATS, default=[]): TSTATS_SCHEMA, } ) def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the heatmiser thermostat.""" - from heatmiserV3 import heatmiser, connection - ipaddress = config.get(CONF_IPADDRESS) - port = str(config.get(CONF_PORT)) - tstats = config.get(CONF_TSTATS) + heatmiser_v3_thermostat = heatmiser.HeatmiserThermostat + + host = config[CONF_HOST] + port = config[CONF_PORT] + + thermostats = config[CONF_THERMOSTATS] - serport = connection.connection(ipaddress, port) - serport.open() + uh1_hub = connection.HeatmiserUH1(host, port) add_entities( [ - HeatmiserV3Thermostat( - heatmiser, tstat.get(CONF_ID), tstat.get(CONF_NAME), serport - ) - for tstat in tstats.values() + HeatmiserV3Thermostat(heatmiser_v3_thermostat, thermostat, uh1_hub) + for thermostat in thermostats ], True, ) @@ -57,15 +68,17 @@ def setup_platform(hass, config, add_entities, discovery_info=None): class HeatmiserV3Thermostat(ClimateDevice): """Representation of a HeatmiserV3 thermostat.""" - def __init__(self, heatmiser, device, name, serport): + def __init__(self, therm, device, uh1): """Initialize the thermostat.""" - self.heatmiser = heatmiser - self.serport = serport + self.therm = therm(device[CONF_ID], "prt", uh1) + self.uh1 = uh1 + self._name = device[CONF_NAME] self._current_temperature = None self._target_temperature = None - self._name = name self._id = device self.dcb = None + self._hvac_mode = HVAC_MODE_HEAT + self._temperature_unit = None @property def supported_features(self): @@ -80,7 +93,23 @@ def name(self): @property def temperature_unit(self): """Return the unit of measurement which this thermostat uses.""" - return TEMP_CELSIUS + return self._temperature_unit + + @property + def hvac_mode(self) -> str: + """Return hvac operation ie. heat, cool mode. + + Need to be one of HVAC_MODE_*. + """ + return self._hvac_mode + + @property + def hvac_modes(self) -> List[str]: + """Return the list of available hvac operation modes. + + Need to be a subset of HVAC_MODES. + """ + return [HVAC_MODE_HEAT, HVAC_MODE_OFF] @property def current_temperature(self): @@ -95,12 +124,25 @@ def target_temperature(self): def set_temperature(self, **kwargs): """Set new target temperature.""" temperature = kwargs.get(ATTR_TEMPERATURE) - self.heatmiser.hmSendAddress(self._id, 18, temperature, 1, self.serport) + self._target_temperature = int(temperature) + self.therm.set_target_temp(self._target_temperature) def update(self): """Get the latest data.""" - self.dcb = self.heatmiser.hmReadAddress(self._id, "prt", self.serport) - low = self.dcb.get("floortemplow ") - high = self.dcb.get("floortemphigh") - self._current_temperature = (high * 256 + low) / 10.0 - self._target_temperature = int(self.dcb.get("roomset")) + self.uh1.reopen() + if not self.uh1.status: + _LOGGER.error("Failed to update device %s", self._name) + return + self.dcb = self.therm.read_dcb() + self._temperature_unit = ( + TEMP_CELSIUS + if (self.therm.get_temperature_format() == "C") + else TEMP_FAHRENHEIT + ) + self._current_temperature = int(self.therm.get_floor_temp()) + self._target_temperature = int(self.therm.get_target_temp()) + self._hvac_mode = ( + HVAC_MODE_OFF + if (int(self.therm.get_current_state()) == 0) + else HVAC_MODE_HEAT + ) diff --git a/homeassistant/components/heatmiser/manifest.json b/homeassistant/components/heatmiser/manifest.json index b3882c63c51f..89bcec081253 100644 --- a/homeassistant/components/heatmiser/manifest.json +++ b/homeassistant/components/heatmiser/manifest.json @@ -3,8 +3,10 @@ "name": "Heatmiser", "documentation": "https://www.home-assistant.io/integrations/heatmiser", "requirements": [ - "heatmiserV3==0.9.1" + "heatmiserV3==1.1.18" ], "dependencies": [], - "codeowners": [] -} + "codeowners": [ + "@andylockran" + ] +} \ No newline at end of file diff --git a/homeassistant/components/here_travel_time/manifest.json b/homeassistant/components/here_travel_time/manifest.json index 0f2bde253de5..5ef71a249e6d 100755 --- a/homeassistant/components/here_travel_time/manifest.json +++ b/homeassistant/components/here_travel_time/manifest.json @@ -3,7 +3,7 @@ "name": "HERE travel time", "documentation": "https://www.home-assistant.io/integrations/here_travel_time", "requirements": [ - "herepy==0.6.3.1" + "herepy==0.6.3.3" ], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/hikvision/binary_sensor.py b/homeassistant/components/hikvision/binary_sensor.py index b898f5d860c2..9db912173002 100644 --- a/homeassistant/components/hikvision/binary_sensor.py +++ b/homeassistant/components/hikvision/binary_sensor.py @@ -1,24 +1,26 @@ """Support for Hikvision event stream events represented as binary sensors.""" -import logging from datetime import timedelta +import logging + +from pyhik.hikvision import HikCamera import voluptuous as vol -from homeassistant.helpers.event import track_point_in_utc_time -from homeassistant.util.dt import utcnow -from homeassistant.components.binary_sensor import BinarySensorDevice, PLATFORM_SCHEMA -import homeassistant.helpers.config_validation as cv +from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorDevice from homeassistant.const import ( + ATTR_LAST_TRIP_TIME, + CONF_CUSTOMIZE, CONF_HOST, - CONF_PORT, CONF_NAME, - CONF_USERNAME, CONF_PASSWORD, + CONF_PORT, CONF_SSL, - EVENT_HOMEASSISTANT_STOP, + CONF_USERNAME, EVENT_HOMEASSISTANT_START, - ATTR_LAST_TRIP_TIME, - CONF_CUSTOMIZE, + EVENT_HOMEASSISTANT_STOP, ) +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.event import track_point_in_utc_time +from homeassistant.util.dt import utcnow _LOGGER = logging.getLogger(__name__) @@ -135,7 +137,6 @@ class HikvisionData: def __init__(self, hass, url, port, name, username, password): """Initialize the data object.""" - from pyhik.hikvision import HikCamera self._url = url self._port = port diff --git a/homeassistant/components/hikvision/manifest.json b/homeassistant/components/hikvision/manifest.json index 11775ed3ae08..d51718964646 100644 --- a/homeassistant/components/hikvision/manifest.json +++ b/homeassistant/components/hikvision/manifest.json @@ -3,7 +3,7 @@ "name": "Hikvision", "documentation": "https://www.home-assistant.io/integrations/hikvision", "requirements": [ - "pyhik==0.2.4" + "pyhik==0.2.5" ], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/hisense_aehw4a1/.translations/bg.json b/homeassistant/components/hisense_aehw4a1/.translations/bg.json new file mode 100644 index 000000000000..c758e9cc20d7 --- /dev/null +++ b/homeassistant/components/hisense_aehw4a1/.translations/bg.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "\u0412 \u043c\u0440\u0435\u0436\u0430\u0442\u0430 \u043d\u0435 \u0441\u0430 \u043d\u0430\u043c\u0435\u0440\u0435\u043d\u0438 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 Hisense AEH-W4A1.", + "single_instance_allowed": "\u0412\u044a\u0437\u043c\u043e\u0436\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 Hisense AEH-W4A1." + }, + "step": { + "confirm": { + "description": "\u0418\u0441\u043a\u0430\u0442\u0435 \u043b\u0438 \u0434\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u0435 Hisense AEH-W4A1?", + "title": "Hisense AEH-W4A1" + } + }, + "title": "Hisense AEH-W4A1" + } +} \ No newline at end of file diff --git a/homeassistant/components/hisense_aehw4a1/.translations/ca.json b/homeassistant/components/hisense_aehw4a1/.translations/ca.json new file mode 100644 index 000000000000..7b237aecdab4 --- /dev/null +++ b/homeassistant/components/hisense_aehw4a1/.translations/ca.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "No s'ha trobat cap dispositiu AEH-W4A1 a la xarxa.", + "single_instance_allowed": "Nom\u00e9s \u00e9s possible una \u00fanica configuraci\u00f3 del AEH-W4A1 de Hisense." + }, + "step": { + "confirm": { + "description": "Vols configurar AEH-W4A1 de Hisense?", + "title": "Hisense AEH-W4A1" + } + }, + "title": "Hisense AEH-W4A1" + } +} \ No newline at end of file diff --git a/homeassistant/components/hisense_aehw4a1/.translations/de.json b/homeassistant/components/hisense_aehw4a1/.translations/de.json new file mode 100644 index 000000000000..8b474ea0418e --- /dev/null +++ b/homeassistant/components/hisense_aehw4a1/.translations/de.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "Es wurden keine Hisense AEH-W4A1-Ger\u00e4te im Netzwerk gefunden.", + "single_instance_allowed": "Es ist nur eine einzige Konfiguration von Hisense AEH-W4A1 m\u00f6glich." + }, + "step": { + "confirm": { + "description": "M\u00f6chten Sie Hisense AEH-W4A1 einrichten?", + "title": "Hisense AEH-W4A1" + } + }, + "title": "Hisense AEH-W4A1" + } +} \ No newline at end of file diff --git a/homeassistant/components/hisense_aehw4a1/.translations/en.json b/homeassistant/components/hisense_aehw4a1/.translations/en.json new file mode 100644 index 000000000000..b70fc8f05ecd --- /dev/null +++ b/homeassistant/components/hisense_aehw4a1/.translations/en.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "No Hisense AEH-W4A1 devices found on the network.", + "single_instance_allowed": "Only a single configuration of Hisense AEH-W4A1 is possible." + }, + "step": { + "confirm": { + "description": "Do you want to set up Hisense AEH-W4A1?", + "title": "Hisense AEH-W4A1" + } + }, + "title": "Hisense AEH-W4A1" + } +} \ No newline at end of file diff --git a/homeassistant/components/hisense_aehw4a1/.translations/es.json b/homeassistant/components/hisense_aehw4a1/.translations/es.json new file mode 100644 index 000000000000..69f071bf5d89 --- /dev/null +++ b/homeassistant/components/hisense_aehw4a1/.translations/es.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "No se encontraron dispositivos Hisense AEH-W4A1 en la red.", + "single_instance_allowed": "Solo es posible una \u00fanica configuraci\u00f3n de Hisense AEH-W4A1." + }, + "step": { + "confirm": { + "description": "\u00bfDesea configurar Hisense AEH-W4A1?", + "title": "Hisense AEH-W4A1" + } + }, + "title": "Hisense AEH-W4A1" + } +} \ No newline at end of file diff --git a/homeassistant/components/hisense_aehw4a1/.translations/fr.json b/homeassistant/components/hisense_aehw4a1/.translations/fr.json new file mode 100644 index 000000000000..50c753538c78 --- /dev/null +++ b/homeassistant/components/hisense_aehw4a1/.translations/fr.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "Aucun p\u00e9riph\u00e9rique AEH-W4A1 trouv\u00e9 sur le r\u00e9seau.", + "single_instance_allowed": "Une seule configuration de AEH-W4A1 est possible." + }, + "step": { + "confirm": { + "description": "Voulez-vous configurer AEH-W4A1?", + "title": "Hisense AEH-W4A1" + } + }, + "title": "Hisense AEH-W4A1" + } +} \ No newline at end of file diff --git a/homeassistant/components/hisense_aehw4a1/.translations/it.json b/homeassistant/components/hisense_aehw4a1/.translations/it.json new file mode 100644 index 000000000000..b584d18e8bf1 --- /dev/null +++ b/homeassistant/components/hisense_aehw4a1/.translations/it.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "Nessun dispositivo Hisense AEH-W4A1 trovato sulla rete.", + "single_instance_allowed": "\u00c8 consentita solo una configurazione di Hisense AEH-W4A1" + }, + "step": { + "confirm": { + "description": "Voui configurare Hisense AEH-W4A1", + "title": "Hisense AEH-W4A1" + } + }, + "title": "Hisense AEH-W4A1" + } +} \ No newline at end of file diff --git a/homeassistant/components/hisense_aehw4a1/.translations/lb.json b/homeassistant/components/hisense_aehw4a1/.translations/lb.json new file mode 100644 index 000000000000..33b933483001 --- /dev/null +++ b/homeassistant/components/hisense_aehw4a1/.translations/lb.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "Keng Hisense AEH-W4A1 Apparater am Netzwierk fonnt.", + "single_instance_allowed": "N\u00ebmmen eng eenzeg Konfiguratioun vun Hisense AEH-W4A1 ass m\u00e9iglech." + }, + "step": { + "confirm": { + "description": "Soll Hisense AEH-W4A1 konfigur\u00e9iert ginn?", + "title": "Hisense AEH-W4A1" + } + }, + "title": "Hisense AEH-W4A1" + } +} \ No newline at end of file diff --git a/homeassistant/components/hisense_aehw4a1/.translations/no.json b/homeassistant/components/hisense_aehw4a1/.translations/no.json new file mode 100644 index 000000000000..e44e818ea607 --- /dev/null +++ b/homeassistant/components/hisense_aehw4a1/.translations/no.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "Ingen Hisense AEH-W4A1-enheter funnet p\u00e5 nettverket.", + "single_instance_allowed": "Bare en enkelt konfigurasjon av Hisense AEH-W4A1 er mulig." + }, + "step": { + "confirm": { + "description": "Vil du konfigurere Hisense AEH-W4A1?", + "title": "Hisense AEH-W4A1" + } + }, + "title": "Hisense AEH-W4A1" + } +} \ No newline at end of file diff --git a/homeassistant/components/hisense_aehw4a1/.translations/pl.json b/homeassistant/components/hisense_aehw4a1/.translations/pl.json new file mode 100644 index 000000000000..e0ab5cddbda9 --- /dev/null +++ b/homeassistant/components/hisense_aehw4a1/.translations/pl.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "Nie znaleziono w sieci urz\u0105dze\u0144 Hisense AEH-W4A1.", + "single_instance_allowed": "Dozwolona jest tylko jedna konfiguracja Hisense AEH-W4A1." + }, + "step": { + "confirm": { + "description": "Chcesz skonfigurowa\u0107 AEH-W4A1?", + "title": "Hisense AEH-W4A1" + } + }, + "title": "Hisense AEH-W4A1" + } +} \ No newline at end of file diff --git a/homeassistant/components/hisense_aehw4a1/.translations/ru.json b/homeassistant/components/hisense_aehw4a1/.translations/ru.json new file mode 100644 index 000000000000..c65a5277f62c --- /dev/null +++ b/homeassistant/components/hisense_aehw4a1/.translations/ru.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 Hisense AEH-W4A1e \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 Hisense AEH-W4A1?", + "title": "Hisense AEH-W4A1" + } + }, + "title": "Hisense AEH-W4A1" + } +} \ No newline at end of file diff --git a/homeassistant/components/hisense_aehw4a1/.translations/sl.json b/homeassistant/components/hisense_aehw4a1/.translations/sl.json new file mode 100644 index 000000000000..3c15eecf6e18 --- /dev/null +++ b/homeassistant/components/hisense_aehw4a1/.translations/sl.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "V omre\u017eju ni bilo najdenih naprav Hisense AEH-W4A1.", + "single_instance_allowed": "Mo\u017ena je samo ena konfiguracija Hisense AEH-W4A1." + }, + "step": { + "confirm": { + "description": "Ali \u017eelite nastaviti Hisense AEH-W4A1?", + "title": "Hisense AEH-W4A1" + } + }, + "title": "Hisense AEH-W4A1" + } +} \ No newline at end of file diff --git a/homeassistant/components/hisense_aehw4a1/.translations/zh-Hant.json b/homeassistant/components/hisense_aehw4a1/.translations/zh-Hant.json new file mode 100644 index 000000000000..d4f87905da94 --- /dev/null +++ b/homeassistant/components/hisense_aehw4a1/.translations/zh-Hant.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "\u5728\u7db2\u8def\u4e0a\u627e\u4e0d\u5230\u6d77\u4fe1 AEH-W4A1 \u8a2d\u5099\u3002", + "single_instance_allowed": "\u50c5\u5141\u8a31\u8a2d\u5b9a\u4e00\u7d44\u6d77\u4fe1 AEH-W4A1\u3002" + }, + "step": { + "confirm": { + "description": "\u662f\u5426\u8981\u8a2d\u5b9a\u6d77\u4fe1 AEH-W4A1\uff1f", + "title": "\u6d77\u4fe1 AEH-W4A1" + } + }, + "title": "\u6d77\u4fe1 AEH-W4A1" + } +} \ No newline at end of file diff --git a/homeassistant/components/hisense_aehw4a1/__init__.py b/homeassistant/components/hisense_aehw4a1/__init__.py new file mode 100644 index 000000000000..721039d0e1c8 --- /dev/null +++ b/homeassistant/components/hisense_aehw4a1/__init__.py @@ -0,0 +1,81 @@ +"""The Hisense AEH-W4A1 integration.""" +import ipaddress +import logging + +from pyaehw4a1.aehw4a1 import AehW4a1 +import pyaehw4a1.exceptions +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.components.climate import DOMAIN as CLIMATE_DOMAIN +from homeassistant.const import CONF_IP_ADDRESS +import homeassistant.helpers.config_validation as cv + +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +def coerce_ip(value): + """Validate that provided value is a valid IP address.""" + if not value: + raise vol.Invalid("Must define an IP address") + try: + ipaddress.IPv4Network(value) + except ValueError: + raise vol.Invalid("Not a valid IP address") + return value + + +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: { + CLIMATE_DOMAIN: vol.Schema( + { + vol.Optional(CONF_IP_ADDRESS, default=[]): vol.All( + cv.ensure_list, [vol.All(cv.string, coerce_ip)] + ) + } + ) + } + }, + extra=vol.ALLOW_EXTRA, +) + + +async def async_setup(hass, config): + """Set up the Hisense AEH-W4A1 integration.""" + conf = config.get(DOMAIN) + hass.data[DOMAIN] = {} + + if conf is not None: + devices = conf[CONF_IP_ADDRESS][:] + for device in devices: + try: + await AehW4a1(device).check() + except pyaehw4a1.exceptions.ConnectionError: + conf[CONF_IP_ADDRESS].remove(device) + _LOGGER.warning("Hisense AEH-W4A1 at %s not found", device) + if conf[CONF_IP_ADDRESS]: + hass.data[DOMAIN] = conf + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, + ) + ) + + return True + + +async def async_setup_entry(hass, entry): + """Set up a config entry for Hisense AEH-W4A1.""" + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, CLIMATE_DOMAIN) + ) + + return True + + +async def async_unload_entry(hass, entry): + """Unload a config entry.""" + return await hass.config_entries.async_forward_entry_unload(entry, CLIMATE_DOMAIN) diff --git a/homeassistant/components/hisense_aehw4a1/climate.py b/homeassistant/components/hisense_aehw4a1/climate.py new file mode 100644 index 000000000000..da18419c2642 --- /dev/null +++ b/homeassistant/components/hisense_aehw4a1/climate.py @@ -0,0 +1,438 @@ +"""Pyaehw4a1 platform to control of Hisense AEH-W4A1 Climate Devices.""" + +import logging + +from pyaehw4a1.aehw4a1 import AehW4a1 +import pyaehw4a1.exceptions + +from homeassistant.components.climate import ClimateDevice +from homeassistant.components.climate.const import ( + FAN_AUTO, + FAN_HIGH, + FAN_LOW, + FAN_MEDIUM, + HVAC_MODE_COOL, + HVAC_MODE_DRY, + HVAC_MODE_FAN_ONLY, + HVAC_MODE_HEAT, + HVAC_MODE_OFF, + PRESET_BOOST, + PRESET_ECO, + PRESET_NONE, + PRESET_SLEEP, + SUPPORT_FAN_MODE, + SUPPORT_PRESET_MODE, + SUPPORT_SWING_MODE, + SUPPORT_TARGET_TEMPERATURE, + SWING_BOTH, + SWING_HORIZONTAL, + SWING_OFF, + SWING_VERTICAL, +) +from homeassistant.const import ( + ATTR_TEMPERATURE, + PRECISION_WHOLE, + TEMP_CELSIUS, + TEMP_FAHRENHEIT, +) + +from . import CONF_IP_ADDRESS, DOMAIN + +SUPPORT_FLAGS = ( + SUPPORT_TARGET_TEMPERATURE + | SUPPORT_FAN_MODE + | SUPPORT_SWING_MODE + | SUPPORT_PRESET_MODE +) + +MIN_TEMP_C = 16 +MAX_TEMP_C = 32 + +MIN_TEMP_F = 61 +MAX_TEMP_F = 90 + +HVAC_MODES = [ + HVAC_MODE_OFF, + HVAC_MODE_HEAT, + HVAC_MODE_COOL, + HVAC_MODE_DRY, + HVAC_MODE_FAN_ONLY, +] + +FAN_MODES = [ + "mute", + FAN_LOW, + FAN_MEDIUM, + FAN_HIGH, + FAN_AUTO, +] + +SWING_MODES = [ + SWING_OFF, + SWING_VERTICAL, + SWING_HORIZONTAL, + SWING_BOTH, +] + +PRESET_MODES = [ + PRESET_NONE, + PRESET_ECO, + PRESET_BOOST, + PRESET_SLEEP, + "sleep_2", + "sleep_3", + "sleep_4", +] + +AC_TO_HA_STATE = { + "0001": HVAC_MODE_HEAT, + "0010": HVAC_MODE_COOL, + "0011": HVAC_MODE_DRY, + "0000": HVAC_MODE_FAN_ONLY, +} + +HA_STATE_TO_AC = { + HVAC_MODE_OFF: "off", + HVAC_MODE_HEAT: "mode_heat", + HVAC_MODE_COOL: "mode_cool", + HVAC_MODE_DRY: "mode_dry", + HVAC_MODE_FAN_ONLY: "mode_fan", +} + +AC_TO_HA_FAN_MODES = { + "00000000": FAN_AUTO, # fan value for heat mode + "00000001": FAN_AUTO, + "00000010": "mute", + "00000100": FAN_LOW, + "00000110": FAN_MEDIUM, + "00001000": FAN_HIGH, +} + +HA_FAN_MODES_TO_AC = { + "mute": "speed_mute", + FAN_LOW: "speed_low", + FAN_MEDIUM: "speed_med", + FAN_HIGH: "speed_max", + FAN_AUTO: "speed_auto", +} + +AC_TO_HA_SWING = { + "00": SWING_OFF, + "10": SWING_VERTICAL, + "01": SWING_HORIZONTAL, + "11": SWING_BOTH, +} + +_LOGGER = logging.getLogger(__name__) + + +def _build_entity(device): + _LOGGER.debug("Found device at %s", device) + return ClimateAehW4a1(device) + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the AEH-W4A1 climate platform.""" + # Priority 1: manual config + if hass.data[DOMAIN].get(CONF_IP_ADDRESS): + devices = hass.data[DOMAIN][CONF_IP_ADDRESS] + else: + # Priority 2: scanned interfaces + devices = await AehW4a1().discovery() + + entities = [_build_entity(device) for device in devices] + async_add_entities(entities, True) + + +class ClimateAehW4a1(ClimateDevice): + """Representation of a Hisense AEH-W4A1 module for climate device.""" + + def __init__(self, device): + """Initialize the climate device.""" + self._unique_id = device + self._device = AehW4a1(device) + self._hvac_modes = HVAC_MODES + self._fan_modes = FAN_MODES + self._swing_modes = SWING_MODES + self._preset_modes = PRESET_MODES + self._available = None + self._on = None + self._temperature_unit = None + self._current_temperature = None + self._target_temperature = None + self._hvac_mode = None + self._fan_mode = None + self._swing_mode = None + self._preset_mode = None + self._previous_state = None + + async def async_update(self): + """Pull state from AEH-W4A1.""" + try: + status = await self._device.command("status_102_0") + except pyaehw4a1.exceptions.ConnectionError as library_error: + _LOGGER.warning( + "Unexpected error of %s: %s", self._unique_id, library_error + ) + self._available = False + return + + self._available = True + + self._on = status["run_status"] + + if status["temperature_Fahrenheit"] == "0": + self._temperature_unit = TEMP_CELSIUS + else: + self._temperature_unit = TEMP_FAHRENHEIT + + self._current_temperature = int(status["indoor_temperature_status"], 2) + + if self._on == "1": + device_mode = status["mode_status"] + self._hvac_mode = AC_TO_HA_STATE[device_mode] + + fan_mode = status["wind_status"] + self._fan_mode = AC_TO_HA_FAN_MODES[fan_mode] + + swing_mode = f'{status["up_down"]}{status["left_right"]}' + self._swing_mode = AC_TO_HA_SWING[swing_mode] + + if self._hvac_mode in (HVAC_MODE_COOL, HVAC_MODE_HEAT): + self._target_temperature = int(status["indoor_temperature_setting"], 2) + else: + self._target_temperature = None + + if status["efficient"] == "1": + self._preset_mode = PRESET_BOOST + elif status["low_electricity"] == "1": + self._preset_mode = PRESET_ECO + elif status["sleep_status"] == "0000001": + self._preset_mode = PRESET_SLEEP + elif status["sleep_status"] == "0000010": + self._preset_mode = "sleep_2" + elif status["sleep_status"] == "0000011": + self._preset_mode = "sleep_3" + elif status["sleep_status"] == "0000100": + self._preset_mode = "sleep_4" + else: + self._preset_mode = PRESET_NONE + else: + self._hvac_mode = HVAC_MODE_OFF + self._fan_mode = None + self._swing_mode = None + self._target_temperature = None + self._preset_mode = None + + @property + def available(self): + """Return True if entity is available.""" + return self._available + + @property + def name(self): + """Return the name of the climate device.""" + return self._unique_id + + @property + def temperature_unit(self): + """Return the unit of measurement.""" + return self._temperature_unit + + @property + def current_temperature(self): + """Return the current temperature.""" + return self._current_temperature + + @property + def target_temperature(self): + """Return the temperature we are trying to reach.""" + return self._target_temperature + + @property + def hvac_mode(self): + """Return hvac target hvac state.""" + return self._hvac_mode + + @property + def hvac_modes(self): + """Return the list of available operation modes.""" + return self._hvac_modes + + @property + def fan_mode(self): + """Return the fan setting.""" + return self._fan_mode + + @property + def fan_modes(self): + """Return the list of available fan modes.""" + return self._fan_modes + + @property + def preset_mode(self): + """Return the preset mode if on.""" + return self._preset_mode + + @property + def preset_modes(self): + """Return the list of available preset modes.""" + return self._preset_modes + + @property + def swing_mode(self): + """Return swing operation.""" + return self._swing_mode + + @property + def swing_modes(self): + """Return the list of available fan modes.""" + return self._swing_modes + + @property + def min_temp(self): + """Return the minimum temperature.""" + if self._temperature_unit == TEMP_CELSIUS: + return MIN_TEMP_C + return MIN_TEMP_F + + @property + def max_temp(self): + """Return the maximum temperature.""" + if self._temperature_unit == TEMP_CELSIUS: + return MAX_TEMP_C + return MAX_TEMP_F + + @property + def precision(self): + """Return the precision of the system.""" + return PRECISION_WHOLE + + @property + def target_temperature_step(self): + """Return the supported step of target temperature.""" + return 1 + + @property + def supported_features(self): + """Return the list of supported features.""" + return SUPPORT_FLAGS + + async def async_set_temperature(self, **kwargs): + """Set new target temperatures.""" + if self._on != "1": + _LOGGER.warning( + "AC at %s is off, could not set temperature", self._unique_id + ) + return + temp = kwargs.get(ATTR_TEMPERATURE) + if temp is not None: + _LOGGER.debug("Setting temp of %s to %s", self._unique_id, temp) + if self._preset_mode != PRESET_NONE: + await self.async_set_preset_mode(PRESET_NONE) + if self._temperature_unit == TEMP_CELSIUS: + await self._device.command(f"temp_{int(temp)}_C") + else: + await self._device.command(f"temp_{int(temp)}_F") + + async def async_set_fan_mode(self, fan_mode): + """Set new fan mode.""" + if self._on != "1": + _LOGGER.warning("AC at %s is off, could not set fan mode", self._unique_id) + return + if self._hvac_mode in (HVAC_MODE_COOL, HVAC_MODE_FAN_ONLY) and ( + self._hvac_mode != HVAC_MODE_FAN_ONLY or fan_mode != FAN_AUTO + ): + _LOGGER.debug("Setting fan mode of %s to %s", self._unique_id, fan_mode) + await self._device.command(HA_FAN_MODES_TO_AC[fan_mode]) + + async def async_set_swing_mode(self, swing_mode): + """Set new target swing operation.""" + if self._on != "1": + _LOGGER.warning( + "AC at %s is off, could not set swing mode", self._unique_id + ) + return + + _LOGGER.debug("Setting swing mode of %s to %s", self._unique_id, swing_mode) + swing_act = self._swing_mode + + if swing_mode == SWING_OFF and swing_act != SWING_OFF: + if swing_act in (SWING_HORIZONTAL, SWING_BOTH): + await self._device.command("hor_dir") + if swing_act in (SWING_VERTICAL, SWING_BOTH): + await self._device.command("vert_dir") + + if swing_mode == SWING_BOTH and swing_act != SWING_BOTH: + if swing_act in (SWING_OFF, SWING_HORIZONTAL): + await self._device.command("vert_swing") + if swing_act in (SWING_OFF, SWING_VERTICAL): + await self._device.command("hor_swing") + + if swing_mode == SWING_VERTICAL and swing_act != SWING_VERTICAL: + if swing_act in (SWING_OFF, SWING_HORIZONTAL): + await self._device.command("vert_swing") + if swing_act in (SWING_BOTH, SWING_HORIZONTAL): + await self._device.command("hor_dir") + + if swing_mode == SWING_HORIZONTAL and swing_act != SWING_HORIZONTAL: + if swing_act in (SWING_BOTH, SWING_VERTICAL): + await self._device.command("vert_dir") + if swing_act in (SWING_OFF, SWING_VERTICAL): + await self._device.command("hor_swing") + + async def async_set_preset_mode(self, preset_mode): + """Set new preset mode.""" + if self._on != "1": + if preset_mode == PRESET_NONE: + return + await self.async_turn_on() + + _LOGGER.debug("Setting preset mode of %s to %s", self._unique_id, preset_mode) + + if preset_mode == PRESET_ECO: + await self._device.command("energysave_on") + self._previous_state = preset_mode + elif preset_mode == PRESET_BOOST: + await self._device.command("turbo_on") + self._previous_state = preset_mode + elif preset_mode == PRESET_SLEEP: + await self._device.command("sleep_1") + self._previous_state = self._hvac_mode + elif preset_mode == "sleep_2": + await self._device.command("sleep_2") + self._previous_state = self._hvac_mode + elif preset_mode == "sleep_3": + await self._device.command("sleep_3") + self._previous_state = self._hvac_mode + elif preset_mode == "sleep_4": + await self._device.command("sleep_4") + self._previous_state = self._hvac_mode + elif self._previous_state is not None: + if self._previous_state == PRESET_ECO: + await self._device.command("energysave_off") + elif self._previous_state == PRESET_BOOST: + await self._device.command("turbo_off") + elif self._previous_state in HA_STATE_TO_AC: + await self._device.command(HA_STATE_TO_AC[self._previous_state]) + self._previous_state = None + + async def async_set_hvac_mode(self, hvac_mode): + """Set new operation mode.""" + _LOGGER.debug("Setting operation mode of %s to %s", self._unique_id, hvac_mode) + if hvac_mode == HVAC_MODE_OFF: + await self.async_turn_off() + else: + await self._device.command(HA_STATE_TO_AC[hvac_mode]) + if self._on != "1": + await self.async_turn_on() + + async def async_turn_on(self): + """Turn on.""" + _LOGGER.debug("Turning %s on", self._unique_id) + await self._device.command("on") + + async def async_turn_off(self): + """Turn off.""" + _LOGGER.debug("Turning %s off", self._unique_id) + await self._device.command("off") diff --git a/homeassistant/components/hisense_aehw4a1/config_flow.py b/homeassistant/components/hisense_aehw4a1/config_flow.py new file mode 100644 index 000000000000..52926ba79687 --- /dev/null +++ b/homeassistant/components/hisense_aehw4a1/config_flow.py @@ -0,0 +1,22 @@ +"""Config flow for Hisense AEH-W4A1 integration.""" +import logging + +from pyaehw4a1.aehw4a1 import AehW4a1 + +from homeassistant import config_entries +from homeassistant.helpers import config_entry_flow + +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +async def _async_has_devices(hass): + """Return if there are devices that can be discovered.""" + aehw4a1_ip_addresses = await AehW4a1().discovery() + return len(aehw4a1_ip_addresses) > 0 + + +config_entry_flow.register_discovery_flow( + DOMAIN, "Hisense AEH-W4A1", _async_has_devices, config_entries.CONN_CLASS_LOCAL_POLL +) diff --git a/homeassistant/components/hisense_aehw4a1/const.py b/homeassistant/components/hisense_aehw4a1/const.py new file mode 100644 index 000000000000..8f381492b62b --- /dev/null +++ b/homeassistant/components/hisense_aehw4a1/const.py @@ -0,0 +1,3 @@ +"""Constants for the Hisense AEH-W4A1 integration.""" + +DOMAIN = "hisense_aehw4a1" diff --git a/homeassistant/components/hisense_aehw4a1/manifest.json b/homeassistant/components/hisense_aehw4a1/manifest.json new file mode 100644 index 000000000000..e4bdf581f9c9 --- /dev/null +++ b/homeassistant/components/hisense_aehw4a1/manifest.json @@ -0,0 +1,13 @@ +{ + "domain": "hisense_aehw4a1", + "name": "Hisense AEH-W4A1", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/hisense_aehw4a1", + "requirements": [ + "pyaehw4a1==0.3.1" + ], + "dependencies": [], + "codeowners": [ + "@bannhead" + ] +} diff --git a/homeassistant/components/hisense_aehw4a1/strings.json b/homeassistant/components/hisense_aehw4a1/strings.json new file mode 100644 index 000000000000..67031c41710d --- /dev/null +++ b/homeassistant/components/hisense_aehw4a1/strings.json @@ -0,0 +1,15 @@ +{ + "config": { + "title": "Hisense AEH-W4A1", + "step": { + "confirm": { + "title": "Hisense AEH-W4A1", + "description": "Do you want to set up Hisense AEH-W4A1?" + } + }, + "abort": { + "single_instance_allowed": "Only a single configuration of Hisense AEH-W4A1 is possible.", + "no_devices_found": "No Hisense AEH-W4A1 devices found on the network." + } + } +} diff --git a/homeassistant/components/history/__init__.py b/homeassistant/components/history/__init__.py index 65607d0f8bfa..133151c7f735 100644 --- a/homeassistant/components/history/__init__.py +++ b/homeassistant/components/history/__init__.py @@ -5,22 +5,23 @@ import logging import time +from sqlalchemy import and_, func import voluptuous as vol +from homeassistant.components import recorder, script +from homeassistant.components.http import HomeAssistantView +from homeassistant.components.recorder.models import States +from homeassistant.components.recorder.util import execute, session_scope from homeassistant.const import ( - HTTP_BAD_REQUEST, + ATTR_HIDDEN, CONF_DOMAINS, CONF_ENTITIES, CONF_EXCLUDE, CONF_INCLUDE, + HTTP_BAD_REQUEST, ) -import homeassistant.util.dt as dt_util -from homeassistant.components import recorder, script -from homeassistant.components.http import HomeAssistantView -from homeassistant.const import ATTR_HIDDEN -from homeassistant.components.recorder.util import session_scope, execute import homeassistant.helpers.config_validation as cv - +import homeassistant.util.dt as dt_util # mypy: allow-untyped-defs, no-check-untyped-defs @@ -58,7 +59,6 @@ def get_significant_states( thermostat so that we get current temperature in our graphs). """ timer_start = time.perf_counter() - from homeassistant.components.recorder.models import States with session_scope(hass=hass) as session: query = session.query(States).filter( @@ -94,7 +94,6 @@ def get_significant_states( def state_changes_during_period(hass, start_time, end_time=None, entity_id=None): """Return states changes during UTC period start_time - end_time.""" - from homeassistant.components.recorder.models import States with session_scope(hass=hass) as session: query = session.query(States).filter( @@ -117,7 +116,6 @@ def state_changes_during_period(hass, start_time, end_time=None, entity_id=None) def get_last_state_changes(hass, number_of_states, entity_id): """Return the last number_of_states.""" - from homeassistant.components.recorder.models import States start_time = dt_util.utcnow() @@ -142,7 +140,6 @@ def get_last_state_changes(hass, number_of_states, entity_id): def get_states(hass, utc_point_in_time, entity_ids=None, run=None, filters=None): """Return the states at a specific point in time.""" - from homeassistant.components.recorder.models import States if run is None: run = recorder.run_information(hass, utc_point_in_time) @@ -151,8 +148,6 @@ def get_states(hass, utc_point_in_time, entity_ids=None, run=None, filters=None) if run is None: return [] - from sqlalchemy import and_, func - with session_scope(hass=hass) as session: query = session.query(States) @@ -386,7 +381,6 @@ def apply(self, query, entity_ids=None): * if include and exclude is defined - select the entities specified in the include and filter out the ones from the exclude list. """ - from homeassistant.components.recorder.models import States # specific entities requested - do not in/exclude anything if entity_ids is not None: diff --git a/homeassistant/components/hlk_sw16/__init__.py b/homeassistant/components/hlk_sw16/__init__.py index f174b00613b3..e7264c4e0dd9 100644 --- a/homeassistant/components/hlk_sw16/__init__.py +++ b/homeassistant/components/hlk_sw16/__init__.py @@ -1,23 +1,24 @@ """Support for HLK-SW16 relay switches.""" import logging +from hlk_sw16 import create_hlk_sw16_connection import voluptuous as vol from homeassistant.const import ( CONF_HOST, + CONF_NAME, CONF_PORT, - EVENT_HOMEASSISTANT_STOP, CONF_SWITCHES, - CONF_NAME, + EVENT_HOMEASSISTANT_STOP, ) from homeassistant.core import callback import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity from homeassistant.helpers.discovery import async_load_platform from homeassistant.helpers.dispatcher import ( - async_dispatcher_send, async_dispatcher_connect, + async_dispatcher_send, ) +from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -59,7 +60,6 @@ async def async_setup(hass, config): """Set up the HLK-SW16 switch.""" # Allow platform to specify function to register new unknown devices - from hlk_sw16 import create_hlk_sw16_connection hass.data[DATA_DEVICE_REGISTER] = {} diff --git a/homeassistant/components/homeassistant/scene.py b/homeassistant/components/homeassistant/scene.py index c505d1534deb..576bf540e006 100644 --- a/homeassistant/components/homeassistant/scene.py +++ b/homeassistant/components/homeassistant/scene.py @@ -55,7 +55,22 @@ def _convert_states(states): return result +def _ensure_no_intersection(value): + """Validate that entities and snapshot_entities do not overlap.""" + if ( + CONF_SNAPSHOT not in value + or CONF_ENTITIES not in value + or not any( + entity_id in value[CONF_SNAPSHOT] for entity_id in value[CONF_ENTITIES] + ) + ): + return value + + raise vol.Invalid("entities and snapshot_entities must not overlap") + + CONF_SCENE_ID = "scene_id" +CONF_SNAPSHOT = "snapshot_entities" STATES_SCHEMA = vol.All(dict, _convert_states) @@ -75,8 +90,16 @@ def _convert_states(states): extra=vol.ALLOW_EXTRA, ) -CREATE_SCENE_SCHEMA = vol.Schema( - {vol.Required(CONF_SCENE_ID): cv.slug, vol.Required(CONF_ENTITIES): STATES_SCHEMA} +CREATE_SCENE_SCHEMA = vol.All( + cv.has_at_least_one_key(CONF_ENTITIES, CONF_SNAPSHOT), + _ensure_no_intersection, + vol.Schema( + { + vol.Required(CONF_SCENE_ID): cv.slug, + vol.Optional(CONF_ENTITIES, default={}): STATES_SCHEMA, + vol.Optional(CONF_SNAPSHOT, default=[]): cv.entity_ids, + } + ), ) SERVICE_APPLY = "apply" @@ -139,7 +162,24 @@ async def apply_service(call): async def create_service(call): """Create a scene.""" - scene_config = SCENECONFIG(call.data[CONF_SCENE_ID], call.data[CONF_ENTITIES]) + snapshot = call.data[CONF_SNAPSHOT] + entities = call.data[CONF_ENTITIES] + + for entity_id in snapshot: + state = hass.states.get(entity_id) + if state is None: + _LOGGER.warning( + "Entity %s does not exist and therefore cannot be snapshotted", + entity_id, + ) + continue + entities[entity_id] = State(entity_id, state.state, state.attributes) + + if not entities: + _LOGGER.warning("Empty scenes are not allowed") + return + + scene_config = SCENECONFIG(call.data[CONF_SCENE_ID], entities) entity_id = f"{SCENE_DOMAIN}.{scene_config.name}" old = platform.entities.get(entity_id) if old is not None: diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index 4c300e0a934e..525c091e177b 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -362,8 +362,7 @@ def start(self, *args): return self.status = STATUS_WAIT - # pylint: disable=unused-import - from . import ( # noqa F401 + from . import ( # noqa: F401 pylint: disable=unused-import type_covers, type_fans, type_lights, diff --git a/homeassistant/components/homekit/accessories.py b/homeassistant/components/homekit/accessories.py index 84f0b7894c4d..ddcc795d2625 100644 --- a/homeassistant/components/homekit/accessories.py +++ b/homeassistant/components/homekit/accessories.py @@ -105,8 +105,18 @@ def __init__( battery_found = self.hass.states.get(self.entity_id).attributes.get( ATTR_BATTERY_LEVEL ) + if self.linked_battery_sensor: - battery_found = self.hass.states.get(self.linked_battery_sensor).state + state = self.hass.states.get(self.linked_battery_sensor) + if state is not None: + battery_found = state.state + else: + self.linked_battery_sensor = None + _LOGGER.warning( + "%s: Battery sensor state missing: %s", + self.entity_id, + self.linked_battery_sensor, + ) if battery_found is None: return diff --git a/homeassistant/components/homekit/type_thermostats.py b/homeassistant/components/homekit/type_thermostats.py index 9adc3cc0600b..b6e1e75d3c65 100644 --- a/homeassistant/components/homekit/type_thermostats.py +++ b/homeassistant/components/homekit/type_thermostats.py @@ -7,6 +7,7 @@ ATTR_CURRENT_TEMPERATURE, ATTR_HVAC_ACTION, ATTR_HVAC_MODE, + ATTR_HVAC_MODES, ATTR_MAX_TEMP, ATTR_MIN_TEMP, ATTR_TARGET_TEMP_HIGH, @@ -23,6 +24,8 @@ HVAC_MODE_HEAT, HVAC_MODE_HEAT_COOL, HVAC_MODE_OFF, + HVAC_MODE_AUTO, + HVAC_MODE_FAN_ONLY, SERVICE_SET_HVAC_MODE as SERVICE_SET_HVAC_MODE_THERMOSTAT, SERVICE_SET_TEMPERATURE as SERVICE_SET_TEMPERATURE_THERMOSTAT, SUPPORT_TARGET_TEMPERATURE_RANGE, @@ -60,13 +63,18 @@ _LOGGER = logging.getLogger(__name__) +HC_HOMEKIT_VALID_MODES_WATER_HEATER = { + "Heat": 1, +} UNIT_HASS_TO_HOMEKIT = {TEMP_CELSIUS: 0, TEMP_FAHRENHEIT: 1} UNIT_HOMEKIT_TO_HASS = {c: s for s, c in UNIT_HASS_TO_HOMEKIT.items()} HC_HASS_TO_HOMEKIT = { HVAC_MODE_OFF: 0, HVAC_MODE_HEAT: 1, HVAC_MODE_COOL: 2, + HVAC_MODE_AUTO: 3, HVAC_MODE_HEAT_COOL: 3, + HVAC_MODE_FAN_ONLY: 2, } HC_HOMEKIT_TO_HASS = {c: s for s, c in HC_HASS_TO_HOMEKIT.items()} @@ -97,9 +105,9 @@ def __init__(self, *args): # Add additional characteristics if auto mode is supported self.chars = [] - features = self.hass.states.get(self.entity_id).attributes.get( - ATTR_SUPPORTED_FEATURES, 0 - ) + state = self.hass.states.get(self.entity_id) + features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0) + if features & SUPPORT_TARGET_TEMPERATURE_RANGE: self.chars.extend( (CHAR_COOLING_THRESHOLD_TEMPERATURE, CHAR_HEATING_THRESHOLD_TEMPERATURE) @@ -107,12 +115,44 @@ def __init__(self, *args): serv_thermostat = self.add_preload_service(SERV_THERMOSTAT, self.chars) - # Current and target mode characteristics + # Current mode characteristics self.char_current_heat_cool = serv_thermostat.configure_char( CHAR_CURRENT_HEATING_COOLING, value=0 ) + + # Target mode characteristics + hc_modes = state.attributes.get(ATTR_HVAC_MODES, None) + if hc_modes is None: + _LOGGER.error( + "%s: HVAC modes not yet available. Please disable auto start for homekit.", + self.entity_id, + ) + hc_modes = ( + HVAC_MODE_HEAT, + HVAC_MODE_COOL, + HVAC_MODE_HEAT_COOL, + HVAC_MODE_OFF, + ) + + # determine available modes for this entity, prefer AUTO over HEAT_COOL and COOL over FAN_ONLY + self.hc_homekit_to_hass = { + c: s + for s, c in HC_HASS_TO_HOMEKIT.items() + if ( + s in hc_modes + and not ( + (s == HVAC_MODE_HEAT_COOL and HVAC_MODE_AUTO in hc_modes) + or (s == HVAC_MODE_FAN_ONLY and HVAC_MODE_COOL in hc_modes) + ) + ) + } + hc_valid_values = {k: v for v, k in self.hc_homekit_to_hass.items()} + self.char_target_heat_cool = serv_thermostat.configure_char( - CHAR_TARGET_HEATING_COOLING, value=0, setter_callback=self.set_heat_cool + CHAR_TARGET_HEATING_COOLING, + value=0, + setter_callback=self.set_heat_cool, + valid_values=hc_valid_values, ) # Current and target temperature characteristics @@ -185,7 +225,7 @@ def set_heat_cool(self, value): """Change operation mode to value if call came from HomeKit.""" _LOGGER.debug("%s: Set heat-cool to %d", self.entity_id, value) self._flag_heat_cool = True - hass_value = HC_HOMEKIT_TO_HASS[value] + hass_value = self.hc_homekit_to_hass[value] params = {ATTR_ENTITY_ID: self.entity_id, ATTR_HVAC_MODE: hass_value} self.call_service( DOMAIN_CLIMATE, SERVICE_SET_HVAC_MODE_THERMOSTAT, params, hass_value @@ -318,7 +358,10 @@ def __init__(self, *args): CHAR_CURRENT_HEATING_COOLING, value=1 ) self.char_target_heat_cool = serv_thermostat.configure_char( - CHAR_TARGET_HEATING_COOLING, value=1, setter_callback=self.set_heat_cool + CHAR_TARGET_HEATING_COOLING, + value=1, + setter_callback=self.set_heat_cool, + valid_values=HC_HOMEKIT_VALID_MODES_WATER_HEATER, ) self.char_current_temp = serv_thermostat.configure_char( diff --git a/homeassistant/components/homekit_controller/.translations/bg.json b/homeassistant/components/homekit_controller/.translations/bg.json index f8ce05b4bbe2..b1909ca2ec00 100644 --- a/homeassistant/components/homekit_controller/.translations/bg.json +++ b/homeassistant/components/homekit_controller/.translations/bg.json @@ -24,7 +24,7 @@ "data": { "pairing_code": "\u041a\u043e\u0434 \u0437\u0430 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" }, - "description": "\u0412\u044a\u0432\u0435\u0434\u0435\u0442\u0435 HomeKit \u043a\u043e\u0434\u0430 \u0437\u0430 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435 \u0437\u0430 \u0434\u0430 \u0438\u0437\u043f\u043e\u043b\u0437\u0432\u0430\u0442\u0435 \u0442\u043e\u0432\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e", + "description": "\u0412\u044a\u0432\u0435\u0434\u0435\u0442\u0435 HomeKit \u043a\u043e\u0434\u0430 \u0437\u0430 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435 (\u0432\u044a\u0432 \u0444\u043e\u0440\u043c\u0430\u0442 XXX-XX-XXX) \u0437\u0430 \u0434\u0430 \u0438\u0437\u043f\u043e\u043b\u0437\u0432\u0430\u0442\u0435 \u0442\u043e\u0432\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e", "title": "\u0421\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435 \u0441 HomeKit \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e" }, "user": { diff --git a/homeassistant/components/homekit_controller/.translations/pt.json b/homeassistant/components/homekit_controller/.translations/pt.json index 37f68408ce44..c60ed155569f 100644 --- a/homeassistant/components/homekit_controller/.translations/pt.json +++ b/homeassistant/components/homekit_controller/.translations/pt.json @@ -4,7 +4,8 @@ "pair": { "data": { "pairing_code": "C\u00f3digo de emparelhamento" - } + }, + "title": "Emparelhar com o acess\u00f3rio HomeKit" }, "user": { "data": { diff --git a/homeassistant/components/homekit_controller/__init__.py b/homeassistant/components/homekit_controller/__init__.py index 6a6492847225..6b53301e8779 100644 --- a/homeassistant/components/homekit_controller/__init__.py +++ b/homeassistant/components/homekit_controller/__init__.py @@ -13,7 +13,7 @@ from .config_flow import HomekitControllerFlowHandler # noqa: F401 from .connection import get_accessory_information, HKDevice from .const import CONTROLLER, ENTITY_MAP, KNOWN_DEVICES -from .const import DOMAIN # noqa: pylint: disable=unused-import +from .const import DOMAIN from .storage import EntityMapStorage _LOGGER = logging.getLogger(__name__) @@ -109,7 +109,6 @@ def _setup_characteristic(self, char): setup_fn = getattr(self, f"_setup_{setup_fn_name}", None) if not setup_fn: return - # pylint: disable=not-callable setup_fn(char) @callback @@ -132,7 +131,6 @@ def async_state_changed(self): if not update_fn: continue - # pylint: disable=not-callable update_fn(result["value"]) self.async_write_ha_state() diff --git a/homeassistant/components/homekit_controller/alarm_control_panel.py b/homeassistant/components/homekit_controller/alarm_control_panel.py index bb45a6c33d93..8cdbe9b2f369 100644 --- a/homeassistant/components/homekit_controller/alarm_control_panel.py +++ b/homeassistant/components/homekit_controller/alarm_control_panel.py @@ -4,6 +4,11 @@ from homekit.model.characteristics import CharacteristicsTypes from homeassistant.components.alarm_control_panel import AlarmControlPanel +from homeassistant.components.alarm_control_panel.const import ( + SUPPORT_ALARM_ARM_AWAY, + SUPPORT_ALARM_ARM_HOME, + SUPPORT_ALARM_ARM_NIGHT, +) from homeassistant.const import ( ATTR_BATTERY_LEVEL, STATE_ALARM_ARMED_AWAY, @@ -88,6 +93,11 @@ def state(self): """Return the state of the device.""" return self._state + @property + def supported_features(self) -> int: + """Return the list of supported features.""" + return SUPPORT_ALARM_ARM_HOME | SUPPORT_ALARM_ARM_AWAY | SUPPORT_ALARM_ARM_NIGHT + async def async_alarm_disarm(self, code=None): """Send disarm command.""" await self.set_alarm_state(STATE_ALARM_DISARMED, code) diff --git a/homeassistant/components/homematic/manifest.json b/homeassistant/components/homematic/manifest.json index 5db547e3f0a4..8a86fd19c7d3 100644 --- a/homeassistant/components/homematic/manifest.json +++ b/homeassistant/components/homematic/manifest.json @@ -3,7 +3,7 @@ "name": "Homematic", "documentation": "https://www.home-assistant.io/integrations/homematic", "requirements": [ - "pyhomematic==0.1.61" + "pyhomematic==0.1.62" ], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/homematicip_cloud/__init__.py b/homeassistant/components/homematicip_cloud/__init__.py index 8cd41e0b980a..62f3f9ec5d4d 100644 --- a/homeassistant/components/homematicip_cloud/__init__.py +++ b/homeassistant/components/homematicip_cloud/__init__.py @@ -1,8 +1,10 @@ """Support for HomematicIP Cloud devices.""" import logging from pathlib import Path +from typing import Optional from homematicip.aio.group import AsyncHeatingGroup +from homematicip.aio.home import AsyncHome from homematicip.base.helpers import handle_config import voluptuous as vol @@ -135,7 +137,7 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: ) ) - async def _async_activate_eco_mode_with_duration(service): + async def _async_activate_eco_mode_with_duration(service) -> None: """Service to activate eco mode with duration.""" duration = service.data[ATTR_DURATION] hapid = service.data.get(ATTR_ACCESSPOINT_ID) @@ -155,7 +157,7 @@ async def _async_activate_eco_mode_with_duration(service): schema=SCHEMA_ACTIVATE_ECO_MODE_WITH_DURATION, ) - async def _async_activate_eco_mode_with_period(service): + async def _async_activate_eco_mode_with_period(service) -> None: """Service to activate eco mode with period.""" endtime = service.data[ATTR_ENDTIME] hapid = service.data.get(ATTR_ACCESSPOINT_ID) @@ -175,7 +177,7 @@ async def _async_activate_eco_mode_with_period(service): schema=SCHEMA_ACTIVATE_ECO_MODE_WITH_PERIOD, ) - async def _async_activate_vacation(service): + async def _async_activate_vacation(service) -> None: """Service to activate vacation.""" endtime = service.data[ATTR_ENDTIME] temperature = service.data[ATTR_TEMPERATURE] @@ -196,7 +198,7 @@ async def _async_activate_vacation(service): schema=SCHEMA_ACTIVATE_VACATION, ) - async def _async_deactivate_eco_mode(service): + async def _async_deactivate_eco_mode(service) -> None: """Service to deactivate eco mode.""" hapid = service.data.get(ATTR_ACCESSPOINT_ID) @@ -215,7 +217,7 @@ async def _async_deactivate_eco_mode(service): schema=SCHEMA_DEACTIVATE_ECO_MODE, ) - async def _async_deactivate_vacation(service): + async def _async_deactivate_vacation(service) -> None: """Service to deactivate vacation.""" hapid = service.data.get(ATTR_ACCESSPOINT_ID) @@ -234,7 +236,7 @@ async def _async_deactivate_vacation(service): schema=SCHEMA_DEACTIVATE_VACATION, ) - async def _set_active_climate_profile(service): + async def _set_active_climate_profile(service) -> None: """Service to set the active climate profile.""" entity_id_list = service.data[ATTR_ENTITY_ID] climate_profile_index = service.data[ATTR_CLIMATE_PROFILE_INDEX] - 1 @@ -257,7 +259,7 @@ async def _set_active_climate_profile(service): schema=SCHEMA_SET_ACTIVE_CLIMATE_PROFILE, ) - async def _async_dump_hap_config(service): + async def _async_dump_hap_config(service) -> None: """Service to dump the configuration of a Homematic IP Access Point.""" config_path = ( service.data.get(ATTR_CONFIG_OUTPUT_PATH) or hass.config.config_dir @@ -287,7 +289,7 @@ async def _async_dump_hap_config(service): schema=SCHEMA_DUMP_HAP_CONFIG, ) - def _get_home(hapid: str): + def _get_home(hapid: str) -> Optional[AsyncHome]: """Return a HmIP home.""" hap = hass.data[DOMAIN].get(hapid) if hap: @@ -324,7 +326,7 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool return True -async def async_unload_entry(hass, entry): +async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool: """Unload a config entry.""" hap = hass.data[DOMAIN].pop(entry.data[HMIPC_HAPID]) return await hap.async_reset() diff --git a/homeassistant/components/homematicip_cloud/alarm_control_panel.py b/homeassistant/components/homematicip_cloud/alarm_control_panel.py index 8ebb35b12c15..f9a912034264 100644 --- a/homeassistant/components/homematicip_cloud/alarm_control_panel.py +++ b/homeassistant/components/homematicip_cloud/alarm_control_panel.py @@ -1,9 +1,14 @@ """Support for HomematicIP Cloud alarm control panel.""" import logging +from typing import Any, Dict from homematicip.functionalHomes import SecurityAndAlarmHome from homeassistant.components.alarm_control_panel import AlarmControlPanel +from homeassistant.components.alarm_control_panel.const import ( + SUPPORT_ALARM_ARM_AWAY, + SUPPORT_ALARM_ARM_HOME, +) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( STATE_ALARM_ARMED_AWAY, @@ -21,7 +26,9 @@ CONST_ALARM_CONTROL_PANEL_NAME = "HmIP Alarm Control Panel" -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): +async def async_setup_platform( + hass, config, async_add_entities, discovery_info=None +) -> None: """Set up the HomematicIP Cloud alarm control devices.""" pass @@ -40,9 +47,10 @@ class HomematicipAlarmControlPanel(AlarmControlPanel): def __init__(self, hap: HomematicipHAP) -> None: """Initialize the alarm control panel.""" self._home = hap.home + _LOGGER.info("Setting up %s", self.name) @property - def device_info(self): + def device_info(self) -> Dict[str, Any]: """Return device specific attributes.""" return { "identifiers": {(HMIPC_DOMAIN, f"ACP {self._home.id}")}, @@ -70,26 +78,31 @@ def state(self) -> str: return STATE_ALARM_DISARMED @property - def _security_and_alarm(self): + def _security_and_alarm(self) -> SecurityAndAlarmHome: return self._home.get_functionalHome(SecurityAndAlarmHome) - async def async_alarm_disarm(self, code=None): + @property + def supported_features(self) -> int: + """Return the list of supported features.""" + return SUPPORT_ALARM_ARM_HOME | SUPPORT_ALARM_ARM_AWAY + + async def async_alarm_disarm(self, code=None) -> None: """Send disarm command.""" await self._home.set_security_zones_activation(False, False) - async def async_alarm_arm_home(self, code=None): + async def async_alarm_arm_home(self, code=None) -> None: """Send arm home command.""" await self._home.set_security_zones_activation(False, True) - async def async_alarm_arm_away(self, code=None): + async def async_alarm_arm_away(self, code=None) -> None: """Send arm away command.""" await self._home.set_security_zones_activation(True, True) - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Register callbacks.""" self._home.on_update(self._async_device_changed) - def _async_device_changed(self, *args, **kwargs): + def _async_device_changed(self, *args, **kwargs) -> None: """Handle device state changes.""" _LOGGER.debug("Event %s (%s)", self.name, CONST_ALARM_CONTROL_PANEL_NAME) self.async_schedule_update_ha_state() diff --git a/homeassistant/components/homematicip_cloud/binary_sensor.py b/homeassistant/components/homematicip_cloud/binary_sensor.py index b5b663055a11..83d48d0a7b19 100644 --- a/homeassistant/components/homematicip_cloud/binary_sensor.py +++ b/homeassistant/components/homematicip_cloud/binary_sensor.py @@ -1,5 +1,6 @@ """Support for HomematicIP Cloud binary sensor.""" import logging +from typing import Any, Dict from homematicip.aio.device import ( AsyncAccelerationSensor, @@ -72,7 +73,9 @@ } -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): +async def async_setup_platform( + hass, config, async_add_entities, discovery_info=None +) -> None: """Set up the HomematicIP Cloud binary sensor devices.""" pass @@ -82,17 +85,17 @@ async def async_setup_entry( ) -> None: """Set up the HomematicIP Cloud binary sensor from a config entry.""" hap = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]] - devices = [] + entities = [] for device in hap.home.devices: if isinstance(device, AsyncAccelerationSensor): - devices.append(HomematicipAccelerationSensor(hap, device)) + entities.append(HomematicipAccelerationSensor(hap, device)) if isinstance(device, (AsyncContactInterface, AsyncFullFlushContactInterface)): - devices.append(HomematicipContactInterface(hap, device)) + entities.append(HomematicipContactInterface(hap, device)) if isinstance( device, (AsyncShutterContact, AsyncShutterContactMagnetic, AsyncRotaryHandleSensor), ): - devices.append(HomematicipShutterContact(hap, device)) + entities.append(HomematicipShutterContact(hap, device)) if isinstance( device, ( @@ -101,31 +104,31 @@ async def async_setup_entry( AsyncMotionDetectorPushButton, ), ): - devices.append(HomematicipMotionDetector(hap, device)) + entities.append(HomematicipMotionDetector(hap, device)) if isinstance(device, AsyncPresenceDetectorIndoor): - devices.append(HomematicipPresenceDetector(hap, device)) + entities.append(HomematicipPresenceDetector(hap, device)) if isinstance(device, AsyncSmokeDetector): - devices.append(HomematicipSmokeDetector(hap, device)) + entities.append(HomematicipSmokeDetector(hap, device)) if isinstance(device, AsyncWaterSensor): - devices.append(HomematicipWaterDetector(hap, device)) + entities.append(HomematicipWaterDetector(hap, device)) if isinstance(device, (AsyncWeatherSensorPlus, AsyncWeatherSensorPro)): - devices.append(HomematicipRainSensor(hap, device)) + entities.append(HomematicipRainSensor(hap, device)) if isinstance( device, (AsyncWeatherSensor, AsyncWeatherSensorPlus, AsyncWeatherSensorPro) ): - devices.append(HomematicipStormSensor(hap, device)) - devices.append(HomematicipSunshineSensor(hap, device)) + entities.append(HomematicipStormSensor(hap, device)) + entities.append(HomematicipSunshineSensor(hap, device)) if isinstance(device, AsyncDevice) and device.lowBat is not None: - devices.append(HomematicipBatterySensor(hap, device)) + entities.append(HomematicipBatterySensor(hap, device)) for group in hap.home.groups: if isinstance(group, AsyncSecurityGroup): - devices.append(HomematicipSecuritySensorGroup(hap, group)) + entities.append(HomematicipSecuritySensorGroup(hap, group)) elif isinstance(group, AsyncSecurityZoneGroup): - devices.append(HomematicipSecurityZoneSensorGroup(hap, group)) + entities.append(HomematicipSecurityZoneSensorGroup(hap, group)) - if devices: - async_add_entities(devices) + if entities: + async_add_entities(entities) class HomematicipAccelerationSensor(HomematicipGenericDevice, BinarySensorDevice): @@ -142,7 +145,7 @@ def is_on(self) -> bool: return self._device.accelerationSensorTriggered @property - def device_state_attributes(self): + def device_state_attributes(self) -> Dict[str, Any]: """Return the state attributes of the acceleration sensor.""" state_attr = super().device_state_attributes @@ -296,7 +299,7 @@ def is_on(self) -> bool: return self._device.sunshine @property - def device_state_attributes(self): + def device_state_attributes(self) -> Dict[str, Any]: """Return the state attributes of the illuminance sensor.""" state_attr = super().device_state_attributes @@ -346,7 +349,7 @@ def available(self) -> bool: return True @property - def device_state_attributes(self): + def device_state_attributes(self) -> Dict[str, Any]: """Return the state attributes of the security zone group.""" state_attr = super().device_state_attributes @@ -390,7 +393,7 @@ def __init__(self, hap: HomematicipHAP, device) -> None: super().__init__(hap, device, "Sensors") @property - def device_state_attributes(self): + def device_state_attributes(self) -> Dict[str, Any]: """Return the state attributes of the security group.""" state_attr = super().device_state_attributes diff --git a/homeassistant/components/homematicip_cloud/climate.py b/homeassistant/components/homematicip_cloud/climate.py index 9673459e820d..e3c922dc5775 100644 --- a/homeassistant/components/homematicip_cloud/climate.py +++ b/homeassistant/components/homematicip_cloud/climate.py @@ -1,6 +1,6 @@ """Support for HomematicIP Cloud climate devices.""" import logging -from typing import Awaitable +from typing import Any, Dict, List, Optional, Union from homematicip.aio.device import AsyncHeatingThermostat, AsyncHeatingThermostatCompact from homematicip.aio.group import AsyncHeatingGroup @@ -10,6 +10,8 @@ from homeassistant.components.climate import ClimateDevice from homeassistant.components.climate.const import ( + CURRENT_HVAC_HEAT, + CURRENT_HVAC_IDLE, HVAC_MODE_AUTO, HVAC_MODE_COOL, HVAC_MODE_HEAT, @@ -41,7 +43,9 @@ HMIP_ECO_CM = "ECO" -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): +async def async_setup_platform( + hass, config, async_add_entities, discovery_info=None +) -> None: """Set up the HomematicIP Cloud climate devices.""" pass @@ -51,13 +55,13 @@ async def async_setup_entry( ) -> None: """Set up the HomematicIP climate from a config entry.""" hap = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]] - devices = [] + entities = [] for device in hap.home.groups: if isinstance(device, AsyncHeatingGroup): - devices.append(HomematicipHeatingGroup(hap, device)) + entities.append(HomematicipHeatingGroup(hap, device)) - if devices: - async_add_entities(devices) + if entities: + async_add_entities(entities) class HomematicipHeatingGroup(HomematicipGenericDevice, ClimateDevice): @@ -74,10 +78,10 @@ def __init__(self, hap: HomematicipHAP, device: AsyncHeatingGroup) -> None: super().__init__(hap, device) self._simple_heating = None if device.actualTemperature is None: - self._simple_heating = self._get_first_radiator_thermostat() + self._simple_heating = self._first_radiator_thermostat @property - def device_info(self): + def device_info(self) -> Dict[str, Any]: """Return device specific attributes.""" return { "identifiers": {(HMIPC_DOMAIN, self._device.id)}, @@ -127,7 +131,7 @@ def hvac_mode(self) -> str: return HVAC_MODE_AUTO @property - def hvac_modes(self): + def hvac_modes(self) -> List[str]: """Return the list of available hvac operation modes.""" if self._disabled_by_cooling_mode and not self._has_switch: return [HVAC_MODE_OFF] @@ -139,7 +143,25 @@ def hvac_modes(self): ) @property - def preset_mode(self): + def hvac_action(self) -> Optional[str]: + """ + Return the current hvac_action. + + This is only relevant for radiator thermostats. + """ + if ( + self._device.floorHeatingMode == "RADIATOR" + and self._has_radiator_thermostat + and self._heat_mode_enabled + ): + return ( + CURRENT_HVAC_HEAT if self._device.valvePosition else CURRENT_HVAC_IDLE + ) + + return None + + @property + def preset_mode(self) -> Optional[str]: """Return the current preset mode.""" if self._device.boostMode: return PRESET_BOOST @@ -162,7 +184,7 @@ def preset_mode(self): ) @property - def preset_modes(self): + def preset_modes(self) -> List[str]: """Return a list of available preset modes incl. hmip profiles.""" # Boost is only available if a radiator thermostat is in the room, # and heat mode is enabled. @@ -190,7 +212,7 @@ def max_temp(self) -> float: """Return the maximum temperature.""" return self._device.maxTemperature - async def async_set_temperature(self, **kwargs): + async def async_set_temperature(self, **kwargs) -> None: """Set new target temperature.""" temperature = kwargs.get(ATTR_TEMPERATURE) if temperature is None: @@ -199,7 +221,7 @@ async def async_set_temperature(self, **kwargs): if self.min_temp <= temperature <= self.max_temp: await self._device.set_point_temperature(temperature) - async def async_set_hvac_mode(self, hvac_mode: str) -> Awaitable[None]: + async def async_set_hvac_mode(self, hvac_mode: str) -> None: """Set new target hvac mode.""" if hvac_mode not in self.hvac_modes: return @@ -209,7 +231,7 @@ async def async_set_hvac_mode(self, hvac_mode: str) -> Awaitable[None]: else: await self._device.set_control_mode(HMIP_MANUAL_CM) - async def async_set_preset_mode(self, preset_mode: str) -> Awaitable[None]: + async def async_set_preset_mode(self, preset_mode: str) -> None: """Set new preset mode.""" if preset_mode not in self.preset_modes: return @@ -225,7 +247,7 @@ async def async_set_preset_mode(self, preset_mode: str) -> Awaitable[None]: await self._device.set_active_profile(profile_idx) @property - def device_state_attributes(self): + def device_state_attributes(self) -> Dict[str, Any]: """Return the state attributes of the access point.""" state_attr = super().device_state_attributes @@ -242,12 +264,12 @@ def device_state_attributes(self): return state_attr @property - def _indoor_climate(self): + def _indoor_climate(self) -> IndoorClimateHome: """Return the hmip indoor climate functional home of this group.""" return self._home.get_functionalHome(IndoorClimateHome) @property - def _device_profiles(self): + def _device_profiles(self) -> List[str]: """Return the relevant profiles.""" return [ profile @@ -258,11 +280,11 @@ def _device_profiles(self): ] @property - def _device_profile_names(self): + def _device_profile_names(self) -> List[str]: """Return a collection of profile names.""" return [profile.name for profile in self._device_profiles] - def _get_profile_idx_by_name(self, profile_name): + def _get_profile_idx_by_name(self, profile_name: str) -> int: """Return a profile index by name.""" relevant_index = self._relevant_profile_group index_name = [ @@ -274,19 +296,19 @@ def _get_profile_idx_by_name(self, profile_name): return relevant_index[index_name[0]] @property - def _heat_mode_enabled(self): + def _heat_mode_enabled(self) -> bool: """Return, if heating mode is enabled.""" return not self._device.cooling @property - def _disabled_by_cooling_mode(self): + def _disabled_by_cooling_mode(self) -> bool: """Return, if group is disabled by the cooling mode.""" return self._device.cooling and ( self._device.coolingIgnored or not self._device.coolingAllowed ) @property - def _relevant_profile_group(self): + def _relevant_profile_group(self) -> List[str]: """Return the relevant profile groups.""" if self._disabled_by_cooling_mode: return [] @@ -305,9 +327,12 @@ def _has_switch(self) -> bool: @property def _has_radiator_thermostat(self) -> bool: """Return, if a radiator thermostat is in the hmip heating group.""" - return bool(self._get_first_radiator_thermostat()) + return bool(self._first_radiator_thermostat) - def _get_first_radiator_thermostat(self): + @property + def _first_radiator_thermostat( + self, + ) -> Optional[Union[AsyncHeatingThermostat, AsyncHeatingThermostatCompact]]: """Return the first radiator thermostat from the hmip heating group.""" for device in self._device.devices: if isinstance( diff --git a/homeassistant/components/homematicip_cloud/config_flow.py b/homeassistant/components/homematicip_cloud/config_flow.py index 1488f02f13b0..8d85dfda3289 100644 --- a/homeassistant/components/homematicip_cloud/config_flow.py +++ b/homeassistant/components/homematicip_cloud/config_flow.py @@ -1,5 +1,5 @@ """Config flow to configure the HomematicIP Cloud component.""" -from typing import Set +from typing import Any, Dict, Set import voluptuous as vol @@ -34,15 +34,15 @@ class HomematicipCloudFlowHandler(config_entries.ConfigFlow): VERSION = 1 CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_PUSH - def __init__(self): + def __init__(self) -> None: """Initialize HomematicIP Cloud config flow.""" self.auth = None - async def async_step_user(self, user_input=None): + async def async_step_user(self, user_input=None) -> Dict[str, Any]: """Handle a flow initialized by the user.""" return await self.async_step_init(user_input) - async def async_step_init(self, user_input=None): + async def async_step_init(self, user_input=None) -> Dict[str, Any]: """Handle a flow start.""" errors = {} @@ -69,7 +69,7 @@ async def async_step_init(self, user_input=None): errors=errors, ) - async def async_step_link(self, user_input=None): + async def async_step_link(self, user_input=None) -> Dict[str, Any]: """Attempt to link with the HomematicIP Cloud access point.""" errors = {} @@ -91,7 +91,7 @@ async def async_step_link(self, user_input=None): return self.async_show_form(step_id="link", errors=errors) - async def async_step_import(self, import_info): + async def async_step_import(self, import_info) -> Dict[str, Any]: """Import a new access point as a config entry.""" hapid = import_info[HMIPC_HAPID] authtoken = import_info[HMIPC_AUTHTOKEN] diff --git a/homeassistant/components/homematicip_cloud/cover.py b/homeassistant/components/homematicip_cloud/cover.py index 63ac6f7310cb..ef8cbacfde2a 100644 --- a/homeassistant/components/homematicip_cloud/cover.py +++ b/homeassistant/components/homematicip_cloud/cover.py @@ -22,7 +22,9 @@ HMIP_SLATS_CLOSED = 1 -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): +async def async_setup_platform( + hass, config, async_add_entities, discovery_info=None +) -> None: """Set up the HomematicIP Cloud cover devices.""" pass @@ -32,15 +34,15 @@ async def async_setup_entry( ) -> None: """Set up the HomematicIP cover from a config entry.""" hap = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]] - devices = [] + entities = [] for device in hap.home.devices: if isinstance(device, AsyncFullFlushBlind): - devices.append(HomematicipCoverSlats(hap, device)) + entities.append(HomematicipCoverSlats(hap, device)) elif isinstance(device, AsyncFullFlushShutter): - devices.append(HomematicipCoverShutter(hap, device)) + entities.append(HomematicipCoverShutter(hap, device)) - if devices: - async_add_entities(devices) + if entities: + async_add_entities(entities) class HomematicipCoverShutter(HomematicipGenericDevice, CoverDevice): @@ -51,7 +53,7 @@ def current_cover_position(self) -> int: """Return current position of cover.""" return int((1 - self._device.shutterLevel) * 100) - async def async_set_cover_position(self, **kwargs): + async def async_set_cover_position(self, **kwargs) -> None: """Move the cover to a specific position.""" position = kwargs[ATTR_POSITION] # HmIP cover is closed:1 -> open:0 @@ -65,15 +67,15 @@ def is_closed(self) -> Optional[bool]: return self._device.shutterLevel == HMIP_COVER_CLOSED return None - async def async_open_cover(self, **kwargs): + async def async_open_cover(self, **kwargs) -> None: """Open the cover.""" await self._device.set_shutter_level(HMIP_COVER_OPEN) - async def async_close_cover(self, **kwargs): + async def async_close_cover(self, **kwargs) -> None: """Close the cover.""" await self._device.set_shutter_level(HMIP_COVER_CLOSED) - async def async_stop_cover(self, **kwargs): + async def async_stop_cover(self, **kwargs) -> None: """Stop the device if in motion.""" await self._device.set_shutter_stop() @@ -86,21 +88,21 @@ def current_cover_tilt_position(self) -> int: """Return current tilt position of cover.""" return int((1 - self._device.slatsLevel) * 100) - async def async_set_cover_tilt_position(self, **kwargs): + async def async_set_cover_tilt_position(self, **kwargs) -> None: """Move the cover to a specific tilt position.""" position = kwargs[ATTR_TILT_POSITION] # HmIP slats is closed:1 -> open:0 level = 1 - position / 100.0 await self._device.set_slats_level(level) - async def async_open_cover_tilt(self, **kwargs): + async def async_open_cover_tilt(self, **kwargs) -> None: """Open the slats.""" await self._device.set_slats_level(HMIP_SLATS_OPEN) - async def async_close_cover_tilt(self, **kwargs): + async def async_close_cover_tilt(self, **kwargs) -> None: """Close the slats.""" await self._device.set_slats_level(HMIP_SLATS_CLOSED) - async def async_stop_cover_tilt(self, **kwargs): + async def async_stop_cover_tilt(self, **kwargs) -> None: """Stop the device if in motion.""" await self._device.set_shutter_stop() diff --git a/homeassistant/components/homematicip_cloud/device.py b/homeassistant/components/homematicip_cloud/device.py index 6c81775b6882..f35b696767ce 100644 --- a/homeassistant/components/homematicip_cloud/device.py +++ b/homeassistant/components/homematicip_cloud/device.py @@ -1,6 +1,6 @@ """Generic device for the HomematicIP Cloud component.""" import logging -from typing import Optional +from typing import Any, Dict, Optional from homematicip.aio.device import AsyncDevice from homematicip.aio.group import AsyncGroup @@ -79,7 +79,7 @@ def __init__(self, hap: HomematicipHAP, device, post: Optional[str] = None) -> N _LOGGER.info("Setting up %s (%s)", self.name, self._device.modelType) @property - def device_info(self): + def device_info(self) -> Dict[str, Any]: """Return device specific attributes.""" # Only physical devices should be HA devices. if isinstance(self._device, AsyncDevice): @@ -96,14 +96,14 @@ def device_info(self): } return None - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Register callbacks.""" self._hap.hmip_device_by_entity_id[self.entity_id] = self._device self._device.on_update(self._async_device_changed) self._device.on_remove(self._async_device_removed) @callback - def _async_device_changed(self, *args, **kwargs): + def _async_device_changed(self, *args, **kwargs) -> None: """Handle device state changes.""" # Don't update disabled entities if self.enabled: @@ -152,7 +152,7 @@ async def async_remove_from_registries(self) -> None: entity_registry.async_remove(entity_id) @callback - def _async_device_removed(self, *args, **kwargs): + def _async_device_removed(self, *args, **kwargs) -> None: """Handle hmip device removal.""" # Set marker showing that the HmIP device hase been removed. self.hmip_device_removed = True @@ -193,7 +193,7 @@ def icon(self) -> Optional[str]: return None @property - def device_state_attributes(self): + def device_state_attributes(self) -> Dict[str, Any]: """Return the state attributes of the generic device.""" state_attr = {} diff --git a/homeassistant/components/homematicip_cloud/hap.py b/homeassistant/components/homematicip_cloud/hap.py index bef04180c6f0..63bdf3166ebf 100644 --- a/homeassistant/components/homematicip_cloud/hap.py +++ b/homeassistant/components/homematicip_cloud/hap.py @@ -22,13 +22,13 @@ class HomematicipAuth: """Manages HomematicIP client registration.""" - def __init__(self, hass, config): + def __init__(self, hass, config) -> None: """Initialize HomematicIP Cloud client registration.""" self.hass = hass self.config = config self.auth = None - async def async_setup(self): + async def async_setup(self) -> bool: """Connect to HomematicIP for registration.""" try: self.auth = await self.get_auth( @@ -38,7 +38,7 @@ async def async_setup(self): except HmipcConnectionError: return False - async def async_checkbutton(self): + async def async_checkbutton(self) -> bool: """Check blue butten has been pressed.""" try: return await self.auth.isRequestAcknowledged() @@ -82,7 +82,7 @@ def __init__(self, hass: HomeAssistantType, config_entry: ConfigEntry) -> None: self._accesspoint_connected = True self.hmip_device_by_entity_id = {} - async def async_setup(self, tries: int = 0): + async def async_setup(self, tries: int = 0) -> bool: """Initialize connection.""" try: self.home = await self.get_hap( @@ -108,7 +108,7 @@ async def async_setup(self, tries: int = 0): return True @callback - def async_update(self, *args, **kwargs): + def async_update(self, *args, **kwargs) -> None: """Async update the home device. Triggered when the HMIP HOME_CHANGED event has fired. @@ -141,23 +141,23 @@ def async_update(self, *args, **kwargs): self.home.update_home_only(args[0]) @callback - def async_create_entity(self, *args, **kwargs): + def async_create_entity(self, *args, **kwargs) -> None: """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): + async def async_create_entity_lazy(self, is_device=True) -> None: """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): + async def get_state(self) -> None: """Update HMIP state and tell Home Assistant.""" await self.home.get_current_state() self.update_all() - def get_state_finished(self, future): + def get_state_finished(self, future) -> None: """Execute when get_state coroutine has finished.""" try: future.result() @@ -167,18 +167,18 @@ def get_state_finished(self, future): _LOGGER.error("Updating state after HMIP access point reconnect failed") self.hass.async_create_task(self.home.disable_events()) - def set_all_to_unavailable(self): + def set_all_to_unavailable(self) -> None: """Set all devices to unavailable and tell Home Assistant.""" for device in self.home.devices: device.unreach = True self.update_all() - def update_all(self): + def update_all(self) -> None: """Signal all devices to update their state.""" for device in self.home.devices: device.fire_update_event() - async def async_connect(self): + async def async_connect(self) -> None: """Start WebSocket connection.""" tries = 0 while True: @@ -210,7 +210,7 @@ async def async_connect(self): except asyncio.CancelledError: break - async def async_reset(self): + async def async_reset(self) -> bool: """Close the websocket connection.""" self._ws_close_requested = True if self._retry_task is not None: diff --git a/homeassistant/components/homematicip_cloud/light.py b/homeassistant/components/homematicip_cloud/light.py index c262b05d019b..79083f031ae3 100644 --- a/homeassistant/components/homematicip_cloud/light.py +++ b/homeassistant/components/homematicip_cloud/light.py @@ -1,5 +1,6 @@ """Support for HomematicIP Cloud lights.""" import logging +from typing import Any, Dict from homematicip.aio.device import ( AsyncBrandDimmer, @@ -33,7 +34,9 @@ ATTR_CURRENT_POWER_W = "current_power_w" -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): +async def async_setup_platform( + hass, config, async_add_entities, discovery_info=None +) -> None: """Old way of setting up HomematicIP Cloud lights.""" pass @@ -43,16 +46,16 @@ async def async_setup_entry( ) -> None: """Set up the HomematicIP Cloud lights from a config entry.""" hap = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]] - devices = [] + entities = [] for device in hap.home.devices: if isinstance(device, AsyncBrandSwitchMeasuring): - devices.append(HomematicipLightMeasuring(hap, device)) + entities.append(HomematicipLightMeasuring(hap, device)) elif isinstance(device, AsyncBrandSwitchNotificationLight): - devices.append(HomematicipLight(hap, device)) - devices.append( + entities.append(HomematicipLight(hap, device)) + entities.append( HomematicipNotificationLight(hap, device, device.topLightChannelIndex) ) - devices.append( + entities.append( HomematicipNotificationLight( hap, device, device.bottomLightChannelIndex ) @@ -61,10 +64,10 @@ async def async_setup_entry( device, (AsyncDimmer, AsyncPluggableDimmer, AsyncBrandDimmer, AsyncFullFlushDimmer), ): - devices.append(HomematicipDimmer(hap, device)) + entities.append(HomematicipDimmer(hap, device)) - if devices: - async_add_entities(devices) + if entities: + async_add_entities(entities) class HomematicipLight(HomematicipGenericDevice, Light): @@ -79,11 +82,11 @@ def is_on(self) -> bool: """Return true if device is on.""" return self._device.on - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs) -> None: """Turn the device on.""" await self._device.turn_on() - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs) -> None: """Turn the device off.""" await self._device.turn_off() @@ -92,7 +95,7 @@ class HomematicipLightMeasuring(HomematicipLight): """Representation of a HomematicIP Cloud measuring light device.""" @property - def device_state_attributes(self): + def device_state_attributes(self) -> Dict[str, Any]: """Return the state attributes of the generic device.""" state_attr = super().device_state_attributes @@ -127,14 +130,14 @@ def supported_features(self) -> int: """Flag supported features.""" return SUPPORT_BRIGHTNESS - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs) -> None: """Turn the light on.""" if ATTR_BRIGHTNESS in kwargs: await self._device.set_dim_level(kwargs[ATTR_BRIGHTNESS] / 255.0) else: await self._device.set_dim_level(1) - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs) -> None: """Turn the light off.""" await self._device.set_dim_level(0) @@ -184,7 +187,7 @@ def hs_color(self) -> tuple: return self._color_switcher.get(simple_rgb_color, [0.0, 0.0]) @property - def device_state_attributes(self): + def device_state_attributes(self) -> Dict[str, Any]: """Return the state attributes of the generic device.""" state_attr = super().device_state_attributes @@ -208,7 +211,7 @@ def unique_id(self) -> str: """Return a unique ID.""" return f"{self.__class__.__name__}_{self.post}_{self._device.id}" - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs) -> None: """Turn the light on.""" # Use hs_color from kwargs, # if not applicable use current hs_color. @@ -236,7 +239,7 @@ async def async_turn_on(self, **kwargs): rampTime=transition, ) - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs) -> None: """Turn the light off.""" simple_rgb_color = self._func_channel.simpleRGBColorState transition = kwargs.get(ATTR_TRANSITION, 0.5) @@ -250,7 +253,7 @@ async def async_turn_off(self, **kwargs): ) -def _convert_color(color) -> RGBColorState: +def _convert_color(color: tuple) -> RGBColorState: """ Convert the given color to the reduced RGBColorState color. diff --git a/homeassistant/components/homematicip_cloud/sensor.py b/homeassistant/components/homematicip_cloud/sensor.py index acbf72f6ae99..a8ca3d17eb94 100644 --- a/homeassistant/components/homematicip_cloud/sensor.py +++ b/homeassistant/components/homematicip_cloud/sensor.py @@ -1,5 +1,6 @@ """Support for HomematicIP Cloud sensors.""" import logging +from typing import Any, Dict from homematicip.aio.device import ( AsyncBrandSwitchMeasuring, @@ -55,7 +56,9 @@ } -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): +async def async_setup_platform( + hass, config, async_add_entities, discovery_info=None +) -> None: """Set up the HomematicIP Cloud sensors devices.""" pass @@ -65,11 +68,11 @@ async def async_setup_entry( ) -> None: """Set up the HomematicIP Cloud sensors from a config entry.""" hap = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]] - devices = [HomematicipAccesspointStatus(hap)] + entities = [HomematicipAccesspointStatus(hap)] for device in hap.home.devices: if isinstance(device, (AsyncHeatingThermostat, AsyncHeatingThermostatCompact)): - devices.append(HomematicipHeatingThermostat(hap, device)) - devices.append(HomematicipTemperatureSensor(hap, device)) + entities.append(HomematicipHeatingThermostat(hap, device)) + entities.append(HomematicipTemperatureSensor(hap, device)) if isinstance( device, ( @@ -81,8 +84,8 @@ async def async_setup_entry( AsyncWeatherSensorPro, ), ): - devices.append(HomematicipTemperatureSensor(hap, device)) - devices.append(HomematicipHumiditySensor(hap, device)) + entities.append(HomematicipTemperatureSensor(hap, device)) + entities.append(HomematicipHumiditySensor(hap, device)) if isinstance( device, ( @@ -96,7 +99,7 @@ async def async_setup_entry( AsyncWeatherSensorPro, ), ): - devices.append(HomematicipIlluminanceSensor(hap, device)) + entities.append(HomematicipIlluminanceSensor(hap, device)) if isinstance( device, ( @@ -105,18 +108,18 @@ async def async_setup_entry( AsyncFullFlushSwitchMeasuring, ), ): - devices.append(HomematicipPowerSensor(hap, device)) + entities.append(HomematicipPowerSensor(hap, device)) if isinstance( device, (AsyncWeatherSensor, AsyncWeatherSensorPlus, AsyncWeatherSensorPro) ): - devices.append(HomematicipWindspeedSensor(hap, device)) + entities.append(HomematicipWindspeedSensor(hap, device)) if isinstance(device, (AsyncWeatherSensorPlus, AsyncWeatherSensorPro)): - devices.append(HomematicipTodayRainSensor(hap, device)) + entities.append(HomematicipTodayRainSensor(hap, device)) if isinstance(device, AsyncPassageDetector): - devices.append(HomematicipPassageDetectorDeltaCounter(hap, device)) + entities.append(HomematicipPassageDetectorDeltaCounter(hap, device)) - if devices: - async_add_entities(devices) + if entities: + async_add_entities(entities) class HomematicipAccesspointStatus(HomematicipGenericDevice): @@ -127,7 +130,7 @@ def __init__(self, hap: HomematicipHAP) -> None: super().__init__(hap, hap.home) @property - def device_info(self): + def device_info(self) -> Dict[str, Any]: """Return device specific attributes.""" # Adds a sensor to the existing HAP device return { @@ -158,7 +161,7 @@ def unit_of_measurement(self) -> str: return "%" @property - def device_state_attributes(self): + def device_state_attributes(self) -> Dict[str, Any]: """Return the state attributes of the access point.""" state_attr = super().device_state_attributes @@ -246,7 +249,7 @@ def unit_of_measurement(self) -> str: return TEMP_CELSIUS @property - def device_state_attributes(self): + def device_state_attributes(self) -> Dict[str, Any]: """Return the state attributes of the windspeed sensor.""" state_attr = super().device_state_attributes @@ -283,7 +286,7 @@ def unit_of_measurement(self) -> str: return "lx" @property - def device_state_attributes(self): + def device_state_attributes(self) -> Dict[str, Any]: """Return the state attributes of the wind speed sensor.""" state_attr = super().device_state_attributes @@ -336,7 +339,7 @@ def unit_of_measurement(self) -> str: return "km/h" @property - def device_state_attributes(self): + def device_state_attributes(self) -> Dict[str, Any]: """Return the state attributes of the wind speed sensor.""" state_attr = super().device_state_attributes @@ -378,7 +381,7 @@ def state(self) -> int: return self._device.leftRightCounterDelta @property - def device_state_attributes(self): + def device_state_attributes(self) -> Dict[str, Any]: """Return the state attributes of the delta counter.""" state_attr = super().device_state_attributes diff --git a/homeassistant/components/homematicip_cloud/switch.py b/homeassistant/components/homematicip_cloud/switch.py index dae6019b3786..8e15313a4fe0 100644 --- a/homeassistant/components/homematicip_cloud/switch.py +++ b/homeassistant/components/homematicip_cloud/switch.py @@ -1,5 +1,6 @@ """Support for HomematicIP Cloud switches.""" import logging +from typing import Any, Dict from homematicip.aio.device import ( AsyncBrandSwitchMeasuring, @@ -24,7 +25,9 @@ _LOGGER = logging.getLogger(__name__) -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): +async def async_setup_platform( + hass, config, async_add_entities, discovery_info=None +) -> None: """Set up the HomematicIP Cloud switch devices.""" pass @@ -34,7 +37,7 @@ async def async_setup_entry( ) -> None: """Set up the HomematicIP switch from a config entry.""" hap = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]] - devices = [] + entities = [] for device in hap.home.devices: if isinstance(device, AsyncBrandSwitchMeasuring): # BrandSwitchMeasuring inherits PlugableSwitchMeasuring @@ -44,27 +47,27 @@ async def async_setup_entry( elif isinstance( device, (AsyncPlugableSwitchMeasuring, AsyncFullFlushSwitchMeasuring) ): - devices.append(HomematicipSwitchMeasuring(hap, device)) + entities.append(HomematicipSwitchMeasuring(hap, device)) elif isinstance( device, (AsyncPlugableSwitch, AsyncPrintedCircuitBoardSwitchBattery) ): - devices.append(HomematicipSwitch(hap, device)) + entities.append(HomematicipSwitch(hap, device)) elif isinstance(device, AsyncOpenCollector8Module): for channel in range(1, 9): - devices.append(HomematicipMultiSwitch(hap, device, channel)) + entities.append(HomematicipMultiSwitch(hap, device, channel)) elif isinstance(device, AsyncMultiIOBox): for channel in range(1, 3): - devices.append(HomematicipMultiSwitch(hap, device, channel)) + entities.append(HomematicipMultiSwitch(hap, device, channel)) elif isinstance(device, AsyncPrintedCircuitBoardSwitch2): for channel in range(1, 3): - devices.append(HomematicipMultiSwitch(hap, device, channel)) + entities.append(HomematicipMultiSwitch(hap, device, channel)) for group in hap.home.groups: if isinstance(group, AsyncSwitchingGroup): - devices.append(HomematicipGroupSwitch(hap, group)) + entities.append(HomematicipGroupSwitch(hap, group)) - if devices: - async_add_entities(devices) + if entities: + async_add_entities(entities) class HomematicipSwitch(HomematicipGenericDevice, SwitchDevice): @@ -79,11 +82,11 @@ def is_on(self) -> bool: """Return true if device is on.""" return self._device.on - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs) -> None: """Turn the device on.""" await self._device.turn_on() - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs) -> None: """Turn the device off.""" await self._device.turn_off() @@ -111,7 +114,7 @@ def available(self) -> bool: return True @property - def device_state_attributes(self): + def device_state_attributes(self) -> Dict[str, Any]: """Return the state attributes of the switch-group.""" state_attr = super().device_state_attributes @@ -120,11 +123,11 @@ def device_state_attributes(self): return state_attr - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs) -> None: """Turn the group on.""" await self._device.turn_on() - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs) -> None: """Turn the group off.""" await self._device.turn_off() @@ -148,7 +151,7 @@ def today_energy_kwh(self) -> int: class HomematicipMultiSwitch(HomematicipGenericDevice, SwitchDevice): """Representation of a HomematicIP Cloud multi switch device.""" - def __init__(self, hap: HomematicipHAP, device, channel: int): + def __init__(self, hap: HomematicipHAP, device, channel: int) -> None: """Initialize the multi switch device.""" self.channel = channel super().__init__(hap, device, f"Channel{channel}") @@ -163,10 +166,10 @@ def is_on(self) -> bool: """Return true if device is on.""" return self._device.functionalChannels[self.channel].on - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs) -> None: """Turn the device on.""" await self._device.turn_on(self.channel) - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs) -> None: """Turn the device off.""" await self._device.turn_off(self.channel) diff --git a/homeassistant/components/homematicip_cloud/weather.py b/homeassistant/components/homematicip_cloud/weather.py index 5aa3f28c45d3..ebc7eacf78ea 100644 --- a/homeassistant/components/homematicip_cloud/weather.py +++ b/homeassistant/components/homematicip_cloud/weather.py @@ -37,7 +37,9 @@ } -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): +async def async_setup_platform( + hass, config, async_add_entities, discovery_info=None +) -> None: """Set up the HomematicIP Cloud weather sensor.""" pass @@ -47,17 +49,17 @@ async def async_setup_entry( ) -> None: """Set up the HomematicIP weather sensor from a config entry.""" hap = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]] - devices = [] + entities = [] for device in hap.home.devices: if isinstance(device, AsyncWeatherSensorPro): - devices.append(HomematicipWeatherSensorPro(hap, device)) + entities.append(HomematicipWeatherSensorPro(hap, device)) elif isinstance(device, (AsyncWeatherSensor, AsyncWeatherSensorPlus)): - devices.append(HomematicipWeatherSensor(hap, device)) + entities.append(HomematicipWeatherSensor(hap, device)) - devices.append(HomematicipHomeWeather(hap)) + entities.append(HomematicipHomeWeather(hap)) - if devices: - async_add_entities(devices) + if entities: + async_add_entities(entities) class HomematicipWeatherSensor(HomematicipGenericDevice, WeatherEntity): diff --git a/homeassistant/components/homeworks/__init__.py b/homeassistant/components/homeworks/__init__.py index bd40336b8ba8..55357acdad44 100644 --- a/homeassistant/components/homeworks/__init__.py +++ b/homeassistant/components/homeworks/__init__.py @@ -1,6 +1,7 @@ """Support for Lutron Homeworks Series 4 and 8 systems.""" import logging +from pyhomeworks.pyhomeworks import HW_BUTTON_PRESSED, HW_BUTTON_RELEASED, Homeworks import voluptuous as vol from homeassistant.const import ( @@ -65,7 +66,6 @@ def setup(hass, base_config): """Start Homeworks controller.""" - from pyhomeworks.pyhomeworks import Homeworks def hw_callback(msg_type, values): """Dispatch state changes.""" @@ -138,7 +138,6 @@ def __init__(self, hass, addr, name): @callback def _update_callback(self, msg_type, values): """Fire events if button is pressed or released.""" - from pyhomeworks.pyhomeworks import HW_BUTTON_PRESSED, HW_BUTTON_RELEASED if msg_type == HW_BUTTON_PRESSED: event = EVENT_BUTTON_PRESS diff --git a/homeassistant/components/homeworks/light.py b/homeassistant/components/homeworks/light.py index d1854b4dbf3b..2c0034ee9862 100644 --- a/homeassistant/components/homeworks/light.py +++ b/homeassistant/components/homeworks/light.py @@ -1,6 +1,8 @@ """Support for Lutron Homeworks lights.""" import logging +from pyhomeworks.pyhomeworks import HW_LIGHT_CHANGED + from homeassistant.components.light import ATTR_BRIGHTNESS, SUPPORT_BRIGHTNESS, Light from homeassistant.const import CONF_NAME from homeassistant.core import callback @@ -93,7 +95,6 @@ def is_on(self): @callback def _update_callback(self, msg_type, values): """Process device specific messages.""" - from pyhomeworks.pyhomeworks import HW_LIGHT_CHANGED if msg_type == HW_LIGHT_CHANGED: self._level = int((values[1] * 255.0) / 100.0) diff --git a/homeassistant/components/honeywell/climate.py b/homeassistant/components/honeywell/climate.py index 15efac87587c..42f4778eb4f4 100644 --- a/homeassistant/components/honeywell/climate.py +++ b/homeassistant/components/honeywell/climate.py @@ -160,9 +160,7 @@ def __init__( self._username = username self._password = password - _LOGGER.debug( - "latestData = %s ", device._data # pylint: disable=protected-access - ) + _LOGGER.debug("latestData = %s ", device._data) # not all honeywell HVACs support all modes mappings = [v for k, v in HVAC_MODE_TO_HW_MODE.items() if device.raw_ui_data[k]] @@ -174,13 +172,13 @@ def __init__( | SUPPORT_TARGET_TEMPERATURE_RANGE ) - if device._data["canControlHumidification"]: # pylint: disable=protected-access + if device._data["canControlHumidification"]: self._supported_features |= SUPPORT_TARGET_HUMIDITY if device.raw_ui_data["SwitchEmergencyHeatAllowed"]: self._supported_features |= SUPPORT_AUX_HEAT - if not device._data["hasFan"]: # pylint: disable=protected-access + if not device._data["hasFan"]: return # not all honeywell fans support all modes diff --git a/homeassistant/components/horizon/media_player.py b/homeassistant/components/horizon/media_player.py index 8bed30e88f32..44e93d26a405 100644 --- a/homeassistant/components/horizon/media_player.py +++ b/homeassistant/components/horizon/media_player.py @@ -2,10 +2,12 @@ from datetime import timedelta import logging +from horimote import Client, keys +from horimote.exceptions import AuthenticationError import voluptuous as vol from homeassistant import util -from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA +from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice from homeassistant.components.media_player.const import ( MEDIA_TYPE_CHANNEL, SUPPORT_NEXT_TRACK, @@ -56,8 +58,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Horizon platform.""" - from horimote import Client, keys - from horimote.exceptions import AuthenticationError host = config[CONF_HOST] name = config[CONF_NAME] @@ -81,12 +81,12 @@ def setup_platform(hass, config, add_entities, discovery_info=None): class HorizonDevice(MediaPlayerDevice): """Representation of a Horizon HD Recorder.""" - def __init__(self, client, name, keys): + def __init__(self, client, name, remote_keys): """Initialize the remote.""" self._client = client self._name = name self._state = None - self._keys = keys + self._keys = remote_keys @property def name(self): @@ -177,7 +177,6 @@ def _send_key(self, key): def _send(self, key=None, channel=None): """Send a key to the Horizon device.""" - from horimote.exceptions import AuthenticationError try: if key: diff --git a/homeassistant/components/html5/const.py b/homeassistant/components/html5/const.py new file mode 100644 index 000000000000..1d0689511b2f --- /dev/null +++ b/homeassistant/components/html5/const.py @@ -0,0 +1,3 @@ +"""Constants for the HTML5 component.""" +DOMAIN = "html5" +SERVICE_DISMISS = "dismiss" diff --git a/homeassistant/components/html5/notify.py b/homeassistant/components/html5/notify.py index 18b7ff27ab4b..481a00e96e13 100644 --- a/homeassistant/components/html5/notify.py +++ b/homeassistant/components/html5/notify.py @@ -34,17 +34,16 @@ ATTR_TARGET, ATTR_TITLE, ATTR_TITLE_DEFAULT, - DOMAIN, PLATFORM_SCHEMA, BaseNotificationService, ) +from .const import DOMAIN, SERVICE_DISMISS + _LOGGER = logging.getLogger(__name__) REGISTRATIONS_FILE = "html5_push_registrations.conf" -SERVICE_DISMISS = "html5_dismiss" - ATTR_GCM_SENDER_ID = "gcm_sender_id" ATTR_GCM_API_KEY = "gcm_api_key" ATTR_VAPID_PUB_KEY = "vapid_pub_key" diff --git a/homeassistant/components/html5/services.yaml b/homeassistant/components/html5/services.yaml index e69de29bb2d1..5fd068a64dcd 100644 --- a/homeassistant/components/html5/services.yaml +++ b/homeassistant/components/html5/services.yaml @@ -0,0 +1,9 @@ +dismiss: + description: Dismiss a html5 notification. + fields: + target: + description: An array of targets. Optional. + example: ['my_phone', 'my_tablet'] + data: + description: Extended information of notification. Supports tag. Optional. + example: '{ "tag": "tagname" }' diff --git a/homeassistant/components/http/__init__.py b/homeassistant/components/http/__init__.py index 4df606a3c1b2..4d3985a7af3b 100644 --- a/homeassistant/components/http/__init__.py +++ b/homeassistant/components/http/__init__.py @@ -20,11 +20,11 @@ from .auth import setup_auth from .ban import setup_bans -from .const import KEY_AUTHENTICATED, KEY_HASS, KEY_HASS_USER, KEY_REAL_IP # noqa +from .const import KEY_AUTHENTICATED, KEY_HASS, KEY_HASS_USER, KEY_REAL_IP # noqa: F401 from .cors import setup_cors from .real_ip import setup_real_ip from .static import CACHE_HEADERS, CachingStaticResource -from .view import HomeAssistantView # noqa +from .view import HomeAssistantView # noqa: F401 # mypy: allow-untyped-defs, no-check-untyped-defs diff --git a/homeassistant/components/http/static.py b/homeassistant/components/http/static.py index e6a70c9f6436..7d84be0f6dd2 100644 --- a/homeassistant/components/http/static.py +++ b/homeassistant/components/http/static.py @@ -13,8 +13,6 @@ CACHE_HEADERS = {hdrs.CACHE_CONTROL: f"public, max-age={CACHE_TIME}"} -# https://github.com/PyCQA/astroid/issues/633 -# pylint: disable=duplicate-bases class CachingStaticResource(StaticResource): """Static Resource handler that will add cache headers.""" diff --git a/homeassistant/components/http/view.py b/homeassistant/components/http/view.py index 804c90d4f96a..31f96833667f 100644 --- a/homeassistant/components/http/view.py +++ b/homeassistant/components/http/view.py @@ -34,8 +34,8 @@ class HomeAssistantView: requires_auth = True cors_allowed = False - # pylint: disable=no-self-use - def context(self, request): + @staticmethod + def context(request): """Generate a context from a request.""" user = request.get("hass_user") if user is None: @@ -43,7 +43,8 @@ def context(self, request): return Context(user_id=user.id) - def json(self, result, status_code=200, headers=None): + @staticmethod + def json(result, status_code=200, headers=None): """Return a JSON response.""" try: msg = json.dumps( diff --git a/homeassistant/components/huawei_lte/.translations/bg.json b/homeassistant/components/huawei_lte/.translations/bg.json index de5cbb32b797..44746468b351 100644 --- a/homeassistant/components/huawei_lte/.translations/bg.json +++ b/homeassistant/components/huawei_lte/.translations/bg.json @@ -1,10 +1,13 @@ { "config": { "abort": { - "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e" + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e", + "already_in_progress": "\u0422\u043e\u0432\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0432\u0435\u0447\u0435 \u0441\u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430", + "not_huawei_lte": "\u041d\u0435 \u0435 Huawei LTE \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e" }, "error": { "connection_failed": "\u0421\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435\u0442\u043e \u0435 \u043d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e", + "connection_timeout": "\u0412\u0440\u0435\u043c\u0435\u0442\u043e \u0437\u0430 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435 \u0438\u0437\u0442\u0435\u0447\u0435", "incorrect_password": "\u041d\u0435\u043f\u0440\u0430\u0432\u0438\u043b\u043d\u0430 \u043f\u0430\u0440\u043e\u043b\u0430", "incorrect_username": "\u041d\u0435\u043f\u0440\u0430\u0432\u0438\u043b\u043d\u043e \u043f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435", "incorrect_username_or_password": "\u041d\u0435\u043f\u0440\u0430\u0432\u0438\u043b\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", diff --git a/homeassistant/components/huawei_lte/.translations/fr.json b/homeassistant/components/huawei_lte/.translations/fr.json index 5effea3d0036..34db4e93bc4a 100644 --- a/homeassistant/components/huawei_lte/.translations/fr.json +++ b/homeassistant/components/huawei_lte/.translations/fr.json @@ -7,7 +7,7 @@ }, "error": { "connection_failed": "La connexion a \u00e9chou\u00e9", - "connection_timeout": "D\u00e9lai de connection d\u00e9pass\u00e9", + "connection_timeout": "D\u00e9lai de connexion d\u00e9pass\u00e9", "incorrect_password": "Mot de passe incorrect", "incorrect_username": "Nom d'utilisateur incorrect", "incorrect_username_or_password": "identifiant ou mot de passe incorrect", diff --git a/homeassistant/components/huawei_lte/.translations/ko.json b/homeassistant/components/huawei_lte/.translations/ko.json index b21e0aa0a23e..a9ac8d7f62c9 100644 --- a/homeassistant/components/huawei_lte/.translations/ko.json +++ b/homeassistant/components/huawei_lte/.translations/ko.json @@ -1,10 +1,13 @@ { "config": { "abort": { - "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "already_in_progress": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "not_huawei_lte": "\ud654\uc6e8\uc774 LTE \uae30\uae30\uac00 \uc544\ub2d8" }, "error": { "connection_failed": "\uc5f0\uacb0\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4", + "connection_timeout": "\uc811\uc18d \uc2dc\uac04 \ucd08\uacfc", "incorrect_password": "\ube44\ubc00\ubc88\ud638\uac00 \uc77c\uce58\ud558\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4", "incorrect_username": "\uc0ac\uc6a9\uc790 \uc774\ub984\uc774 \uc77c\uce58\ud558\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4", "incorrect_username_or_password": "\uc0ac\uc6a9\uc790 \uc774\ub984 \ub610\ub294 \ube44\ubc00\ubc88\ud638\uac00 \uc77c\uce58\ud558\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4", diff --git a/homeassistant/components/huawei_lte/.translations/nn.json b/homeassistant/components/huawei_lte/.translations/nn.json new file mode 100644 index 000000000000..1a5c63f10f86 --- /dev/null +++ b/homeassistant/components/huawei_lte/.translations/nn.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Huawei LTE" + } +} \ No newline at end of file diff --git a/homeassistant/components/huawei_lte/.translations/pt.json b/homeassistant/components/huawei_lte/.translations/pt.json new file mode 100644 index 000000000000..6e3a06ac662c --- /dev/null +++ b/homeassistant/components/huawei_lte/.translations/pt.json @@ -0,0 +1,34 @@ +{ + "config": { + "abort": { + "already_configured": "Este dispositivo j\u00e1 foi configurado", + "already_in_progress": "Este dispositivo j\u00e1 est\u00e1 a ser configurado" + }, + "error": { + "incorrect_password": "Palavra-passe incorreta", + "incorrect_username": "Nome de Utilizador incorreto", + "incorrect_username_or_password": "Nome de utilizador ou palavra passe incorretos" + }, + "step": { + "user": { + "data": { + "password": "Palavra-passe", + "url": "", + "username": "Utilizador" + }, + "title": "Configurar o Huawei LTE" + } + }, + "title": "" + }, + "options": { + "step": { + "init": { + "data": { + "recipient": "Destinat\u00e1rios de notifica\u00e7\u00e3o por SMS", + "track_new_devices": "Seguir novos dispositivos" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/huawei_lte/.translations/sl.json b/homeassistant/components/huawei_lte/.translations/sl.json index 0b4964069b2e..5022e358ca72 100644 --- a/homeassistant/components/huawei_lte/.translations/sl.json +++ b/homeassistant/components/huawei_lte/.translations/sl.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Ta naprava je \u017ee nastavljena", + "already_configured": "Ta naprava je \u017ee konfigurirana", "already_in_progress": "Ta naprava se \u017ee nastavlja", "not_huawei_lte": "Ni naprava Huawei LTE" }, diff --git a/homeassistant/components/huawei_lte/__init__.py b/homeassistant/components/huawei_lte/__init__.py index fa1423edcca2..ada1f0a8abdc 100644 --- a/homeassistant/components/huawei_lte/__init__.py +++ b/homeassistant/components/huawei_lte/__init__.py @@ -21,6 +21,7 @@ from requests.exceptions import Timeout from url_normalize import url_normalize +from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN from homeassistant.components.device_tracker import DOMAIN as DEVICE_TRACKER_DOMAIN from homeassistant.components.notify import DOMAIN as NOTIFY_DOMAIN from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN @@ -54,6 +55,7 @@ KEY_DEVICE_INFORMATION, KEY_DEVICE_SIGNAL, KEY_DIALUP_MOBILE_DATASWITCH, + KEY_MONITORING_STATUS, KEY_MONITORING_TRAFFIC_STATISTICS, KEY_WLAN_HOST_LIST, UPDATE_OPTIONS_SIGNAL, @@ -101,6 +103,13 @@ extra=vol.ALLOW_EXTRA, ) +CONFIG_ENTRY_PLATFORMS = ( + BINARY_SENSOR_DOMAIN, + DEVICE_TRACKER_DOMAIN, + SENSOR_DOMAIN, + SWITCH_DOMAIN, +) + @attr.s class Router: @@ -139,7 +148,7 @@ def device_name(self) -> str: @property def device_connections(self) -> Set[Tuple[str, str]]: """Get router connections for device registry.""" - return {(dr.CONNECTION_NETWORK_MAC, self.mac)} + return {(dr.CONNECTION_NETWORK_MAC, self.mac)} if self.mac else set() def update(self) -> None: """Update router data.""" @@ -170,6 +179,7 @@ def get_data(key: str, func: Callable[[None], Any]) -> None: get_data(KEY_DEVICE_BASIC_INFORMATION, self.client.device.basic_information) get_data(KEY_DEVICE_SIGNAL, self.client.device.signal) get_data(KEY_DIALUP_MOBILE_DATASWITCH, self.client.dial_up.mobile_dataswitch) + get_data(KEY_MONITORING_STATUS, self.client.monitoring.status) get_data( KEY_MONITORING_TRAFFIC_STATISTICS, self.client.monitoring.traffic_statistics ) @@ -314,7 +324,7 @@ def signal_update() -> None: ) # Forward config entry setup to platforms - for domain in (DEVICE_TRACKER_DOMAIN, SENSOR_DOMAIN, SWITCH_DOMAIN): + for domain in CONFIG_ENTRY_PLATFORMS: hass.async_create_task( hass.config_entries.async_forward_entry_setup(config_entry, domain) ) @@ -357,7 +367,7 @@ async def async_unload_entry( """Unload config entry.""" # Forward config entry unload to platforms - for domain in (DEVICE_TRACKER_DOMAIN, SENSOR_DOMAIN, SWITCH_DOMAIN): + for domain in CONFIG_ENTRY_PLATFORMS: await hass.config_entries.async_forward_entry_unload(config_entry, domain) # Forget about the router and invoke its cleanup diff --git a/homeassistant/components/huawei_lte/binary_sensor.py b/homeassistant/components/huawei_lte/binary_sensor.py new file mode 100644 index 000000000000..4fcb400c32a7 --- /dev/null +++ b/homeassistant/components/huawei_lte/binary_sensor.py @@ -0,0 +1,122 @@ +"""Support for Huawei LTE binary sensors.""" + +import logging +from typing import Optional + +import attr +from huawei_lte_api.enums.cradle import ConnectionStatusEnum + +from homeassistant.components.binary_sensor import ( + DOMAIN as BINARY_SENSOR_DOMAIN, + BinarySensorDevice, +) +from homeassistant.const import CONF_URL +from . import HuaweiLteBaseEntity +from .const import DOMAIN, KEY_MONITORING_STATUS + + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up from config entry.""" + router = hass.data[DOMAIN].routers[config_entry.data[CONF_URL]] + entities = [] + + if router.data.get(KEY_MONITORING_STATUS): + entities.append(HuaweiLteMobileConnectionBinarySensor(router)) + + async_add_entities(entities, True) + + +@attr.s +class HuaweiLteBaseBinarySensor(HuaweiLteBaseEntity, BinarySensorDevice): + """Huawei LTE binary sensor device base class.""" + + key: str + item: str + _raw_state: Optional[str] = attr.ib(init=False, default=None) + + async def async_added_to_hass(self): + """Subscribe to needed data on add.""" + await super().async_added_to_hass() + self.router.subscriptions[self.key].add(f"{BINARY_SENSOR_DOMAIN}/{self.item}") + + async def async_will_remove_from_hass(self): + """Unsubscribe from needed data on remove.""" + await super().async_will_remove_from_hass() + self.router.subscriptions[self.key].remove( + f"{BINARY_SENSOR_DOMAIN}/{self.item}" + ) + + async def async_update(self): + """Update state.""" + try: + value = self.router.data[self.key][self.item] + except KeyError: + _LOGGER.debug("%s[%s] not in data", self.key, self.item) + self._available = False + return + self._available = True + self._raw_state = str(value) + + +CONNECTION_STATE_ATTRIBUTES = { + str(ConnectionStatusEnum.CONNECTING): "Connecting", + str(ConnectionStatusEnum.DISCONNECTING): "Disconnecting", + str(ConnectionStatusEnum.CONNECT_FAILED): "Connect failed", + str(ConnectionStatusEnum.CONNECT_STATUS_NULL): "Status not available", + str(ConnectionStatusEnum.CONNECT_STATUS_ERROR): "Status error", +} + + +@attr.s +class HuaweiLteMobileConnectionBinarySensor(HuaweiLteBaseBinarySensor): + """Huawei LTE mobile connection binary sensor.""" + + def __attrs_post_init__(self): + """Initialize identifiers.""" + self.key = KEY_MONITORING_STATUS + self.item = "ConnectionStatus" + + @property + def _entity_name(self) -> str: + return "Mobile connection" + + @property + def _device_unique_id(self) -> str: + return f"{self.key}.{self.item}" + + @property + def is_on(self) -> bool: + """Return whether the binary sensor is on.""" + return self._raw_state and int(self._raw_state) in ( + ConnectionStatusEnum.CONNECTED, + ConnectionStatusEnum.DISCONNECTING, + ) + + @property + def assumed_state(self) -> bool: + """Return True if real state is assumed, not known.""" + return not self._raw_state or int(self._raw_state) not in ( + ConnectionStatusEnum.CONNECT_FAILED, + ConnectionStatusEnum.CONNECTED, + ConnectionStatusEnum.DISCONNECTED, + ) + + @property + def icon(self): + """Return mobile connectivity sensor icon.""" + return "mdi:signal" if self.is_on else "mdi:signal-off" + + @property + def device_state_attributes(self): + """Get additional attributes related to connection status.""" + attributes = super().device_state_attributes + if self._raw_state in CONNECTION_STATE_ATTRIBUTES: + if attributes is None: + attributes = {} + attributes["additional_state"] = CONNECTION_STATE_ATTRIBUTES[ + self._raw_state + ] + return attributes diff --git a/homeassistant/components/huawei_lte/const.py b/homeassistant/components/huawei_lte/const.py index 8dae63f6538b..b6e079576ac1 100644 --- a/homeassistant/components/huawei_lte/const.py +++ b/homeassistant/components/huawei_lte/const.py @@ -16,9 +16,12 @@ KEY_DEVICE_INFORMATION = "device_information" KEY_DEVICE_SIGNAL = "device_signal" KEY_DIALUP_MOBILE_DATASWITCH = "dialup_mobile_dataswitch" +KEY_MONITORING_STATUS = "monitoring_status" KEY_MONITORING_TRAFFIC_STATISTICS = "monitoring_traffic_statistics" KEY_WLAN_HOST_LIST = "wlan_host_list" +BINARY_SENSOR_KEYS = {KEY_MONITORING_STATUS} + DEVICE_TRACKER_KEYS = {KEY_WLAN_HOST_LIST} SENSOR_KEYS = { @@ -29,4 +32,4 @@ SWITCH_KEYS = {KEY_DIALUP_MOBILE_DATASWITCH} -ALL_KEYS = DEVICE_TRACKER_KEYS | SENSOR_KEYS | SWITCH_KEYS +ALL_KEYS = BINARY_SENSOR_KEYS | DEVICE_TRACKER_KEYS | SENSOR_KEYS | SWITCH_KEYS diff --git a/homeassistant/components/huawei_lte/device_tracker.py b/homeassistant/components/huawei_lte/device_tracker.py index d95d99e71264..f5f834fa1867 100644 --- a/homeassistant/components/huawei_lte/device_tracker.py +++ b/homeassistant/components/huawei_lte/device_tracker.py @@ -2,7 +2,7 @@ import logging import re -from typing import Any, Dict, Set +from typing import Any, Dict, List, Optional, Set import attr from stringcase import snakecase @@ -40,13 +40,17 @@ async def async_setup_entry(hass, config_entry, async_add_entities): # Initialize already tracked entities tracked: Set[str] = set() registry = await entity_registry.async_get_registry(hass) + known_entities: List[HuaweiLteScannerEntity] = [] for entity in registry.entities.values(): if ( entity.domain == DEVICE_TRACKER_DOMAIN and entity.config_entry_id == config_entry.entry_id ): tracked.add(entity.unique_id) - async_add_new_entities(hass, router.url, async_add_entities, tracked, True) + known_entities.append( + HuaweiLteScannerEntity(router, entity.unique_id.partition("-")[2]) + ) + async_add_entities(known_entities, True) # Tell parent router to poll hosts list to gather new devices router.subscriptions[KEY_WLAN_HOST_LIST].add(_DEVICE_SCAN) @@ -66,13 +70,8 @@ async def _async_maybe_add_new_entities(url: str) -> None: async_add_new_entities(hass, router.url, async_add_entities, tracked) -def async_add_new_entities( - hass, router_url, async_add_entities, tracked, included: bool = False -): - """Add new entities. - - :param included: if True, setup only items in tracked, and vice versa - """ +def async_add_new_entities(hass, router_url, async_add_entities, tracked): + """Add new entities that are not already being tracked.""" router = hass.data[DOMAIN].routers[router_url] try: hosts = router.data[KEY_WLAN_HOST_LIST]["Hosts"]["Host"] @@ -83,8 +82,7 @@ def async_add_new_entities( new_entities = [] for host in (x for x in hosts if x.get("MacAddress")): entity = HuaweiLteScannerEntity(router, host["MacAddress"]) - tracking = entity.unique_id in tracked - if tracking != included: + if entity.unique_id in tracked: continue tracked.add(entity.unique_id) new_entities.append(entity) @@ -113,12 +111,16 @@ class HuaweiLteScannerEntity(HuaweiLteBaseEntity, ScannerEntity): mac: str = attr.ib() _is_connected: bool = attr.ib(init=False, default=False) - _name: str = attr.ib(init=False, default="device") + _hostname: Optional[str] = attr.ib(init=False, default=None) _device_state_attributes: Dict[str, Any] = attr.ib(init=False, factory=dict) + def __attrs_post_init__(self): + """Initialize internal state.""" + self._device_state_attributes["mac_address"] = self.mac + @property def _entity_name(self) -> str: - return self._name + return self._hostname or self.mac @property def _device_unique_id(self) -> str: @@ -145,11 +147,9 @@ async def async_update(self) -> None: host = next((x for x in hosts if x.get("MacAddress") == self.mac), None) self._is_connected = host is not None if self._is_connected: - self._name = host.get("HostName", self.mac) + self._hostname = host.get("HostName") self._device_state_attributes = { - _better_snakecase(k): v - for k, v in host.items() - if k not in ("MacAddress", "HostName") + _better_snakecase(k): v for k, v in host.items() if k != "HostName" } diff --git a/homeassistant/components/huawei_lte/manifest.json b/homeassistant/components/huawei_lte/manifest.json index 4ea54188688d..8fd4ba4bec13 100644 --- a/homeassistant/components/huawei_lte/manifest.json +++ b/homeassistant/components/huawei_lte/manifest.json @@ -5,7 +5,7 @@ "documentation": "https://www.home-assistant.io/integrations/huawei_lte", "requirements": [ "getmac==0.8.1", - "huawei-lte-api==1.4.3", + "huawei-lte-api==1.4.4", "stringcase==1.2.0", "url-normalize==1.4.1" ], diff --git a/homeassistant/components/huawei_lte/sensor.py b/homeassistant/components/huawei_lte/sensor.py index 99170d4e7c0b..3cc36b30d8ec 100644 --- a/homeassistant/components/huawei_lte/sensor.py +++ b/homeassistant/components/huawei_lte/sensor.py @@ -11,7 +11,6 @@ DEVICE_CLASS_SIGNAL_STRENGTH, DOMAIN as SENSOR_DOMAIN, ) -from homeassistant.helpers import entity_registry from . import HuaweiLteBaseEntity from .const import ( @@ -170,23 +169,6 @@ async def async_setup_entry(hass, config_entry, async_add_entities): HuaweiLteSensor(router, key, item, SENSOR_META.get((key, item), {})) ) - # Pre-0.97 unique id migration. Old ones used the device serial number - # (see comments in HuaweiLteData._setup_lte for more info), as well as - # had a bug that joined the path str with periods, not the path components, - # resulting e.g. *_device_signal.sinr to end up as - # *_d.e.v.i.c.e._.s.i.g.n.a.l...s.i.n.r - entreg = await entity_registry.async_get_registry(hass) - for entid, ent in entreg.entities.items(): - if ent.platform != DOMAIN: - continue - for sensor in sensors: - oldsuf = ".".join(f"{sensor.key}.{sensor.item}") - if ent.unique_id.endswith(f"_{oldsuf}"): - entreg.async_update_entity(entid, new_unique_id=sensor.unique_id) - _LOGGER.debug( - "Updated entity %s unique id to %s", entid, sensor.unique_id - ) - async_add_entities(sensors, True) diff --git a/homeassistant/components/hue/.translations/bg.json b/homeassistant/components/hue/.translations/bg.json index 04ee6d138314..5f28f4bde40a 100644 --- a/homeassistant/components/hue/.translations/bg.json +++ b/homeassistant/components/hue/.translations/bg.json @@ -26,6 +26,6 @@ "title": "\u0421\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435 \u043d\u0430 \u0445\u044a\u0431" } }, - "title": "\u0411\u0430\u0437\u043e\u0432\u0430 \u0441\u0442\u0430\u043d\u0446\u0438\u044f Philips Hue" + "title": "Philips Hue" } } \ No newline at end of file diff --git a/homeassistant/components/hue/.translations/pl.json b/homeassistant/components/hue/.translations/pl.json index 33b1ffbfe86c..3866af9d7fca 100644 --- a/homeassistant/components/hue/.translations/pl.json +++ b/homeassistant/components/hue/.translations/pl.json @@ -22,7 +22,7 @@ "title": "Wybierz mostek Hue" }, "link": { - "description": "Naci\u015bnij przycisk na mostku, aby zarejestrowa\u0107 Philips Hue z Home Assistant.", + "description": "Naci\u015bnij przycisk na mostku, aby zarejestrowa\u0107 Philips Hue w Home Assistant.\n\n![Location of button on bridge](/static/images/config_philips_hue.jpg)", "title": "Hub Link" } }, diff --git a/homeassistant/components/hue/.translations/ro.json b/homeassistant/components/hue/.translations/ro.json index a2ecf8964b61..9da771a52dcc 100644 --- a/homeassistant/components/hue/.translations/ro.json +++ b/homeassistant/components/hue/.translations/ro.json @@ -19,6 +19,7 @@ "link": { "description": "Ap\u0103sa\u021bi butonul de pe pod pentru a \u00eenregistra Philips Hue cu Home Assistant. \n\n ! [Loca\u021bia butonului pe pod] (/ static / images / config_philips_hue.jpg)" } - } + }, + "title": "Philips Hue" } } \ No newline at end of file diff --git a/homeassistant/components/hue/bridge.py b/homeassistant/components/hue/bridge.py index 5015ec669aaa..5a5e55773a5d 100644 --- a/homeassistant/components/hue/bridge.py +++ b/homeassistant/components/hue/bridge.py @@ -3,6 +3,7 @@ import aiohue import async_timeout +import slugify as unicode_slug import voluptuous as vol from homeassistant.exceptions import ConfigEntryNotReady @@ -32,6 +33,7 @@ def __init__(self, hass, config_entry, allow_unreachable, allow_groups): self.available = True self.authorized = False self.api = None + self.parallel_updates_semaphore = None @property def host(self): @@ -77,9 +79,19 @@ async def async_setup(self, tries=0): DOMAIN, SERVICE_HUE_SCENE, self.hue_activate_scene, schema=SCENE_SCHEMA ) + self.parallel_updates_semaphore = asyncio.Semaphore( + 3 if self.api.config.modelid == "BSB001" else 10 + ) + self.authorized = True return True + async def async_request_call(self, coro): + """Process request batched.""" + + async with self.parallel_updates_semaphore: + return await coro + async def async_reset(self): """Reset this bridge to default state. @@ -173,7 +185,11 @@ async def get_bridge(hass, host, username=None): with async_timeout.timeout(10): # Create username if we don't have one if not username: - await bridge.create_user(f"home-assistant#{hass.config.location_name}") + device_name = unicode_slug.slugify( + hass.config.location_name, max_length=19 + ) + await bridge.create_user(f"home-assistant#{device_name}") + # Initialize bridge (and validate our username) await bridge.initialize() diff --git a/homeassistant/components/hue/config_flow.py b/homeassistant/components/hue/config_flow.py index ebd71ba7c1cf..375042c88351 100644 --- a/homeassistant/components/hue/config_flow.py +++ b/homeassistant/components/hue/config_flow.py @@ -16,6 +16,7 @@ from .errors import AuthenticationRequired, CannotConnect HUE_MANUFACTURERURL = "http://www.philips.com" +HUE_IGNORED_BRIDGE_NAMES = ["HASS Bridge", "Espalexa"] @callback @@ -133,14 +134,16 @@ async def async_step_ssdp(self, discovery_info): This flow is triggered by the SSDP component. It will check if the host is already configured and delegate to the import step if not. """ - from homeassistant.components.ssdp import ATTR_MANUFACTURERURL + from homeassistant.components.ssdp import ATTR_MANUFACTURERURL, ATTR_NAME if discovery_info[ATTR_MANUFACTURERURL] != HUE_MANUFACTURERURL: return self.async_abort(reason="not_hue_bridge") - # Filter out emulated Hue - if "HASS Bridge" in discovery_info.get("name", ""): - return self.async_abort(reason="already_configured") + if any( + name in discovery_info.get(ATTR_NAME, "") + for name in HUE_IGNORED_BRIDGE_NAMES + ): + return self.async_abort(reason="not_hue_bridge") host = self.context["host"] = discovery_info.get("host") diff --git a/homeassistant/components/hue/light.py b/homeassistant/components/hue/light.py index d58e4608b655..ad511639d578 100644 --- a/homeassistant/components/hue/light.py +++ b/homeassistant/components/hue/light.py @@ -202,7 +202,7 @@ async def async_update_items( try: start = monotonic() with async_timeout.timeout(4): - await api.update() + await bridge.async_request_call(api.update()) except aiohue.Unauthorized: await bridge.handle_unauthorized_error() return @@ -434,9 +434,9 @@ async def async_turn_on(self, **kwargs): command["effect"] = "none" if self.is_group: - await self.light.set_action(**command) + await self.bridge.async_request_call(self.light.set_action(**command)) else: - await self.light.set_state(**command) + await self.bridge.async_request_call(self.light.set_state(**command)) async def async_turn_off(self, **kwargs): """Turn the specified or all lights off.""" @@ -457,9 +457,9 @@ async def async_turn_off(self, **kwargs): command["alert"] = "none" if self.is_group: - await self.light.set_action(**command) + await self.bridge.async_request_call(self.light.set_action(**command)) else: - await self.light.set_state(**command) + await self.bridge.async_request_call(self.light.set_state(**command)) async def async_update(self): """Synchronize state with bridge.""" diff --git a/homeassistant/components/hue/sensor_base.py b/homeassistant/components/hue/sensor_base.py index bf64fed0c951..f7882b102c07 100644 --- a/homeassistant/components/hue/sensor_base.py +++ b/homeassistant/components/hue/sensor_base.py @@ -101,7 +101,7 @@ async def async_update_items(self): try: start = monotonic() with async_timeout.timeout(4): - await api.update() + await self.bridge.async_request_call(api.update()) except Unauthorized: await self.bridge.handle_unauthorized_error() return diff --git a/homeassistant/components/hunterdouglas_powerview/scene.py b/homeassistant/components/hunterdouglas_powerview/scene.py index d55a970f1e41..3f2ac79306c3 100644 --- a/homeassistant/components/hunterdouglas_powerview/scene.py +++ b/homeassistant/components/hunterdouglas_powerview/scene.py @@ -1,12 +1,16 @@ """Support for Powerview scenes from a Powerview hub.""" import logging +from aiopvapi.helpers.aiorequest import AioRequest +from aiopvapi.resources.scene import Scene as PvScene +from aiopvapi.rooms import Rooms +from aiopvapi.scenes import Scenes import voluptuous as vol -import homeassistant.helpers.config_validation as cv -from homeassistant.components.scene import Scene, DOMAIN +from homeassistant.components.scene import DOMAIN, Scene from homeassistant.const import CONF_PLATFORM from homeassistant.helpers.aiohttp_client import async_get_clientsession +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import async_generate_entity_id _LOGGER = logging.getLogger(__name__) @@ -33,11 +37,6 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up home assistant scene entries.""" - # from aiopvapi.hub import Hub - from aiopvapi.helpers.aiorequest import AioRequest - from aiopvapi.scenes import Scenes - from aiopvapi.rooms import Rooms - from aiopvapi.resources.scene import Scene as PvScene hub_address = config.get(HUB_ADDRESS) websession = async_get_clientsession(hass) diff --git a/homeassistant/components/hydrawise/__init__.py b/homeassistant/components/hydrawise/__init__.py index e111c157460c..57ed29d9780e 100644 --- a/homeassistant/components/hydrawise/__init__.py +++ b/homeassistant/components/hydrawise/__init__.py @@ -2,12 +2,13 @@ from datetime import timedelta import logging +from hydrawiser.core import Hydrawiser from requests.exceptions import ConnectTimeout, HTTPError import voluptuous as vol from homeassistant.const import ATTR_ATTRIBUTION, CONF_ACCESS_TOKEN, CONF_SCAN_INTERVAL -import homeassistant.helpers.config_validation as cv from homeassistant.core import callback +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_connect, dispatcher_send from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import track_time_interval @@ -73,8 +74,6 @@ def setup(hass, config): scan_interval = conf.get(CONF_SCAN_INTERVAL) try: - from hydrawiser.core import Hydrawiser - hydrawise = Hydrawiser(user_token=access_token) hass.data[DATA_HYDRAWISE] = HydrawiseHub(hydrawise) except (ConnectTimeout, HTTPError) as ex: diff --git a/homeassistant/components/ialarm/alarm_control_panel.py b/homeassistant/components/ialarm/alarm_control_panel.py index 845c6b9021f1..24ab2bc7a80b 100644 --- a/homeassistant/components/ialarm/alarm_control_panel.py +++ b/homeassistant/components/ialarm/alarm_control_panel.py @@ -2,10 +2,15 @@ import logging import re +from pyialarm import IAlarm import voluptuous as vol import homeassistant.components.alarm_control_panel as alarm from homeassistant.components.alarm_control_panel import PLATFORM_SCHEMA +from homeassistant.components.alarm_control_panel.const import ( + SUPPORT_ALARM_ARM_AWAY, + SUPPORT_ALARM_ARM_HOME, +) from homeassistant.const import ( CONF_CODE, CONF_HOST, @@ -62,7 +67,6 @@ class IAlarmPanel(alarm.AlarmControlPanel): def __init__(self, name, code, username, password, url): """Initialize the iAlarm status.""" - from pyialarm import IAlarm self._name = name self._code = str(code) if code else None @@ -91,6 +95,11 @@ def state(self): """Return the state of the device.""" return self._state + @property + def supported_features(self) -> int: + """Return the list of supported features.""" + return SUPPORT_ALARM_ARM_HOME | SUPPORT_ALARM_ARM_AWAY + def update(self): """Return the state of the device.""" status = self._client.get_status() diff --git a/homeassistant/components/iaqualink/.translations/lb.json b/homeassistant/components/iaqualink/.translations/lb.json index 4beb11214bc2..db8d67eea755 100644 --- a/homeassistant/components/iaqualink/.translations/lb.json +++ b/homeassistant/components/iaqualink/.translations/lb.json @@ -12,7 +12,7 @@ "password": "Passwuert", "username": "Benotzernumm / E-Mail Adresse" }, - "description": "Gitt den Benotznumm an d'Passwuert fir \u00e4ren iAqualink Kont un.", + "description": "Gitt den Benotzernumm an d'Passwuert fir \u00e4ren iAqualink Kont un.", "title": "Mat iAqualink verbannen" } }, diff --git a/homeassistant/components/iaqualink/.translations/pt.json b/homeassistant/components/iaqualink/.translations/pt.json new file mode 100644 index 000000000000..24825307e767 --- /dev/null +++ b/homeassistant/components/iaqualink/.translations/pt.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "Palavra-passe", + "username": "Nome de utilizador / Endere\u00e7o de e-mail" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/iaqualink/.translations/ru.json b/homeassistant/components/iaqualink/.translations/ru.json index 9a93c19ef20b..8c8e30fe067d 100644 --- a/homeassistant/components/iaqualink/.translations/ru.json +++ b/homeassistant/components/iaqualink/.translations/ru.json @@ -12,7 +12,7 @@ "password": "\u041f\u0430\u0440\u043e\u043b\u044c", "username": "\u041b\u043e\u0433\u0438\u043d / \u0410\u0434\u0440\u0435\u0441 \u044d\u043b. \u043f\u043e\u0447\u0442\u044b" }, - "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u043b\u043e\u0433\u0438\u043d \u0438 \u043f\u0430\u0440\u043e\u043b\u044c \u0434\u043b\u044f \u0412\u0430\u0448\u0435\u0439 \u0443\u0447\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 iAqualink.", + "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u043b\u043e\u0433\u0438\u043d \u0438 \u043f\u0430\u0440\u043e\u043b\u044c \u0434\u043b\u044f \u0412\u0430\u0448\u0435\u0439 \u0443\u0447\u0451\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 iAqualink.", "title": "Jandy iAqualink" } }, diff --git a/homeassistant/components/icloud/const.py b/homeassistant/components/icloud/const.py new file mode 100644 index 000000000000..fe8010df703b --- /dev/null +++ b/homeassistant/components/icloud/const.py @@ -0,0 +1,6 @@ +"""Constants for the iCloud component.""" +DOMAIN = "icloud" +SERVICE_LOST_IPHONE = "lost_iphone" +SERVICE_UPDATE = "update" +SERVICE_RESET_ACCOUNT = "reset_account" +SERVICE_SET_INTERVAL = "set_interval" diff --git a/homeassistant/components/icloud/device_tracker.py b/homeassistant/components/icloud/device_tracker.py index 2ecf904314fe..3d9fb4715da0 100644 --- a/homeassistant/components/icloud/device_tracker.py +++ b/homeassistant/components/icloud/device_tracker.py @@ -1,25 +1,38 @@ """Platform that supports scanning iCloud.""" import logging -import random import os +import random +from pyicloud import PyiCloudService +from pyicloud.exceptions import ( + PyiCloudException, + PyiCloudFailedLoginException, + PyiCloudNoDevicesException, +) import voluptuous as vol -from homeassistant.const import CONF_USERNAME, CONF_PASSWORD from homeassistant.components.device_tracker import PLATFORM_SCHEMA from homeassistant.components.device_tracker.const import ( - DOMAIN, ATTR_ATTRIBUTES, ENTITY_ID_FORMAT, ) from homeassistant.components.device_tracker.legacy import DeviceScanner from homeassistant.components.zone import async_active_zone -from homeassistant.helpers.event import track_utc_time_change +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.event import track_utc_time_change from homeassistant.util import slugify +from homeassistant.util.async_ import run_callback_threadsafe import homeassistant.util.dt as dt_util from homeassistant.util.location import distance -from homeassistant.util.async_ import run_callback_threadsafe + +from .const import ( + DOMAIN, + SERVICE_LOST_IPHONE, + SERVICE_RESET_ACCOUNT, + SERVICE_SET_INTERVAL, + SERVICE_UPDATE, +) _LOGGER = logging.getLogger(__name__) @@ -138,7 +151,7 @@ def lost_iphone(call): ICLOUDTRACKERS[account].lost_iphone(devicename) hass.services.register( - DOMAIN, "icloud_lost_iphone", lost_iphone, schema=SERVICE_SCHEMA + DOMAIN, SERVICE_LOST_IPHONE, lost_iphone, schema=SERVICE_SCHEMA ) def update_icloud(call): @@ -149,9 +162,7 @@ def update_icloud(call): if account in ICLOUDTRACKERS: ICLOUDTRACKERS[account].update_icloud(devicename) - hass.services.register( - DOMAIN, "icloud_update", update_icloud, schema=SERVICE_SCHEMA - ) + hass.services.register(DOMAIN, SERVICE_UPDATE, update_icloud, schema=SERVICE_SCHEMA) def reset_account_icloud(call): """Reset an iCloud account.""" @@ -161,7 +172,7 @@ def reset_account_icloud(call): ICLOUDTRACKERS[account].reset_account_icloud() hass.services.register( - DOMAIN, "icloud_reset_account", reset_account_icloud, schema=SERVICE_SCHEMA + DOMAIN, SERVICE_RESET_ACCOUNT, reset_account_icloud, schema=SERVICE_SCHEMA ) def setinterval(call): @@ -174,7 +185,7 @@ def setinterval(call): ICLOUDTRACKERS[account].setinterval(interval, devicename) hass.services.register( - DOMAIN, "icloud_set_interval", setinterval, schema=SERVICE_SCHEMA + DOMAIN, SERVICE_SET_INTERVAL, setinterval, schema=SERVICE_SCHEMA ) # Tells the bootstrapper that the component was successfully initialized @@ -214,12 +225,6 @@ def __init__( def reset_account_icloud(self): """Reset an iCloud account.""" - from pyicloud import PyiCloudService - from pyicloud.exceptions import ( - PyiCloudFailedLoginException, - PyiCloudNoDevicesException, - ) - icloud_dir = self.hass.config.path("icloud") if not os.path.exists(icloud_dir): os.makedirs(icloud_dir) @@ -297,8 +302,6 @@ def icloud_need_trusted_device(self): def icloud_verification_callback(self, callback_data): """Handle the chosen trusted device.""" - from pyicloud.exceptions import PyiCloudException - self._verification_code = callback_data.get("code") try: @@ -344,8 +347,6 @@ def keep_alive(self, now): return if self.api.requires_2fa: - from pyicloud.exceptions import PyiCloudException - try: if self._trusted_device is None: self.icloud_need_trusted_device() @@ -436,8 +437,6 @@ def determine_interval(self, devicename, latitude, longitude, battery): def update_device(self, devicename): """Update the device_tracker entity.""" - from pyicloud.exceptions import PyiCloudNoDevicesException - # An entity will not be created by see() when track=false in # 'known_devices.yaml', but we need to see() it at least once entity = self.hass.states.get(ENTITY_ID_FORMAT.format(devicename)) @@ -503,8 +502,6 @@ def lost_iphone(self, devicename): def update_icloud(self, devicename=None): """Request device information from iCloud and update device_tracker.""" - from pyicloud.exceptions import PyiCloudNoDevicesException - if self.api is None: return diff --git a/homeassistant/components/icloud/services.yaml b/homeassistant/components/icloud/services.yaml index e69de29bb2d1..7b2d3b80e843 100644 --- a/homeassistant/components/icloud/services.yaml +++ b/homeassistant/components/icloud/services.yaml @@ -0,0 +1,39 @@ +lost_iphone: + description: Service to play the lost iphone sound on an iDevice. + fields: + account_name: + description: Name of the account in the config that will be used to look for the device. This is optional, if it isn't given it will use all accounts. + example: 'bart' + device_name: + description: Name of the device that will play the sound. This is optional, if it isn't given it will play on all devices for the given account. + example: 'iphonebart' + +set_interval: + description: Service to set the interval of an iDevice. + fields: + account_name: + description: Name of the account in the config that will be used to look for the device. This is optional, if it isn't given it will use all accounts. + example: 'bart' + device_name: + description: Name of the device that will get a new interval. This is optional, if it isn't given it will change the interval for all devices for the given account. + example: 'iphonebart' + interval: + description: The interval (in minutes) that the iDevice will have until the according device_tracker entity changes from zone or until this service is used again. This is optional, if it isn't given the interval of the device will revert back to the original interval based on the current state. + example: 1 + +update: + description: Service to ask for an update of an iDevice. + fields: + account_name: + description: Name of the account in the config that will be used to look for the device. This is optional, if it isn't given it will use all accounts. + example: 'bart' + device_name: + description: Name of the device that will be updated. This is optional, if it isn't given it will update all devices for the given account. + example: 'iphonebart' + +reset_account: + description: Service to restart an iCloud account. Helpful when not all devices are found after initializing or when you add a new device. + fields: + account_name: + description: Name of the account in the config that will be restarted. This is optional, if it isn't given it will restart all accounts. + example: 'bart' diff --git a/homeassistant/components/idteck_prox/__init__.py b/homeassistant/components/idteck_prox/__init__.py index 089347a0f730..9cc4f3de9d60 100644 --- a/homeassistant/components/idteck_prox/__init__.py +++ b/homeassistant/components/idteck_prox/__init__.py @@ -1,15 +1,16 @@ """Component for interfacing RFK101 proximity card readers.""" import logging +from rfk101py.rfk101py import rfk101py import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.const import ( CONF_HOST, - CONF_PORT, CONF_NAME, + CONF_PORT, EVENT_HOMEASSISTANT_STOP, ) +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -68,7 +69,6 @@ def __init__(self, hass, host, port, name): def connect(self): """Connect to the reader.""" - from rfk101py.rfk101py import rfk101py self._connection = rfk101py(self._host, self._port, self._callback) diff --git a/homeassistant/components/ifttt/.translations/pl.json b/homeassistant/components/ifttt/.translations/pl.json index ca81a5105317..206702eb593c 100644 --- a/homeassistant/components/ifttt/.translations/pl.json +++ b/homeassistant/components/ifttt/.translations/pl.json @@ -5,7 +5,7 @@ "one_instance_allowed": "Wymagana jest tylko jedna instancja." }, "create_entry": { - "default": "Aby wys\u0142a\u0107 zdarzenia do Home Assistant'a, b\u0119dziesz musia\u0142 u\u017cy\u0107 akcji \"Make a web request\" z [IFTTT Webhook apletu]({applet_url}). \n\n Wprowad\u017a nast\u0119puj\u0105ce dane:\n\n - URL: `{webhook_url}`\n - Metoda: POST\n - Typ zawarto\u015bci: application/json\n\nZapoznaj si\u0119 z [dokumentacj\u0105]({docs_url}) na temat konfiguracji automatyzacji, by obs\u0142u\u017cy\u0107 przychodz\u0105ce dane." + "default": "Aby wysy\u0142a\u0107 zdarzenia do Home Assistant'a, musisz u\u017cy\u0107 akcji \"Make a web request\" z [IFTTT Webhook apletu]({applet_url}). \n\n Wprowad\u017a nast\u0119puj\u0105ce dane:\n\n - URL: `{webhook_url}`\n - Metoda: POST\n - Typ zawarto\u015bci: application/json\n\nZapoznaj si\u0119 z [dokumentacj\u0105]({docs_url}) na temat konfiguracji automatyzacji, by obs\u0142u\u017cy\u0107 przychodz\u0105ce dane." }, "step": { "user": { diff --git a/homeassistant/components/ifttt/__init__.py b/homeassistant/components/ifttt/__init__.py index 05d773e9fd68..362b01bb5d80 100644 --- a/homeassistant/components/ifttt/__init__.py +++ b/homeassistant/components/ifttt/__init__.py @@ -23,6 +23,7 @@ CONF_KEY = "key" +SERVICE_PUSH_ALARM_STATE = "push_alarm_state" SERVICE_TRIGGER = "trigger" SERVICE_TRIGGER_SCHEMA = vol.Schema( diff --git a/homeassistant/components/ifttt/alarm_control_panel.py b/homeassistant/components/ifttt/alarm_control_panel.py index e4d8b6ce654b..9c9ec88ccc71 100644 --- a/homeassistant/components/ifttt/alarm_control_panel.py +++ b/homeassistant/components/ifttt/alarm_control_panel.py @@ -4,8 +4,17 @@ import voluptuous as vol -import homeassistant.components.alarm_control_panel as alarm -from homeassistant.components.alarm_control_panel import DOMAIN, PLATFORM_SCHEMA +from homeassistant.components.alarm_control_panel import ( + AlarmControlPanel, + FORMAT_NUMBER, + FORMAT_TEXT, +) +from homeassistant.components.alarm_control_panel import PLATFORM_SCHEMA +from homeassistant.components.alarm_control_panel.const import ( + SUPPORT_ALARM_ARM_AWAY, + SUPPORT_ALARM_ARM_HOME, + SUPPORT_ALARM_ARM_NIGHT, +) from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_STATE, @@ -19,7 +28,7 @@ ) import homeassistant.helpers.config_validation as cv -from . import ATTR_EVENT, DOMAIN as IFTTT_DOMAIN, SERVICE_TRIGGER +from . import ATTR_EVENT, DOMAIN, SERVICE_PUSH_ALARM_STATE, SERVICE_TRIGGER _LOGGER = logging.getLogger(__name__) @@ -55,8 +64,6 @@ } ) -SERVICE_PUSH_ALARM_STATE = "ifttt_push_alarm_state" - PUSH_ALARM_STATE_SERVICE_SCHEMA = vol.Schema( {vol.Required(ATTR_ENTITY_ID): cv.entity_ids, vol.Required(ATTR_STATE): cv.string} ) @@ -101,7 +108,7 @@ async def push_state_update(service): ) -class IFTTTAlarmPanel(alarm.AlarmControlPanel): +class IFTTTAlarmPanel(AlarmControlPanel): """Representation of an alarm control panel controlled through IFTTT.""" def __init__( @@ -127,6 +134,11 @@ def state(self): """Return the state of the device.""" return self._state + @property + def supported_features(self) -> int: + """Return the list of supported features.""" + return SUPPORT_ALARM_ARM_HOME | SUPPORT_ALARM_ARM_AWAY | SUPPORT_ALARM_ARM_NIGHT + @property def assumed_state(self): """Notify that this platform return an assumed state.""" @@ -138,8 +150,8 @@ def code_format(self): if self._code is None: return None if isinstance(self._code, str) and re.search("^\\d+$", self._code): - return alarm.FORMAT_NUMBER - return alarm.FORMAT_TEXT + return FORMAT_NUMBER + return FORMAT_TEXT def alarm_disarm(self, code=None): """Send disarm command.""" @@ -169,7 +181,7 @@ def set_alarm_state(self, event, state): """Call the IFTTT trigger service to change the alarm state.""" data = {ATTR_EVENT: event} - self.hass.services.call(IFTTT_DOMAIN, SERVICE_TRIGGER, data) + self.hass.services.call(DOMAIN, SERVICE_TRIGGER, data) _LOGGER.debug("Called IFTTT integration to trigger event %s", event) if self._optimistic: self._state = state diff --git a/homeassistant/components/ifttt/services.yaml b/homeassistant/components/ifttt/services.yaml index 8669bc07fb43..693c654f2582 100644 --- a/homeassistant/components/ifttt/services.yaml +++ b/homeassistant/components/ifttt/services.yaml @@ -1,5 +1,14 @@ # Describes the format for available ifttt services +push_alarm_state: + description: Update the alarm state to the specified value. + fields: + entity_id: + description: Name of the alarm control panel which state has to be updated. + example: 'alarm_control_panel.downstairs' + state: + description: The state to which the alarm control panel has to be set. + example: 'armed_night' trigger: description: Triggers the configured IFTTT Webhook. @@ -15,4 +24,4 @@ trigger: example: 'some additional data' value3: description: Generic field to send data via the event. - example: 'even more data' \ No newline at end of file + example: 'even more data' diff --git a/homeassistant/components/iglo/light.py b/homeassistant/components/iglo/light.py index d93ebcb920aa..59e6db2a81f1 100644 --- a/homeassistant/components/iglo/light.py +++ b/homeassistant/components/iglo/light.py @@ -2,6 +2,7 @@ import logging import math +from iglo import Lamp import voluptuous as vol from homeassistant.components.light import ( @@ -9,11 +10,11 @@ ATTR_COLOR_TEMP, ATTR_EFFECT, ATTR_HS_COLOR, + PLATFORM_SCHEMA, SUPPORT_BRIGHTNESS, - SUPPORT_COLOR_TEMP, SUPPORT_COLOR, + SUPPORT_COLOR_TEMP, SUPPORT_EFFECT, - PLATFORM_SCHEMA, Light, ) from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT @@ -47,7 +48,6 @@ class IGloLamp(Light): def __init__(self, name, host, port): """Initialize the light.""" - from iglo import Lamp self._name = name self._lamp = Lamp(0, host, port) diff --git a/homeassistant/components/ihc/__init__.py b/homeassistant/components/ihc/__init__.py index a55b94eb26a5..b246943b6add 100644 --- a/homeassistant/components/ihc/__init__.py +++ b/homeassistant/components/ihc/__init__.py @@ -2,6 +2,8 @@ import logging import os.path +from defusedxml import ElementTree +from ihcsdk.ihccontroller import IHCController import voluptuous as vol from homeassistant.components.binary_sensor import DEVICE_CLASSES_SCHEMA @@ -214,7 +216,6 @@ def setup(hass, config): def ihc_setup(hass, config, conf, controller_id): """Set up the IHC component.""" - from ihcsdk.ihccontroller import IHCController url = conf[CONF_URL] username = conf[CONF_USERNAME] @@ -272,7 +273,6 @@ def autosetup_ihc_products( hass: HomeAssistantType, config, ihc_controller, controller_id ): """Auto setup of IHC products from the IHC project file.""" - from defusedxml import ElementTree project_xml = ihc_controller.get_project() if not project_xml: diff --git a/homeassistant/components/image_processing/__init__.py b/homeassistant/components/image_processing/__init__.py index 4c90441e7f01..78ae15eb5372 100644 --- a/homeassistant/components/image_processing/__init__.py +++ b/homeassistant/components/image_processing/__init__.py @@ -11,7 +11,7 @@ from homeassistant.core import callback from homeassistant.exceptions import HomeAssistantError import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.config_validation import ENTITY_SERVICE_SCHEMA +from homeassistant.helpers.config_validation import make_entity_service_schema from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent from homeassistant.util.async_ import run_callback_threadsafe @@ -124,7 +124,7 @@ async def async_scan_service(service): await asyncio.wait(update_tasks) hass.services.async_register( - DOMAIN, SERVICE_SCAN, async_scan_service, schema=ENTITY_SERVICE_SCHEMA + DOMAIN, SERVICE_SCAN, async_scan_service, schema=make_entity_service_schema({}) ) return True diff --git a/homeassistant/components/image_processing/services.yaml b/homeassistant/components/image_processing/services.yaml index 0689c34c1a3e..1f1fa347dc9b 100644 --- a/homeassistant/components/image_processing/services.yaml +++ b/homeassistant/components/image_processing/services.yaml @@ -6,16 +6,3 @@ scan: entity_id: description: Name(s) of entities to scan immediately. example: 'image_processing.alpr_garage' - -facebox_teach_face: - description: Teach facebox a face using a file. - fields: - entity_id: - description: The facebox entity to teach. - example: 'image_processing.facebox' - name: - description: The name of the face to teach. - example: 'my_name' - file_path: - description: The path to the image file. - example: '/images/my_image.jpg' diff --git a/homeassistant/components/input_boolean/__init__.py b/homeassistant/components/input_boolean/__init__.py index 6027b0b3da1a..4a32ad167973 100644 --- a/homeassistant/components/input_boolean/__init__.py +++ b/homeassistant/components/input_boolean/__init__.py @@ -13,7 +13,6 @@ ) from homeassistant.loader import bind_hass import homeassistant.helpers.config_validation as cv -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 @@ -68,17 +67,11 @@ async def async_setup(hass, config): if not entities: return False - component.async_register_entity_service( - SERVICE_TURN_ON, ENTITY_SERVICE_SCHEMA, "async_turn_on" - ) + component.async_register_entity_service(SERVICE_TURN_ON, {}, "async_turn_on") - component.async_register_entity_service( - SERVICE_TURN_OFF, ENTITY_SERVICE_SCHEMA, "async_turn_off" - ) + component.async_register_entity_service(SERVICE_TURN_OFF, {}, "async_turn_off") - component.async_register_entity_service( - SERVICE_TOGGLE, ENTITY_SERVICE_SCHEMA, "async_toggle" - ) + component.async_register_entity_service(SERVICE_TOGGLE, {}, "async_toggle") await component.async_add_entities(entities) return True diff --git a/homeassistant/components/input_datetime/__init__.py b/homeassistant/components/input_datetime/__init__.py index 85fac4130a3e..36180ed2bad1 100644 --- a/homeassistant/components/input_datetime/__init__.py +++ b/homeassistant/components/input_datetime/__init__.py @@ -6,7 +6,6 @@ from homeassistant.const import ATTR_DATE, ATTR_TIME, CONF_ICON, CONF_NAME import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.config_validation import ENTITY_SERVICE_SCHEMA from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.util import dt as dt_util @@ -27,14 +26,6 @@ SERVICE_SET_DATETIME = "set_datetime" -SERVICE_SET_DATETIME_SCHEMA = ENTITY_SERVICE_SCHEMA.extend( - { - vol.Optional(ATTR_DATE): cv.date, - vol.Optional(ATTR_TIME): cv.time, - vol.Optional(ATTR_DATETIME): cv.datetime, - } -) - def has_date_or_time(conf): """Check at least date or time is true.""" @@ -87,7 +78,6 @@ async def async_set_datetime_service(entity, call): time = call.data.get(ATTR_TIME) date = call.data.get(ATTR_DATE) dttm = call.data.get(ATTR_DATETIME) - # pylint: disable=too-many-boolean-expressions if ( dttm and (date or time) @@ -109,7 +99,13 @@ async def async_set_datetime_service(entity, call): entity.async_set_datetime(date, time) component.async_register_entity_service( - SERVICE_SET_DATETIME, SERVICE_SET_DATETIME_SCHEMA, async_set_datetime_service + SERVICE_SET_DATETIME, + { + vol.Optional(ATTR_DATE): cv.date, + vol.Optional(ATTR_TIME): cv.time, + vol.Optional(ATTR_DATETIME): cv.datetime, + }, + async_set_datetime_service, ) await component.async_add_entities(entities) diff --git a/homeassistant/components/input_datetime/reproduce_state.py b/homeassistant/components/input_datetime/reproduce_state.py index 09a30e652105..17c7fcb9d566 100644 --- a/homeassistant/components/input_datetime/reproduce_state.py +++ b/homeassistant/components/input_datetime/reproduce_state.py @@ -31,18 +31,12 @@ def is_valid_datetime(string: str) -> bool: def is_valid_date(string: str) -> bool: """Test if string dt is a valid date.""" - try: - return dt_util.parse_date(string) is not None - except ValueError: - return False + return dt_util.parse_date(string) is not None def is_valid_time(string: str) -> bool: """Test if string dt is a valid time.""" - try: - return dt_util.parse_time(string) is not None - except ValueError: - return False + return dt_util.parse_time(string) is not None async def _async_reproduce_state( diff --git a/homeassistant/components/input_number/__init__.py b/homeassistant/components/input_number/__init__.py index 9b4d5a961ba3..77625ffa7f8e 100644 --- a/homeassistant/components/input_number/__init__.py +++ b/homeassistant/components/input_number/__init__.py @@ -4,7 +4,6 @@ import voluptuous as vol 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, @@ -38,10 +37,6 @@ SERVICE_INCREMENT = "increment" SERVICE_DECREMENT = "decrement" -SERVICE_SET_VALUE_SCHEMA = ENTITY_SERVICE_SCHEMA.extend( - {vol.Required(ATTR_VALUE): vol.Coerce(float)} -) - def _cv_input_number(cfg): """Configure validation helper for input number (voluptuous).""" @@ -110,16 +105,14 @@ async def async_setup(hass, config): return False component.async_register_entity_service( - SERVICE_SET_VALUE, SERVICE_SET_VALUE_SCHEMA, "async_set_value" + SERVICE_SET_VALUE, + {vol.Required(ATTR_VALUE): vol.Coerce(float)}, + "async_set_value", ) - component.async_register_entity_service( - SERVICE_INCREMENT, ENTITY_SERVICE_SCHEMA, "async_increment" - ) + component.async_register_entity_service(SERVICE_INCREMENT, {}, "async_increment") - component.async_register_entity_service( - SERVICE_DECREMENT, ENTITY_SERVICE_SCHEMA, "async_decrement" - ) + component.async_register_entity_service(SERVICE_DECREMENT, {}, "async_decrement") await component.async_add_entities(entities) return True diff --git a/homeassistant/components/input_select/__init__.py b/homeassistant/components/input_select/__init__.py index 8cb3001c52e3..ae609e092715 100644 --- a/homeassistant/components/input_select/__init__.py +++ b/homeassistant/components/input_select/__init__.py @@ -5,7 +5,6 @@ from homeassistant.const import CONF_ICON, CONF_NAME import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.config_validation import ENTITY_SERVICE_SCHEMA from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.restore_state import RestoreEntity @@ -22,9 +21,6 @@ SERVICE_SELECT_OPTION = "select_option" -SERVICE_SELECT_OPTION_SCHEMA = ENTITY_SERVICE_SCHEMA.extend( - {vol.Required(ATTR_OPTION): cv.string} -) SERVICE_SELECT_NEXT = "select_next" @@ -32,14 +28,6 @@ SERVICE_SET_OPTIONS = "set_options" -SERVICE_SET_OPTIONS_SCHEMA = ENTITY_SERVICE_SCHEMA.extend( - { - vol.Required(ATTR_OPTIONS): vol.All( - cv.ensure_list, vol.Length(min=1), [cv.string] - ) - } -) - def _cv_input_select(cfg): """Configure validation helper for input select (voluptuous).""" @@ -92,23 +80,27 @@ async def async_setup(hass, config): return False component.async_register_entity_service( - SERVICE_SELECT_OPTION, SERVICE_SELECT_OPTION_SCHEMA, "async_select_option" + SERVICE_SELECT_OPTION, + {vol.Required(ATTR_OPTION): cv.string}, + "async_select_option", ) component.async_register_entity_service( - SERVICE_SELECT_NEXT, - ENTITY_SERVICE_SCHEMA, - lambda entity, call: entity.async_offset_index(1), + SERVICE_SELECT_NEXT, {}, lambda entity, call: entity.async_offset_index(1), ) component.async_register_entity_service( - SERVICE_SELECT_PREVIOUS, - ENTITY_SERVICE_SCHEMA, - lambda entity, call: entity.async_offset_index(-1), + SERVICE_SELECT_PREVIOUS, {}, lambda entity, call: entity.async_offset_index(-1), ) component.async_register_entity_service( - SERVICE_SET_OPTIONS, SERVICE_SET_OPTIONS_SCHEMA, "async_set_options" + SERVICE_SET_OPTIONS, + { + vol.Required(ATTR_OPTIONS): vol.All( + cv.ensure_list, vol.Length(min=1), [cv.string] + ) + }, + "async_set_options", ) await component.async_add_entities(entities) diff --git a/homeassistant/components/input_text/__init__.py b/homeassistant/components/input_text/__init__.py index 1b4670cf1e64..d43e47c11ca7 100644 --- a/homeassistant/components/input_text/__init__.py +++ b/homeassistant/components/input_text/__init__.py @@ -4,7 +4,6 @@ import voluptuous as vol 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, @@ -34,10 +33,6 @@ SERVICE_SET_VALUE = "set_value" -SERVICE_SET_VALUE_SCHEMA = ENTITY_SERVICE_SCHEMA.extend( - {vol.Required(ATTR_VALUE): cv.string} -) - def _cv_input_text(cfg): """Configure validation helper for input box (voluptuous).""" @@ -111,7 +106,7 @@ async def async_setup(hass, config): return False component.async_register_entity_service( - SERVICE_SET_VALUE, SERVICE_SET_VALUE_SCHEMA, "async_set_value" + SERVICE_SET_VALUE, {vol.Required(ATTR_VALUE): cv.string}, "async_set_value" ) await component.async_add_entities(entities) diff --git a/homeassistant/components/intent/__init__.py b/homeassistant/components/intent/__init__.py new file mode 100644 index 000000000000..31ab36ecc89f --- /dev/null +++ b/homeassistant/components/intent/__init__.py @@ -0,0 +1,87 @@ +"""The Intent integration.""" +import asyncio +import logging + +import voluptuous as vol + +from homeassistant.core import HomeAssistant +from homeassistant.const import EVENT_COMPONENT_LOADED +from homeassistant.setup import ATTR_COMPONENT +from homeassistant.components import http +from homeassistant.components.http.data_validator import RequestDataValidator +from homeassistant.helpers import config_validation as cv, intent +from homeassistant.loader import async_get_integration, IntegrationNotFound + +from .const import DOMAIN + +CONFIG_SCHEMA = vol.Schema({DOMAIN: vol.Schema({})}, extra=vol.ALLOW_EXTRA) +_LOGGER = logging.getLogger(__name__) + + +async def async_setup(hass: HomeAssistant, config: dict): + """Set up the Intent component.""" + hass.http.register_view(IntentHandleView()) + + tasks = [_async_process_intent(hass, comp) for comp in hass.config.components] + + async def async_component_loaded(event): + """Handle a new component loaded.""" + await _async_process_intent(hass, event.data[ATTR_COMPONENT]) + + hass.bus.async_listen(EVENT_COMPONENT_LOADED, async_component_loaded) + + if tasks: + await asyncio.gather(*tasks) + + return True + + +async def _async_process_intent(hass: HomeAssistant, component_name: str): + """Process the intents of a component.""" + try: + integration = await async_get_integration(hass, component_name) + platform = integration.get_platform(DOMAIN) + except (IntegrationNotFound, ImportError): + return + + try: + await platform.async_setup_intents(hass) + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Error setting up intents for %s", component_name) + + +class IntentHandleView(http.HomeAssistantView): + """View to handle intents from JSON.""" + + url = "/api/intent/handle" + name = "api:intent:handle" + + @RequestDataValidator( + vol.Schema( + { + vol.Required("name"): cv.string, + vol.Optional("data"): vol.Schema({cv.string: object}), + } + ) + ) + async def post(self, request, data): + """Handle intent with name/data.""" + hass = request.app["hass"] + + try: + intent_name = data["name"] + slots = { + key: {"value": value} for key, value in data.get("data", {}).items() + } + intent_result = await intent.async_handle( + hass, DOMAIN, intent_name, slots, "", self.context(request) + ) + except intent.IntentHandleError as err: + intent_result = intent.IntentResponse() + intent_result.async_set_speech(str(err)) + + if intent_result is None: + intent_result = intent.IntentResponse() + intent_result.async_set_speech("Sorry, I couldn't handle that") + + return self.json(intent_result) diff --git a/homeassistant/components/intent/const.py b/homeassistant/components/intent/const.py new file mode 100644 index 000000000000..61b97c205378 --- /dev/null +++ b/homeassistant/components/intent/const.py @@ -0,0 +1,3 @@ +"""Constants for the Intent integration.""" + +DOMAIN = "intent" diff --git a/homeassistant/components/intent/manifest.json b/homeassistant/components/intent/manifest.json new file mode 100644 index 000000000000..005abde47d68 --- /dev/null +++ b/homeassistant/components/intent/manifest.json @@ -0,0 +1,11 @@ +{ + "domain": "intent", + "name": "Intent", + "config_flow": false, + "documentation": "https://www.home-assistant.io/integrations/intent", + "requirements": [], + "ssdp": [], + "homekit": {}, + "dependencies": ["http"], + "codeowners": ["@home-assistant/core"] +} diff --git a/homeassistant/components/intent_script/__init__.py b/homeassistant/components/intent_script/__init__.py index 75a0c0e8f976..ce4b8b27a51a 100644 --- a/homeassistant/components/intent_script/__init__.py +++ b/homeassistant/components/intent_script/__init__.py @@ -80,7 +80,9 @@ async def async_handle(self, intent_obj): if action is not None: if is_async_action: - intent_obj.hass.async_create_task(action.async_run(slots)) + intent_obj.hass.async_create_task( + action.async_run(slots, intent_obj.context) + ) else: await action.async_run(slots) diff --git a/homeassistant/components/ios/notify.py b/homeassistant/components/ios/notify.py index ee74b369629b..80dbad5336d4 100644 --- a/homeassistant/components/ios/notify.py +++ b/homeassistant/components/ios/notify.py @@ -48,12 +48,6 @@ def get_service(hass, config, discovery_info=None): hass.config.components.add("notify.ios") if not ios.devices_with_push(hass): - _LOGGER.error( - "The notify.ios platform was loaded but no " - "devices exist! Please check the documentation at " - "https://home-assistant.io/ecosystem/ios/notifications" - "/ for more information" - ) return None return iOSNotificationService() diff --git a/homeassistant/components/iota/__init__.py b/homeassistant/components/iota/__init__.py index 900dd028e6a4..497e94a08d6b 100644 --- a/homeassistant/components/iota/__init__.py +++ b/homeassistant/components/iota/__init__.py @@ -1,7 +1,8 @@ """Support for IOTA wallets.""" -import logging from datetime import timedelta +import logging +from iota import Iota import voluptuous as vol import homeassistant.helpers.config_validation as cv @@ -77,6 +78,5 @@ def device_state_attributes(self): @property def api(self): """Construct API object for interaction with the IRI node.""" - from iota import Iota return Iota(adapter=self.iri, seed=self._seed) diff --git a/homeassistant/components/iperf3/__init__.py b/homeassistant/components/iperf3/__init__.py index 753ea60efa4f..9272a725bb7f 100644 --- a/homeassistant/components/iperf3/__init__.py +++ b/homeassistant/components/iperf3/__init__.py @@ -85,17 +85,7 @@ async def async_setup(hass, config): conf = config[DOMAIN] for host in conf[CONF_HOSTS]: - host_name = host[CONF_HOST] - - client = iperf3.Client() - client.duration = host[CONF_DURATION] - client.server_hostname = host_name - client.port = host[CONF_PORT] - client.num_streams = host[CONF_PARALLEL] - client.protocol = host[CONF_PROTOCOL] - client.verbose = False - - data = hass.data[DOMAIN][host_name] = Iperf3Data(hass, client) + data = hass.data[DOMAIN][host[CONF_HOST]] = Iperf3Data(hass, host) if not conf[CONF_MANUAL]: async_track_time_interval(hass, data.update, conf[CONF_SCAN_INTERVAL]) @@ -123,26 +113,37 @@ def update(call): class Iperf3Data: """Get the latest data from iperf3.""" - def __init__(self, hass, client): + def __init__(self, hass, host): """Initialize the data object.""" self._hass = hass - self._client = client + self._host = host self.data = {ATTR_DOWNLOAD: None, ATTR_UPLOAD: None, ATTR_VERSION: None} + def create_client(self): + """Create a new iperf3 client to use for measurement.""" + client = iperf3.Client() + client.duration = self._host[CONF_DURATION] + client.server_hostname = self._host[CONF_HOST] + client.port = self._host[CONF_PORT] + client.num_streams = self._host[CONF_PARALLEL] + client.protocol = self._host[CONF_PROTOCOL] + client.verbose = False + return client + @property def protocol(self): """Return the protocol used for this connection.""" - return self._client.protocol + return self._host[CONF_PROTOCOL] @property def host(self): """Return the host connected to.""" - return self._client.server_hostname + return self._host[CONF_HOST] @property def port(self): """Return the port on the host connected to.""" - return self._client.port + return self._host[CONF_PORT] def update(self, now=None): """Get the latest data from iperf3.""" @@ -165,9 +166,10 @@ def update(self, now=None): def _run_test(self, test_type): """Run and return the iperf3 data.""" - self._client.reverse = test_type == ATTR_DOWNLOAD + client = self.create_client() + client.reverse = test_type == ATTR_DOWNLOAD try: - result = self._client.run() + result = client.run() except (AttributeError, OSError, ValueError) as error: _LOGGER.error("Iperf3 error: %s", error) return None diff --git a/homeassistant/components/iperf3/manifest.json b/homeassistant/components/iperf3/manifest.json index c3b1e27c77ac..6b7cadfd5ded 100644 --- a/homeassistant/components/iperf3/manifest.json +++ b/homeassistant/components/iperf3/manifest.json @@ -6,5 +6,7 @@ "iperf3==0.1.11" ], "dependencies": [], - "codeowners": [] + "codeowners": [ + "@rohankapoorcom" + ] } diff --git a/homeassistant/components/ipma/__init__.py b/homeassistant/components/ipma/__init__.py index 39cb670999fa..702e12a8a633 100644 --- a/homeassistant/components/ipma/__init__.py +++ b/homeassistant/components/ipma/__init__.py @@ -1,7 +1,7 @@ """Component for the Portuguese weather service - IPMA.""" from homeassistant.core import Config, HomeAssistant -from .config_flow import IpmaFlowHandler # noqa -from .const import DOMAIN # noqa +from .config_flow import IpmaFlowHandler # noqa: F401 +from .const import DOMAIN # noqa: F401 DEFAULT_NAME = "ipma" diff --git a/homeassistant/components/iqvia/manifest.json b/homeassistant/components/iqvia/manifest.json index 04723a6a1f67..7a5eb7e56df3 100644 --- a/homeassistant/components/iqvia/manifest.json +++ b/homeassistant/components/iqvia/manifest.json @@ -3,12 +3,7 @@ "name": "IQVIA", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/iqvia", - "requirements": [ - "numpy==1.17.3", - "pyiqvia==0.2.1" - ], + "requirements": ["numpy==1.17.4", "pyiqvia==0.2.1"], "dependencies": [], - "codeowners": [ - "@bachya" - ] -} \ No newline at end of file + "codeowners": ["@bachya"] +} diff --git a/homeassistant/components/irish_rail_transport/sensor.py b/homeassistant/components/irish_rail_transport/sensor.py index 6f2bbae5ebad..883f4ed7b397 100644 --- a/homeassistant/components/irish_rail_transport/sensor.py +++ b/homeassistant/components/irish_rail_transport/sensor.py @@ -1,12 +1,13 @@ """Support for Irish Rail RTPI information.""" -import logging from datetime import timedelta +import logging +from pyirishrail.pyirishrail import IrishRailRTPI import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import CONF_NAME, ATTR_ATTRIBUTION +from homeassistant.const import ATTR_ATTRIBUTION, CONF_NAME +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -47,7 +48,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Irish Rail transport sensor.""" - from pyirishrail.pyirishrail import IrishRailRTPI station = config.get(CONF_STATION) direction = config.get(CONF_DIRECTION) diff --git a/homeassistant/components/isy994/binary_sensor.py b/homeassistant/components/isy994/binary_sensor.py index 0c4fb80be8cd..eed5f1a81a0b 100644 --- a/homeassistant/components/isy994/binary_sensor.py +++ b/homeassistant/components/isy994/binary_sensor.py @@ -110,7 +110,6 @@ def __init__(self, node) -> None: self._negative_node = None self._heartbeat_device = None self._device_class_from_type = _detect_device_type(self._node) - # pylint: disable=protected-access if _is_val_unknown(self._node.status._val): self._computed_state = None self._status_was_unknown = True diff --git a/homeassistant/components/jewish_calendar/sensor.py b/homeassistant/components/jewish_calendar/sensor.py index 54a3d1497aac..d0376694a440 100644 --- a/homeassistant/components/jewish_calendar/sensor.py +++ b/homeassistant/components/jewish_calendar/sensor.py @@ -122,8 +122,9 @@ def get_state(self, after_shkia_date, after_tzais_date): # Compute the weekly portion based on the upcoming shabbat. return after_tzais_date.upcoming_shabbat.parasha if self._type == "holiday": - self._holiday_attrs["type"] = after_shkia_date.holiday_type.name self._holiday_attrs["id"] = after_shkia_date.holiday_name + self._holiday_attrs["type"] = after_shkia_date.holiday_type.name + self._holiday_attrs["type_id"] = after_shkia_date.holiday_type.value return after_shkia_date.holiday_description if self._type == "omer_count": return after_shkia_date.omer_day diff --git a/homeassistant/components/joaoapps_join/__init__.py b/homeassistant/components/joaoapps_join/__init__.py index b988411762eb..10cbcf6b5c06 100644 --- a/homeassistant/components/joaoapps_join/__init__.py +++ b/homeassistant/components/joaoapps_join/__init__.py @@ -1,10 +1,19 @@ """Support for Joaoapps Join services.""" import logging +from pyjoin import ( + get_devices, + ring_device, + send_file, + send_notification, + send_sms, + send_url, + set_wallpaper, +) import voluptuous as vol +from homeassistant.const import CONF_API_KEY, CONF_NAME import homeassistant.helpers.config_validation as cv -from homeassistant.const import CONF_NAME, CONF_API_KEY _LOGGER = logging.getLogger(__name__) @@ -35,14 +44,6 @@ def register_device(hass, api_key, name, device_id, device_ids, device_names): """Register services for each join device listed.""" - from pyjoin import ( - ring_device, - set_wallpaper, - send_sms, - send_file, - send_url, - send_notification, - ) def ring_service(service): """Service to ring devices.""" @@ -114,7 +115,6 @@ def send_sms_service(service): def setup(hass, config): """Set up the Join services.""" - from pyjoin import get_devices for device in config[DOMAIN]: api_key = device.get(CONF_API_KEY) diff --git a/homeassistant/components/joaoapps_join/notify.py b/homeassistant/components/joaoapps_join/notify.py index 2e6b3d1c67a3..14b8fe1a814d 100644 --- a/homeassistant/components/joaoapps_join/notify.py +++ b/homeassistant/components/joaoapps_join/notify.py @@ -1,6 +1,9 @@ """Support for Join notifications.""" import logging + +from pyjoin import get_devices, send_notification import voluptuous as vol + from homeassistant.components.notify import ( ATTR_DATA, ATTR_TITLE, @@ -34,8 +37,6 @@ def get_service(hass, config, discovery_info=None): device_ids = config.get(CONF_DEVICE_IDS) device_names = config.get(CONF_DEVICE_NAMES) if api_key: - from pyjoin import get_devices - if not get_devices(api_key): _LOGGER.error("Error connecting to Join. Check the API key") return False @@ -60,7 +61,6 @@ def __init__(self, api_key, device_id, device_ids, device_names): def send_message(self, message="", **kwargs): """Send a message to a user.""" - from pyjoin import send_notification title = kwargs.get(ATTR_TITLE, ATTR_TITLE_DEFAULT) data = kwargs.get(ATTR_DATA) or {} diff --git a/homeassistant/components/juicenet/manifest.json b/homeassistant/components/juicenet/manifest.json index 076567573c72..fac59cb3b8d6 100644 --- a/homeassistant/components/juicenet/manifest.json +++ b/homeassistant/components/juicenet/manifest.json @@ -3,7 +3,7 @@ "name": "Juicenet", "documentation": "https://www.home-assistant.io/integrations/juicenet", "requirements": [ - "python-juicenet==0.1.5" + "python-juicenet==0.1.6" ], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/keenetic_ndms2/device_tracker.py b/homeassistant/components/keenetic_ndms2/device_tracker.py index faab0dde62d3..598e29cf5830 100644 --- a/homeassistant/components/keenetic_ndms2/device_tracker.py +++ b/homeassistant/components/keenetic_ndms2/device_tracker.py @@ -1,15 +1,16 @@ """Support for Zyxel Keenetic NDMS2 based routers.""" import logging +from ndms2_client import Client, ConnectionException, TelnetConnection 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_PORT, CONF_PASSWORD, CONF_USERNAME +from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_USERNAME +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -45,7 +46,6 @@ class KeeneticNDMS2DeviceScanner(DeviceScanner): def __init__(self, config): """Initialize the scanner.""" - from ndms2_client import Client, TelnetConnection self.last_results = [] @@ -88,8 +88,6 @@ def _update_info(self): """Get ARP from keenetic router.""" _LOGGER.debug("Fetching devices from router...") - from ndms2_client import ConnectionException - try: self.last_results = [ dev diff --git a/homeassistant/components/keenetic_ndms2/manifest.json b/homeassistant/components/keenetic_ndms2/manifest.json index 4613d2d96080..df78c98aa3ca 100644 --- a/homeassistant/components/keenetic_ndms2/manifest.json +++ b/homeassistant/components/keenetic_ndms2/manifest.json @@ -3,7 +3,7 @@ "name": "Keenetic ndms2", "documentation": "https://www.home-assistant.io/integrations/keenetic_ndms2", "requirements": [ - "ndms2_client==0.0.10" + "ndms2_client==0.0.11" ], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/keyboard/services.yaml b/homeassistant/components/keyboard/services.yaml index e69de29bb2d1..a9896c3d6cf1 100644 --- a/homeassistant/components/keyboard/services.yaml +++ b/homeassistant/components/keyboard/services.yaml @@ -0,0 +1,17 @@ +volume_up: + description: Simulates a key press of the "Volume Up" button on HomeAssistant's host machine. + +volume_down: + description: Simulates a key press of the "Volume Down" button on HomeAssistant's host machine. + +volume_mute: + description: Simulates a key press of the "Volume Mute" button on HomeAssistant's host machine. + +media_play_pause: + description: Simulates a key press of the "Media Play/Pause" button on HomeAssistant's host machine. + +media_next_track: + description: Simulates a key press of the "Media Next Track" button on HomeAssistant's host machine. + +media_prev_track: + description: Simulates a key press of the "Media Previous Track" button on HomeAssistant's host machine. diff --git a/homeassistant/components/kiwi/lock.py b/homeassistant/components/kiwi/lock.py index 4949ceeb1d82..b13906b44f5c 100644 --- a/homeassistant/components/kiwi/lock.py +++ b/homeassistant/components/kiwi/lock.py @@ -1,21 +1,22 @@ """Support for the KIWI.KI lock platform.""" import logging +from kiwiki import KiwiClient, KiwiException import voluptuous as vol -import homeassistant.helpers.config_validation as cv -from homeassistant.components.lock import LockDevice, PLATFORM_SCHEMA +from homeassistant.components.lock import PLATFORM_SCHEMA, LockDevice from homeassistant.const import ( - CONF_PASSWORD, - CONF_USERNAME, ATTR_ID, - ATTR_LONGITUDE, ATTR_LATITUDE, + ATTR_LONGITUDE, + CONF_PASSWORD, + CONF_USERNAME, STATE_LOCKED, STATE_UNLOCKED, ) -from homeassistant.helpers.event import async_call_later from homeassistant.core import callback +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.event import async_call_later _LOGGER = logging.getLogger(__name__) @@ -32,7 +33,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the KIWI lock platform.""" - from kiwiki import KiwiClient, KiwiException try: kiwi = KiwiClient(config[CONF_USERNAME], config[CONF_PASSWORD]) @@ -98,7 +98,6 @@ def clear_unlock_state(self, _): def unlock(self, **kwargs): """Unlock the device.""" - from kiwiki import KiwiException try: self._client.open_door(self.lock_id) diff --git a/homeassistant/components/kwb/sensor.py b/homeassistant/components/kwb/sensor.py index 49815faf7aed..b7872ca1ab4c 100644 --- a/homeassistant/components/kwb/sensor.py +++ b/homeassistant/components/kwb/sensor.py @@ -1,18 +1,19 @@ """Support for KWB Easyfire.""" import logging +from pykwb import kwb import voluptuous as vol +from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( - CONF_HOST, - CONF_PORT, CONF_DEVICE, + CONF_HOST, CONF_NAME, + CONF_PORT, EVENT_HOMEASSISTANT_STOP, ) -from homeassistant.helpers.entity import Entity -from homeassistant.components.sensor import PLATFORM_SCHEMA import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -56,8 +57,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): raw = config.get(CONF_RAW) client_name = config.get(CONF_NAME) - from pykwb import kwb - if connection_type == "serial": easyfire = kwb.KWBEasyfire(MODE_SERIAL, "", 0, device) elif connection_type == "tcp": diff --git a/homeassistant/components/lacrosse/sensor.py b/homeassistant/components/lacrosse/sensor.py index ccfd647d746b..b8bde797b39b 100644 --- a/homeassistant/components/lacrosse/sensor.py +++ b/homeassistant/components/lacrosse/sensor.py @@ -2,6 +2,8 @@ from datetime import timedelta import logging +import pylacrosse +from serial import SerialException import voluptuous as vol from homeassistant.components.sensor import ENTITY_ID_FORMAT, PLATFORM_SCHEMA @@ -61,8 +63,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the LaCrosse sensors.""" - import pylacrosse - from serial import SerialException usb_device = config.get(CONF_DEVICE) baud = int(config.get(CONF_BAUD)) diff --git a/homeassistant/components/lametric/__init__.py b/homeassistant/components/lametric/__init__.py index a24ad79104d0..9281affa492a 100644 --- a/homeassistant/components/lametric/__init__.py +++ b/homeassistant/components/lametric/__init__.py @@ -1,6 +1,7 @@ """Support for LaMetric time.""" import logging +from lmnotify import LaMetricManager import voluptuous as vol import homeassistant.helpers.config_validation as cv @@ -50,7 +51,6 @@ class HassLaMetricManager: def __init__(self, client_id, client_secret): """Initialize HassLaMetricManager and connect to LaMetric.""" - from lmnotify import LaMetricManager _LOGGER.debug("Connecting to LaMetric") self.manager = LaMetricManager(client_id, client_secret) diff --git a/homeassistant/components/lametric/notify.py b/homeassistant/components/lametric/notify.py index 901fb07fc558..b8dd610b1a0f 100644 --- a/homeassistant/components/lametric/notify.py +++ b/homeassistant/components/lametric/notify.py @@ -1,6 +1,8 @@ """Support for LaMetric notifications.""" import logging +from lmnotify import Model, SimpleFrame, Sound +from oauthlib.oauth2 import TokenExpiredError from requests.exceptions import ConnectionError as RequestsConnectionError import voluptuous as vol @@ -59,8 +61,6 @@ def __init__(self, hasslametricmanager, icon, lifetime, cycles, priority): def send_message(self, message="", **kwargs): """Send a message to some LaMetric device.""" - from lmnotify import SimpleFrame, Sound, Model - from oauthlib.oauth2 import TokenExpiredError targets = kwargs.get(ATTR_TARGET) data = kwargs.get(ATTR_DATA) diff --git a/homeassistant/components/launch_library/sensor.py b/homeassistant/components/launch_library/sensor.py index 14a75704312d..32335526194b 100644 --- a/homeassistant/components/launch_library/sensor.py +++ b/homeassistant/components/launch_library/sensor.py @@ -2,13 +2,14 @@ from datetime import timedelta import logging +from pylaunches.api import Launches 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 -from homeassistant.helpers.entity import Entity from homeassistant.helpers.aiohttp_client import async_get_clientsession +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -25,7 +26,6 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Create the launch sensor.""" - from pylaunches.api import Launches name = config[CONF_NAME] diff --git a/homeassistant/components/lg_netcast/media_player.py b/homeassistant/components/lg_netcast/media_player.py index d11887d8a8a4..0be51c337e86 100644 --- a/homeassistant/components/lg_netcast/media_player.py +++ b/homeassistant/components/lg_netcast/media_player.py @@ -2,11 +2,12 @@ from datetime import timedelta import logging +from pylgnetcast import LgNetCastClient, LgNetCastError from requests import RequestException import voluptuous as vol from homeassistant import util -from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA +from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice from homeassistant.components.media_player.const import ( MEDIA_TYPE_CHANNEL, SUPPORT_NEXT_TRACK, @@ -57,7 +58,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the LG TV platform.""" - from pylgnetcast import LgNetCastClient host = config.get(CONF_HOST) access_token = config.get(CONF_ACCESS_TOKEN) @@ -87,7 +87,6 @@ def __init__(self, client, name): def send_command(self, command): """Send remote control commands to the TV.""" - from pylgnetcast import LgNetCastError try: with self._client as client: @@ -98,7 +97,6 @@ def send_command(self, command): @util.Throttle(MIN_TIME_BETWEEN_SCANS, MIN_TIME_BETWEEN_FORCED_SCANS) def update(self): """Retrieve the latest data from the LG TV.""" - from pylgnetcast import LgNetCastError try: with self._client as client: diff --git a/homeassistant/components/life360/.translations/pt.json b/homeassistant/components/life360/.translations/pt.json new file mode 100644 index 000000000000..9c848bd8ec88 --- /dev/null +++ b/homeassistant/components/life360/.translations/pt.json @@ -0,0 +1,15 @@ +{ + "config": { + "error": { + "invalid_username": "Nome de utilizador incorreto" + }, + "step": { + "user": { + "data": { + "password": "Palavra-passe", + "username": "Nome de Utilizador" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/life360/.translations/ru.json b/homeassistant/components/life360/.translations/ru.json index eba3a47ead80..c3f2601eb992 100644 --- a/homeassistant/components/life360/.translations/ru.json +++ b/homeassistant/components/life360/.translations/ru.json @@ -1,17 +1,17 @@ { "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." + "invalid_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0443\u0447\u0451\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435.", + "user_already_configured": "\u0423\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430." }, "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.", + "invalid_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0443\u0447\u0451\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\u0451\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/lifx/light.py b/homeassistant/components/lifx/light.py index 50e36e8db0ac..1fb614f856fb 100644 --- a/homeassistant/components/lifx/light.py +++ b/homeassistant/components/lifx/light.py @@ -60,23 +60,24 @@ MESSAGE_RETRIES = 8 UNAVAILABLE_GRACE = 90 -SERVICE_LIFX_SET_STATE = "lifx_set_state" +SERVICE_LIFX_SET_STATE = "set_state" ATTR_INFRARED = "infrared" ATTR_ZONES = "zones" ATTR_POWER = "power" -LIFX_SET_STATE_SCHEMA = LIGHT_TURN_ON_SCHEMA.extend( +LIFX_SET_STATE_SCHEMA = cv.make_entity_service_schema( { + **LIGHT_TURN_ON_SCHEMA, ATTR_INFRARED: vol.All(vol.Coerce(int), vol.Clamp(min=0, max=255)), ATTR_ZONES: vol.All(cv.ensure_list, [cv.positive_int]), ATTR_POWER: cv.boolean, } ) -SERVICE_EFFECT_PULSE = "lifx_effect_pulse" -SERVICE_EFFECT_COLORLOOP = "lifx_effect_colorloop" -SERVICE_EFFECT_STOP = "lifx_effect_stop" +SERVICE_EFFECT_PULSE = "effect_pulse" +SERVICE_EFFECT_COLORLOOP = "effect_colorloop" +SERVICE_EFFECT_STOP = "effect_stop" ATTR_POWER_ON = "power_on" ATTR_PERIOD = "period" @@ -282,7 +283,7 @@ def cleanup(self, event=None): SERVICE_EFFECT_PULSE, SERVICE_EFFECT_COLORLOOP, ]: - self.hass.services.async_remove(DOMAIN, service) + self.hass.services.async_remove(LIFX_DOMAIN, service) def register_set_state(self): """Register the LIFX set_state service call.""" @@ -298,7 +299,7 @@ async def service_handler(service): await asyncio.wait(tasks) self.hass.services.async_register( - DOMAIN, + LIFX_DOMAIN, SERVICE_LIFX_SET_STATE, service_handler, schema=LIFX_SET_STATE_SCHEMA, @@ -314,21 +315,24 @@ async def service_handler(service): await self.start_effect(entities, service.service, **service.data) self.hass.services.async_register( - DOMAIN, + LIFX_DOMAIN, SERVICE_EFFECT_PULSE, service_handler, schema=LIFX_EFFECT_PULSE_SCHEMA, ) self.hass.services.async_register( - DOMAIN, + LIFX_DOMAIN, SERVICE_EFFECT_COLORLOOP, service_handler, schema=LIFX_EFFECT_COLORLOOP_SCHEMA, ) self.hass.services.async_register( - DOMAIN, SERVICE_EFFECT_STOP, service_handler, schema=LIFX_EFFECT_STOP_SCHEMA + LIFX_DOMAIN, + SERVICE_EFFECT_STOP, + service_handler, + schema=LIFX_EFFECT_STOP_SCHEMA, ) async def start_effect(self, entities, service, **kwargs): @@ -366,16 +370,11 @@ async def start_effect(self, entities, service, **kwargs): async def async_service_to_entities(self, service): """Return the known entities that a service call mentions.""" entity_ids = await async_extract_entity_ids(self.hass, service) - if entity_ids: - entities = [ - entity - for entity in self.entities.values() - if entity.entity_id in entity_ids - ] - else: - entities = list(self.entities.values()) - - return entities + return [ + entity + for entity in self.entities.values() + if entity.entity_id in entity_ids + ] @callback def register(self, bulb): @@ -652,7 +651,7 @@ async def default_effect(self, **kwargs): """Start an effect with default parameters.""" service = kwargs[ATTR_EFFECT] data = {ATTR_ENTITY_ID: self.entity_id} - await self.hass.services.async_call(DOMAIN, service, data) + await self.hass.services.async_call(LIFX_DOMAIN, service, data) async def async_update(self): """Update bulb status.""" diff --git a/homeassistant/components/lifx/services.yaml b/homeassistant/components/lifx/services.yaml index e69de29bb2d1..ebf2032a9a51 100644 --- a/homeassistant/components/lifx/services.yaml +++ b/homeassistant/components/lifx/services.yaml @@ -0,0 +1,77 @@ +set_state: + description: Set a color/brightness and possibliy turn the light on/off. + fields: + entity_id: + description: Name(s) of entities to set a state on. + example: "light.garage" + "...": + description: All turn_on parameters can be used to specify a color. + infrared: + description: Automatic infrared level (0..255) when light brightness is low. + example: 255 + zones: + description: List of zone numbers to affect (8 per LIFX Z, starts at 0). + example: "[0,5]" + transition: + description: Duration in seconds it takes to get to the final state. + example: 10 + power: + description: Turn the light on (True) or off (False). Leave out to keep the power as it is. + example: True + +effect_pulse: + description: Run a flash effect by changing to a color and back. + fields: + entity_id: + description: Name(s) of entities to run the effect on. + example: "light.kitchen" + mode: + description: "Decides how colors are changed. Possible values: blink, breathe, ping, strobe, solid." + example: strobe + brightness: + description: Number between 0..255 indicating brightness of the temporary color. + example: 120 + color_name: + description: A human readable color name. + example: "red" + rgb_color: + description: The temporary color in RGB-format. + example: "[255, 100, 100]" + period: + description: Duration of the effect in seconds (default 1.0). + example: 3 + cycles: + description: Number of times the effect should run (default 1.0). + example: 2 + power_on: + description: Powered off lights are temporarily turned on during the effect (default True). + example: False + +effect_colorloop: + description: Run an effect with looping colors. + fields: + entity_id: + description: Name(s) of entities to run the effect on. + example: "light.disco1, light.disco2, light.disco3" + brightness: + description: Number between 0 and 255 indicating brightness of the effect. Leave this out to maintain the current brightness of each participating light. + example: 120 + period: + description: Duration (in seconds) between color changes (default 60). + example: 180 + change: + description: Hue movement per period, in degrees on a color wheel (ranges from 0 to 360, default 20). + example: 45 + spread: + description: Maximum hue difference between participating lights, in degrees on a color wheel (ranges from 0 to 360, default 30). + example: 0 + power_on: + description: Powered off lights are temporarily turned on during the effect (default True). + example: False + +effect_stop: + description: Stop a running effect. + fields: + entity_id: + description: Name(s) of entities to stop effects on. Leave out to stop effects everywhere. + example: "light.bedroom" diff --git a/homeassistant/components/light/.translations/pt.json b/homeassistant/components/light/.translations/pt.json new file mode 100644 index 000000000000..272516f4c6b9 --- /dev/null +++ b/homeassistant/components/light/.translations/pt.json @@ -0,0 +1,17 @@ +{ + "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" + }, + "trigger_type": { + "turned_off": "{entity_name} foi desligado", + "turned_on": "{entity_name} foi ligado" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py index fbd908a9e45d..f01258e2ab47 100644 --- a/homeassistant/components/light/__init__.py +++ b/homeassistant/components/light/__init__.py @@ -19,14 +19,13 @@ ) from homeassistant.exceptions import UnknownUser, Unauthorized import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.config_validation import ( # noqa +from homeassistant.helpers.config_validation import ( # noqa: F401 PLATFORM_SCHEMA, PLATFORM_SCHEMA_BASE, - ENTITY_SERVICE_SCHEMA, + make_entity_service_schema, ) from homeassistant.helpers.entity import ToggleEntity from homeassistant.helpers.entity_component import EntityComponent -from homeassistant.helpers import intent from homeassistant.loader import bind_hass import homeassistant.util.color as color_util @@ -94,55 +93,41 @@ VALID_BRIGHTNESS = vol.All(vol.Coerce(int), vol.Clamp(min=0, max=255)) VALID_BRIGHTNESS_PCT = vol.All(vol.Coerce(float), vol.Range(min=0, max=100)) -LIGHT_TURN_ON_SCHEMA = ENTITY_SERVICE_SCHEMA.extend( - { - vol.Exclusive(ATTR_PROFILE, COLOR_GROUP): cv.string, - ATTR_TRANSITION: VALID_TRANSITION, - ATTR_BRIGHTNESS: VALID_BRIGHTNESS, - ATTR_BRIGHTNESS_PCT: VALID_BRIGHTNESS_PCT, - vol.Exclusive(ATTR_COLOR_NAME, COLOR_GROUP): cv.string, - vol.Exclusive(ATTR_RGB_COLOR, COLOR_GROUP): vol.All( - vol.ExactSequence((cv.byte, cv.byte, cv.byte)), vol.Coerce(tuple) - ), - vol.Exclusive(ATTR_XY_COLOR, COLOR_GROUP): vol.All( - vol.ExactSequence((cv.small_float, cv.small_float)), vol.Coerce(tuple) - ), - vol.Exclusive(ATTR_HS_COLOR, COLOR_GROUP): vol.All( - vol.ExactSequence( - ( - vol.All(vol.Coerce(float), vol.Range(min=0, max=360)), - vol.All(vol.Coerce(float), vol.Range(min=0, max=100)), - ) - ), - vol.Coerce(tuple), - ), - vol.Exclusive(ATTR_COLOR_TEMP, COLOR_GROUP): vol.All( - vol.Coerce(int), vol.Range(min=1) - ), - vol.Exclusive(ATTR_KELVIN, COLOR_GROUP): vol.All( - vol.Coerce(int), vol.Range(min=0) - ), - ATTR_WHITE_VALUE: vol.All(vol.Coerce(int), vol.Range(min=0, max=255)), - ATTR_FLASH: vol.In([FLASH_SHORT, FLASH_LONG]), - ATTR_EFFECT: cv.string, - } -) - - -LIGHT_TURN_OFF_SCHEMA = { +LIGHT_TURN_ON_SCHEMA = { + vol.Exclusive(ATTR_PROFILE, COLOR_GROUP): cv.string, ATTR_TRANSITION: VALID_TRANSITION, + ATTR_BRIGHTNESS: VALID_BRIGHTNESS, + ATTR_BRIGHTNESS_PCT: VALID_BRIGHTNESS_PCT, + vol.Exclusive(ATTR_COLOR_NAME, COLOR_GROUP): cv.string, + vol.Exclusive(ATTR_RGB_COLOR, COLOR_GROUP): vol.All( + vol.ExactSequence((cv.byte, cv.byte, cv.byte)), vol.Coerce(tuple) + ), + vol.Exclusive(ATTR_XY_COLOR, COLOR_GROUP): vol.All( + vol.ExactSequence((cv.small_float, cv.small_float)), vol.Coerce(tuple) + ), + vol.Exclusive(ATTR_HS_COLOR, COLOR_GROUP): vol.All( + vol.ExactSequence( + ( + vol.All(vol.Coerce(float), vol.Range(min=0, max=360)), + vol.All(vol.Coerce(float), vol.Range(min=0, max=100)), + ) + ), + vol.Coerce(tuple), + ), + vol.Exclusive(ATTR_COLOR_TEMP, COLOR_GROUP): vol.All( + vol.Coerce(int), vol.Range(min=1) + ), + vol.Exclusive(ATTR_KELVIN, COLOR_GROUP): vol.All(vol.Coerce(int), vol.Range(min=0)), + ATTR_WHITE_VALUE: vol.All(vol.Coerce(int), vol.Range(min=0, max=255)), ATTR_FLASH: vol.In([FLASH_SHORT, FLASH_LONG]), + ATTR_EFFECT: cv.string, } -LIGHT_TOGGLE_SCHEMA = LIGHT_TURN_ON_SCHEMA - PROFILE_SCHEMA = vol.Schema( vol.ExactSequence((str, cv.small_float, cv.small_float, cv.byte)) ) -INTENT_SET = "HassLightSet" - _LOGGER = logging.getLogger(__name__) @@ -196,63 +181,6 @@ def preprocess_turn_off(params): return (False, None) # Light should be turned on -class SetIntentHandler(intent.IntentHandler): - """Handle set color intents.""" - - intent_type = INTENT_SET - slot_schema = { - vol.Required("name"): cv.string, - vol.Optional("color"): color_util.color_name_to_rgb, - vol.Optional("brightness"): vol.All(vol.Coerce(int), vol.Range(0, 100)), - } - - async def async_handle(self, intent_obj): - """Handle the hass intent.""" - hass = intent_obj.hass - slots = self.async_validate_slots(intent_obj.slots) - state = hass.helpers.intent.async_match_state( - slots["name"]["value"], - [state for state in hass.states.async_all() if state.domain == DOMAIN], - ) - - service_data = {ATTR_ENTITY_ID: state.entity_id} - speech_parts = [] - - if "color" in slots: - intent.async_test_feature(state, SUPPORT_COLOR, "changing colors") - service_data[ATTR_RGB_COLOR] = slots["color"]["value"] - # Use original passed in value of the color because we don't have - # human readable names for that internally. - speech_parts.append( - "the color {}".format(intent_obj.slots["color"]["value"]) - ) - - if "brightness" in slots: - intent.async_test_feature(state, SUPPORT_BRIGHTNESS, "changing brightness") - service_data[ATTR_BRIGHTNESS_PCT] = slots["brightness"]["value"] - speech_parts.append("{}% brightness".format(slots["brightness"]["value"])) - - await hass.services.async_call(DOMAIN, SERVICE_TURN_ON, service_data) - - response = intent_obj.create_response() - - if not speech_parts: # No attributes changed - speech = f"Turned on {state.name}" - else: - parts = [f"Changed {state.name} to"] - for index, part in enumerate(speech_parts): - if index == 0: - parts.append(f" {part}") - elif index != len(speech_parts) - 1: - parts.append(f", {part}") - else: - parts.append(f" and {part}") - speech = "".join(parts) - - response.async_set_speech(speech) - return response - - async def async_setup(hass, config): """Expose light control via state machine and services.""" component = hass.data[DOMAIN] = EntityComponent( @@ -328,19 +256,22 @@ async def async_handle_light_on_service(service): DOMAIN, SERVICE_TURN_ON, async_handle_light_on_service, - schema=LIGHT_TURN_ON_SCHEMA, + schema=cv.make_entity_service_schema(LIGHT_TURN_ON_SCHEMA), ) component.async_register_entity_service( - SERVICE_TURN_OFF, LIGHT_TURN_OFF_SCHEMA, "async_turn_off" + SERVICE_TURN_OFF, + { + ATTR_TRANSITION: VALID_TRANSITION, + ATTR_FLASH: vol.In([FLASH_SHORT, FLASH_LONG]), + }, + "async_turn_off", ) component.async_register_entity_service( - SERVICE_TOGGLE, LIGHT_TOGGLE_SCHEMA, "async_toggle" + SERVICE_TOGGLE, LIGHT_TURN_ON_SCHEMA, "async_toggle" ) - hass.helpers.intent.async_register(SetIntentHandler()) - return True @@ -460,8 +391,8 @@ def effect(self): return None @property - def state_attributes(self): - """Return optional state attributes.""" + def capability_attributes(self): + """Return capability attributes.""" data = {} supported_features = self.supported_features @@ -472,25 +403,35 @@ def state_attributes(self): if supported_features & SUPPORT_EFFECT: data[ATTR_EFFECT_LIST] = self.effect_list - if self.is_on: - if supported_features & SUPPORT_BRIGHTNESS: - data[ATTR_BRIGHTNESS] = self.brightness + return data - if supported_features & SUPPORT_COLOR_TEMP: - data[ATTR_COLOR_TEMP] = self.color_temp + @property + def state_attributes(self): + """Return state attributes.""" + if not self.is_on: + return None - if supported_features & SUPPORT_COLOR and self.hs_color: - # pylint: disable=unsubscriptable-object,not-an-iterable - hs_color = self.hs_color - data[ATTR_HS_COLOR] = (round(hs_color[0], 3), round(hs_color[1], 3)) - data[ATTR_RGB_COLOR] = color_util.color_hs_to_RGB(*hs_color) - data[ATTR_XY_COLOR] = color_util.color_hs_to_xy(*hs_color) + data = {} + supported_features = self.supported_features - if supported_features & SUPPORT_WHITE_VALUE: - data[ATTR_WHITE_VALUE] = self.white_value + if supported_features & SUPPORT_BRIGHTNESS: + data[ATTR_BRIGHTNESS] = self.brightness + + if supported_features & SUPPORT_COLOR_TEMP: + data[ATTR_COLOR_TEMP] = self.color_temp - if supported_features & SUPPORT_EFFECT: - data[ATTR_EFFECT] = self.effect + if supported_features & SUPPORT_COLOR and self.hs_color: + # pylint: disable=unsubscriptable-object,not-an-iterable + hs_color = self.hs_color + data[ATTR_HS_COLOR] = (round(hs_color[0], 3), round(hs_color[1], 3)) + data[ATTR_RGB_COLOR] = color_util.color_hs_to_RGB(*hs_color) + data[ATTR_XY_COLOR] = color_util.color_hs_to_xy(*hs_color) + + if supported_features & SUPPORT_WHITE_VALUE: + data[ATTR_WHITE_VALUE] = self.white_value + + if supported_features & SUPPORT_EFFECT: + data[ATTR_EFFECT] = self.effect return {key: val for key, val in data.items() if val is not None} diff --git a/homeassistant/components/light/intent.py b/homeassistant/components/light/intent.py new file mode 100644 index 000000000000..93b9748fc4a3 --- /dev/null +++ b/homeassistant/components/light/intent.py @@ -0,0 +1,84 @@ +"""Intents for the light integration.""" +import voluptuous as vol + +from homeassistant.core import HomeAssistant +from homeassistant.helpers import intent +import homeassistant.util.color as color_util +import homeassistant.helpers.config_validation as cv + +from . import ( + ATTR_ENTITY_ID, + SUPPORT_COLOR, + ATTR_RGB_COLOR, + ATTR_BRIGHTNESS_PCT, + SUPPORT_BRIGHTNESS, + DOMAIN, + SERVICE_TURN_ON, +) + + +INTENT_SET = "HassLightSet" + + +async def async_setup_intents(hass: HomeAssistant) -> None: + """Set up the light intents.""" + hass.helpers.intent.async_register(SetIntentHandler()) + + +class SetIntentHandler(intent.IntentHandler): + """Handle set color intents.""" + + intent_type = INTENT_SET + slot_schema = { + vol.Required("name"): cv.string, + vol.Optional("color"): color_util.color_name_to_rgb, + vol.Optional("brightness"): vol.All(vol.Coerce(int), vol.Range(0, 100)), + } + + async def async_handle(self, intent_obj: intent.Intent) -> intent.IntentResponse: + """Handle the hass intent.""" + hass = intent_obj.hass + slots = self.async_validate_slots(intent_obj.slots) + state = hass.helpers.intent.async_match_state( + slots["name"]["value"], + [state for state in hass.states.async_all() if state.domain == DOMAIN], + ) + + service_data = {ATTR_ENTITY_ID: state.entity_id} + speech_parts = [] + + if "color" in slots: + intent.async_test_feature(state, SUPPORT_COLOR, "changing colors") + service_data[ATTR_RGB_COLOR] = slots["color"]["value"] + # Use original passed in value of the color because we don't have + # human readable names for that internally. + speech_parts.append( + "the color {}".format(intent_obj.slots["color"]["value"]) + ) + + if "brightness" in slots: + intent.async_test_feature(state, SUPPORT_BRIGHTNESS, "changing brightness") + service_data[ATTR_BRIGHTNESS_PCT] = slots["brightness"]["value"] + speech_parts.append("{}% brightness".format(slots["brightness"]["value"])) + + await hass.services.async_call( + DOMAIN, SERVICE_TURN_ON, service_data, context=intent_obj.context + ) + + response = intent_obj.create_response() + + if not speech_parts: # No attributes changed + speech = f"Turned on {state.name}" + else: + parts = [f"Changed {state.name} to"] + for index, part in enumerate(speech_parts): + if index == 0: + parts.append(f" {part}") + elif index != len(speech_parts) - 1: + parts.append(f", {part}") + else: + parts.append(f" and {part}") + speech = "".join(parts) + + response.async_set_speech(speech) + return response diff --git a/homeassistant/components/light/services.yaml b/homeassistant/components/light/services.yaml index 9173f79f9647..449e5ea5aaf6 100644 --- a/homeassistant/components/light/services.yaml +++ b/homeassistant/components/light/services.yaml @@ -119,101 +119,3 @@ toggle: values: - colorloop - random - -lifx_set_state: - description: Set a color/brightness and possibliy turn the light on/off. - fields: - entity_id: - description: Name(s) of entities to set a state on. - example: "light.garage" - "...": - description: All turn_on parameters can be used to specify a color. - infrared: - description: Automatic infrared level (0..255) when light brightness is low. - example: 255 - zones: - description: List of zone numbers to affect (8 per LIFX Z, starts at 0). - example: "[0,5]" - transition: - description: Duration in seconds it takes to get to the final state. - example: 10 - power: - description: Turn the light on (True) or off (False). Leave out to keep the power as it is. - example: True - -lifx_effect_pulse: - description: Run a flash effect by changing to a color and back. - fields: - entity_id: - description: Name(s) of entities to run the effect on. - example: "light.kitchen" - mode: - description: "Decides how colors are changed. Possible values: blink, breathe, ping, strobe, solid." - example: strobe - brightness: - description: Number between 0..255 indicating brightness of the temporary color. - example: 120 - color_name: - description: A human readable color name. - example: "red" - rgb_color: - description: The temporary color in RGB-format. - example: "[255, 100, 100]" - period: - description: Duration of the effect in seconds (default 1.0). - example: 3 - cycles: - description: Number of times the effect should run (default 1.0). - example: 2 - power_on: - description: Powered off lights are temporarily turned on during the effect (default True). - example: False - -lifx_effect_colorloop: - description: Run an effect with looping colors. - fields: - entity_id: - description: Name(s) of entities to run the effect on. - example: "light.disco1, light.disco2, light.disco3" - brightness: - description: Number between 0 and 255 indicating brightness of the effect. Leave this out to maintain the current brightness of each participating light. - example: 120 - period: - description: Duration (in seconds) between color changes (default 60). - example: 180 - change: - description: Hue movement per period, in degrees on a color wheel (ranges from 0 to 360, default 20). - example: 45 - spread: - description: Maximum hue difference between participating lights, in degrees on a color wheel (ranges from 0 to 360, default 30). - example: 0 - power_on: - description: Powered off lights are temporarily turned on during the effect (default True). - example: False - -lifx_effect_stop: - description: Stop a running effect. - fields: - entity_id: - description: Name(s) of entities to stop effects on. Leave out to stop effects everywhere. - example: "light.bedroom" - -xiaomi_miio_set_scene: - description: Set a fixed scene. - fields: - entity_id: - description: Name of the light entity. - example: "light.xiaomi_miio" - scene: - description: Number of the fixed scene, between 1 and 4. - example: 1 - -xiaomi_miio_set_delayed_turn_off: - description: Delayed turn off. - fields: - entity_id: - description: Name of the light entity. - example: "light.xiaomi_miio" - time_period: - description: Time period for the delayed turn off. - example: "5, '0:05', {'minutes': 5}" diff --git a/homeassistant/components/lightwave/__init__.py b/homeassistant/components/lightwave/__init__.py index f3445a3c94ab..4a27d4a7f4a2 100644 --- a/homeassistant/components/lightwave/__init__.py +++ b/homeassistant/components/lightwave/__init__.py @@ -1,7 +1,9 @@ """Support for device connected via Lightwave WiFi-link hub.""" +from lightwave.lightwave import LWLink import voluptuous as vol -import homeassistant.helpers.config_validation as cv + from homeassistant.const import CONF_HOST, CONF_LIGHTS, CONF_NAME, CONF_SWITCHES +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.discovery import async_load_platform LIGHTWAVE_LINK = "lightwave_link" @@ -32,7 +34,6 @@ async def async_setup(hass, config): """Try to start embedded Lightwave broker.""" - from lightwave.lightwave import LWLink host = config[DOMAIN][CONF_HOST] hass.data[LIGHTWAVE_LINK] = LWLink(host) diff --git a/homeassistant/components/limitlessled/light.py b/homeassistant/components/limitlessled/light.py index be320ee43077..e0ef635ae87b 100644 --- a/homeassistant/components/limitlessled/light.py +++ b/homeassistant/components/limitlessled/light.py @@ -1,9 +1,16 @@ """Support for LimitlessLED bulbs.""" import logging +from limitlessled import Color +from limitlessled.bridge import Bridge +from limitlessled.group.dimmer import DimmerGroup +from limitlessled.group.rgbw import RgbwGroup +from limitlessled.group.rgbww import RgbwwGroup +from limitlessled.group.white import WhiteGroup +from limitlessled.pipeline import Pipeline +from limitlessled.presets import COLORLOOP import voluptuous as vol -from homeassistant.const import CONF_NAME, CONF_HOST, CONF_PORT, CONF_TYPE, STATE_ON from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, @@ -14,18 +21,19 @@ EFFECT_COLORLOOP, EFFECT_WHITE, FLASH_LONG, + PLATFORM_SCHEMA, SUPPORT_BRIGHTNESS, + SUPPORT_COLOR, SUPPORT_COLOR_TEMP, SUPPORT_EFFECT, SUPPORT_FLASH, - SUPPORT_COLOR, SUPPORT_TRANSITION, Light, - PLATFORM_SCHEMA, ) +from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT, CONF_TYPE, STATE_ON import homeassistant.helpers.config_validation as cv -from homeassistant.util.color import color_temperature_mired_to_kelvin, color_hs_to_RGB from homeassistant.helpers.restore_state import RestoreEntity +from homeassistant.util.color import color_hs_to_RGB, color_temperature_mired_to_kelvin _LOGGER = logging.getLogger(__name__) @@ -137,7 +145,6 @@ def rewrite_legacy(config): def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the LimitlessLED lights.""" - from limitlessled.bridge import Bridge # Two legacy configuration formats are supported to maintain backwards # compatibility. @@ -172,7 +179,6 @@ def decorator(function): # pylint: disable=protected-access def wrapper(self, **kwargs): """Wrap a group state change.""" - from limitlessled.pipeline import Pipeline pipeline = Pipeline() transition_time = DEFAULT_TRANSITION @@ -199,10 +205,6 @@ class LimitlessLEDGroup(Light, RestoreEntity): def __init__(self, group, config): """Initialize a group.""" - from limitlessled.group.rgbw import RgbwGroup - from limitlessled.group.white import WhiteGroup - from limitlessled.group.dimmer import DimmerGroup - from limitlessled.group.rgbww import RgbwwGroup if isinstance(group, WhiteGroup): self._supported = SUPPORT_LIMITLESSLED_WHITE @@ -366,8 +368,6 @@ def turn_on(self, transition_time, pipeline, **kwargs): # Add effects. if ATTR_EFFECT in kwargs and self._effect_list: if kwargs[ATTR_EFFECT] == EFFECT_COLORLOOP: - from limitlessled.presets import COLORLOOP - self._effect = EFFECT_COLORLOOP pipeline.append(COLORLOOP) if kwargs[ATTR_EFFECT] == EFFECT_WHITE: @@ -389,6 +389,5 @@ def limitlessled_brightness(self): def limitlessled_color(self): """Convert Home Assistant HS list to RGB Color tuple.""" - from limitlessled import Color return Color(*color_hs_to_RGB(*tuple(self._color))) diff --git a/homeassistant/components/linky/.translations/pt.json b/homeassistant/components/linky/.translations/pt.json new file mode 100644 index 000000000000..daf1ce751816 --- /dev/null +++ b/homeassistant/components/linky/.translations/pt.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "username_exists": "Conta j\u00e1 configurada" + }, + "error": { + "username_exists": "Conta j\u00e1 configurada" + }, + "step": { + "user": { + "data": { + "password": "Palavra-passe", + "username": "" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/linky/.translations/ru.json b/homeassistant/components/linky/.translations/ru.json index 463343490a79..da34fbbdb621 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\u0451\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\u0451\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": { @@ -16,7 +16,7 @@ "password": "\u041f\u0430\u0440\u043e\u043b\u044c", "username": "\u0410\u0434\u0440\u0435\u0441 \u044d\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0439 \u043f\u043e\u0447\u0442\u044b" }, - "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0412\u0430\u0448\u0438 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435.", + "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0412\u0430\u0448\u0438 \u0443\u0447\u0451\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435.", "title": "Linky" } }, diff --git a/homeassistant/components/litejet/__init__.py b/homeassistant/components/litejet/__init__.py index 6df79c7c968a..1d3ab4f6b19a 100644 --- a/homeassistant/components/litejet/__init__.py +++ b/homeassistant/components/litejet/__init__.py @@ -1,11 +1,12 @@ """Support for the LiteJet lighting system.""" import logging +from pylitejet import LiteJet import voluptuous as vol -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers import discovery from homeassistant.const import CONF_PORT +from homeassistant.helpers import discovery +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -30,7 +31,6 @@ def setup(hass, config): """Set up the LiteJet component.""" - from pylitejet import LiteJet url = config[DOMAIN].get(CONF_PORT) diff --git a/homeassistant/components/local_file/camera.py b/homeassistant/components/local_file/camera.py index d705cefc3fcc..9bd476cfb275 100644 --- a/homeassistant/components/local_file/camera.py +++ b/homeassistant/components/local_file/camera.py @@ -11,15 +11,17 @@ CAMERA_SERVICE_SCHEMA, PLATFORM_SCHEMA, ) -from homeassistant.components.camera.const import DOMAIN from homeassistant.helpers import config_validation as cv -_LOGGER = logging.getLogger(__name__) +from .const import ( + CONF_FILE_PATH, + DATA_LOCAL_FILE, + DEFAULT_NAME, + DOMAIN, + SERVICE_UPDATE_FILE_PATH, +) -CONF_FILE_PATH = "file_path" -DATA_LOCAL_FILE = "local_file_cameras" -DEFAULT_NAME = "Local File" -SERVICE_UPDATE_FILE_PATH = "local_file_update_file_path" +_LOGGER = logging.getLogger(__name__) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { diff --git a/homeassistant/components/local_file/const.py b/homeassistant/components/local_file/const.py new file mode 100644 index 000000000000..5225a70daedc --- /dev/null +++ b/homeassistant/components/local_file/const.py @@ -0,0 +1,6 @@ +"""Constants for the Local File Camera component.""" +DOMAIN = "local_file" +SERVICE_UPDATE_FILE_PATH = "update_file_path" +CONF_FILE_PATH = "file_path" +DATA_LOCAL_FILE = "local_file_cameras" +DEFAULT_NAME = "Local File" diff --git a/homeassistant/components/local_file/services.yaml b/homeassistant/components/local_file/services.yaml index b359b411b6a8..b8c615f33350 100644 --- a/homeassistant/components/local_file/services.yaml +++ b/homeassistant/components/local_file/services.yaml @@ -1,4 +1,4 @@ -local_file_update_file_path: +update_file_path: description: Use this service to change the file displayed by the camera. fields: entity_id: diff --git a/homeassistant/components/locative/.translations/zh-Hant.json b/homeassistant/components/locative/.translations/zh-Hant.json index 7dd598c8fc21..5135eb33c9f2 100644 --- a/homeassistant/components/locative/.translations/zh-Hant.json +++ b/homeassistant/components/locative/.translations/zh-Hant.json @@ -5,7 +5,7 @@ "one_instance_allowed": "\u50c5\u9700\u8a2d\u5b9a\u4e00\u7d44\u7269\u4ef6\u5373\u53ef\u3002" }, "create_entry": { - "default": "\u6b32\u50b3\u9001\u4f4d\u7f6e\u81f3 Home Assistant\uff0c\u5c07\u9700\u65bc Locative App \u5167\u8a2d\u5b9a webhook \u529f\u80fd\u3002\n\n\u8acb\u586b\u5beb\u4e0b\u5217\u8cc7\u8a0a\uff1a\n\n- URL: `{webhook_url}`\n- Method: POST\n\n\u8acb\u53c3\u95b1 [\u6587\u4ef6]({docs_url})\u4ee5\u4e86\u89e3\u66f4\u8a73\u7d30\u8cc7\u6599\u3002" + "default": "\u6b32\u50b3\u9001\u5ea7\u6a19\u81f3 Home Assistant\uff0c\u5c07\u9700\u65bc Locative App \u5167\u8a2d\u5b9a webhook \u529f\u80fd\u3002\n\n\u8acb\u586b\u5beb\u4e0b\u5217\u8cc7\u8a0a\uff1a\n\n- URL: `{webhook_url}`\n- Method: POST\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/lock/.translations/bg.json b/homeassistant/components/lock/.translations/bg.json index 0e77bcf10330..54b80842f4f2 100644 --- a/homeassistant/components/lock/.translations/bg.json +++ b/homeassistant/components/lock/.translations/bg.json @@ -8,6 +8,10 @@ "condition_type": { "is_locked": "{entity_name} \u0435 \u0437\u0430\u043a\u043b\u044e\u0447\u0435\u043d", "is_unlocked": "{entity_name} \u0435 \u043e\u0442\u043a\u043b\u044e\u0447\u0435\u043d" + }, + "trigger_type": { + "locked": "{entity_name} \u0437\u0430\u043a\u043b\u044e\u0447\u0435\u043d", + "unlocked": "{entity_name} \u043e\u0442\u043a\u043b\u044e\u0447\u0435\u043d" } } } \ No newline at end of file diff --git a/homeassistant/components/lock/.translations/nl.json b/homeassistant/components/lock/.translations/nl.json index 099b73083346..61370236a97a 100644 --- a/homeassistant/components/lock/.translations/nl.json +++ b/homeassistant/components/lock/.translations/nl.json @@ -8,6 +8,10 @@ "condition_type": { "is_locked": "{entity_name} is vergrendeld", "is_unlocked": "{entity_name} is ontgrendeld" + }, + "trigger_type": { + "locked": "{entity_name} vergrendeld", + "unlocked": "{entity_name} ontgrendeld" } } } \ No newline at end of file diff --git a/homeassistant/components/lock/.translations/pl.json b/homeassistant/components/lock/.translations/pl.json index a3fe7358398c..a3123919615d 100644 --- a/homeassistant/components/lock/.translations/pl.json +++ b/homeassistant/components/lock/.translations/pl.json @@ -8,6 +8,10 @@ "condition_type": { "is_locked": "zamek {entity_name} jest zamkni\u0119ty", "is_unlocked": "zamek {entity_name} jest otwarty" + }, + "trigger_type": { + "locked": "nast\u0105pi zamkni\u0119cie {entity_name}", + "unlocked": "nast\u0105pi otwarcie {entity_name}" } } } \ No newline at end of file diff --git a/homeassistant/components/lock/.translations/pt.json b/homeassistant/components/lock/.translations/pt.json new file mode 100644 index 000000000000..05bcf4416600 --- /dev/null +++ b/homeassistant/components/lock/.translations/pt.json @@ -0,0 +1,8 @@ +{ + "device_automation": { + "trigger_type": { + "locked": "{entity_name} fechada", + "unlocked": "{entity_name} aberta" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lock/__init__.py b/homeassistant/components/lock/__init__.py index 503bd3a8c783..c443a5219d7d 100644 --- a/homeassistant/components/lock/__init__.py +++ b/homeassistant/components/lock/__init__.py @@ -8,8 +8,8 @@ from homeassistant.loader import bind_hass from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.entity import Entity -from homeassistant.helpers.config_validation import ( # noqa - ENTITY_SERVICE_SCHEMA, +from homeassistant.helpers.config_validation import ( # noqa: F401 + make_entity_service_schema, PLATFORM_SCHEMA, PLATFORM_SCHEMA_BASE, ) @@ -40,7 +40,7 @@ MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10) -LOCK_SERVICE_SCHEMA = ENTITY_SERVICE_SCHEMA.extend({vol.Optional(ATTR_CODE): cv.string}) +LOCK_SERVICE_SCHEMA = make_entity_service_schema({vol.Optional(ATTR_CODE): cv.string}) # Bitfield of features supported by the lock entity SUPPORT_OPEN = 1 diff --git a/homeassistant/components/lock/services.yaml b/homeassistant/components/lock/services.yaml index d17e00addd19..fea02bb025e2 100644 --- a/homeassistant/components/lock/services.yaml +++ b/homeassistant/components/lock/services.yaml @@ -20,23 +20,6 @@ get_usercode: description: Code slot to retrieve a code from. example: 1 -nuki_lock_n_go: - description: "Nuki Lock 'n' Go" - fields: - entity_id: - description: Entity id of the Nuki lock. - example: 'lock.front_door' - unlatch: - description: Whether to unlatch the lock. - example: false - -nuki_unlatch: - description: Nuki unlatch. - fields: - entity_id: - description: Entity id of the Nuki lock. - example: 'lock.front_door' - lock: description: Lock all or specified locks. fields: @@ -79,66 +62,3 @@ unlock: code: description: An optional code to unlock the lock with. example: 1234 - -wink_set_lock_vacation_mode: - description: Set vacation mode for all or specified locks. Disables all user codes. - fields: - entity_id: - description: Name of lock to unlock. - example: 'lock.front_door' - enabled: - description: enable or disable. true or false. - example: true - -wink_set_lock_alarm_mode: - description: Set alarm mode for all or specified locks. - fields: - entity_id: - description: Name of lock to unlock. - example: 'lock.front_door' - mode: - description: One of tamper, activity, or forced_entry. - example: tamper - -wink_set_lock_alarm_sensitivity: - description: Set alarm sensitivity for all or specified locks. - fields: - entity_id: - description: Name of lock to unlock. - example: 'lock.front_door' - sensitivity: - description: One of low, medium_low, medium, medium_high, high. - example: medium - -wink_set_lock_alarm_state: - description: Set alarm state. - fields: - entity_id: - description: Name of lock to unlock. - example: 'lock.front_door' - enabled: - description: enable or disable. true or false. - example: true - -wink_set_lock_beeper_state: - description: Set beeper state. - fields: - entity_id: - description: Name of lock to unlock. - example: 'lock.front_door' - enabled: - description: enable or disable. true or false. - example: true - -wink_add_new_lock_key_code: - description: Add a new user key code. - fields: - entity_id: - description: Name of lock to unlock. - example: 'lock.front_door' - name: - description: name of the new key code. - example: Bob - code: - description: new key code, length must match length of other codes. Default length is 4. - example: 1234 diff --git a/homeassistant/components/logi_circle/.translations/es.json b/homeassistant/components/logi_circle/.translations/es.json index 4819ff5cdd77..7209bdfefd5a 100644 --- a/homeassistant/components/logi_circle/.translations/es.json +++ b/homeassistant/components/logi_circle/.translations/es.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_setup": "Solo puedes configurar una cuenta de Logi Circle.", - "external_error": "La excepci\u00f3n se produjo a partir de otro flujo.", + "external_error": "Se produjo una excepci\u00f3n de otro flujo.", "external_setup": "Logi Circle se ha configurado correctamente a partir de otro flujo.", "no_flows": "Es necesario configurar Logi Circle antes de poder autenticarse con \u00e9l. [Echa un vistazo a las instrucciones] (https://www.home-assistant.io/components/logi_circle/)." }, diff --git a/homeassistant/components/logi_circle/.translations/pl.json b/homeassistant/components/logi_circle/.translations/pl.json index 2266ea841c5f..333a295ad06d 100644 --- a/homeassistant/components/logi_circle/.translations/pl.json +++ b/homeassistant/components/logi_circle/.translations/pl.json @@ -4,7 +4,7 @@ "already_setup": "Mo\u017cesz skonfigurowa\u0107 tylko jedno konto Logi Circle.", "external_error": "Wyst\u0105pi\u0142 wyj\u0105tek z innego przep\u0142ywu.", "external_setup": "Logi Circle zosta\u0142o pomy\u015blnie skonfigurowane z innego przep\u0142ywu.", - "no_flows": "Musisz skonfigurowa\u0107 Logi Circle, zanim b\u0119dziesz m\u00f3g\u0142 si\u0119 z nim uwierzytelni\u0107. [Przeczytaj instrukcj\u0119](https://www.home-assistant.io/components/logi_circle/)." + "no_flows": "Musisz skonfigurowa\u0107 Logi Circle, aby m\u00f3c si\u0119 z nim uwierzytelni\u0107. Zapoznaj si\u0119 z [instrukcj\u0105](https://www.home-assistant.io/components/logi_circle/)." }, "create_entry": { "default": "Pomy\u015blnie uwierzytelniono z Logi Circle." diff --git a/homeassistant/components/logi_circle/.translations/ru.json b/homeassistant/components/logi_circle/.translations/ru.json index 40c7c8853dae..9cecf3081b68 100644 --- a/homeassistant/components/logi_circle/.translations/ru.json +++ b/homeassistant/components/logi_circle/.translations/ru.json @@ -16,7 +16,7 @@ }, "step": { "auth": { - "description": "\u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043f\u043e [\u0441\u0441\u044b\u043b\u043a\u0435]({authorization_url}) \u0438 \u0420\u0430\u0437\u0440\u0435\u0448\u0438\u0442\u0435 \u0434\u043e\u0441\u0442\u0443\u043f \u043a \u0412\u0430\u0448\u0435\u0439 \u0443\u0447\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 Logi Circle, \u0437\u0430\u0442\u0435\u043c \u0432\u0435\u0440\u043d\u0438\u0442\u0435\u0441\u044c \u0441\u044e\u0434\u0430 \u0438 \u043d\u0430\u0436\u043c\u0438\u0442\u0435 \u041f\u041e\u0414\u0422\u0412\u0415\u0420\u0414\u0418\u0422\u042c.", + "description": "\u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043f\u043e [\u0441\u0441\u044b\u043b\u043a\u0435]({authorization_url}) \u0438 \u0420\u0430\u0437\u0440\u0435\u0448\u0438\u0442\u0435 \u0434\u043e\u0441\u0442\u0443\u043f \u043a \u0412\u0430\u0448\u0435\u0439 \u0443\u0447\u0451\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 Logi Circle, \u0437\u0430\u0442\u0435\u043c \u0432\u0435\u0440\u043d\u0438\u0442\u0435\u0441\u044c \u0441\u044e\u0434\u0430 \u0438 \u043d\u0430\u0436\u043c\u0438\u0442\u0435 \u041f\u041e\u0414\u0422\u0412\u0415\u0420\u0414\u0418\u0422\u042c.", "title": "Logi Circle" }, "user": { diff --git a/homeassistant/components/logi_circle/__init__.py b/homeassistant/components/logi_circle/__init__.py index f7ed3a73fce0..b77f17101a86 100644 --- a/homeassistant/components/logi_circle/__init__.py +++ b/homeassistant/components/logi_circle/__init__.py @@ -2,7 +2,10 @@ import asyncio import logging +from aiohttp.client_exceptions import ClientResponseError import async_timeout +from logi_circle import LogiCircle +from logi_circle.exception import AuthorizationFailed import voluptuous as vol from homeassistant import config_entries @@ -116,9 +119,6 @@ async def async_setup(hass, config): async def async_setup_entry(hass, entry): """Set up Logi Circle from a config entry.""" - from logi_circle import LogiCircle - from logi_circle.exception import AuthorizationFailed - from aiohttp.client_exceptions import ClientResponseError logi_circle = LogiCircle( client_id=entry.data[CONF_CLIENT_ID], diff --git a/homeassistant/components/logi_circle/config_flow.py b/homeassistant/components/logi_circle/config_flow.py index 2a25c5f00a49..ce8460233d61 100644 --- a/homeassistant/components/logi_circle/config_flow.py +++ b/homeassistant/components/logi_circle/config_flow.py @@ -3,6 +3,8 @@ from collections import OrderedDict import async_timeout +from logi_circle import LogiCircle +from logi_circle.exception import AuthorizationFailed import voluptuous as vol from homeassistant import config_entries @@ -120,7 +122,6 @@ async def async_step_auth(self, user_input=None): def _get_authorization_url(self): """Create temporary Circle session and generate authorization url.""" - from logi_circle import LogiCircle flow = self.hass.data[DATA_FLOW_IMPL][self.flow_impl] client_id = flow[CONF_CLIENT_ID] @@ -148,8 +149,6 @@ async def async_step_code(self, code=None): async def _async_create_session(self, code): """Create Logi Circle session and entries.""" - from logi_circle import LogiCircle - from logi_circle.exception import AuthorizationFailed flow = self.hass.data[DATA_FLOW_IMPL][DOMAIN] client_id = flow[CONF_CLIENT_ID] diff --git a/homeassistant/components/london_underground/sensor.py b/homeassistant/components/london_underground/sensor.py index 07881fce40f6..12f40f7b4613 100644 --- a/homeassistant/components/london_underground/sensor.py +++ b/homeassistant/components/london_underground/sensor.py @@ -2,6 +2,7 @@ from datetime import timedelta import logging +from london_tube_status import TubeData import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -43,7 +44,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Tube sensor.""" - from london_tube_status import TubeData data = TubeData() data.update() diff --git a/homeassistant/components/luci/device_tracker.py b/homeassistant/components/luci/device_tracker.py index 59c3251a437a..9d71b3d263a7 100644 --- a/homeassistant/components/luci/device_tracker.py +++ b/homeassistant/components/luci/device_tracker.py @@ -1,6 +1,7 @@ """Support for OpenWRT (luci) routers.""" import logging +from openwrt_luci_rpc import OpenWrtRpc import voluptuous as vol from homeassistant.components.device_tracker import ( @@ -45,7 +46,6 @@ class LuciDeviceScanner(DeviceScanner): def __init__(self, config): """Initialize the scanner.""" - from openwrt_luci_rpc import OpenWrtRpc self.router = OpenWrtRpc( config[CONF_HOST], diff --git a/homeassistant/components/lupusec/alarm_control_panel.py b/homeassistant/components/lupusec/alarm_control_panel.py index 245743d0f653..c6ad817bfbf6 100644 --- a/homeassistant/components/lupusec/alarm_control_panel.py +++ b/homeassistant/components/lupusec/alarm_control_panel.py @@ -2,6 +2,10 @@ from datetime import timedelta from homeassistant.components.alarm_control_panel import AlarmControlPanel +from homeassistant.components.alarm_control_panel.const import ( + SUPPORT_ALARM_ARM_AWAY, + SUPPORT_ALARM_ARM_HOME, +) from homeassistant.const import ( STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, @@ -51,6 +55,11 @@ def state(self): state = None return state + @property + def supported_features(self) -> int: + """Return the list of supported features.""" + return SUPPORT_ALARM_ARM_HOME | SUPPORT_ALARM_ARM_AWAY + def alarm_arm_away(self, code=None): """Send arm away command.""" self._device.set_away() diff --git a/homeassistant/components/lupusec/manifest.json b/homeassistant/components/lupusec/manifest.json index bb6b18243ec1..2fea92afd0e2 100644 --- a/homeassistant/components/lupusec/manifest.json +++ b/homeassistant/components/lupusec/manifest.json @@ -3,8 +3,8 @@ "name": "Lupusec", "documentation": "https://www.home-assistant.io/integrations/lupusec", "requirements": [ - "lupupy==0.0.17" + "lupupy==0.0.18" ], "dependencies": [], - "codeowners": [] + "codeowners": ["@majuss"] } diff --git a/homeassistant/components/lutron/__init__.py b/homeassistant/components/lutron/__init__.py index ac9a4eab417d..ac6ffa46b27c 100644 --- a/homeassistant/components/lutron/__init__.py +++ b/homeassistant/components/lutron/__init__.py @@ -1,11 +1,12 @@ """Component for interacting with a Lutron RadioRA 2 system.""" import logging +from pylutron import Button, Lutron import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.const import ATTR_ID, CONF_HOST, CONF_PASSWORD, CONF_USERNAME from homeassistant.helpers import discovery +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.util import slugify @@ -36,7 +37,6 @@ def setup(hass, base_config): """Set up the Lutron component.""" - from pylutron import Lutron hass.data[LUTRON_BUTTONS] = [] hass.data[LUTRON_CONTROLLER] = None @@ -147,7 +147,6 @@ def __init__(self, hass, keypad, button): def button_callback(self, button, context, event, params): """Fire an event about a button being pressed or released.""" - from pylutron import Button # Events per button type: # RaiseLower -> pressed/released diff --git a/homeassistant/components/lutron/binary_sensor.py b/homeassistant/components/lutron/binary_sensor.py index a86d56c325fb..866c82a7b2ac 100644 --- a/homeassistant/components/lutron/binary_sensor.py +++ b/homeassistant/components/lutron/binary_sensor.py @@ -2,8 +2,8 @@ from pylutron import OccupancyGroup from homeassistant.components.binary_sensor import ( - BinarySensorDevice, DEVICE_CLASS_OCCUPANCY, + BinarySensorDevice, ) from . import LUTRON_CONTROLLER, LUTRON_DEVICES, LutronDevice diff --git a/homeassistant/components/lutron_caseta/.translations/bg.json b/homeassistant/components/lutron_caseta/.translations/bg.json new file mode 100644 index 000000000000..cfc3c290afee --- /dev/null +++ b/homeassistant/components/lutron_caseta/.translations/bg.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Lutron Cas\u00e9ta" + } +} \ No newline at end of file diff --git a/homeassistant/components/lutron_caseta/.translations/ca.json b/homeassistant/components/lutron_caseta/.translations/ca.json new file mode 100644 index 000000000000..cfc3c290afee --- /dev/null +++ b/homeassistant/components/lutron_caseta/.translations/ca.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Lutron Cas\u00e9ta" + } +} \ No newline at end of file diff --git a/homeassistant/components/lutron_caseta/.translations/en.json b/homeassistant/components/lutron_caseta/.translations/en.json new file mode 100644 index 000000000000..cfc3c290afee --- /dev/null +++ b/homeassistant/components/lutron_caseta/.translations/en.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Lutron Cas\u00e9ta" + } +} \ No newline at end of file diff --git a/homeassistant/components/lutron_caseta/.translations/es.json b/homeassistant/components/lutron_caseta/.translations/es.json new file mode 100644 index 000000000000..cfc3c290afee --- /dev/null +++ b/homeassistant/components/lutron_caseta/.translations/es.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Lutron Cas\u00e9ta" + } +} \ No newline at end of file diff --git a/homeassistant/components/lutron_caseta/.translations/fr.json b/homeassistant/components/lutron_caseta/.translations/fr.json new file mode 100644 index 000000000000..cfc3c290afee --- /dev/null +++ b/homeassistant/components/lutron_caseta/.translations/fr.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Lutron Cas\u00e9ta" + } +} \ No newline at end of file diff --git a/homeassistant/components/lutron_caseta/.translations/it.json b/homeassistant/components/lutron_caseta/.translations/it.json new file mode 100644 index 000000000000..cfc3c290afee --- /dev/null +++ b/homeassistant/components/lutron_caseta/.translations/it.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Lutron Cas\u00e9ta" + } +} \ No newline at end of file diff --git a/homeassistant/components/lutron_caseta/.translations/lb.json b/homeassistant/components/lutron_caseta/.translations/lb.json new file mode 100644 index 000000000000..cfc3c290afee --- /dev/null +++ b/homeassistant/components/lutron_caseta/.translations/lb.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Lutron Cas\u00e9ta" + } +} \ No newline at end of file diff --git a/homeassistant/components/lutron_caseta/.translations/no.json b/homeassistant/components/lutron_caseta/.translations/no.json new file mode 100644 index 000000000000..cfc3c290afee --- /dev/null +++ b/homeassistant/components/lutron_caseta/.translations/no.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Lutron Cas\u00e9ta" + } +} \ No newline at end of file diff --git a/homeassistant/components/lutron_caseta/.translations/pl.json b/homeassistant/components/lutron_caseta/.translations/pl.json new file mode 100644 index 000000000000..cfc3c290afee --- /dev/null +++ b/homeassistant/components/lutron_caseta/.translations/pl.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Lutron Cas\u00e9ta" + } +} \ No newline at end of file diff --git a/homeassistant/components/lutron_caseta/.translations/ru.json b/homeassistant/components/lutron_caseta/.translations/ru.json new file mode 100644 index 000000000000..cfc3c290afee --- /dev/null +++ b/homeassistant/components/lutron_caseta/.translations/ru.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Lutron Cas\u00e9ta" + } +} \ No newline at end of file diff --git a/homeassistant/components/lutron_caseta/.translations/sl.json b/homeassistant/components/lutron_caseta/.translations/sl.json new file mode 100644 index 000000000000..cfc3c290afee --- /dev/null +++ b/homeassistant/components/lutron_caseta/.translations/sl.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Lutron Cas\u00e9ta" + } +} \ No newline at end of file diff --git a/homeassistant/components/lutron_caseta/.translations/zh-Hant.json b/homeassistant/components/lutron_caseta/.translations/zh-Hant.json new file mode 100644 index 000000000000..cfc3c290afee --- /dev/null +++ b/homeassistant/components/lutron_caseta/.translations/zh-Hant.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Lutron Cas\u00e9ta" + } +} \ No newline at end of file diff --git a/homeassistant/components/lutron_caseta/__init__.py b/homeassistant/components/lutron_caseta/__init__.py index 4d4b2e90fd6b..aaac06a6bd5a 100644 --- a/homeassistant/components/lutron_caseta/__init__.py +++ b/homeassistant/components/lutron_caseta/__init__.py @@ -1,11 +1,12 @@ """Component for interacting with a Lutron Caseta system.""" import logging +from pylutron_caseta.smartbridge import Smartbridge import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.const import CONF_HOST from homeassistant.helpers import discovery +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -32,12 +33,11 @@ extra=vol.ALLOW_EXTRA, ) -LUTRON_CASETA_COMPONENTS = ["light", "switch", "cover", "scene"] +LUTRON_CASETA_COMPONENTS = ["light", "switch", "cover", "scene", "fan"] async def async_setup(hass, base_config): """Set up the Lutron component.""" - from pylutron_caseta.smartbridge import Smartbridge config = base_config.get(DOMAIN) keyfile = hass.config.path(config[CONF_KEYFILE]) @@ -76,28 +76,39 @@ def __init__(self, device, bridge): [:param]device the device metadata [:param]bridge the smartbridge object """ - self._device_id = device["device_id"] - self._device_type = device["type"] - self._device_name = device["name"] - self._device_zone = device["zone"] - self._state = None + self._device = device self._smartbridge = bridge async def async_added_to_hass(self): """Register callbacks.""" self._smartbridge.add_subscriber( - self._device_id, self.async_schedule_update_ha_state + self.device_id, self.async_schedule_update_ha_state ) + @property + def device_id(self): + """Return the device ID used for calling pylutron_caseta.""" + return self._device["device_id"] + @property def name(self): """Return the name of the device.""" - return self._device_name + return self._device["name"] + + @property + def serial(self): + """Return the serial number of the device.""" + return self._device["serial"] + + @property + def unique_id(self): + """Return the unique ID of the device (serial).""" + return str(self.serial) @property def device_state_attributes(self): """Return the state attributes.""" - attr = {"Device ID": self._device_id, "Zone ID": self._device_zone} + attr = {"Device ID": self.device_id, "Zone ID": self._device["zone"]} return attr @property diff --git a/homeassistant/components/lutron_caseta/cover.py b/homeassistant/components/lutron_caseta/cover.py index 786e569da322..afd669153e0c 100644 --- a/homeassistant/components/lutron_caseta/cover.py +++ b/homeassistant/components/lutron_caseta/cover.py @@ -38,28 +38,28 @@ def supported_features(self): @property def is_closed(self): """Return if the cover is closed.""" - return self._state["current_state"] < 1 + return self._device["current_state"] < 1 @property def current_cover_position(self): """Return the current position of cover.""" - return self._state["current_state"] + return self._device["current_state"] async def async_close_cover(self, **kwargs): """Close the cover.""" - self._smartbridge.set_value(self._device_id, 0) + self._smartbridge.set_value(self.device_id, 0) async def async_open_cover(self, **kwargs): """Open the cover.""" - self._smartbridge.set_value(self._device_id, 100) + self._smartbridge.set_value(self.device_id, 100) async def async_set_cover_position(self, **kwargs): """Move the shade to a specific position.""" if ATTR_POSITION in kwargs: position = kwargs[ATTR_POSITION] - self._smartbridge.set_value(self._device_id, position) + self._smartbridge.set_value(self.device_id, position) async def async_update(self): """Call when forcing a refresh of the device.""" - self._state = self._smartbridge.get_device_by_id(self._device_id) - _LOGGER.debug(self._state) + self._device = self._smartbridge.get_device_by_id(self.device_id) + _LOGGER.debug(self._device) diff --git a/homeassistant/components/lutron_caseta/fan.py b/homeassistant/components/lutron_caseta/fan.py new file mode 100644 index 000000000000..1227371ac073 --- /dev/null +++ b/homeassistant/components/lutron_caseta/fan.py @@ -0,0 +1,96 @@ +"""Support for Lutron Caseta fans.""" +import logging + +from pylutron_caseta import FAN_HIGH, FAN_LOW, FAN_MEDIUM, FAN_MEDIUM_HIGH, FAN_OFF + +from homeassistant.components.fan import ( + DOMAIN, + SPEED_HIGH, + SPEED_LOW, + SPEED_MEDIUM, + SPEED_OFF, + SUPPORT_SET_SPEED, + FanEntity, +) + +from . import LUTRON_CASETA_SMARTBRIDGE, LutronCasetaDevice + +_LOGGER = logging.getLogger(__name__) + +VALUE_TO_SPEED = { + None: SPEED_OFF, + FAN_OFF: SPEED_OFF, + FAN_LOW: SPEED_LOW, + FAN_MEDIUM: SPEED_MEDIUM, + FAN_MEDIUM_HIGH: SPEED_MEDIUM, + FAN_HIGH: SPEED_HIGH, +} + +SPEED_TO_VALUE = { + SPEED_OFF: FAN_OFF, + SPEED_LOW: FAN_LOW, + SPEED_MEDIUM: FAN_MEDIUM, + SPEED_HIGH: FAN_HIGH, +} + +FAN_SPEEDS = [SPEED_OFF, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH] + + +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Set up Lutron fan.""" + entities = [] + bridge = hass.data[LUTRON_CASETA_SMARTBRIDGE] + fan_devices = bridge.get_devices_by_domain(DOMAIN) + + for fan_device in fan_devices: + entity = LutronCasetaFan(fan_device, bridge) + entities.append(entity) + + async_add_entities(entities, True) + + +class LutronCasetaFan(LutronCasetaDevice, FanEntity): + """Representation of a Lutron Caseta fan. Including Fan Speed.""" + + @property + def speed(self) -> str: + """Return the current speed.""" + return VALUE_TO_SPEED[self._device["fan_speed"]] + + @property + def speed_list(self) -> list: + """Get the list of available speeds.""" + return FAN_SPEEDS + + @property + def supported_features(self) -> int: + """Flag supported features. Speed Only.""" + return SUPPORT_SET_SPEED + + async def async_turn_on(self, speed: str = None, **kwargs): + """Turn the fan on.""" + if speed is None: + speed = SPEED_MEDIUM + await self.async_set_speed(speed) + + async def async_turn_off(self, **kwargs): + """Turn the fan off.""" + await self.async_set_speed(SPEED_OFF) + + async def async_set_speed(self, speed: str) -> None: + """Set the speed of the fan.""" + self._smartbridge.set_fan(self.device_id, SPEED_TO_VALUE[speed]) + + @property + def is_on(self): + """Return true if device is on.""" + return VALUE_TO_SPEED[self._device["fan_speed"]] in [ + SPEED_LOW, + SPEED_MEDIUM, + SPEED_HIGH, + ] + + async def async_update(self): + """Update when forcing a refresh of the device.""" + self._device = self._smartbridge.get_device_by_id(self.device_id) + _LOGGER.debug("State of this lutron fan device is %s", self._device) diff --git a/homeassistant/components/lutron_caseta/light.py b/homeassistant/components/lutron_caseta/light.py index a764ad4b73a2..af225d2939db 100644 --- a/homeassistant/components/lutron_caseta/light.py +++ b/homeassistant/components/lutron_caseta/light.py @@ -37,23 +37,23 @@ def supported_features(self): @property def brightness(self): """Return the brightness of the light.""" - return to_hass_level(self._state["current_state"]) + return to_hass_level(self._device["current_state"]) async def async_turn_on(self, **kwargs): """Turn the light on.""" brightness = kwargs.get(ATTR_BRIGHTNESS, 255) - self._smartbridge.set_value(self._device_id, to_lutron_level(brightness)) + self._smartbridge.set_value(self.device_id, to_lutron_level(brightness)) async def async_turn_off(self, **kwargs): """Turn the light off.""" - self._smartbridge.set_value(self._device_id, 0) + self._smartbridge.set_value(self.device_id, 0) @property def is_on(self): """Return true if device is on.""" - return self._state["current_state"] > 0 + return self._device["current_state"] > 0 async def async_update(self): """Call when forcing a refresh of the device.""" - self._state = self._smartbridge.get_device_by_id(self._device_id) - _LOGGER.debug(self._state) + self._device = self._smartbridge.get_device_by_id(self.device_id) + _LOGGER.debug(self._device) diff --git a/homeassistant/components/lutron_caseta/manifest.json b/homeassistant/components/lutron_caseta/manifest.json index d1501a562db4..e9df5ad1d46f 100644 --- a/homeassistant/components/lutron_caseta/manifest.json +++ b/homeassistant/components/lutron_caseta/manifest.json @@ -3,7 +3,7 @@ "name": "Lutron caseta", "documentation": "https://www.home-assistant.io/integrations/lutron_caseta", "requirements": [ - "pylutron-caseta==0.5.0" + "pylutron-caseta==0.5.1" ], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/lutron_caseta/strings.json b/homeassistant/components/lutron_caseta/strings.json new file mode 100644 index 000000000000..cb7ab8c767eb --- /dev/null +++ b/homeassistant/components/lutron_caseta/strings.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Lutron Caséta" + } +} \ No newline at end of file diff --git a/homeassistant/components/lutron_caseta/switch.py b/homeassistant/components/lutron_caseta/switch.py index fabd4e7fa76e..f6eb846ecfb0 100644 --- a/homeassistant/components/lutron_caseta/switch.py +++ b/homeassistant/components/lutron_caseta/switch.py @@ -27,18 +27,18 @@ class LutronCasetaLight(LutronCasetaDevice, SwitchDevice): async def async_turn_on(self, **kwargs): """Turn the switch on.""" - self._smartbridge.turn_on(self._device_id) + self._smartbridge.turn_on(self.device_id) async def async_turn_off(self, **kwargs): """Turn the switch off.""" - self._smartbridge.turn_off(self._device_id) + self._smartbridge.turn_off(self.device_id) @property def is_on(self): """Return true if device is on.""" - return self._state["current_state"] > 0 + return self._device["current_state"] > 0 async def async_update(self): """Update when forcing a refresh of the device.""" - self._state = self._smartbridge.get_device_by_id(self._device_id) - _LOGGER.debug(self._state) + self._device = self._smartbridge.get_device_by_id(self.device_id) + _LOGGER.debug(self._device) diff --git a/homeassistant/components/lyft/sensor.py b/homeassistant/components/lyft/sensor.py index 339b996c5d84..1b90d66398e8 100644 --- a/homeassistant/components/lyft/sensor.py +++ b/homeassistant/components/lyft/sensor.py @@ -1,13 +1,16 @@ """Support for the Lyft API.""" -import logging from datetime import timedelta +import logging +from lyft_rides.auth import ClientCredentialGrant +from lyft_rides.client import LyftRidesClient +from lyft_rides.errors import APIError import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle -import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -38,8 +41,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Lyft sensor.""" - from lyft_rides.auth import ClientCredentialGrant - from lyft_rides.errors import APIError auth_flow = ClientCredentialGrant( client_id=config.get(CONF_CLIENT_ID), @@ -208,7 +209,6 @@ def __init__( @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): """Get the latest product info and estimates from the Lyft API.""" - from lyft_rides.errors import APIError try: self.fetch_data() @@ -217,7 +217,6 @@ def update(self): def fetch_data(self): """Get the latest product info and estimates from the Lyft API.""" - from lyft_rides.client import LyftRidesClient client = LyftRidesClient(self._session) diff --git a/homeassistant/components/mailgun/__init__.py b/homeassistant/components/mailgun/__init__.py index 4bcca0848f43..57c83d8c20cf 100644 --- a/homeassistant/components/mailgun/__init__.py +++ b/homeassistant/components/mailgun/__init__.py @@ -6,13 +6,12 @@ import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.const import CONF_API_KEY, CONF_DOMAIN, CONF_WEBHOOK_ID from homeassistant.helpers import config_entry_flow +import homeassistant.helpers.config_validation as cv from .const import DOMAIN - _LOGGER = logging.getLogger(__name__) CONF_SANDBOX = "sandbox" diff --git a/homeassistant/components/mailgun/config_flow.py b/homeassistant/components/mailgun/config_flow.py index e1c6abbc1cab..6fe87e7cbf4a 100644 --- a/homeassistant/components/mailgun/config_flow.py +++ b/homeassistant/components/mailgun/config_flow.py @@ -1,13 +1,13 @@ """Config flow for Mailgun.""" from homeassistant.helpers import config_entry_flow -from .const import DOMAIN +from .const import DOMAIN config_entry_flow.register_webhook_flow( DOMAIN, "Mailgun Webhook", { - "mailgun_url": "https://documentation.mailgun.com/en/latest/user_manual.html#webhooks", # noqa: E501 pylint: disable=line-too-long + "mailgun_url": "https://documentation.mailgun.com/en/latest/user_manual.html#webhooks", "docs_url": "https://www.home-assistant.io/integrations/mailgun/", }, ) diff --git a/homeassistant/components/mailgun/notify.py b/homeassistant/components/mailgun/notify.py index efa5a17430ca..c2222cfd742b 100644 --- a/homeassistant/components/mailgun/notify.py +++ b/homeassistant/components/mailgun/notify.py @@ -1,6 +1,12 @@ """Support for the Mailgun mail notifications.""" import logging +from pymailgunner import ( + Client, + MailgunCredentialsError, + MailgunDomainError, + MailgunError, +) import voluptuous as vol from homeassistant.components.notify import ( @@ -58,7 +64,6 @@ def __init__(self, domain, sandbox, api_key, sender, recipient): def initialize_client(self): """Initialize the connection to Mailgun.""" - from pymailgunner import Client self._client = Client(self._api_key, self._domain, self._sandbox) _LOGGER.debug("Mailgun domain: %s", self._client.domain) @@ -68,7 +73,6 @@ def initialize_client(self): def connection_is_valid(self): """Check whether the provided credentials are valid.""" - from pymailgunner import MailgunCredentialsError, MailgunDomainError try: self.initialize_client() @@ -82,7 +86,6 @@ def connection_is_valid(self): def send_message(self, message="", **kwargs): """Send a mail to the recipient.""" - from pymailgunner import MailgunError subject = kwargs.get(ATTR_TITLE, ATTR_TITLE_DEFAULT) data = kwargs.get(ATTR_DATA) diff --git a/homeassistant/components/manual/alarm_control_panel.py b/homeassistant/components/manual/alarm_control_panel.py index ac234dc0ac9b..b41da2d51bdb 100644 --- a/homeassistant/components/manual/alarm_control_panel.py +++ b/homeassistant/components/manual/alarm_control_panel.py @@ -7,6 +7,13 @@ import voluptuous as vol import homeassistant.components.alarm_control_panel as alarm +from homeassistant.components.alarm_control_panel.const import ( + SUPPORT_ALARM_ARM_AWAY, + SUPPORT_ALARM_ARM_CUSTOM_BYPASS, + SUPPORT_ALARM_ARM_HOME, + SUPPORT_ALARM_ARM_NIGHT, + SUPPORT_ALARM_TRIGGER, +) from homeassistant.const import ( CONF_CODE, CONF_DELAY_TIME, @@ -25,8 +32,8 @@ ) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.event import track_point_in_time -import homeassistant.util.dt as dt_util from homeassistant.helpers.restore_state import RestoreEntity +import homeassistant.util.dt as dt_util _LOGGER = logging.getLogger(__name__) @@ -234,6 +241,17 @@ def state(self): return self._state + @property + def supported_features(self) -> int: + """Return the list of supported features.""" + return ( + SUPPORT_ALARM_ARM_HOME + | SUPPORT_ALARM_ARM_AWAY + | SUPPORT_ALARM_ARM_NIGHT + | SUPPORT_ALARM_TRIGGER + | SUPPORT_ALARM_ARM_CUSTOM_BYPASS + ) + @property def _active_state(self): """Get the current state.""" diff --git a/homeassistant/components/manual_mqtt/alarm_control_panel.py b/homeassistant/components/manual_mqtt/alarm_control_panel.py index c57fa275516f..f11dac357e61 100644 --- a/homeassistant/components/manual_mqtt/alarm_control_panel.py +++ b/homeassistant/components/manual_mqtt/alarm_control_panel.py @@ -6,30 +6,33 @@ import voluptuous as vol +from homeassistant.components import mqtt import homeassistant.components.alarm_control_panel as alarm -import homeassistant.util.dt as dt_util +from homeassistant.components.alarm_control_panel.const import ( + SUPPORT_ALARM_ARM_AWAY, + SUPPORT_ALARM_ARM_HOME, + SUPPORT_ALARM_ARM_NIGHT, + SUPPORT_ALARM_TRIGGER, +) from homeassistant.const import ( + CONF_CODE, + CONF_DELAY_TIME, + CONF_DISARM_AFTER_TRIGGER, + CONF_NAME, + CONF_PENDING_TIME, + CONF_PLATFORM, + CONF_TRIGGER_TIME, STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT, STATE_ALARM_DISARMED, STATE_ALARM_PENDING, STATE_ALARM_TRIGGERED, - CONF_PLATFORM, - CONF_NAME, - CONF_CODE, - CONF_DELAY_TIME, - CONF_PENDING_TIME, - CONF_TRIGGER_TIME, - CONF_DISARM_AFTER_TRIGGER, ) -from homeassistant.components import mqtt - -from homeassistant.helpers.event import async_track_state_change from homeassistant.core import callback - import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.event import track_point_in_time +from homeassistant.helpers.event import async_track_state_change, track_point_in_time +import homeassistant.util.dt as dt_util _LOGGER = logging.getLogger(__name__) @@ -278,6 +281,16 @@ def state(self): return self._state + @property + def supported_features(self) -> int: + """Return the list of supported features.""" + return ( + SUPPORT_ALARM_ARM_HOME + | SUPPORT_ALARM_ARM_AWAY + | SUPPORT_ALARM_ARM_NIGHT + | SUPPORT_ALARM_TRIGGER + ) + @property def _active_state(self): """Get the current state.""" diff --git a/homeassistant/components/matrix/__init__.py b/homeassistant/components/matrix/__init__.py index 4a9435808bbf..71735bd7e51b 100644 --- a/homeassistant/components/matrix/__init__.py +++ b/homeassistant/components/matrix/__init__.py @@ -1,22 +1,23 @@ """The matrix bot component.""" +from functools import partial import logging import os -from functools import partial +from matrix_client.client import MatrixClient, MatrixRequestError import voluptuous as vol -import homeassistant.helpers.config_validation as cv -from homeassistant.components.notify import ATTR_TARGET, ATTR_MESSAGE +from homeassistant.components.notify import ATTR_MESSAGE, ATTR_TARGET from homeassistant.const import ( - CONF_USERNAME, + CONF_NAME, CONF_PASSWORD, + CONF_USERNAME, CONF_VERIFY_SSL, - CONF_NAME, - EVENT_HOMEASSISTANT_STOP, EVENT_HOMEASSISTANT_START, + EVENT_HOMEASSISTANT_STOP, ) -from homeassistant.util.json import load_json, save_json from homeassistant.exceptions import HomeAssistantError +import homeassistant.helpers.config_validation as cv +from homeassistant.util.json import load_json, save_json _LOGGER = logging.getLogger(__name__) @@ -76,7 +77,6 @@ def setup(hass, config): """Set up the Matrix bot component.""" - from matrix_client.client import MatrixRequestError config = config[DOMAIN] @@ -249,7 +249,6 @@ def _join_or_get_room(self, room_id_or_alias): def _join_rooms(self): """Join the rooms that we listen for commands in.""" - from matrix_client.client import MatrixRequestError for room_id in self._listening_rooms: try: @@ -287,7 +286,6 @@ def _store_auth_token(self, token): def _login(self): """Login to the matrix homeserver and return the client instance.""" - from matrix_client.client import MatrixRequestError # Attempt to generate a valid client using either of the two possible # login methods: @@ -328,7 +326,6 @@ def _login(self): def _login_by_token(self): """Login using authentication token and return the client.""" - from matrix_client.client import MatrixClient return MatrixClient( base_url=self._homeserver, @@ -339,7 +336,6 @@ def _login_by_token(self): def _login_by_password(self): """Login using password authentication and return the client.""" - from matrix_client.client import MatrixClient _client = MatrixClient( base_url=self._homeserver, valid_cert_check=self._verify_tls @@ -353,7 +349,6 @@ def _login_by_password(self): def _send_message(self, message, target_rooms): """Send the message to the matrix server.""" - from matrix_client.client import MatrixRequestError for target_room in target_rooms: try: diff --git a/homeassistant/components/matrix/notify.py b/homeassistant/components/matrix/notify.py index 44a0587ba6d2..06e9712becc1 100644 --- a/homeassistant/components/matrix/notify.py +++ b/homeassistant/components/matrix/notify.py @@ -3,13 +3,13 @@ import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.notify import ( + ATTR_MESSAGE, ATTR_TARGET, PLATFORM_SCHEMA, BaseNotificationService, - ATTR_MESSAGE, ) +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/matrix/services.yaml b/homeassistant/components/matrix/services.yaml index e69de29bb2d1..1cf83de2c332 100644 --- a/homeassistant/components/matrix/services.yaml +++ b/homeassistant/components/matrix/services.yaml @@ -0,0 +1,9 @@ +send_message: + description: Send message to target room(s) + fields: + message: + description: The message to be sent + example: 'This is a message I am sending to matrix' + target: + description: A list of room(s) to send the message to + example: '#hasstest:matrix.org' \ No newline at end of file diff --git a/homeassistant/components/maxcube/__init__.py b/homeassistant/components/maxcube/__init__.py index 65a969bbcb8c..1b65cb161e12 100644 --- a/homeassistant/components/maxcube/__init__.py +++ b/homeassistant/components/maxcube/__init__.py @@ -1,14 +1,16 @@ """Support for the MAX! Cube LAN Gateway.""" import logging -import time from socket import timeout from threading import Lock +import time +from maxcube.connection import MaxCubeConnection +from maxcube.cube import MaxCube import voluptuous as vol +from homeassistant.const import CONF_HOST, CONF_PORT, CONF_SCAN_INTERVAL import homeassistant.helpers.config_validation as cv from homeassistant.helpers.discovery import load_platform -from homeassistant.const import CONF_HOST, CONF_PORT, CONF_SCAN_INTERVAL _LOGGER = logging.getLogger(__name__) @@ -46,8 +48,6 @@ def setup(hass, config): """Establish connection to MAX! Cube.""" - from maxcube.connection import MaxCubeConnection - from maxcube.cube import MaxCube if DATA_KEY not in hass.data: hass.data[DATA_KEY] = {} diff --git a/homeassistant/components/maxcube/climate.py b/homeassistant/components/maxcube/climate.py index e09dfc2d99f4..ff4b219ec21f 100644 --- a/homeassistant/components/maxcube/climate.py +++ b/homeassistant/components/maxcube/climate.py @@ -4,16 +4,16 @@ from maxcube.device import ( MAX_DEVICE_MODE_AUTOMATIC, + MAX_DEVICE_MODE_BOOST, MAX_DEVICE_MODE_MANUAL, MAX_DEVICE_MODE_VACATION, - MAX_DEVICE_MODE_BOOST, ) from homeassistant.components.climate import ClimateDevice from homeassistant.components.climate.const import ( HVAC_MODE_AUTO, - SUPPORT_TARGET_TEMPERATURE, SUPPORT_PRESET_MODE, + SUPPORT_TARGET_TEMPERATURE, ) from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS diff --git a/homeassistant/components/media_extractor/__init__.py b/homeassistant/components/media_extractor/__init__.py index 97cb7d9978a0..7dc05368dcde 100644 --- a/homeassistant/components/media_extractor/__init__.py +++ b/homeassistant/components/media_extractor/__init__.py @@ -2,6 +2,8 @@ import logging import voluptuous as vol +from youtube_dl import YoutubeDL +from youtube_dl.utils import DownloadError, ExtractorError from homeassistant.components.media_player import MEDIA_PLAYER_PLAY_MEDIA_SCHEMA from homeassistant.components.media_player.const import ( @@ -44,7 +46,10 @@ def play_media(call): MediaExtractor(hass, config[DOMAIN], call.data).extract_and_send() hass.services.register( - DOMAIN, SERVICE_PLAY_MEDIA, play_media, schema=MEDIA_PLAYER_PLAY_MEDIA_SCHEMA + DOMAIN, + SERVICE_PLAY_MEDIA, + play_media, + schema=cv.make_entity_service_schema(MEDIA_PLAYER_PLAY_MEDIA_SCHEMA), ) return True @@ -98,9 +103,6 @@ def extract_and_send(self): def get_stream_selector(self): """Return format selector for the media URL.""" - from youtube_dl import YoutubeDL - from youtube_dl.utils import DownloadError, ExtractorError - ydl = YoutubeDL({"quiet": True, "logger": _LOGGER}) try: diff --git a/homeassistant/components/media_extractor/manifest.json b/homeassistant/components/media_extractor/manifest.json index f413ffd16dbc..16f491f0caef 100644 --- a/homeassistant/components/media_extractor/manifest.json +++ b/homeassistant/components/media_extractor/manifest.json @@ -3,7 +3,7 @@ "name": "Media extractor", "documentation": "https://www.home-assistant.io/integrations/media_extractor", "requirements": [ - "youtube_dl==2019.11.05" + "youtube_dl==2019.11.28" ], "dependencies": [ "media_player" diff --git a/homeassistant/components/media_player/.translations/no.json b/homeassistant/components/media_player/.translations/no.json index a05d907774ff..388143b53ec4 100644 --- a/homeassistant/components/media_player/.translations/no.json +++ b/homeassistant/components/media_player/.translations/no.json @@ -5,7 +5,7 @@ "is_off": "{entity_name} er sl\u00e5tt av", "is_on": "{entity_name} er sl\u00e5tt p\u00e5", "is_paused": "{entity_name} er satt p\u00e5 pause", - "is_playing": "{entity_name} spiller" + "is_playing": "{entity_name} spiller n\u00e5" } } } \ No newline at end of file diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py index 98da19fd98ef..a6cd4dda4ead 100644 --- a/homeassistant/components/media_player/__init__.py +++ b/homeassistant/components/media_player/__init__.py @@ -39,8 +39,7 @@ ) from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.config_validation import ( # noqa - ENTITY_SERVICE_SCHEMA, +from homeassistant.helpers.config_validation import ( # noqa: F401 PLATFORM_SCHEMA, PLATFORM_SCHEMA_BASE, ) @@ -123,42 +122,12 @@ DEVICE_CLASSES_SCHEMA = vol.All(vol.Lower, vol.In(DEVICE_CLASSES)) -# Service call validation schemas -MEDIA_PLAYER_SET_VOLUME_SCHEMA = ENTITY_SERVICE_SCHEMA.extend( - {vol.Required(ATTR_MEDIA_VOLUME_LEVEL): cv.small_float} -) - -MEDIA_PLAYER_MUTE_VOLUME_SCHEMA = ENTITY_SERVICE_SCHEMA.extend( - {vol.Required(ATTR_MEDIA_VOLUME_MUTED): cv.boolean} -) - -MEDIA_PLAYER_MEDIA_SEEK_SCHEMA = ENTITY_SERVICE_SCHEMA.extend( - { - vol.Required(ATTR_MEDIA_SEEK_POSITION): vol.All( - vol.Coerce(float), vol.Range(min=0) - ) - } -) - -MEDIA_PLAYER_SELECT_SOURCE_SCHEMA = ENTITY_SERVICE_SCHEMA.extend( - {vol.Required(ATTR_INPUT_SOURCE): cv.string} -) -MEDIA_PLAYER_SELECT_SOUND_MODE_SCHEMA = ENTITY_SERVICE_SCHEMA.extend( - {vol.Required(ATTR_SOUND_MODE): cv.string} -) - -MEDIA_PLAYER_PLAY_MEDIA_SCHEMA = ENTITY_SERVICE_SCHEMA.extend( - { - vol.Required(ATTR_MEDIA_CONTENT_TYPE): cv.string, - vol.Required(ATTR_MEDIA_CONTENT_ID): cv.string, - vol.Optional(ATTR_MEDIA_ENQUEUE): cv.boolean, - } -) - -MEDIA_PLAYER_SET_SHUFFLE_SCHEMA = ENTITY_SERVICE_SCHEMA.extend( - {vol.Required(ATTR_MEDIA_SHUFFLE): cv.boolean} -) +MEDIA_PLAYER_PLAY_MEDIA_SCHEMA = { + vol.Required(ATTR_MEDIA_CONTENT_TYPE): cv.string, + vol.Required(ATTR_MEDIA_CONTENT_ID): cv.string, + vol.Optional(ATTR_MEDIA_ENQUEUE): cv.boolean, +} ATTR_TO_PROPERTY = [ ATTR_MEDIA_VOLUME_LEVEL, @@ -223,65 +192,56 @@ async def async_setup(hass, config): await component.async_setup(config) component.async_register_entity_service( - SERVICE_TURN_ON, ENTITY_SERVICE_SCHEMA, "async_turn_on", [SUPPORT_TURN_ON] + SERVICE_TURN_ON, {}, "async_turn_on", [SUPPORT_TURN_ON] ) component.async_register_entity_service( - SERVICE_TURN_OFF, ENTITY_SERVICE_SCHEMA, "async_turn_off", [SUPPORT_TURN_OFF] + SERVICE_TURN_OFF, {}, "async_turn_off", [SUPPORT_TURN_OFF] ) component.async_register_entity_service( - SERVICE_TOGGLE, - ENTITY_SERVICE_SCHEMA, - "async_toggle", - [SUPPORT_TURN_OFF | SUPPORT_TURN_ON], + SERVICE_TOGGLE, {}, "async_toggle", [SUPPORT_TURN_OFF | SUPPORT_TURN_ON], ) component.async_register_entity_service( SERVICE_VOLUME_UP, - ENTITY_SERVICE_SCHEMA, + {}, "async_volume_up", [SUPPORT_VOLUME_SET, SUPPORT_VOLUME_STEP], ) component.async_register_entity_service( SERVICE_VOLUME_DOWN, - ENTITY_SERVICE_SCHEMA, + {}, "async_volume_down", [SUPPORT_VOLUME_SET, SUPPORT_VOLUME_STEP], ) component.async_register_entity_service( SERVICE_MEDIA_PLAY_PAUSE, - ENTITY_SERVICE_SCHEMA, + {}, "async_media_play_pause", [SUPPORT_PLAY | SUPPORT_PAUSE], ) component.async_register_entity_service( - SERVICE_MEDIA_PLAY, ENTITY_SERVICE_SCHEMA, "async_media_play", [SUPPORT_PLAY] + SERVICE_MEDIA_PLAY, {}, "async_media_play", [SUPPORT_PLAY] ) component.async_register_entity_service( - SERVICE_MEDIA_PAUSE, ENTITY_SERVICE_SCHEMA, "async_media_pause", [SUPPORT_PAUSE] + SERVICE_MEDIA_PAUSE, {}, "async_media_pause", [SUPPORT_PAUSE] ) component.async_register_entity_service( - SERVICE_MEDIA_STOP, ENTITY_SERVICE_SCHEMA, "async_media_stop", [SUPPORT_STOP] + SERVICE_MEDIA_STOP, {}, "async_media_stop", [SUPPORT_STOP] ) component.async_register_entity_service( - SERVICE_MEDIA_NEXT_TRACK, - ENTITY_SERVICE_SCHEMA, - "async_media_next_track", - [SUPPORT_NEXT_TRACK], + SERVICE_MEDIA_NEXT_TRACK, {}, "async_media_next_track", [SUPPORT_NEXT_TRACK], ) component.async_register_entity_service( SERVICE_MEDIA_PREVIOUS_TRACK, - ENTITY_SERVICE_SCHEMA, + {}, "async_media_previous_track", [SUPPORT_PREVIOUS_TRACK], ) component.async_register_entity_service( - SERVICE_CLEAR_PLAYLIST, - ENTITY_SERVICE_SCHEMA, - "async_clear_playlist", - [SUPPORT_CLEAR_PLAYLIST], + SERVICE_CLEAR_PLAYLIST, {}, "async_clear_playlist", [SUPPORT_CLEAR_PLAYLIST], ) component.async_register_entity_service( SERVICE_VOLUME_SET, - MEDIA_PLAYER_SET_VOLUME_SCHEMA, + {vol.Required(ATTR_MEDIA_VOLUME_LEVEL): cv.small_float}, lambda entity, call: entity.async_set_volume_level( volume=call.data[ATTR_MEDIA_VOLUME_LEVEL] ), @@ -289,7 +249,7 @@ async def async_setup(hass, config): ) component.async_register_entity_service( SERVICE_VOLUME_MUTE, - MEDIA_PLAYER_MUTE_VOLUME_SCHEMA, + {vol.Required(ATTR_MEDIA_VOLUME_MUTED): cv.boolean}, lambda entity, call: entity.async_mute_volume( mute=call.data[ATTR_MEDIA_VOLUME_MUTED] ), @@ -297,7 +257,11 @@ async def async_setup(hass, config): ) component.async_register_entity_service( SERVICE_MEDIA_SEEK, - MEDIA_PLAYER_MEDIA_SEEK_SCHEMA, + { + vol.Required(ATTR_MEDIA_SEEK_POSITION): vol.All( + vol.Coerce(float), vol.Range(min=0) + ) + }, lambda entity, call: entity.async_media_seek( position=call.data[ATTR_MEDIA_SEEK_POSITION] ), @@ -305,13 +269,13 @@ async def async_setup(hass, config): ) component.async_register_entity_service( SERVICE_SELECT_SOURCE, - MEDIA_PLAYER_SELECT_SOURCE_SCHEMA, + {vol.Required(ATTR_INPUT_SOURCE): cv.string}, "async_select_source", [SUPPORT_SELECT_SOURCE], ) component.async_register_entity_service( SERVICE_SELECT_SOUND_MODE, - MEDIA_PLAYER_SELECT_SOUND_MODE_SCHEMA, + {vol.Required(ATTR_SOUND_MODE): cv.string}, "async_select_sound_mode", [SUPPORT_SELECT_SOUND_MODE], ) @@ -327,7 +291,7 @@ async def async_setup(hass, config): ) component.async_register_entity_service( SERVICE_SHUFFLE_SET, - MEDIA_PLAYER_SET_SHUFFLE_SCHEMA, + {vol.Required(ATTR_MEDIA_SHUFFLE): cv.boolean}, "async_set_shuffle", [SUPPORT_SHUFFLE_SET], ) diff --git a/homeassistant/components/media_player/services.yaml b/homeassistant/components/media_player/services.yaml index 5421085c3080..ad6b8a78957b 100644 --- a/homeassistant/components/media_player/services.yaml +++ b/homeassistant/components/media_player/services.yaml @@ -107,20 +107,6 @@ media_seek: description: Position to seek to. The format is platform dependent. example: 100 -monoprice_snapshot: - description: Take a snapshot of the media player zone. - fields: - entity_id: - description: Name(s) of entities that will be snapshot. Platform dependent. - example: 'media_player.living_room' - -monoprice_restore: - description: Restore a snapshot of the media player zone. - fields: - entity_id: - description: Name(s) of entities that will be restored. Platform dependent. - example: 'media_player.living_room' - play_media: description: Send the media player the command for playing media. fields: @@ -170,155 +156,3 @@ shuffle_set: shuffle: description: True/false for enabling/disabling shuffle. example: true - -channels_seek_forward: - description: Seek forward by a set number of seconds. - fields: - entity_id: - description: Name of entity for the instance of Channels to seek in. - example: 'media_player.family_room_channels' - -channels_seek_backward: - description: Seek backward by a set number of seconds. - fields: - entity_id: - description: Name of entity for the instance of Channels to seek in. - example: 'media_player.family_room_channels' - -channels_seek_by: - description: Seek by an inputted number of seconds. - fields: - entity_id: - description: Name of entity for the instance of Channels to seek in. - example: 'media_player.family_room_channels' - seconds: - description: Number of seconds to seek by. Negative numbers seek backwards. - example: 120 - -soundtouch_play_everywhere: - description: Play on all Bose Soundtouch devices. - fields: - master: - description: Name of the master entity that will coordinate the grouping. Platform dependent. It is a shortcut for creating a multi-room zone with all devices - example: 'media_player.soundtouch_home' - -soundtouch_create_zone: - description: Create a Sountouch multi-room zone. - fields: - master: - description: Name of the master entity that will coordinate the multi-room zone. Platform dependent. - example: 'media_player.soundtouch_home' - slaves: - description: Name of slaves entities to add to the new zone. - example: 'media_player.soundtouch_bedroom' - -soundtouch_add_zone_slave: - description: Add a slave to a Sountouch multi-room zone. - fields: - master: - description: Name of the master entity that is coordinating the multi-room zone. Platform dependent. - example: 'media_player.soundtouch_home' - slaves: - description: Name of slaves entities to add to the existing zone. - example: 'media_player.soundtouch_bedroom' - -soundtouch_remove_zone_slave: - description: Remove a slave from the Sounttouch multi-room zone. - fields: - master: - description: Name of the master entity that is coordinating the multi-room zone. Platform dependent. - example: 'media_player.soundtouch_home' - slaves: - description: Name of slaves entities to remove from the existing zone. - example: 'media_player.soundtouch_bedroom' - -squeezebox_call_method: - description: 'Call a Squeezebox JSON/RPC API method.' - fields: - entity_id: - description: Name(s) of the Squeexebox entities where to run the API method. - example: 'media_player.squeezebox_radio' - command: - description: Name of the Squeezebox command. - example: 'playlist' - parameters: - description: Optional array of parameters to be appended to the command. See 'Command Line Interface' official help page from Logitech for details. - example: '["loadtracks", "track.titlesearch=highway to hell"]' - -yamaha_enable_output: - description: Enable or disable an output port - fields: - entity_id: - description: Name(s) of entites to enable/disable port on. - example: 'media_player.yamaha' - port: - description: Name of port to enable/disable. - example: 'hdmi1' - enabled: - description: Boolean indicating if port should be enabled or not. - example: true - -bluesound_join: - description: Group player together. - fields: - master: - description: Entity ID of the player that should become the master of the group. - example: 'media_player.bluesound_livingroom' - entity_id: - description: Name(s) of entities that will coordinate the grouping. Platform dependent. - example: 'media_player.bluesound_livingroom' - -bluesound_unjoin: - description: Unjoin the player from a group. - fields: - entity_id: - description: Name(s) of entities that will be unjoined from their group. Platform dependent. - example: 'media_player.bluesound_livingroom' - -bluesound_set_sleep_timer: - description: "Set a Bluesound timer. It will increase timer in steps: 15, 30, 45, 60, 90, 0" - fields: - entity_id: - description: Name(s) of entities that will have a timer set. - example: 'media_player.bluesound_livingroom' - -bluesound_clear_sleep_timer: - description: Clear a Bluesound timer. - fields: - entity_id: - description: Name(s) of entities that will have the timer cleared. - example: 'media_player.bluesound_livingroom' - -songpal_set_sound_setting: - description: Change sound setting. - - fields: - entity_id: - description: Target device. - example: 'media_player.my_soundbar' - name: - description: Name of the setting. - example: 'nightMode' - value: - description: Value to set. - example: 'on' - -blackbird_set_all_zones: - description: Set all Blackbird zones to a single source. - fields: - entity_id: - description: Name of any blackbird zone. - example: 'media_player.zone_1' - source: - description: Name of source to switch to. - example: 'Source 1' - -epson_select_cmode: - description: Select Color mode of Epson projector - fields: - entity_id: - description: Name of projector - example: 'media_player.epson_projector' - cmode: - description: Name of Cmode - example: 'cinema' diff --git a/homeassistant/components/met/.translations/bg.json b/homeassistant/components/met/.translations/bg.json index aabb1aeda3f4..aa85bed1d130 100644 --- a/homeassistant/components/met/.translations/bg.json +++ b/homeassistant/components/met/.translations/bg.json @@ -1,7 +1,7 @@ { "config": { "error": { - "name_exists": "\u0418\u043c\u0435\u0442\u043e \u0432\u0435\u0447\u0435 \u0441\u044a\u0449\u0435\u0441\u0442\u0432\u0443\u0432\u0430" + "name_exists": "\u041c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435\u0442\u043e \u0432\u0435\u0447\u0435 \u0441\u044a\u0449\u0435\u0441\u0442\u0432\u0443\u0432\u0430" }, "step": { "user": { diff --git a/homeassistant/components/met/.translations/pt.json b/homeassistant/components/met/.translations/pt.json index c7081cd694a0..2ba2911d8904 100644 --- a/homeassistant/components/met/.translations/pt.json +++ b/homeassistant/components/met/.translations/pt.json @@ -1,5 +1,8 @@ { "config": { + "error": { + "name_exists": "A localiza\u00e7\u00e3o j\u00e1 existe" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/met/__init__.py b/homeassistant/components/met/__init__.py index 63b95b03821a..305038c3e6a6 100644 --- a/homeassistant/components/met/__init__.py +++ b/homeassistant/components/met/__init__.py @@ -1,7 +1,7 @@ """The met component.""" from homeassistant.core import Config, HomeAssistant -from .config_flow import MetFlowHandler # noqa -from .const import DOMAIN # noqa +from .config_flow import MetFlowHandler # noqa: F401 +from .const import DOMAIN # noqa: F401 async def async_setup(hass: HomeAssistant, config: Config) -> bool: diff --git a/homeassistant/components/meteo_france/__init__.py b/homeassistant/components/meteo_france/__init__.py index cfcd78400bd5..73b8dbb0e396 100644 --- a/homeassistant/components/meteo_france/__init__.py +++ b/homeassistant/components/meteo_france/__init__.py @@ -2,6 +2,8 @@ import datetime import logging +from meteofrance.client import meteofranceClient, meteofranceError +from vigilancemeteo import VigilanceMeteoError, VigilanceMeteoFranceProxy import voluptuous as vol from homeassistant.const import CONF_MONITORED_CONDITIONS @@ -9,7 +11,7 @@ from homeassistant.helpers.discovery import load_platform from homeassistant.util import Throttle -from .const import DOMAIN, CONF_CITY, SENSOR_TYPES, DATA_METEO_FRANCE +from .const import CONF_CITY, DATA_METEO_FRANCE, DOMAIN, SENSOR_TYPES _LOGGER = logging.getLogger(__name__) @@ -61,7 +63,6 @@ def setup(hass, config): # all weather_alert entities. if need_weather_alert_watcher: _LOGGER.debug("Weather Alert monitoring expected. Loading vigilancemeteo") - from vigilancemeteo import VigilanceMeteoFranceProxy, VigilanceMeteoError weather_alert_client = VigilanceMeteoFranceProxy() try: @@ -79,8 +80,6 @@ def setup(hass, config): city = location[CONF_CITY] - from meteofrance.client import meteofranceClient, meteofranceError - try: client = meteofranceClient(city) except meteofranceError as exp: @@ -127,7 +126,6 @@ def get_data(self): @Throttle(SCAN_INTERVAL) def update(self): """Get the latest data from Meteo-France.""" - from meteofrance.client import meteofranceError try: self._client.update() diff --git a/homeassistant/components/meteo_france/sensor.py b/homeassistant/components/meteo_france/sensor.py index 8c2bd32048fc..f0c08ac18220 100644 --- a/homeassistant/components/meteo_france/sensor.py +++ b/homeassistant/components/meteo_france/sensor.py @@ -1,6 +1,8 @@ """Support for Meteo-France raining forecast sensor.""" import logging +from vigilancemeteo import DepartmentWeatherAlert + from homeassistant.const import ATTR_ATTRIBUTION, CONF_MONITORED_CONDITIONS from homeassistant.helpers.entity import Entity @@ -8,11 +10,11 @@ ATTRIBUTION, CONF_CITY, DATA_METEO_FRANCE, - SENSOR_TYPES, + SENSOR_TYPE_CLASS, SENSOR_TYPE_ICON, SENSOR_TYPE_NAME, SENSOR_TYPE_UNIT, - SENSOR_TYPE_CLASS, + SENSOR_TYPES, ) _LOGGER = logging.getLogger(__name__) @@ -31,8 +33,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): client = hass.data[DATA_METEO_FRANCE][city] weather_alert_client = hass.data[DATA_METEO_FRANCE]["weather_alert_client"] - from vigilancemeteo import DepartmentWeatherAlert - alert_watcher = None if "weather_alert" in monitored_conditions: datas = hass.data[DATA_METEO_FRANCE][city].get_data() diff --git a/homeassistant/components/meteo_france/weather.py b/homeassistant/components/meteo_france/weather.py index 00da55809ffb..c96080808e97 100644 --- a/homeassistant/components/meteo_france/weather.py +++ b/homeassistant/components/meteo_france/weather.py @@ -9,8 +9,8 @@ ATTR_FORECAST_TIME, WeatherEntity, ) -import homeassistant.util.dt as dt_util from homeassistant.const import TEMP_CELSIUS +import homeassistant.util.dt as dt_util from .const import ATTRIBUTION, CONDITION_CLASSES, CONF_CITY, DATA_METEO_FRANCE diff --git a/homeassistant/components/meteoalarm/binary_sensor.py b/homeassistant/components/meteoalarm/binary_sensor.py index 55041f59cf23..7d3bea4c9959 100644 --- a/homeassistant/components/meteoalarm/binary_sensor.py +++ b/homeassistant/components/meteoalarm/binary_sensor.py @@ -2,6 +2,7 @@ from datetime import timedelta import logging +from meteoalertapi import Meteoalert import voluptuous as vol from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorDevice @@ -33,7 +34,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the MeteoAlarm binary sensor platform.""" - from meteoalertapi import Meteoalert country = config[CONF_COUNTRY] province = config[CONF_PROVINCE] diff --git a/homeassistant/components/mfi/sensor.py b/homeassistant/components/mfi/sensor.py index 5d9b3be738a6..671a52bbf016 100644 --- a/homeassistant/components/mfi/sensor.py +++ b/homeassistant/components/mfi/sensor.py @@ -1,23 +1,24 @@ """Support for Ubiquiti mFi sensors.""" import logging +from mficlient.client import FailedToLogin, MFiClient import requests import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( - CONF_PASSWORD, - CONF_USERNAME, - TEMP_CELSIUS, - STATE_ON, - STATE_OFF, CONF_HOST, + CONF_PASSWORD, + CONF_PORT, CONF_SSL, + CONF_USERNAME, CONF_VERIFY_SSL, - CONF_PORT, + STATE_OFF, + STATE_ON, + TEMP_CELSIUS, ) -from homeassistant.helpers.entity import Entity import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -57,8 +58,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): default_port = 6443 if use_tls else 6080 port = int(config.get(CONF_PORT, default_port)) - from mficlient.client import FailedToLogin, MFiClient - try: client = MFiClient( host, username, password, port=port, use_tls=use_tls, verify=verify_tls diff --git a/homeassistant/components/mfi/switch.py b/homeassistant/components/mfi/switch.py index 1da09e7f78cd..00cb23a102ef 100644 --- a/homeassistant/components/mfi/switch.py +++ b/homeassistant/components/mfi/switch.py @@ -1,16 +1,17 @@ """Support for Ubiquiti mFi switches.""" import logging +from mficlient.client import FailedToLogin, MFiClient import requests import voluptuous as vol -from homeassistant.components.switch import SwitchDevice, PLATFORM_SCHEMA +from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchDevice from homeassistant.const import ( CONF_HOST, - CONF_PORT, CONF_PASSWORD, - CONF_USERNAME, + CONF_PORT, CONF_SSL, + CONF_USERNAME, CONF_VERIFY_SSL, ) import homeassistant.helpers.config_validation as cv @@ -44,8 +45,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): default_port = 6443 if use_tls else 6080 port = int(config.get(CONF_PORT, default_port)) - from mficlient.client import FailedToLogin, MFiClient - try: client = MFiClient( host, username, password, port=port, use_tls=use_tls, verify=verify_tls diff --git a/homeassistant/components/mhz19/sensor.py b/homeassistant/components/mhz19/sensor.py index 460decd41b67..aedd5ea9b092 100644 --- a/homeassistant/components/mhz19/sensor.py +++ b/homeassistant/components/mhz19/sensor.py @@ -1,20 +1,21 @@ """Support for CO2 sensor connected to a serial port.""" -import logging from datetime import timedelta +import logging +from pmsensor import co2sensor import voluptuous as vol +from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( ATTR_TEMPERATURE, - CONF_NAME, CONF_MONITORED_CONDITIONS, + CONF_NAME, TEMP_FAHRENHEIT, ) -from homeassistant.helpers.entity import Entity import homeassistant.helpers.config_validation as cv -from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.util.temperature import celsius_to_fahrenheit +from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle +from homeassistant.util.temperature import celsius_to_fahrenheit _LOGGER = logging.getLogger(__name__) @@ -41,7 +42,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the available CO2 sensors.""" - from pmsensor import co2sensor try: co2sensor.read_mh_z19(config.get(CONF_SERIAL_DEVICE)) @@ -116,9 +116,9 @@ def device_state_attributes(self): class MHZClient: """Get the latest data from the MH-Z sensor.""" - def __init__(self, co2sensor, serial): + def __init__(self, co2sens, serial): """Initialize the sensor.""" - self.co2sensor = co2sensor + self.co2sensor = co2sens self._serial = serial self.data = dict() diff --git a/homeassistant/components/microsoft/tts.py b/homeassistant/components/microsoft/tts.py index 447d2a4d46a3..074605e07fe5 100644 --- a/homeassistant/components/microsoft/tts.py +++ b/homeassistant/components/microsoft/tts.py @@ -2,6 +2,7 @@ from http.client import HTTPException import logging +from pycsspeechtts import pycsspeechtts import voluptuous as vol from homeassistant.components.tts import CONF_LANG, PLATFORM_SCHEMA, Provider @@ -142,7 +143,6 @@ def get_tts_audio(self, message, language, options=None): """Load TTS from Microsoft.""" if language is None: language = self._lang - from pycsspeechtts import pycsspeechtts try: trans = pycsspeechtts.TTSTranslator(self._apikey, self._region) diff --git a/homeassistant/components/miflora/sensor.py b/homeassistant/components/miflora/sensor.py index a08c4ce5eacb..815a6e97bb8c 100644 --- a/homeassistant/components/miflora/sensor.py +++ b/homeassistant/components/miflora/sensor.py @@ -106,6 +106,7 @@ def __init__(self, poller, parameter, name, unit, icon, force_update, median): self._icon = icon self._name = name self._state = None + self._available = False self.data = [] self._force_update = force_update # Median is used to filter out outliers. median of 3 will filter @@ -132,6 +133,11 @@ def state(self): """Return the state of the sensor.""" return self._state + @property + def available(self): + """Return True if entity is available.""" + return self._available + @property def unit_of_measurement(self): """Return the units of measurement.""" @@ -156,15 +162,14 @@ def update(self): try: _LOGGER.debug("Polling data for %s", self.name) data = self.poller.parameter_value(self.parameter) - except OSError as ioerr: - _LOGGER.info("Polling error %s", ioerr) - return - except BluetoothBackendException as bterror: - _LOGGER.info("Polling error %s", bterror) + except (OSError, BluetoothBackendException) as err: + _LOGGER.info("Polling error %s: %s", type(err).__name__, err) + self._available = False return if data is not None: _LOGGER.debug("%s = %s", self.name, data) + self._available = True self.data.append(data) else: _LOGGER.info("Did not receive any data from Mi Flora sensor %s", self.name) diff --git a/homeassistant/components/mill/climate.py b/homeassistant/components/mill/climate.py index 1166bd451c0c..b08015fe5480 100644 --- a/homeassistant/components/mill/climate.py +++ b/homeassistant/components/mill/climate.py @@ -6,7 +6,6 @@ from homeassistant.components.climate import PLATFORM_SCHEMA, ClimateDevice from homeassistant.components.climate.const import ( - DOMAIN, HVAC_MODE_HEAT, HVAC_MODE_OFF, SUPPORT_FAN_MODE, @@ -22,15 +21,18 @@ from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_get_clientsession -_LOGGER = logging.getLogger(__name__) +from .const import ( + ATTR_AWAY_TEMP, + ATTR_COMFORT_TEMP, + ATTR_ROOM_NAME, + ATTR_SLEEP_TEMP, + DOMAIN, + MAX_TEMP, + MIN_TEMP, + SERVICE_SET_ROOM_TEMP, +) -ATTR_AWAY_TEMP = "away_temp" -ATTR_COMFORT_TEMP = "comfort_temp" -ATTR_ROOM_NAME = "room_name" -ATTR_SLEEP_TEMP = "sleep_temp" -MAX_TEMP = 35 -MIN_TEMP = 5 -SERVICE_SET_ROOM_TEMP = "mill_set_room_temperature" +_LOGGER = logging.getLogger(__name__) SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE | SUPPORT_FAN_MODE diff --git a/homeassistant/components/mill/const.py b/homeassistant/components/mill/const.py new file mode 100644 index 000000000000..65c67b72b6e6 --- /dev/null +++ b/homeassistant/components/mill/const.py @@ -0,0 +1,10 @@ +"""Constants for the Mill heater component.""" + +ATTR_AWAY_TEMP = "away_temp" +ATTR_COMFORT_TEMP = "comfort_temp" +ATTR_ROOM_NAME = "room_name" +ATTR_SLEEP_TEMP = "sleep_temp" +MAX_TEMP = 35 +MIN_TEMP = 5 +DOMAIN = "mill" +SERVICE_SET_ROOM_TEMP = "set_room_temperature" diff --git a/homeassistant/components/mill/services.yaml b/homeassistant/components/mill/services.yaml index e69de29bb2d1..e9e59b50170f 100644 --- a/homeassistant/components/mill/services.yaml +++ b/homeassistant/components/mill/services.yaml @@ -0,0 +1,15 @@ +set_room_temperature: + description: Set Mill room temperatures. + fields: + room_name: + description: Name of room to change. + example: 'kitchen' + away_temp: + description: Away temp. + example: 12 + comfort_temp: + description: Comfort temp. + example: 22 + sleep_temp: + description: Sleep temp. + example: 17 \ No newline at end of file diff --git a/homeassistant/components/mitemp_bt/manifest.json b/homeassistant/components/mitemp_bt/manifest.json index 612e7c19f8bc..0f8da91ffd09 100644 --- a/homeassistant/components/mitemp_bt/manifest.json +++ b/homeassistant/components/mitemp_bt/manifest.json @@ -3,7 +3,7 @@ "name": "Mitemp bt", "documentation": "https://www.home-assistant.io/integrations/mitemp_bt", "requirements": [ - "mitemp_bt==0.0.1" + "mitemp_bt==0.0.3" ], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/mobile_app/.translations/pl.json b/homeassistant/components/mobile_app/.translations/pl.json index feb00c20779d..5fa53384ff08 100644 --- a/homeassistant/components/mobile_app/.translations/pl.json +++ b/homeassistant/components/mobile_app/.translations/pl.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "install_app": "Otw\u00f3rz aplikacj\u0119 mobiln\u0105, aby skonfigurowa\u0107 integracj\u0119 z Home Assistant. Zapoznaj si\u0119 z [dokumentacj\u0105] ({apps_url}), by zobaczy\u0107 list\u0119 kompatybilnych aplikacji." + "install_app": "Otw\u00f3rz aplikacj\u0119 mobiln\u0105, aby skonfigurowa\u0107 integracj\u0119 z Home Assistant. Zapoznaj si\u0119 z [dokumentacj\u0105]({apps_url}), by zobaczy\u0107 list\u0119 kompatybilnych aplikacji." }, "step": { "confirm": { diff --git a/homeassistant/components/mobile_app/__init__.py b/homeassistant/components/mobile_app/__init__.py index ca2a58d1f961..56594f3e2c30 100644 --- a/homeassistant/components/mobile_app/__init__.py +++ b/homeassistant/components/mobile_app/__init__.py @@ -76,14 +76,9 @@ async def async_setup_entry(hass, entry): device_registry = await dr.async_get_registry(hass) - identifiers = { - (ATTR_DEVICE_ID, registration[ATTR_DEVICE_ID]), - (CONF_WEBHOOK_ID, registration[CONF_WEBHOOK_ID]), - } - device = device_registry.async_get_or_create( config_entry_id=entry.entry_id, - identifiers=identifiers, + identifiers={(DOMAIN, registration[ATTR_DEVICE_ID])}, manufacturer=registration[ATTR_MANUFACTURER], model=registration[ATTR_MODEL], name=registration[ATTR_DEVICE_NAME], diff --git a/homeassistant/components/mobile_app/const.py b/homeassistant/components/mobile_app/const.py index 0b6a93a39ea4..318076d5fd93 100644 --- a/homeassistant/components/mobile_app/const.py +++ b/homeassistant/components/mobile_app/const.py @@ -160,7 +160,7 @@ vol.Required(ATTR_GPS_ACCURACY): cv.positive_int, vol.Optional(ATTR_BATTERY): cv.positive_int, vol.Optional(ATTR_SPEED): cv.positive_int, - vol.Optional(ATTR_ALTITUDE): cv.positive_int, + vol.Optional(ATTR_ALTITUDE): vol.Coerce(float), vol.Optional(ATTR_COURSE): cv.positive_int, vol.Optional(ATTR_VERTICAL_ACCURACY): cv.positive_int, } diff --git a/homeassistant/components/mobile_app/helpers.py b/homeassistant/components/mobile_app/helpers.py index 2fb949720d63..cad25f371dda 100644 --- a/homeassistant/components/mobile_app/helpers.py +++ b/homeassistant/components/mobile_app/helpers.py @@ -111,7 +111,7 @@ def error_response( def supports_encryption() -> bool: """Test if we support encryption.""" try: - import nacl # noqa pylint: disable=unused-import + import nacl # noqa: F401 pylint: disable=unused-import return True except OSError: diff --git a/homeassistant/components/mobile_app/http_api.py b/homeassistant/components/mobile_app/http_api.py index ee69f15fb11e..11ca39e8b600 100644 --- a/homeassistant/components/mobile_app/http_api.py +++ b/homeassistant/components/mobile_app/http_api.py @@ -6,11 +6,7 @@ from nacl.secret import SecretBox from homeassistant.auth.util import generate_secret -from homeassistant.components.cloud import ( - CloudNotAvailable, - async_create_cloudhook, - async_remote_ui_url, -) + from homeassistant.components.http import HomeAssistantView from homeassistant.components.http.data_validator import RequestDataValidator from homeassistant.const import CONF_WEBHOOK_ID, HTTP_CREATED @@ -41,8 +37,12 @@ async def post(self, request: Request, data: Dict) -> Response: webhook_id = generate_secret() - if hass.components.cloud.async_active_subscription(): - data[CONF_CLOUDHOOK_URL] = await async_create_cloudhook(hass, webhook_id) + cloud_loaded = "cloud" in hass.config.components + + if cloud_loaded and hass.components.cloud.async_active_subscription(): + data[ + CONF_CLOUDHOOK_URL + ] = await hass.components.cloud.async_create_cloudhook(webhook_id) data[ATTR_DEVICE_ID] = str(uuid.uuid4()).replace("-", "") @@ -59,10 +59,11 @@ async def post(self, request: Request, data: Dict) -> Response: ) remote_ui_url = None - try: - remote_ui_url = async_remote_ui_url(hass) - except CloudNotAvailable: - pass + if cloud_loaded: + try: + remote_ui_url = hass.components.cloud.async_remote_ui_url() + except hass.components.cloud.CloudNotAvailable: + pass return self.json( { diff --git a/homeassistant/components/mobile_app/manifest.json b/homeassistant/components/mobile_app/manifest.json index ab140b4148e4..29ee35e002c0 100644 --- a/homeassistant/components/mobile_app/manifest.json +++ b/homeassistant/components/mobile_app/manifest.json @@ -3,15 +3,7 @@ "name": "Home Assistant Mobile App Support", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/mobile_app", - "requirements": [ - "PyNaCl==1.3.0" - ], - "dependencies": [ - "cloud", - "http", - "webhook" - ], - "codeowners": [ - "@robbiet480" - ] + "requirements": ["PyNaCl==1.3.0"], + "dependencies": ["http", "webhook"], + "codeowners": ["@robbiet480"] } diff --git a/homeassistant/components/mobile_app/webhook.py b/homeassistant/components/mobile_app/webhook.py index 66188500fd6a..98687e6658fd 100644 --- a/homeassistant/components/mobile_app/webhook.py +++ b/homeassistant/components/mobile_app/webhook.py @@ -4,7 +4,6 @@ from aiohttp.web import HTTPBadRequest, Request, Response import voluptuous as vol -from homeassistant.components.cloud import CloudNotAvailable, async_remote_ui_url from homeassistant.components.frontend import MANIFEST_JSON from homeassistant.components.zone.const import DOMAIN as ZONE_DOMAIN from homeassistant.const import ( @@ -148,7 +147,6 @@ async def handle_webhook( blocking=True, context=context, ) - # noqa: E722 pylint: disable=broad-except except (vol.Invalid, ServiceNotFound, Exception) as ex: _LOGGER.error( "Error when calling service during mobile_app " @@ -174,7 +172,6 @@ async def handle_webhook( tpl = item[ATTR_TEMPLATE] attach(hass, tpl) resp[key] = tpl.async_render(item.get(ATTR_TEMPLATE_VARIABLES)) - # noqa: E722 pylint: disable=broad-except except TemplateError as ex: resp[key] = {"error": str(ex)} @@ -312,9 +309,10 @@ async def handle_webhook( if CONF_CLOUDHOOK_URL in registration: resp[CONF_CLOUDHOOK_URL] = registration[CONF_CLOUDHOOK_URL] - try: - resp[CONF_REMOTE_UI_URL] = async_remote_ui_url(hass) - except CloudNotAvailable: - pass + if "cloud" in hass.config.components: + try: + resp[CONF_REMOTE_UI_URL] = hass.components.cloud.async_remote_ui_url() + except hass.components.cloud.CloudNotAvailable: + pass return webhook_response(resp, registration=registration, headers=headers) diff --git a/homeassistant/components/mobile_app/websocket_api.py b/homeassistant/components/mobile_app/websocket_api.py index 813d0a9cf893..bc5305c36fa6 100644 --- a/homeassistant/components/mobile_app/websocket_api.py +++ b/homeassistant/components/mobile_app/websocket_api.py @@ -1,7 +1,6 @@ """Websocket API for mobile_app.""" import voluptuous as vol -from homeassistant.components.cloud import async_delete_cloudhook from homeassistant.components.websocket_api import ( ActiveConnection, async_register_command, @@ -117,6 +116,6 @@ async def websocket_delete_registration( return error_message(msg["id"], "internal_error", "Error deleting registration") if CONF_CLOUDHOOK_URL in registration and "cloud" in hass.config.components: - await async_delete_cloudhook(hass, webhook_id) + await hass.components.cloud.async_delete_cloudhook(webhook_id) connection.send_message(result_message(msg["id"], "ok")) diff --git a/homeassistant/components/modbus/climate.py b/homeassistant/components/modbus/climate.py index 64b45b03c959..c6764482d962 100644 --- a/homeassistant/components/modbus/climate.py +++ b/homeassistant/components/modbus/climate.py @@ -7,9 +7,15 @@ from homeassistant.components.climate import PLATFORM_SCHEMA, ClimateDevice from homeassistant.components.climate.const import ( SUPPORT_TARGET_TEMPERATURE, - HVAC_MODE_HEAT, + HVAC_MODE_AUTO, +) +from homeassistant.const import ( + ATTR_TEMPERATURE, + CONF_NAME, + CONF_SLAVE, + TEMP_CELSIUS, + TEMP_FAHRENHEIT, ) -from homeassistant.const import ATTR_TEMPERATURE, CONF_NAME, CONF_SLAVE import homeassistant.helpers.config_validation as cv from . import CONF_HUB, DEFAULT_HUB, DOMAIN as MODBUS_DOMAIN @@ -21,12 +27,17 @@ CONF_DATA_TYPE = "data_type" CONF_COUNT = "data_count" CONF_PRECISION = "precision" - +CONF_SCALE = "scale" +CONF_OFFSET = "offset" +CONF_UNIT = "temperature_unit" DATA_TYPE_INT = "int" DATA_TYPE_UINT = "uint" DATA_TYPE_FLOAT = "float" +CONF_MAX_TEMP = "max_temp" +CONF_MIN_TEMP = "min_temp" +CONF_STEP = "temp_step" SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE -HVAC_MODES = [HVAC_MODE_HEAT] +HVAC_MODES = [HVAC_MODE_AUTO] PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { @@ -40,6 +51,12 @@ ), vol.Optional(CONF_HUB, default=DEFAULT_HUB): cv.string, vol.Optional(CONF_PRECISION, default=1): cv.positive_int, + vol.Optional(CONF_SCALE, default=1): vol.Coerce(float), + vol.Optional(CONF_OFFSET, default=0): vol.Coerce(float), + vol.Optional(CONF_MAX_TEMP, default=5): cv.positive_int, + vol.Optional(CONF_MIN_TEMP, default=35): cv.positive_int, + vol.Optional(CONF_STEP, default=0.5): vol.Coerce(float), + vol.Optional(CONF_UNIT, default="C"): cv.string, } ) @@ -53,6 +70,12 @@ def setup_platform(hass, config, add_entities, discovery_info=None): data_type = config.get(CONF_DATA_TYPE) count = config.get(CONF_COUNT) precision = config.get(CONF_PRECISION) + scale = config.get(CONF_SCALE) + offset = config.get(CONF_OFFSET) + unit = config.get(CONF_UNIT) + max_temp = config.get(CONF_MAX_TEMP) + min_temp = config.get(CONF_MIN_TEMP) + temp_step = config.get(CONF_STEP) hub_name = config.get(CONF_HUB) hub = hass.data[MODBUS_DOMAIN][hub_name] @@ -67,6 +90,12 @@ def setup_platform(hass, config, add_entities, discovery_info=None): data_type, count, precision, + scale, + offset, + unit, + max_temp, + min_temp, + temp_step, ) ], True, @@ -86,6 +115,12 @@ def __init__( data_type, count, precision, + scale, + offset, + unit, + max_temp, + min_temp, + temp_step, ): """Initialize the unit.""" self._hub = hub @@ -98,6 +133,12 @@ def __init__( self._data_type = data_type self._count = int(count) self._precision = precision + self._scale = scale + self._offset = offset + self._unit = unit + self._max_temp = max_temp + self._min_temp = min_temp + self._temp_step = temp_step self._structure = ">f" data_types = { @@ -123,7 +164,7 @@ def update(self): @property def hvac_mode(self): """Return the current HVAC mode.""" - return HVAC_MODE_HEAT + return HVAC_MODE_AUTO @property def hvac_modes(self): @@ -145,9 +186,31 @@ def target_temperature(self): """Return the target temperature.""" return self._target_temperature + @property + def temperature_unit(self): + """Return the unit of measurement.""" + return TEMP_FAHRENHEIT if self._unit == "F" else TEMP_CELSIUS + + @property + def min_temp(self): + """Return the minimum temperature.""" + return self._min_temp + + @property + def max_temp(self): + """Return the maximum temperature.""" + return self._max_temp + + @property + def target_temperature_step(self): + """Return the supported step of target temperature.""" + return self._temp_step + def set_temperature(self, **kwargs): """Set new target temperature.""" - target_temperature = kwargs.get(ATTR_TEMPERATURE) + target_temperature = int( + (kwargs.get(ATTR_TEMPERATURE) - self._offset) / self._scale + ) if target_temperature is None: return byte_string = struct.pack(self._structure, target_temperature) @@ -170,7 +233,10 @@ def read_register(self, register): [x.to_bytes(2, byteorder="big") for x in result.registers] ) val = struct.unpack(self._structure, byte_string)[0] - register_value = format(val, f".{self._precision}f") + register_value = format( + (self._scale * val) + self._offset, f".{self._precision}f" + ) + register_value = float(register_value) return register_value def write_register(self, register, value): diff --git a/homeassistant/components/modbus/manifest.json b/homeassistant/components/modbus/manifest.json index 8d271d5a95f1..356a9f6a9c00 100644 --- a/homeassistant/components/modbus/manifest.json +++ b/homeassistant/components/modbus/manifest.json @@ -6,5 +6,7 @@ "pymodbus==1.5.2" ], "dependencies": [], - "codeowners": [] + "codeowners": [ + "@adamchengtkc" + ] } diff --git a/homeassistant/components/modem_callerid/sensor.py b/homeassistant/components/modem_callerid/sensor.py index 7acb345e27e4..7ffda3e61243 100644 --- a/homeassistant/components/modem_callerid/sensor.py +++ b/homeassistant/components/modem_callerid/sensor.py @@ -1,15 +1,18 @@ """A sensor for incoming calls using a USB modem that supports caller ID.""" import logging + +from basicmodem.basicmodem import BasicModem as bm import voluptuous as vol + +from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( - STATE_IDLE, - EVENT_HOMEASSISTANT_STOP, - CONF_NAME, CONF_DEVICE, + CONF_NAME, + EVENT_HOMEASSISTANT_STOP, + STATE_IDLE, ) -from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.helpers.entity import Entity import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) DEFAULT_NAME = "Modem CallerID" @@ -29,7 +32,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up modem caller ID sensor platform.""" - from basicmodem.basicmodem import BasicModem as bm name = config.get(CONF_NAME) port = config.get(CONF_DEVICE) diff --git a/homeassistant/components/monoprice/const.py b/homeassistant/components/monoprice/const.py new file mode 100644 index 000000000000..e8d813d25294 --- /dev/null +++ b/homeassistant/components/monoprice/const.py @@ -0,0 +1,5 @@ +"""Constants for the Monoprice 6-Zone Amplifier Media Player component.""" + +DOMAIN = "monoprice" +SERVICE_SNAPSHOT = "snapshot" +SERVICE_RESTORE = "restore" diff --git a/homeassistant/components/monoprice/media_player.py b/homeassistant/components/monoprice/media_player.py index 5f2af33e23a2..1b1d9d2adf47 100644 --- a/homeassistant/components/monoprice/media_player.py +++ b/homeassistant/components/monoprice/media_player.py @@ -5,7 +5,6 @@ from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA from homeassistant.components.media_player.const import ( - DOMAIN, SUPPORT_SELECT_SOURCE, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, @@ -21,6 +20,7 @@ STATE_ON, ) import homeassistant.helpers.config_validation as cv +from .const import DOMAIN, SERVICE_RESTORE, SERVICE_SNAPSHOT _LOGGER = logging.getLogger(__name__) @@ -42,9 +42,6 @@ DATA_MONOPRICE = "monoprice" -SERVICE_SNAPSHOT = "snapshot" -SERVICE_RESTORE = "restore" - # Valid zone ids: 11-16 or 21-26 or 31-36 ZONE_IDS = vol.All( vol.Coerce(int), diff --git a/homeassistant/components/monoprice/services.yaml b/homeassistant/components/monoprice/services.yaml index e69de29bb2d1..420270e10aca 100644 --- a/homeassistant/components/monoprice/services.yaml +++ b/homeassistant/components/monoprice/services.yaml @@ -0,0 +1,13 @@ +snapshot: + description: Take a snapshot of the media player zone. + fields: + entity_id: + description: Name(s) of entities that will be snapshot. Platform dependent. + example: 'media_player.living_room' + +restore: + description: Restore a snapshot of the media player zone. + fields: + entity_id: + description: Name(s) of entities that will be restored. Platform dependent. + example: 'media_player.living_room' diff --git a/homeassistant/components/moon/sensor.py b/homeassistant/components/moon/sensor.py index 39247b096cce..3a7dd9e20844 100644 --- a/homeassistant/components/moon/sensor.py +++ b/homeassistant/components/moon/sensor.py @@ -1,19 +1,38 @@ """Support for tracking the moon phases.""" import logging +from astral import Astral import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import CONF_NAME -import homeassistant.util.dt as dt_util -from homeassistant.helpers.entity import Entity import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity +import homeassistant.util.dt as dt_util _LOGGER = logging.getLogger(__name__) DEFAULT_NAME = "Moon" -ICON = "mdi:brightness-3" +STATE_FIRST_QUARTER = "first_quarter" +STATE_FULL_MOON = "full_moon" +STATE_LAST_QUARTER = "last_quarter" +STATE_NEW_MOON = "new_moon" +STATE_WANING_CRESCENT = "waning_crescent" +STATE_WANING_GIBBOUS = "waning_gibbous" +STATE_WAXING_GIBBOUS = "waxing_gibbous" +STATE_WAXING_CRESCENT = "waxing_crescent" + +MOON_ICONS = { + STATE_FIRST_QUARTER: "mdi:moon-first-quarter", + STATE_FULL_MOON: "mdi:moon-full", + STATE_LAST_QUARTER: "mdi:moon-last-quarter", + STATE_NEW_MOON: "mdi:moon-new", + STATE_WANING_CRESCENT: "mdi:moon-waning-crescent", + STATE_WANING_GIBBOUS: "mdi:moon-waning-gibbous", + STATE_WAXING_CRESCENT: "mdi:moon-waxing-crescent", + STATE_WAXING_GIBBOUS: "mdi:moon-waxing-gibbous", +} PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( {vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string} @@ -31,7 +50,7 @@ class MoonSensor(Entity): """Representation of a Moon sensor.""" def __init__(self, name): - """Initialize the sensor.""" + """Initialize the moon sensor.""" self._name = name self._state = None @@ -44,29 +63,27 @@ def name(self): def state(self): """Return the state of the device.""" if self._state == 0: - return "new_moon" + return STATE_NEW_MOON if self._state < 7: - return "waxing_crescent" + return STATE_WAXING_CRESCENT if self._state == 7: - return "first_quarter" + return STATE_FIRST_QUARTER if self._state < 14: - return "waxing_gibbous" + return STATE_WAXING_GIBBOUS if self._state == 14: - return "full_moon" + return STATE_FULL_MOON if self._state < 21: - return "waning_gibbous" + return STATE_WANING_GIBBOUS if self._state == 21: - return "last_quarter" - return "waning_crescent" + return STATE_LAST_QUARTER + return STATE_WANING_CRESCENT @property def icon(self): """Icon to use in the frontend, if any.""" - return ICON + return MOON_ICONS.get(self.state) async def async_update(self): """Get the time and updates the states.""" - from astral import Astral - today = dt_util.as_local(dt_util.utcnow()).date() self._state = Astral().moon_phase(today) diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index 2fab599ac3f8..ad9166e24106 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -16,8 +16,6 @@ import attr import requests.certs import voluptuous as vol -import paho.mqtt.client as mqtt -from paho.mqtt.matcher import MQTTMatcher from homeassistant import config_entries from homeassistant.components import websocket_api @@ -47,7 +45,7 @@ from homeassistant.util.logging import catch_log_exception # Loading the config flow file will register the flow -from . import config_flow, discovery, server # noqa pylint: disable=unused-import +from . import config_flow, discovery, server # noqa: F401 pylint: disable=unused-import from .const import ( CONF_BROKER, CONF_DISCOVERY, @@ -335,7 +333,6 @@ def embedded_broker_deprecated(value): ) -# pylint: disable=invalid-name SubscribePayloadType = Union[str, bytes] # Only bytes if encoding is None @@ -726,6 +723,11 @@ def __init__( tls_version: Optional[int], ) -> None: """Initialize Home Assistant MQTT client.""" + # We don't import them on the top because some integrations + # should be able to optionally rely on MQTT. + # pylint: disable=import-outside-toplevel + import paho.mqtt.client as mqtt + self.hass = hass self.broker = broker self.port = port @@ -787,6 +789,9 @@ async def async_connect(self) -> str: This method is a coroutine. """ + # pylint: disable=import-outside-toplevel + import paho.mqtt.client as mqtt + result: int = None try: result = await self.hass.async_add_job( @@ -878,6 +883,9 @@ def _mqtt_on_connect(self, _mqttc, _userdata, _flags, result_code: int) -> None: Resubscribe to all topics we were subscribed to and publish birth message. """ + # pylint: disable=import-outside-toplevel + import paho.mqtt.client as mqtt + if result_code != mqtt.CONNACK_ACCEPTED: _LOGGER.error( "Unable to connect to the MQTT broker: %s", @@ -969,6 +977,9 @@ def _mqtt_on_disconnect(self, _mqttc, _userdata, result_code: int) -> None: def _raise_on_error(result_code: int) -> None: """Raise error if error result.""" + # pylint: disable=import-outside-toplevel + import paho.mqtt.client as mqtt + if result_code != 0: raise HomeAssistantError( "Error talking to MQTT: {}".format(mqtt.error_string(result_code)) @@ -977,6 +988,9 @@ def _raise_on_error(result_code: int) -> None: def _match_topic(subscription: str, topic: str) -> bool: """Test if topic matches subscription.""" + # pylint: disable=import-outside-toplevel + from paho.mqtt.matcher import MQTTMatcher + matcher = MQTTMatcher() matcher[subscription] = True try: diff --git a/homeassistant/components/mqtt/alarm_control_panel.py b/homeassistant/components/mqtt/alarm_control_panel.py index 7171a55b2709..43d0bb570a8e 100644 --- a/homeassistant/components/mqtt/alarm_control_panel.py +++ b/homeassistant/components/mqtt/alarm_control_panel.py @@ -6,6 +6,11 @@ from homeassistant.components import mqtt import homeassistant.components.alarm_control_panel as alarm +from homeassistant.components.alarm_control_panel.const import ( + SUPPORT_ALARM_ARM_AWAY, + SUPPORT_ALARM_ARM_HOME, + SUPPORT_ALARM_ARM_NIGHT, +) from homeassistant.const import ( CONF_CODE, CONF_DEVICE, @@ -223,6 +228,11 @@ def state(self): """Return the state of the device.""" return self._state + @property + def supported_features(self) -> int: + """Return the list of supported features.""" + return SUPPORT_ALARM_ARM_HOME | SUPPORT_ALARM_ARM_AWAY | SUPPORT_ALARM_ARM_NIGHT + @property def code_format(self): """Return one or more digits/characters.""" diff --git a/homeassistant/components/mqtt/climate.py b/homeassistant/components/mqtt/climate.py index 4b163c523fa6..9b46057a414e 100644 --- a/homeassistant/components/mqtt/climate.py +++ b/homeassistant/components/mqtt/climate.py @@ -756,12 +756,14 @@ async def async_set_preset_mode(self, preset_mode): if self._away: optimistic_update = optimistic_update or self._set_away_mode(False) elif preset_mode == PRESET_AWAY: + if self._hold: + self._set_hold_mode(None) optimistic_update = optimistic_update or self._set_away_mode(True) - - if self._hold: - optimistic_update = optimistic_update or self._set_hold_mode(None) - elif preset_mode not in (None, PRESET_AWAY): - optimistic_update = optimistic_update or self._set_hold_mode(preset_mode) + else: + hold_mode = preset_mode + if preset_mode == PRESET_NONE: + hold_mode = None + optimistic_update = optimistic_update or self._set_hold_mode(hold_mode) if optimistic_update: self.async_write_ha_state() diff --git a/homeassistant/components/mqtt/config_flow.py b/homeassistant/components/mqtt/config_flow.py index a8a378e723c0..d3c6ee819b5f 100644 --- a/homeassistant/components/mqtt/config_flow.py +++ b/homeassistant/components/mqtt/config_flow.py @@ -3,7 +3,6 @@ import queue import voluptuous as vol -import paho.mqtt.client as mqtt from homeassistant import config_entries from homeassistant.const import ( @@ -126,6 +125,8 @@ async def async_step_hassio_confirm(self, user_input=None): def try_connection(broker, port, username, password, protocol="3.1"): """Test if we can connect to an MQTT broker.""" + import paho.mqtt.client as mqtt + if protocol == "3.1": proto = mqtt.MQTTv31 else: diff --git a/homeassistant/components/mqtt/models.py b/homeassistant/components/mqtt/models.py index 5f014aadd081..46aaa23732f0 100644 --- a/homeassistant/components/mqtt/models.py +++ b/homeassistant/components/mqtt/models.py @@ -3,7 +3,6 @@ import attr -# pylint: disable=invalid-name PublishPayloadType = Union[str, bytes, int, float, None] diff --git a/homeassistant/components/mqtt/server.py b/homeassistant/components/mqtt/server.py index f5d369a75c79..3ed2fb71b14d 100644 --- a/homeassistant/components/mqtt/server.py +++ b/homeassistant/components/mqtt/server.py @@ -4,8 +4,6 @@ import tempfile import voluptuous as vol -from hbmqtt.broker import Broker, BrokerException -from passlib.apps import custom_app_context from homeassistant.const import EVENT_HOMEASSISTANT_STOP import homeassistant.helpers.config_validation as cv @@ -37,6 +35,9 @@ def async_start(hass, password, server_config): This method is a coroutine. """ + # pylint: disable=import-outside-toplevel + from hbmqtt.broker import Broker, BrokerException + passwd = tempfile.NamedTemporaryFile() gen_server_config, client_config = generate_config(hass, passwd, password) @@ -65,6 +66,9 @@ def async_shutdown_mqtt_server(event): def generate_config(hass, passwd, password): """Generate a configuration based on current Home Assistant instance.""" + # pylint: disable=import-outside-toplevel + from passlib.apps import custom_app_context + config = { "listeners": { "default": { diff --git a/homeassistant/components/mycroft/notify.py b/homeassistant/components/mycroft/notify.py index 93b724f97cda..335eff875461 100644 --- a/homeassistant/components/mycroft/notify.py +++ b/homeassistant/components/mycroft/notify.py @@ -1,8 +1,9 @@ """Mycroft AI notification platform.""" import logging -from homeassistant.components.notify import BaseNotificationService +from mycroftapi import MycroftAPI +from homeassistant.components.notify import BaseNotificationService _LOGGER = logging.getLogger(__name__) @@ -21,7 +22,6 @@ def __init__(self, mycroft_ip): def send_message(self, message="", **kwargs): """Send a message mycroft to speak on instance.""" - from mycroftapi import MycroftAPI text = message mycroft = MycroftAPI(self.mycroft_ip) diff --git a/homeassistant/components/mysensors/const.py b/homeassistant/components/mysensors/const.py index 45f603a2cb44..ccb646eb47ee 100644 --- a/homeassistant/components/mysensors/const.py +++ b/homeassistant/components/mysensors/const.py @@ -25,6 +25,8 @@ TYPE = "type" UPDATE_DELAY = 0.1 +SERVICE_SEND_IR_CODE = "send_ir_code" + BINARY_SENSOR_TYPES = { "S_DOOR": {"V_TRIPPED"}, "S_MOTION": {"V_TRIPPED"}, diff --git a/homeassistant/components/mysensors/services.yaml b/homeassistant/components/mysensors/services.yaml index e69de29bb2d1..74a5ff0f183f 100644 --- a/homeassistant/components/mysensors/services.yaml +++ b/homeassistant/components/mysensors/services.yaml @@ -0,0 +1,9 @@ +send_ir_code: + description: Set an IR code as a state attribute for a MySensors IR device switch and turn the switch on. + fields: + entity_id: + description: Name(s) of entities that should have the IR code set and be turned on. Platform dependent. + example: 'switch.living_room_1_1' + V_IR_SEND: + description: IR code to send. + example: '0xC284' \ No newline at end of file diff --git a/homeassistant/components/mysensors/switch.py b/homeassistant/components/mysensors/switch.py index c624aaafa343..fecec53370bc 100644 --- a/homeassistant/components/mysensors/switch.py +++ b/homeassistant/components/mysensors/switch.py @@ -6,8 +6,9 @@ from homeassistant.components.switch import DOMAIN, SwitchDevice from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON +from .const import DOMAIN as MYSENSORS_DOMAIN, SERVICE_SEND_IR_CODE + ATTR_IR_CODE = "V_IR_SEND" -SERVICE_SEND_IR_CODE = "mysensors_send_ir_code" SEND_IR_CODE_SERVICE_SCHEMA = vol.Schema( {vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, vol.Required(ATTR_IR_CODE): cv.string} @@ -64,7 +65,7 @@ async def async_send_ir_code_service(service): await device.async_turn_on(**kwargs) hass.services.async_register( - DOMAIN, + MYSENSORS_DOMAIN, SERVICE_SEND_IR_CODE, async_send_ir_code_service, schema=SEND_IR_CODE_SERVICE_SCHEMA, diff --git a/homeassistant/components/mystrom/light.py b/homeassistant/components/mystrom/light.py index d878ee60302c..56fe369144bb 100644 --- a/homeassistant/components/mystrom/light.py +++ b/homeassistant/components/mystrom/light.py @@ -1,21 +1,23 @@ """Support for myStrom Wifi bulbs.""" import logging +from pymystrom.bulb import MyStromBulb +from pymystrom.exceptions import MyStromConnectionError import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.light import ( - Light, - PLATFORM_SCHEMA, ATTR_BRIGHTNESS, + ATTR_EFFECT, + ATTR_HS_COLOR, + PLATFORM_SCHEMA, SUPPORT_BRIGHTNESS, + SUPPORT_COLOR, SUPPORT_EFFECT, - ATTR_EFFECT, SUPPORT_FLASH, - SUPPORT_COLOR, - ATTR_HS_COLOR, + Light, ) from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -39,8 +41,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the myStrom Light platform.""" - from pymystrom.bulb import MyStromBulb - from pymystrom.exceptions import MyStromConnectionError host = config.get(CONF_HOST) mac = config.get(CONF_MAC) @@ -107,7 +107,6 @@ def is_on(self): def turn_on(self, **kwargs): """Turn on the light.""" - from pymystrom.exceptions import MyStromConnectionError brightness = kwargs.get(ATTR_BRIGHTNESS, 255) effect = kwargs.get(ATTR_EFFECT) @@ -136,7 +135,6 @@ def turn_on(self, **kwargs): def turn_off(self, **kwargs): """Turn off the bulb.""" - from pymystrom.exceptions import MyStromConnectionError try: self._bulb.set_off() @@ -145,7 +143,6 @@ def turn_off(self, **kwargs): def update(self): """Fetch new state data for this light.""" - from pymystrom.exceptions import MyStromConnectionError try: self._state = self._bulb.get_status() diff --git a/homeassistant/components/mystrom/switch.py b/homeassistant/components/mystrom/switch.py index 0eca5598cc9f..3a045e0391dc 100644 --- a/homeassistant/components/mystrom/switch.py +++ b/homeassistant/components/mystrom/switch.py @@ -3,8 +3,8 @@ import voluptuous as vol -from homeassistant.components.switch import SwitchDevice, PLATFORM_SCHEMA -from homeassistant.const import CONF_NAME, CONF_HOST +from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchDevice +from homeassistant.const import CONF_HOST, CONF_NAME import homeassistant.helpers.config_validation as cv DEFAULT_NAME = "myStrom Switch" diff --git a/homeassistant/components/n26/__init__.py b/homeassistant/components/n26/__init__.py index e89d78a76f94..f8379cb310f1 100644 --- a/homeassistant/components/n26/__init__.py +++ b/homeassistant/components/n26/__init__.py @@ -2,9 +2,9 @@ from datetime import datetime, timedelta, timezone import logging -import voluptuous as vol - from n26 import api as n26_api, config as n26_config +from requests import HTTPError +import voluptuous as vol from homeassistant.const import CONF_PASSWORD, CONF_SCAN_INTERVAL, CONF_USERNAME import homeassistant.helpers.config_validation as cv @@ -51,8 +51,6 @@ def setup(hass, config): api = n26_api.Api(n26_config.Config(user, password)) - from requests import HTTPError - try: api.get_token() except HTTPError as err: diff --git a/homeassistant/components/nad/media_player.py b/homeassistant/components/nad/media_player.py index 61003d980e1f..0c29aac427f0 100644 --- a/homeassistant/components/nad/media_player.py +++ b/homeassistant/components/nad/media_player.py @@ -1,10 +1,10 @@ """Support for interfacing with NAD receivers through RS-232.""" import logging +from nad_receiver import NADReceiver, NADReceiverTCP, NADReceiverTelnet import voluptuous as vol -import homeassistant.helpers.config_validation as cv -from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA +from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice from homeassistant.components.media_player.const import ( SUPPORT_SELECT_SOURCE, SUPPORT_TURN_OFF, @@ -13,7 +13,8 @@ SUPPORT_VOLUME_SET, SUPPORT_VOLUME_STEP, ) -from homeassistant.const import CONF_NAME, STATE_OFF, STATE_ON, CONF_HOST +from homeassistant.const import CONF_HOST, CONF_NAME, STATE_OFF, STATE_ON +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -64,8 +65,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the NAD platform.""" if config.get(CONF_TYPE) == "RS232": - from nad_receiver import NADReceiver - add_entities( [ NAD( @@ -79,8 +78,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): True, ) elif config.get(CONF_TYPE) == "Telnet": - from nad_receiver import NADReceiverTelnet - add_entities( [ NAD( @@ -94,8 +91,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): True, ) else: - from nad_receiver import NADReceiverTCP - add_entities( [ NADtcp( diff --git a/homeassistant/components/nanoleaf/light.py b/homeassistant/components/nanoleaf/light.py index e3f3cfbeab14..4b08d0b9751a 100644 --- a/homeassistant/components/nanoleaf/light.py +++ b/homeassistant/components/nanoleaf/light.py @@ -1,6 +1,7 @@ """Support for Nanoleaf Lights.""" import logging +from pynanoleaf import Nanoleaf, Unavailable import voluptuous as vol from homeassistant.components.light import ( @@ -54,7 +55,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Nanoleaf light.""" - from pynanoleaf import Nanoleaf, Unavailable if DATA_NANOLEAF not in hass.data: hass.data[DATA_NANOLEAF] = dict() @@ -222,7 +222,6 @@ def turn_off(self, **kwargs): def update(self): """Fetch new state data for this light.""" - from pynanoleaf import Unavailable try: self._available = self._light.available diff --git a/homeassistant/components/neato/.translations/pt.json b/homeassistant/components/neato/.translations/pt.json new file mode 100644 index 000000000000..b46423599731 --- /dev/null +++ b/homeassistant/components/neato/.translations/pt.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "Palavra-passe", + "username": "Nome de Utilizador" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/neato/.translations/ru.json b/homeassistant/components/neato/.translations/ru.json index 999e45880cfd..57989a346fa3 100644 --- a/homeassistant/components/neato/.translations/ru.json +++ b/homeassistant/components/neato/.translations/ru.json @@ -2,13 +2,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.", - "invalid_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435." + "invalid_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0443\u0447\u0451\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.", + "invalid_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0443\u0447\u0451\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": { diff --git a/homeassistant/components/neato/__init__.py b/homeassistant/components/neato/__init__.py index ddf9789f678f..5a697e7b9ad1 100644 --- a/homeassistant/components/neato/__init__.py +++ b/homeassistant/components/neato/__init__.py @@ -21,7 +21,6 @@ NEATO_MAP_DATA, NEATO_PERSISTENT_MAPS, NEATO_ROBOTS, - SCAN_INTERVAL_MINUTES, VALID_VENDORS, ) @@ -161,7 +160,7 @@ def login(self): self.logged_in = True _LOGGER.debug("Successfully connected to Neato API") - @Throttle(timedelta(minutes=SCAN_INTERVAL_MINUTES)) + @Throttle(timedelta(minutes=1)) def update_robots(self): """Update the robot states.""" _LOGGER.debug("Running HUB.update_robots %s", self._hass.data.get(NEATO_ROBOTS)) diff --git a/homeassistant/components/neato/const.py b/homeassistant/components/neato/const.py index 6dbaeb10d365..cfe8a2dad9d1 100644 --- a/homeassistant/components/neato/const.py +++ b/homeassistant/components/neato/const.py @@ -11,6 +11,8 @@ SCAN_INTERVAL_MINUTES = 5 +SERVICE_NEATO_CUSTOM_CLEANING = "custom_cleaning" + VALID_VENDORS = ["neato", "vorwerk"] MODE = {1: "Eco", 2: "Turbo"} diff --git a/homeassistant/components/neato/sensor.py b/homeassistant/components/neato/sensor.py index 36175151e0e7..fd5d8036f5fa 100644 --- a/homeassistant/components/neato/sensor.py +++ b/homeassistant/components/neato/sensor.py @@ -41,22 +41,14 @@ class NeatoSensor(Entity): def __init__(self, neato, robot): """Initialize Neato sensor.""" self.robot = robot - self.neato = neato - self._available = self.neato.logged_in if self.neato is not None else False + self._available = neato.logged_in if neato is not None else False self._robot_name = f"{self.robot.name} {BATTERY}" self._robot_serial = self.robot.serial self._state = None def update(self): """Update Neato Sensor.""" - if self.neato is None: - _LOGGER.error("Error while updating sensor") - self._state = None - self._available = False - return - try: - self.neato.update_robots() self._state = self.robot.state except NeatoRobotException as ex: if self._available: diff --git a/homeassistant/components/neato/services.yaml b/homeassistant/components/neato/services.yaml index e69de29bb2d1..b40edabd7796 100644 --- a/homeassistant/components/neato/services.yaml +++ b/homeassistant/components/neato/services.yaml @@ -0,0 +1,18 @@ +custom_cleaning: + description: Zone Cleaning service call specific to Neato Botvacs. + fields: + entity_id: + description: Name of the vacuum entity. [Required] + example: 'vacuum.neato' + mode: + description: "Set the cleaning mode: 1 for eco and 2 for turbo. Defaults to turbo if not set." + example: 2 + navigation: + description: "Set the navigation mode: 1 for normal, 2 for extra care, 3 for deep. Defaults to normal if not set." + example: 1 + category: + description: "Whether to use a persistent map or not for cleaning (i.e. No go lines): 2 for no map, 4 for map. Default to using map if not set (and fallback to no map if no map is found)." + example: 2 + zone: + description: Only supported on the Botvac D7. Name of the zone to clean. Defaults to no zone i.e. complete house cleanup. + example: "Kitchen" \ No newline at end of file diff --git a/homeassistant/components/neato/switch.py b/homeassistant/components/neato/switch.py index 8536af63945e..6aa0e11a43ef 100644 --- a/homeassistant/components/neato/switch.py +++ b/homeassistant/components/neato/switch.py @@ -45,8 +45,7 @@ def __init__(self, neato, robot, switch_type): """Initialize the Neato Connected switches.""" self.type = switch_type self.robot = robot - self.neato = neato - self._available = self.neato.logged_in if self.neato is not None else False + self._available = neato.logged_in if neato is not None else False self._robot_name = f"{self.robot.name} {SWITCH_TYPES[self.type][0]}" self._state = None self._schedule_state = None @@ -55,15 +54,8 @@ def __init__(self, neato, robot, switch_type): def update(self): """Update the states of Neato switches.""" - if self.neato is None: - _LOGGER.error("Error while updating switches") - self._state = None - self._available = False - return - _LOGGER.debug("Running switch update") try: - self.neato.update_robots() self._state = self.robot.state except NeatoRobotException as ex: if self._available: # Print only once when available diff --git a/homeassistant/components/neato/vacuum.py b/homeassistant/components/neato/vacuum.py index 40ed79042c76..d8a3e4ded452 100644 --- a/homeassistant/components/neato/vacuum.py +++ b/homeassistant/components/neato/vacuum.py @@ -7,7 +7,6 @@ from homeassistant.components.vacuum import ( ATTR_STATUS, - DOMAIN, STATE_CLEANING, STATE_DOCKED, STATE_ERROR, @@ -40,6 +39,7 @@ NEATO_PERSISTENT_MAPS, NEATO_ROBOTS, SCAN_INTERVAL_MINUTES, + SERVICE_NEATO_CUSTOM_CLEANING, ) _LOGGER = logging.getLogger(__name__) @@ -73,8 +73,6 @@ ATTR_CATEGORY = "category" ATTR_ZONE = "zone" -SERVICE_NEATO_CUSTOM_CLEANING = "neato_custom_cleaning" - SERVICE_NEATO_CUSTOM_CLEANING_SCHEMA = vol.Schema( { vol.Required(ATTR_ENTITY_ID): cv.entity_ids, @@ -126,7 +124,7 @@ def service_to_entities(call): return entities hass.services.async_register( - DOMAIN, + NEATO_DOMAIN, SERVICE_NEATO_CUSTOM_CLEANING, neato_custom_cleaning_service, schema=SERVICE_NEATO_CUSTOM_CLEANING_SCHEMA, @@ -139,8 +137,7 @@ class NeatoConnectedVacuum(StateVacuumDevice): def __init__(self, neato, robot, mapdata, persistent_maps): """Initialize the Neato Connected Vacuum.""" self.robot = robot - self.neato = neato - self._available = self.neato.logged_in if self.neato is not None else False + self._available = neato.logged_in if neato is not None else False self._mapdata = mapdata self._name = f"{self.robot.name}" self._robot_has_map = self.robot.has_persistent_maps @@ -165,17 +162,14 @@ def __init__(self, neato, robot, mapdata, persistent_maps): def update(self): """Update the states of Neato Vacuums.""" - if self.neato is None: - _LOGGER.error("Error while updating vacuum") - self._state = None - self._available = False - return - _LOGGER.debug("Running Neato Vacuums update") try: if self._robot_stats is None: - self._robot_stats = self.robot.get_robot_info().json() - self.neato.update_robots() + self._robot_stats = self.robot.get_general_info().json().get("data") + except NeatoRobotException: + _LOGGER.warning("Couldn't fetch robot information of %s", self._name) + + try: self._state = self.robot.state except NeatoRobotException as ex: if self._available: # print only once when available @@ -323,13 +317,11 @@ def device_state_attributes(self): @property def device_info(self): """Device info for neato robot.""" - return { - "identifiers": {(NEATO_DOMAIN, self._robot_serial)}, - "name": self._name, - "manufacturer": self._robot_stats["data"]["mfg_name"], - "model": self._robot_stats["data"]["modelName"], - "sw_version": self._state["meta"]["firmware"], - } + info = {"identifiers": {(NEATO_DOMAIN, self._robot_serial)}, "name": self._name} + if self._robot_stats: + info["manufacturer"] = self._robot_stats["battery"]["vendor"] + info["model"] = self._robot_stats["model"] + info["sw_version"] = self._robot_stats["firmware"] def start(self): """Start cleaning or resume cleaning.""" diff --git a/homeassistant/components/nederlandse_spoorwegen/sensor.py b/homeassistant/components/nederlandse_spoorwegen/sensor.py index 0741ed4cb496..0b8239623730 100644 --- a/homeassistant/components/nederlandse_spoorwegen/sensor.py +++ b/homeassistant/components/nederlandse_spoorwegen/sensor.py @@ -2,6 +2,7 @@ from datetime import datetime, timedelta import logging +import ns_api import requests import voluptuous as vol @@ -46,7 +47,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the departure sensor.""" - import ns_api nsapi = ns_api.NSAPI(config.get(CONF_EMAIL), config.get(CONF_PASSWORD)) try: diff --git a/homeassistant/components/nello/lock.py b/homeassistant/components/nello/lock.py index 3efe0a9cc5fc..19f8e7aa14c9 100644 --- a/homeassistant/components/nello/lock.py +++ b/homeassistant/components/nello/lock.py @@ -2,11 +2,12 @@ from itertools import filterfalse import logging +from pynello.private import Nello import voluptuous as vol -import homeassistant.helpers.config_validation as cv -from homeassistant.components.lock import LockDevice, PLATFORM_SCHEMA +from homeassistant.components.lock import PLATFORM_SCHEMA, LockDevice from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -21,7 +22,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Nello lock platform.""" - from pynello.private import Nello nello = Nello(config.get(CONF_USERNAME), config.get(CONF_PASSWORD)) add_entities([NelloLock(lock) for lock in nello.locations], True) diff --git a/homeassistant/components/ness_alarm/alarm_control_panel.py b/homeassistant/components/ness_alarm/alarm_control_panel.py index 1b45c52ab710..d2feebfb64f4 100644 --- a/homeassistant/components/ness_alarm/alarm_control_panel.py +++ b/homeassistant/components/ness_alarm/alarm_control_panel.py @@ -3,6 +3,11 @@ import logging import homeassistant.components.alarm_control_panel as alarm +from homeassistant.components.alarm_control_panel.const import ( + SUPPORT_ALARM_ARM_AWAY, + SUPPORT_ALARM_ARM_HOME, + SUPPORT_ALARM_TRIGGER, +) from homeassistant.const import ( STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMING, @@ -62,6 +67,11 @@ def state(self): """Return the state of the device.""" return self._state + @property + def supported_features(self) -> int: + """Return the list of supported features.""" + return SUPPORT_ALARM_ARM_HOME | SUPPORT_ALARM_ARM_AWAY | SUPPORT_ALARM_TRIGGER + async def async_alarm_disarm(self, code=None): """Send disarm command.""" await self._client.disarm(code) diff --git a/homeassistant/components/nest/.translations/pl.json b/homeassistant/components/nest/.translations/pl.json index 482a67eb2218..d40c872e3006 100644 --- a/homeassistant/components/nest/.translations/pl.json +++ b/homeassistant/components/nest/.translations/pl.json @@ -4,7 +4,7 @@ "already_setup": "Mo\u017cesz skonfigurowa\u0107 tylko jedno konto Nest.", "authorize_url_fail": "Nieznany b\u0142\u0105d podczas generowania url autoryzacji.", "authorize_url_timeout": "Przekroczono limit czasu generowania URL autoryzacji.", - "no_flows": "Musisz skonfigurowa\u0107 Nest, zanim b\u0119dziesz m\u00f3g\u0142 wykona\u0107 uwierzytelnienie. [Przeczytaj instrukcj\u0119](https://www.home-assistant.io/components/nest/)." + "no_flows": "Musisz skonfigurowa\u0107 Nest, aby m\u00f3c si\u0119 z nim uwierzytelni\u0107. Zapoznaj si\u0119 z [instrukcj\u0105](https://www.home-assistant.io/components/nest/)." }, "error": { "internal_error": "Wewn\u0119trzny b\u0142\u0105d sprawdzania poprawno\u015bci kodu", diff --git a/homeassistant/components/netdata/sensor.py b/homeassistant/components/netdata/sensor.py index aab901506a8f..edabef9535c4 100644 --- a/homeassistant/components/netdata/sensor.py +++ b/homeassistant/components/netdata/sensor.py @@ -2,6 +2,8 @@ from datetime import timedelta import logging +from netdata import Netdata +from netdata.exceptions import NetdataError import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -53,7 +55,6 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the Netdata sensor.""" - from netdata import Netdata name = config.get(CONF_NAME) host = config.get(CONF_HOST) @@ -154,7 +155,6 @@ def __init__(self, api): @Throttle(MIN_TIME_BETWEEN_UPDATES) async def async_update(self): """Get the latest data from the Netdata REST API.""" - from netdata.exceptions import NetdataError try: await self.api.get_allmetrics() diff --git a/homeassistant/components/netgear_lte/manifest.json b/homeassistant/components/netgear_lte/manifest.json index 7e085d063077..f8c4c39cb83a 100644 --- a/homeassistant/components/netgear_lte/manifest.json +++ b/homeassistant/components/netgear_lte/manifest.json @@ -3,7 +3,7 @@ "name": "Netgear lte", "documentation": "https://www.home-assistant.io/integrations/netgear_lte", "requirements": [ - "eternalegypt==0.0.10" + "eternalegypt==0.0.11" ], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/netio/switch.py b/homeassistant/components/netio/switch.py index 77af9a34d687..4c9b6343f2b9 100644 --- a/homeassistant/components/netio/switch.py +++ b/homeassistant/components/netio/switch.py @@ -1,22 +1,23 @@ """The Netio switch component.""" -import logging from collections import namedtuple from datetime import timedelta +import logging +from pynetio import Netio import voluptuous as vol -from homeassistant.core import callback from homeassistant import util from homeassistant.components.http import HomeAssistantView +from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchDevice from homeassistant.const import ( CONF_HOST, + CONF_PASSWORD, CONF_PORT, CONF_USERNAME, - CONF_PASSWORD, EVENT_HOMEASSISTANT_STOP, STATE_ON, ) -from homeassistant.components.switch import SwitchDevice, PLATFORM_SCHEMA +from homeassistant.core import callback import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -50,7 +51,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Netio platform.""" - from pynetio import Netio host = config.get(CONF_HOST) username = config.get(CONF_USERNAME) diff --git a/homeassistant/components/nilu/manifest.json b/homeassistant/components/nilu/manifest.json index fe7a92bc2705..77df26312e99 100644 --- a/homeassistant/components/nilu/manifest.json +++ b/homeassistant/components/nilu/manifest.json @@ -6,5 +6,7 @@ "niluclient==0.1.2" ], "dependencies": [], - "codeowners": [] -} + "codeowners": [ + "@hfurubotten" + ] +} \ No newline at end of file diff --git a/homeassistant/components/nmap_tracker/device_tracker.py b/homeassistant/components/nmap_tracker/device_tracker.py index 7998f8758269..d41e80f17a2f 100644 --- a/homeassistant/components/nmap_tracker/device_tracker.py +++ b/homeassistant/components/nmap_tracker/device_tracker.py @@ -1,19 +1,20 @@ """Support for scanning a network with nmap.""" -import logging from collections import namedtuple from datetime import timedelta +import logging from getmac import get_mac_address +from nmap import PortScanner, PortScannerError import voluptuous as vol -import homeassistant.helpers.config_validation as cv -import homeassistant.util.dt as dt_util from homeassistant.components.device_tracker import ( DOMAIN, PLATFORM_SCHEMA, DeviceScanner, ) from homeassistant.const import CONF_HOSTS +import homeassistant.helpers.config_validation as cv +import homeassistant.util.dt as dt_util _LOGGER = logging.getLogger(__name__) @@ -91,8 +92,6 @@ def _update_info(self): """ _LOGGER.debug("Scanning...") - from nmap import PortScanner, PortScannerError - scanner = PortScanner() options = self._options diff --git a/homeassistant/components/nmbs/sensor.py b/homeassistant/components/nmbs/sensor.py index 8b2182665f64..26802808c0a0 100644 --- a/homeassistant/components/nmbs/sensor.py +++ b/homeassistant/components/nmbs/sensor.py @@ -1,6 +1,7 @@ """Get ride details and liveboard details for NMBS (Belgian railway).""" import logging +from pyrail import iRail import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -64,7 +65,6 @@ def get_ride_duration(departure_time, arrival_time, delay=0): def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the NMBS sensor with iRail API.""" - from pyrail import iRail api_client = iRail() diff --git a/homeassistant/components/noaa_tides/sensor.py b/homeassistant/components/noaa_tides/sensor.py index e5f31dba1568..063a163a8ab2 100644 --- a/homeassistant/components/noaa_tides/sensor.py +++ b/homeassistant/components/noaa_tides/sensor.py @@ -2,6 +2,7 @@ from datetime import datetime, timedelta import logging +from py_noaa import coops # pylint: disable=import-error import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -109,7 +110,6 @@ def state(self): def update(self): """Get the latest data from NOAA Tides and Currents API.""" - from py_noaa import coops # pylint: disable=import-error begin = datetime.now() delta = timedelta(days=2) diff --git a/homeassistant/components/notify/services.yaml b/homeassistant/components/notify/services.yaml index 1b7944cc7da0..23b1c968c4a3 100644 --- a/homeassistant/components/notify/services.yaml +++ b/homeassistant/components/notify/services.yaml @@ -16,16 +16,6 @@ notify: description: Extended information for notification. Optional depending on the platform. example: platform specific -html5_dismiss: - description: Dismiss a html5 notification. - fields: - target: - description: An array of targets. Optional. - example: ['my_phone', 'my_tablet'] - data: - description: Extended information of notification. Supports tag. Optional. - example: '{ "tag": "tagname" }' - apns_register: description: Registers a device to receive push notifications. fields: diff --git a/homeassistant/components/notion/.translations/pt.json b/homeassistant/components/notion/.translations/pt.json new file mode 100644 index 000000000000..e379229ec3a1 --- /dev/null +++ b/homeassistant/components/notion/.translations/pt.json @@ -0,0 +1,14 @@ +{ + "config": { + "error": { + "invalid_credentials": "Nome de utilizador ou palavra passe incorretos" + }, + "step": { + "user": { + "data": { + "password": "Palavra-passe" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/notion/.translations/ru.json b/homeassistant/components/notion/.translations/ru.json index 6c1d5f5d8d77..15b540732a73 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\u0451\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": "\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." + "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\u0451\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u044c\u044e." }, "step": { "user": { diff --git a/homeassistant/components/notion/__init__.py b/homeassistant/components/notion/__init__.py index 62deb4999d9c..1e04c4a8e8ef 100644 --- a/homeassistant/components/notion/__init__.py +++ b/homeassistant/components/notion/__init__.py @@ -144,8 +144,12 @@ async def async_unload_entry(hass, config_entry): cancel = hass.data[DOMAIN][DATA_LISTENER].pop(config_entry.entry_id) cancel() - for component in ("binary_sensor", "sensor"): - await hass.config_entries.async_forward_entry_unload(config_entry, component) + tasks = [ + hass.config_entries.async_forward_entry_unload(config_entry, component) + for component in ("binary_sensor", "sensor") + ] + + await asyncio.gather(*tasks) return True diff --git a/homeassistant/components/nsw_rural_fire_service_feed/geo_location.py b/homeassistant/components/nsw_rural_fire_service_feed/geo_location.py index 9a9679f95751..a04d2bd69b21 100644 --- a/homeassistant/components/nsw_rural_fire_service_feed/geo_location.py +++ b/homeassistant/components/nsw_rural_fire_service_feed/geo_location.py @@ -3,6 +3,7 @@ import logging from typing import Optional +from aio_geojson_nsw_rfs_incidents import NswRuralFireServiceIncidentsFeedManager import voluptuous as vol from homeassistant.components.geo_location import PLATFORM_SCHEMA, GeolocationEvent @@ -14,11 +15,16 @@ CONF_RADIUS, CONF_SCAN_INTERVAL, EVENT_HOMEASSISTANT_START, + EVENT_HOMEASSISTANT_STOP, ) from homeassistant.core import callback -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.dispatcher import async_dispatcher_connect, dispatcher_send -from homeassistant.helpers.event import track_time_interval +from homeassistant.helpers import ConfigType, aiohttp_client, config_validation as cv +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, + async_dispatcher_send, +) +from homeassistant.helpers.event import async_track_time_interval +from homeassistant.helpers.typing import HomeAssistantType _LOGGER = logging.getLogger(__name__) @@ -58,7 +64,9 @@ ) -def setup_platform(hass, config, add_entities, discovery_info=None): +async def async_setup_platform( + hass: HomeAssistantType, config: ConfigType, async_add_entities, discovery_info=None +): """Set up the NSW Rural Fire Service Feed platform.""" scan_interval = config.get(CONF_SCAN_INTERVAL, SCAN_INTERVAL) coordinates = ( @@ -68,30 +76,40 @@ def setup_platform(hass, config, add_entities, discovery_info=None): radius_in_km = config[CONF_RADIUS] categories = config.get(CONF_CATEGORIES) # Initialize the entity manager. - feed = NswRuralFireServiceFeedEntityManager( - hass, add_entities, scan_interval, coordinates, radius_in_km, categories + manager = NswRuralFireServiceFeedEntityManager( + hass, async_add_entities, scan_interval, coordinates, radius_in_km, categories ) - def start_feed_manager(event): + async def start_feed_manager(event): """Start feed manager.""" - feed.startup() + await manager.async_init() + + async def stop_feed_manager(event): + """Stop feed manager.""" + await manager.async_stop() - hass.bus.listen_once(EVENT_HOMEASSISTANT_START, start_feed_manager) + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, start_feed_manager) + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, stop_feed_manager) + hass.async_create_task(manager.async_update()) class NswRuralFireServiceFeedEntityManager: """Feed Entity Manager for NSW Rural Fire Service GeoJSON feed.""" def __init__( - self, hass, add_entities, scan_interval, coordinates, radius_in_km, categories + self, + hass, + async_add_entities, + scan_interval, + coordinates, + radius_in_km, + categories, ): """Initialize the Feed Entity Manager.""" - from geojson_client.nsw_rural_fire_service_feed import ( - NswRuralFireServiceFeedManager, - ) - self._hass = hass - self._feed_manager = NswRuralFireServiceFeedManager( + websession = aiohttp_client.async_get_clientsession(hass) + self._feed_manager = NswRuralFireServiceIncidentsFeedManager( + websession, self._generate_entity, self._update_entity, self._remove_entity, @@ -99,37 +117,52 @@ def __init__( filter_radius=radius_in_km, filter_categories=categories, ) - self._add_entities = add_entities + self._async_add_entities = async_add_entities self._scan_interval = scan_interval + self._track_time_remove_callback = None - def startup(self): - """Start up this manager.""" - self._feed_manager.update() - self._init_regular_updates() + async def async_init(self): + """Schedule initial and regular updates based on configured time interval.""" - def _init_regular_updates(self): - """Schedule regular updates at the specified interval.""" - track_time_interval( - self._hass, lambda now: self._feed_manager.update(), self._scan_interval + async def update(event_time): + """Update.""" + await self.async_update() + + # Trigger updates at regular intervals. + self._track_time_remove_callback = async_track_time_interval( + self._hass, update, self._scan_interval ) + _LOGGER.debug("Feed entity manager initialized") + + async def async_update(self): + """Refresh data.""" + await self._feed_manager.update() + _LOGGER.debug("Feed entity manager updated") + + async def async_stop(self): + """Stop this feed entity manager from refreshing.""" + if self._track_time_remove_callback: + self._track_time_remove_callback() + _LOGGER.debug("Feed entity manager stopped") + def get_entry(self, external_id): """Get feed entry by external id.""" return self._feed_manager.feed_entries.get(external_id) - def _generate_entity(self, external_id): + async def _generate_entity(self, external_id): """Generate new entity.""" new_entity = NswRuralFireServiceLocationEvent(self, external_id) # Add new entities to HA. - self._add_entities([new_entity], True) + self._async_add_entities([new_entity], True) - def _update_entity(self, external_id): + async def _update_entity(self, external_id): """Update entity.""" - dispatcher_send(self._hass, SIGNAL_UPDATE_ENTITY.format(external_id)) + async_dispatcher_send(self._hass, SIGNAL_UPDATE_ENTITY.format(external_id)) - def _remove_entity(self, external_id): + async def _remove_entity(self, external_id): """Remove entity.""" - dispatcher_send(self._hass, SIGNAL_DELETE_ENTITY.format(external_id)) + async_dispatcher_send(self._hass, SIGNAL_DELETE_ENTITY.format(external_id)) class NswRuralFireServiceLocationEvent(GeolocationEvent): @@ -169,11 +202,14 @@ async def async_added_to_hass(self): self._update_callback, ) + async def async_will_remove_from_hass(self) -> None: + """Call when entity will be removed from hass.""" + self._remove_signal_delete() + self._remove_signal_update() + @callback def _delete_callback(self): """Remove this entity.""" - self._remove_signal_delete() - self._remove_signal_update() self.hass.async_create_task(self.async_remove()) @callback diff --git a/homeassistant/components/nsw_rural_fire_service_feed/manifest.json b/homeassistant/components/nsw_rural_fire_service_feed/manifest.json index 3d16f0a57e34..7dd7d10d6beb 100644 --- a/homeassistant/components/nsw_rural_fire_service_feed/manifest.json +++ b/homeassistant/components/nsw_rural_fire_service_feed/manifest.json @@ -3,7 +3,7 @@ "name": "Nsw rural fire service feed", "documentation": "https://www.home-assistant.io/integrations/nsw_rural_fire_service_feed", "requirements": [ - "geojson_client==0.4" + "aio_geojson_nsw_rfs_incidents==0.1" ], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/nuheat/climate.py b/homeassistant/components/nuheat/climate.py index 5a4e4e233d16..5cf9bd6fc580 100644 --- a/homeassistant/components/nuheat/climate.py +++ b/homeassistant/components/nuheat/climate.py @@ -22,7 +22,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.util import Throttle -from . import DOMAIN as NUHEAT_DOMAIN +from . import DOMAIN _LOGGER = logging.getLogger(__name__) @@ -52,7 +52,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): return temperature_unit = hass.config.units.temperature_unit - api, serial_numbers = hass.data[NUHEAT_DOMAIN] + api, serial_numbers = hass.data[DOMAIN] thermostats = [ NuHeatThermostat(api, serial_number, temperature_unit) for serial_number in serial_numbers @@ -75,7 +75,7 @@ def resume_program_set_service(service): thermostat.schedule_update_ha_state(True) hass.services.register( - NUHEAT_DOMAIN, + DOMAIN, SERVICE_RESUME_PROGRAM, resume_program_set_service, schema=RESUME_PROGRAM_SCHEMA, diff --git a/homeassistant/components/nuheat/services.yaml b/homeassistant/components/nuheat/services.yaml index e69de29bb2d1..6639fcd98986 100644 --- a/homeassistant/components/nuheat/services.yaml +++ b/homeassistant/components/nuheat/services.yaml @@ -0,0 +1,6 @@ +resume_program: + description: Resume the programmed schedule. + fields: + entity_id: + description: Name(s) of entities to change. + example: 'climate.kitchen' diff --git a/homeassistant/components/nuimo_controller/__init__.py b/homeassistant/components/nuimo_controller/__init__.py index 8fa3897b7357..013c2caf23d6 100644 --- a/homeassistant/components/nuimo_controller/__init__.py +++ b/homeassistant/components/nuimo_controller/__init__.py @@ -3,10 +3,12 @@ import threading import time +# pylint: disable=import-error +from nuimo import NuimoController, NuimoDiscoveryManager import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.const import CONF_MAC, CONF_NAME, EVENT_HOMEASSISTANT_STOP +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -104,8 +106,6 @@ def stop(self, event): def _attach(self): """Create a Nuimo object from MAC address or discovery.""" - # pylint: disable=import-error - from nuimo import NuimoController, NuimoDiscoveryManager if self._nuimo: self._nuimo.disconnect() diff --git a/homeassistant/components/nuimo_controller/services.yaml b/homeassistant/components/nuimo_controller/services.yaml index e69de29bb2d1..ba537544a3b9 100644 --- a/homeassistant/components/nuimo_controller/services.yaml +++ b/homeassistant/components/nuimo_controller/services.yaml @@ -0,0 +1,18 @@ +led_matrix: + description: Sends an LED Matrix to your display + fields: + matrix: + description: "A string representation of the matrix to be displayed. See the SDK documentation for more info: https://github.com/getSenic/nuimo-linux-python#write-to-nuimos-led-matrix" + example: + '........ + 0000000. + .000000. + ..00000. + .0.0000. + .00.000. + .000000. + .000000. + ........' + interval: + description: Display interval in seconds + example: 0.5 \ No newline at end of file diff --git a/homeassistant/components/nuki/services.yaml b/homeassistant/components/nuki/services.yaml index e69de29bb2d1..1300b48e0ddd 100644 --- a/homeassistant/components/nuki/services.yaml +++ b/homeassistant/components/nuki/services.yaml @@ -0,0 +1,10 @@ +lock_n_go: + description: "Nuki Lock 'n' Go" + fields: + entity_id: + description: Entity id of the Nuki lock. + example: 'lock.front_door' + unlatch: + description: Whether to unlatch the lock. + example: false + diff --git a/homeassistant/components/nut/sensor.py b/homeassistant/components/nut/sensor.py index db485734777e..34e3bfaf0861 100644 --- a/homeassistant/components/nut/sensor.py +++ b/homeassistant/components/nut/sensor.py @@ -1,25 +1,26 @@ """Provides a sensor to track various status aspects of a UPS.""" -import logging from datetime import timedelta +import logging +from pynut2.nut2 import PyNUTClient, PyNUTError import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA -import homeassistant.helpers.config_validation as cv from homeassistant.const import ( + ATTR_STATE, + CONF_ALIAS, CONF_HOST, - CONF_PORT, CONF_NAME, - CONF_USERNAME, CONF_PASSWORD, - TEMP_CELSIUS, + CONF_PORT, CONF_RESOURCES, - CONF_ALIAS, - ATTR_STATE, - STATE_UNKNOWN, + CONF_USERNAME, POWER_WATT, + STATE_UNKNOWN, + TEMP_CELSIUS, ) from homeassistant.exceptions import PlatformNotReady +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle @@ -270,7 +271,6 @@ class PyNUTData: def __init__(self, host, port, alias, username, password): """Initialize the data object.""" - from pynut2.nut2 import PyNUTClient, PyNUTError self._host = host self._port = port diff --git a/homeassistant/components/nx584/alarm_control_panel.py b/homeassistant/components/nx584/alarm_control_panel.py index d3d867ff3786..62bc7ae32bba 100644 --- a/homeassistant/components/nx584/alarm_control_panel.py +++ b/homeassistant/components/nx584/alarm_control_panel.py @@ -6,6 +6,10 @@ import homeassistant.components.alarm_control_panel as alarm from homeassistant.components.alarm_control_panel import PLATFORM_SCHEMA +from homeassistant.components.alarm_control_panel.const import ( + SUPPORT_ALARM_ARM_AWAY, + SUPPORT_ALARM_ARM_HOME, +) from homeassistant.const import ( CONF_HOST, CONF_NAME, @@ -79,6 +83,11 @@ def state(self): """Return the state of the device.""" return self._state + @property + def supported_features(self) -> int: + """Return the list of supported features.""" + return SUPPORT_ALARM_ARM_HOME | SUPPORT_ALARM_ARM_AWAY + def update(self): """Process new events from panel.""" try: diff --git a/homeassistant/components/nzbget/sensor.py b/homeassistant/components/nzbget/sensor.py index 20b49a492f3a..3556c88a6da9 100644 --- a/homeassistant/components/nzbget/sensor.py +++ b/homeassistant/components/nzbget/sensor.py @@ -18,6 +18,7 @@ "download_rate": ["DownloadRate", "Speed", "MB/s"], "download_size": ["DownloadedSizeMB", "Size", "MB"], "free_disk_space": ["FreeDiskSpaceMB", "Disk Free", "MB"], + "post_job_count": ["PostJobCount", "Post Processing Jobs", "Jobs"], "post_paused": ["PostPaused", "Post Processing Paused", None], "remaining_size": ["RemainingSizeMB", "Queue Size", "MB"], "uptime": ["UpTimeSec", "Uptime", "min"], diff --git a/homeassistant/components/ombi/__init__.py b/homeassistant/components/ombi/__init__.py index 860c7d4dcb4d..750772ce8f5b 100644 --- a/homeassistant/components/ombi/__init__.py +++ b/homeassistant/components/ombi/__init__.py @@ -7,6 +7,7 @@ from homeassistant.const import ( CONF_API_KEY, CONF_HOST, + CONF_PASSWORD, CONF_PORT, CONF_SSL, CONF_USERNAME, @@ -57,13 +58,15 @@ def urlbase(value) -> str: { DOMAIN: vol.Schema( { - vol.Required(CONF_API_KEY): cv.string, vol.Required(CONF_HOST): cv.string, vol.Required(CONF_USERNAME): cv.string, + vol.Exclusive(CONF_API_KEY, "auth"): cv.string, + vol.Exclusive(CONF_PASSWORD, "auth"): 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, - } + }, + cv.has_at_least_one_key("auth"), ) }, extra=vol.ALLOW_EXTRA, @@ -77,12 +80,14 @@ def setup(hass, config): 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], + username=config[DOMAIN][CONF_USERNAME], + password=config[DOMAIN].get(CONF_PASSWORD), + api_key=config[DOMAIN].get(CONF_API_KEY), ) try: + ombi.authenticate() ombi.test_connection() except pyombi.OmbiError as err: _LOGGER.warning("Unable to setup Ombi: %s", err) diff --git a/homeassistant/components/ombi/manifest.json b/homeassistant/components/ombi/manifest.json index fb6daf00f664..0407aa5a106d 100644 --- a/homeassistant/components/ombi/manifest.json +++ b/homeassistant/components/ombi/manifest.json @@ -4,5 +4,5 @@ "documentation": "https://www.home-assistant.io/integrations/ombi/", "dependencies": [], "codeowners": ["@larssont"], - "requirements": ["pyombi==0.1.5"] + "requirements": ["pyombi==0.1.10"] } diff --git a/homeassistant/components/onvif/camera.py b/homeassistant/components/onvif/camera.py index affbbb62338b..3f244530dca6 100644 --- a/homeassistant/components/onvif/camera.py +++ b/homeassistant/components/onvif/camera.py @@ -278,6 +278,10 @@ async def async_obtain_input_uri(self): _LOGGER.debug("Retrieving stream uri") + # Fix Onvif setup error on Goke GK7102 based IP camera + # where we need to recreate media_service #26781 + media_service = self._camera.create_media_service() + req = media_service.create_type("GetStreamUri") req.ProfileToken = profiles[self._profile_index].token req.StreamSetup = { diff --git a/homeassistant/components/opencv/manifest.json b/homeassistant/components/opencv/manifest.json index bd82da000cf1..4fe6026dfeff 100644 --- a/homeassistant/components/opencv/manifest.json +++ b/homeassistant/components/opencv/manifest.json @@ -2,10 +2,7 @@ "domain": "opencv", "name": "Opencv", "documentation": "https://www.home-assistant.io/integrations/opencv", - "requirements": [ - "numpy==1.17.3", - "opencv-python-headless==4.1.1.26" - ], + "requirements": ["numpy==1.17.4", "opencv-python-headless==4.1.2.30"], "dependencies": [], "codeowners": [] -} \ No newline at end of file +} diff --git a/homeassistant/components/opengarage/cover.py b/homeassistant/components/opengarage/cover.py index 1243a9164fd2..6b5cbc912e62 100644 --- a/homeassistant/components/opengarage/cover.py +++ b/homeassistant/components/opengarage/cover.py @@ -18,6 +18,8 @@ CONF_COVERS, CONF_HOST, CONF_PORT, + CONF_SSL, + CONF_VERIFY_SSL, STATE_CLOSING, STATE_OPENING, ) @@ -42,6 +44,8 @@ vol.Required(CONF_HOST): cv.string, vol.Optional(CONF_NAME): cv.string, vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Optional(CONF_SSL, default=False): cv.boolean, + vol.Optional(CONF_VERIFY_SSL, default=True): cv.boolean, } ) @@ -60,6 +64,8 @@ def setup_platform(hass, config, add_entities, discovery_info=None): CONF_NAME: device_config.get(CONF_NAME), CONF_HOST: device_config.get(CONF_HOST), CONF_PORT: device_config.get(CONF_PORT), + CONF_SSL: device_config.get(CONF_SSL), + CONF_VERIFY_SSL: device_config.get(CONF_VERIFY_SSL), CONF_DEVICE_KEY: device_config.get(CONF_DEVICE_KEY), } @@ -73,13 +79,16 @@ class OpenGarageCover(CoverDevice): def __init__(self, args): """Initialize the cover.""" - self.opengarage_url = "http://{}:{}".format(args[CONF_HOST], args[CONF_PORT]) + self.opengarage_url = "{}://{}:{}".format( + "https" if args[CONF_SSL] else "http", args[CONF_HOST], args[CONF_PORT] + ) self._name = args[CONF_NAME] self._device_key = args[CONF_DEVICE_KEY] self._state = None self._state_before_move = None self._device_state_attributes = {} self._available = True + self._verify_ssl = args[CONF_VERIFY_SSL] @property def name(self): @@ -155,7 +164,9 @@ def _push_button(self): result = -1 try: result = requests.get( - f"{self.opengarage_url}/cc?dkey={self._device_key}&click=1", timeout=10 + f"{self.opengarage_url}/cc?dkey={self._device_key}&click=1", + timeout=10, + verify=self._verify_ssl, ).json()["result"] except requests.exceptions.RequestException as ex: _LOGGER.error( diff --git a/homeassistant/components/openhome/manifest.json b/homeassistant/components/openhome/manifest.json index 2ec58b86125f..7640c3887478 100644 --- a/homeassistant/components/openhome/manifest.json +++ b/homeassistant/components/openhome/manifest.json @@ -2,9 +2,7 @@ "domain": "openhome", "name": "Openhome", "documentation": "https://www.home-assistant.io/integrations/openhome", - "requirements": [ - "openhomedevice==0.4.2" - ], + "requirements": ["openhomedevice==0.6.3"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/openhome/media_player.py b/homeassistant/components/openhome/media_player.py index 0443187ac630..222c1d87ec0b 100644 --- a/homeassistant/components/openhome/media_player.py +++ b/homeassistant/components/openhome/media_player.py @@ -1,6 +1,8 @@ """Support for Openhome Devices.""" import logging +from openhomedevice.Device import Device + from homeassistant.components.media_player import MediaPlayerDevice from homeassistant.components.media_player.const import ( SUPPORT_NEXT_TRACK, @@ -17,14 +19,7 @@ ) from homeassistant.const import STATE_IDLE, STATE_OFF, STATE_PAUSED, STATE_PLAYING -SUPPORT_OPENHOME = ( - SUPPORT_SELECT_SOURCE - | SUPPORT_VOLUME_STEP - | SUPPORT_VOLUME_MUTE - | SUPPORT_VOLUME_SET - | SUPPORT_TURN_OFF - | SUPPORT_TURN_ON -) +SUPPORT_OPENHOME = SUPPORT_SELECT_SOURCE | SUPPORT_TURN_OFF | SUPPORT_TURN_ON _LOGGER = logging.getLogger(__name__) @@ -33,7 +28,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Openhome platform.""" - from openhomedevice.Device import Device if not discovery_info: return True @@ -79,14 +73,19 @@ def update(self): self._in_standby = self._device.IsInStandby() self._transport_state = self._device.TransportState() self._track_information = self._device.TrackInfo() - self._volume_level = self._device.VolumeLevel() - self._volume_muted = self._device.IsMuted() self._source = self._device.Source() self._name = self._device.Room().decode("utf-8") self._supported_features = SUPPORT_OPENHOME source_index = {} source_names = list() + if self._device.VolumeEnabled(): + self._supported_features |= ( + SUPPORT_VOLUME_STEP | SUPPORT_VOLUME_MUTE | SUPPORT_VOLUME_SET + ) + self._volume_level = self._device.VolumeLevel() + self._volume_muted = self._device.IsMuted() + for source in self._device.Sources(): source_names.append(source["name"]) source_index[source["name"]] = source["index"] diff --git a/homeassistant/components/opensensemap/air_quality.py b/homeassistant/components/opensensemap/air_quality.py index d525e807aed2..cf27f86cc9ff 100644 --- a/homeassistant/components/opensensemap/air_quality.py +++ b/homeassistant/components/opensensemap/air_quality.py @@ -2,6 +2,8 @@ from datetime import timedelta import logging +from opensensemap_api import OpenSenseMap +from opensensemap_api.exceptions import OpenSenseMapError import voluptuous as vol from homeassistant.components.air_quality import PLATFORM_SCHEMA, AirQualityEntity @@ -26,7 +28,6 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the openSenseMap air quality platform.""" - from opensensemap_api import OpenSenseMap name = config.get(CONF_NAME) station_id = config[CONF_STATION_ID] @@ -88,7 +89,6 @@ def __init__(self, api): @Throttle(SCAN_INTERVAL) async def async_update(self): """Get the latest data from the Pi-hole.""" - from opensensemap_api.exceptions import OpenSenseMapError try: await self.api.get_data() diff --git a/homeassistant/components/opentherm_gw/__init__.py b/homeassistant/components/opentherm_gw/__init__.py index 643f80ae8f9d..3a1255e36978 100644 --- a/homeassistant/components/opentherm_gw/__init__.py +++ b/homeassistant/components/opentherm_gw/__init__.py @@ -1,4 +1,5 @@ """Support for OpenTherm Gateway devices.""" +import asyncio import logging from datetime import datetime, date @@ -344,6 +345,18 @@ async def set_setback_temp(call): ) +async def async_unload_entry(hass, entry): + """Cleanup and disconnect from gateway.""" + await asyncio.gather( + hass.config_entries.async_forward_entry_unload(entry, COMP_BINARY_SENSOR), + hass.config_entries.async_forward_entry_unload(entry, COMP_CLIMATE), + hass.config_entries.async_forward_entry_unload(entry, COMP_SENSOR), + ) + gateway = hass.data[DATA_OPENTHERM_GW][DATA_GATEWAYS][entry.data[CONF_ID]] + await gateway.cleanup() + return True + + class OpenThermGatewayDevice: """OpenTherm Gateway device class.""" @@ -358,18 +371,21 @@ def __init__(self, hass, config_entry): self.update_signal = f"{DATA_OPENTHERM_GW}_{self.gw_id}_update" self.options_update_signal = f"{DATA_OPENTHERM_GW}_{self.gw_id}_options_update" self.gateway = pyotgw.pyotgw() + self.gw_version = None + + async def cleanup(self, event=None): + """Reset overrides on the gateway.""" + await self.gateway.set_control_setpoint(0) + await self.gateway.set_max_relative_mod("-") + await self.gateway.disconnect() async def connect_and_subscribe(self): """Connect to serial device and subscribe report handler.""" - await self.gateway.connect(self.hass.loop, self.device_path) + self.status = await self.gateway.connect(self.hass.loop, self.device_path) _LOGGER.debug("Connected to OpenTherm Gateway at %s", self.device_path) + self.gw_version = self.status.get(gw_vars.OTGW_BUILD) - async def cleanup(event): - """Reset overrides on the gateway.""" - await self.gateway.set_control_setpoint(0) - await self.gateway.set_max_relative_mod("-") - - self.hass.bus.async_listen(EVENT_HOMEASSISTANT_STOP, cleanup) + self.hass.bus.async_listen(EVENT_HOMEASSISTANT_STOP, self.cleanup) async def handle_report(status): """Handle reports from the OpenTherm Gateway.""" diff --git a/homeassistant/components/opentherm_gw/binary_sensor.py b/homeassistant/components/opentherm_gw/binary_sensor.py index 36867feda61e..39fd78f5fe8f 100644 --- a/homeassistant/components/opentherm_gw/binary_sensor.py +++ b/homeassistant/components/opentherm_gw/binary_sensor.py @@ -7,6 +7,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import async_generate_entity_id +from . import DOMAIN from .const import BINARY_SENSOR_INFO, DATA_GATEWAYS, DATA_OPENTHERM_GW @@ -44,14 +45,27 @@ def __init__(self, gw_dev, var, device_class, friendly_name_format): self._state = None self._device_class = device_class self._friendly_name = friendly_name_format.format(gw_dev.name) + self._unsub_updates = None async def async_added_to_hass(self): """Subscribe to updates from the component.""" _LOGGER.debug("Added OpenTherm Gateway binary sensor %s", self._friendly_name) - async_dispatcher_connect( + self._unsub_updates = async_dispatcher_connect( self.hass, self._gateway.update_signal, self.receive_report ) + async def async_will_remove_from_hass(self): + """Unsubscribe from updates from the component.""" + _LOGGER.debug( + "Removing OpenTherm Gateway binary sensor %s", self._friendly_name + ) + self._unsub_updates() + + @property + def entity_registry_enabled_default(self): + """Disable binary_sensors by default.""" + return False + @callback def receive_report(self, status): """Handle status updates from the component.""" @@ -63,6 +77,22 @@ def name(self): """Return the friendly name.""" return self._friendly_name + @property + def device_info(self): + """Return device info.""" + return { + "identifiers": {(DOMAIN, self._gateway.gw_id)}, + "name": self._gateway.name, + "manufacturer": "Schelte Bron", + "model": "OpenTherm Gateway", + "sw_version": self._gateway.gw_version, + } + + @property + def unique_id(self): + """Return a unique ID.""" + return f"{self._gateway.gw_id}-{self._var}" + @property def is_on(self): """Return true if the binary sensor is on.""" diff --git a/homeassistant/components/opentherm_gw/climate.py b/homeassistant/components/opentherm_gw/climate.py index 44f143d64da2..8c21c6560c14 100644 --- a/homeassistant/components/opentherm_gw/climate.py +++ b/homeassistant/components/opentherm_gw/climate.py @@ -3,7 +3,7 @@ from pyotgw import vars as gw_vars -from homeassistant.components.climate import ClimateDevice +from homeassistant.components.climate import ClimateDevice, ENTITY_ID_FORMAT from homeassistant.components.climate.const import ( CURRENT_HVAC_COOL, CURRENT_HVAC_HEAT, @@ -25,12 +25,17 @@ ) from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity import async_generate_entity_id +from . import DOMAIN from .const import CONF_FLOOR_TEMP, CONF_PRECISION, DATA_GATEWAYS, DATA_OPENTHERM_GW _LOGGER = logging.getLogger(__name__) +DEFAULT_FLOOR_TEMP = False +DEFAULT_PRECISION = None + SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE | SUPPORT_PRESET_MODE @@ -53,9 +58,12 @@ class OpenThermClimate(ClimateDevice): def __init__(self, gw_dev, options): """Initialize the device.""" self._gateway = gw_dev + self.entity_id = async_generate_entity_id( + ENTITY_ID_FORMAT, gw_dev.gw_id, hass=gw_dev.hass + ) self.friendly_name = gw_dev.name - self.floor_temp = options[CONF_FLOOR_TEMP] - self.temp_precision = options.get(CONF_PRECISION) + self.floor_temp = options.get(CONF_FLOOR_TEMP, DEFAULT_FLOOR_TEMP) + self.temp_precision = options.get(CONF_PRECISION, DEFAULT_PRECISION) self._current_operation = None self._current_temperature = None self._hvac_mode = HVAC_MODE_HEAT @@ -65,24 +73,32 @@ def __init__(self, gw_dev, options): self._away_mode_b = None self._away_state_a = False self._away_state_b = False + self._unsub_options = None + self._unsub_updates = None @callback def update_options(self, entry): """Update climate entity options.""" self.floor_temp = entry.options[CONF_FLOOR_TEMP] - self.temp_precision = entry.options.get(CONF_PRECISION) + self.temp_precision = entry.options[CONF_PRECISION] self.async_schedule_update_ha_state() async def async_added_to_hass(self): """Connect to the OpenTherm Gateway device.""" _LOGGER.debug("Added OpenTherm Gateway climate device %s", self.friendly_name) - async_dispatcher_connect( + self._unsub_updates = async_dispatcher_connect( self.hass, self._gateway.update_signal, self.receive_report ) - async_dispatcher_connect( + self._unsub_options = async_dispatcher_connect( self.hass, self._gateway.options_update_signal, self.update_options ) + async def async_will_remove_from_hass(self): + """Unsubscribe from updates from the component.""" + _LOGGER.debug("Removing OpenTherm Gateway climate %s", self.friendly_name) + self._unsub_options() + self._unsub_updates() + @callback def receive_report(self, status): """Receive and handle a new report from the Gateway.""" @@ -136,6 +152,17 @@ def name(self): """Return the friendly name.""" return self.friendly_name + @property + def device_info(self): + """Return device info.""" + return { + "identifiers": {(DOMAIN, self._gateway.gw_id)}, + "name": self._gateway.name, + "manufacturer": "Schelte Bron", + "model": "OpenTherm Gateway", + "sw_version": self._gateway.gw_version, + } + @property def unique_id(self): """Return a unique ID.""" diff --git a/homeassistant/components/opentherm_gw/manifest.json b/homeassistant/components/opentherm_gw/manifest.json index a632096cd75e..990df0ba4e3c 100644 --- a/homeassistant/components/opentherm_gw/manifest.json +++ b/homeassistant/components/opentherm_gw/manifest.json @@ -3,7 +3,7 @@ "name": "Opentherm Gateway", "documentation": "https://www.home-assistant.io/integrations/opentherm_gw", "requirements": [ - "pyotgw==0.5b0" + "pyotgw==0.5b1" ], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/opentherm_gw/sensor.py b/homeassistant/components/opentherm_gw/sensor.py index c77a73cd1803..cd9ce9fb095a 100644 --- a/homeassistant/components/opentherm_gw/sensor.py +++ b/homeassistant/components/opentherm_gw/sensor.py @@ -7,6 +7,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import Entity, async_generate_entity_id +from . import DOMAIN from .const import DATA_GATEWAYS, DATA_OPENTHERM_GW, SENSOR_INFO @@ -47,14 +48,25 @@ def __init__(self, gw_dev, var, device_class, unit, friendly_name_format): self._device_class = device_class self._unit = unit self._friendly_name = friendly_name_format.format(gw_dev.name) + self._unsub_updates = None async def async_added_to_hass(self): """Subscribe to updates from the component.""" _LOGGER.debug("Added OpenTherm Gateway sensor %s", self._friendly_name) - async_dispatcher_connect( + self._unsub_updates = async_dispatcher_connect( self.hass, self._gateway.update_signal, self.receive_report ) + async def async_will_remove_from_hass(self): + """Unsubscribe from updates from the component.""" + _LOGGER.debug("Removing OpenTherm Gateway sensor %s", self._friendly_name) + self._unsub_updates() + + @property + def entity_registry_enabled_default(self): + """Disable sensors by default.""" + return False + @callback def receive_report(self, status): """Handle status updates from the component.""" @@ -69,6 +81,22 @@ def name(self): """Return the friendly name of the sensor.""" return self._friendly_name + @property + def device_info(self): + """Return device info.""" + return { + "identifiers": {(DOMAIN, self._gateway.gw_id)}, + "name": self._gateway.name, + "manufacturer": "Schelte Bron", + "model": "OpenTherm Gateway", + "sw_version": self._gateway.gw_version, + } + + @property + def unique_id(self): + """Return a unique ID.""" + return f"{self._gateway.gw_id}-{self._var}" + @property def device_class(self): """Return the device class.""" diff --git a/homeassistant/components/openuv/__init__.py b/homeassistant/components/openuv/__init__.py index 62a8c642bc8e..16b7a50a4aea 100644 --- a/homeassistant/components/openuv/__init__.py +++ b/homeassistant/components/openuv/__init__.py @@ -233,8 +233,12 @@ async def async_unload_entry(hass, config_entry): """Unload an OpenUV config entry.""" hass.data[DOMAIN][DATA_OPENUV_CLIENT].pop(config_entry.entry_id) - for component in ("binary_sensor", "sensor"): - await hass.config_entries.async_forward_entry_unload(config_entry, component) + tasks = [ + hass.config_entries.async_forward_entry_unload(config_entry, component) + for component in ("binary_sensor", "sensor") + ] + + await asyncio.gather(*tasks) return True diff --git a/homeassistant/components/opple/light.py b/homeassistant/components/opple/light.py index 5a6657a323d4..9ee53704d108 100644 --- a/homeassistant/components/opple/light.py +++ b/homeassistant/components/opple/light.py @@ -1,6 +1,7 @@ """Support for the Opple light.""" import logging +from pyoppleio.OppleLightDevice import OppleLightDevice import voluptuous as vol from homeassistant.components.light import ( @@ -46,7 +47,6 @@ class OppleLight(Light): def __init__(self, name, host): """Initialize an Opple light.""" - from pyoppleio.OppleLightDevice import OppleLightDevice self._device = OppleLightDevice(host) diff --git a/homeassistant/components/orvibo/switch.py b/homeassistant/components/orvibo/switch.py index 38d0d2c05d4b..75a95e053ae1 100644 --- a/homeassistant/components/orvibo/switch.py +++ b/homeassistant/components/orvibo/switch.py @@ -1,15 +1,16 @@ """Support for Orvibo S20 Wifi Smart Switches.""" import logging +from orvibo.s20 import S20, S20Exception, discover import voluptuous as vol -from homeassistant.components.switch import SwitchDevice, PLATFORM_SCHEMA +from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchDevice from homeassistant.const import ( + CONF_DISCOVERY, CONF_HOST, + CONF_MAC, CONF_NAME, CONF_SWITCHES, - CONF_MAC, - CONF_DISCOVERY, ) import homeassistant.helpers.config_validation as cv @@ -37,7 +38,6 @@ def setup_platform(hass, config, add_entities_callback, discovery_info=None): """Set up S20 switches.""" - from orvibo.s20 import discover, S20, S20Exception switch_data = {} switches = [] @@ -67,7 +67,6 @@ class S20Switch(SwitchDevice): def __init__(self, name, s20): """Initialize the S20 device.""" - from orvibo.s20 import S20Exception self._name = name self._s20 = s20 diff --git a/homeassistant/components/owlet/__init__.py b/homeassistant/components/owlet/__init__.py index f9543c7fa6e6..afde50cae49a 100644 --- a/homeassistant/components/owlet/__init__.py +++ b/homeassistant/components/owlet/__init__.py @@ -1,6 +1,7 @@ """Support for Owlet baby monitors.""" import logging +from pyowlet.PyOwlet import PyOwlet import voluptuous as vol from homeassistant.const import CONF_NAME, CONF_PASSWORD, CONF_USERNAME @@ -41,7 +42,6 @@ def setup(hass, config): """Set up owlet component.""" - from pyowlet.PyOwlet import PyOwlet username = config[DOMAIN][CONF_USERNAME] password = config[DOMAIN][CONF_PASSWORD] diff --git a/homeassistant/components/owntracks/config_flow.py b/homeassistant/components/owntracks/config_flow.py index a59cd869c74e..ff4a649e0ce7 100644 --- a/homeassistant/components/owntracks/config_flow.py +++ b/homeassistant/components/owntracks/config_flow.py @@ -4,21 +4,12 @@ from homeassistant.auth.util import generate_secret from .const import DOMAIN # noqa pylint: disable=unused-import +from .helper import supports_encryption CONF_SECRET = "secret" CONF_CLOUDHOOK = "cloudhook" -def supports_encryption(): - """Test if we support encryption.""" - try: - import nacl # noqa pylint: disable=unused-import - - return True - except OSError: - return False - - class OwnTracksFlow(config_entries.ConfigFlow, domain=DOMAIN): """Set up OwnTracks.""" diff --git a/homeassistant/components/owntracks/helper.py b/homeassistant/components/owntracks/helper.py new file mode 100644 index 000000000000..b6ed307112cc --- /dev/null +++ b/homeassistant/components/owntracks/helper.py @@ -0,0 +1,10 @@ +"""Helper for OwnTracks.""" +try: + import nacl +except ImportError: + nacl = None + + +def supports_encryption() -> bool: + """Test if we support encryption.""" + return nacl is not None diff --git a/homeassistant/components/owntracks/messages.py b/homeassistant/components/owntracks/messages.py index 0cb65c774b57..7c388f9eb17d 100644 --- a/homeassistant/components/owntracks/messages.py +++ b/homeassistant/components/owntracks/messages.py @@ -2,6 +2,9 @@ import json import logging +from nacl.secret import SecretBox +from nacl.encoding import Base64Encoder + from homeassistant.components import zone as zone_comp from homeassistant.components.device_tracker import ( SOURCE_TYPE_GPS, @@ -11,6 +14,7 @@ from homeassistant.const import STATE_HOME from homeassistant.util import decorator, slugify +from .helper import supports_encryption _LOGGER = logging.getLogger(__name__) @@ -22,8 +26,6 @@ def get_cipher(): Async friendly. """ - from nacl.secret import SecretBox - from nacl.encoding import Base64Encoder def decrypt(ciphertext, key): """Decrypt ciphertext using key.""" @@ -105,7 +107,11 @@ def _set_gps_from_zone(kwargs, location, zone): def _decrypt_payload(secret, topic, ciphertext): """Decrypt encrypted payload.""" try: - keylen, decrypt = get_cipher() + if supports_encryption(): + keylen, decrypt = get_cipher() + else: + _LOGGER.warning("Ignoring encrypted payload because nacl not installed") + return None except OSError: _LOGGER.warning("Ignoring encrypted payload because nacl not installed") return None diff --git a/homeassistant/components/panasonic_viera/media_player.py b/homeassistant/components/panasonic_viera/media_player.py index 0b19a8fa5527..d0615edfc330 100644 --- a/homeassistant/components/panasonic_viera/media_player.py +++ b/homeassistant/components/panasonic_viera/media_player.py @@ -21,6 +21,7 @@ SUPPORT_VOLUME_STEP, ) from homeassistant.const import ( + CONF_BROADCAST_ADDRESS, CONF_HOST, CONF_MAC, CONF_NAME, @@ -36,6 +37,7 @@ DEFAULT_NAME = "Panasonic Viera TV" DEFAULT_PORT = 55000 +DEFAULT_BROADCAST_ADDRESS = "255.255.255.255" DEFAULT_APP_POWER = False SUPPORT_VIERATV = ( @@ -55,6 +57,9 @@ { vol.Required(CONF_HOST): cv.string, vol.Optional(CONF_MAC): cv.string, + vol.Optional( + CONF_BROADCAST_ADDRESS, default=DEFAULT_BROADCAST_ADDRESS + ): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, vol.Optional(CONF_APP_POWER, default=DEFAULT_APP_POWER): cv.boolean, @@ -65,6 +70,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Panasonic Viera TV platform.""" mac = config.get(CONF_MAC) + broadcast = config.get(CONF_BROADCAST_ADDRESS) name = config.get(CONF_NAME) port = config.get(CONF_PORT) app_power = config.get(CONF_APP_POWER) @@ -86,14 +92,16 @@ def setup_platform(hass, config, add_entities, discovery_info=None): host = config.get(CONF_HOST) remote = RemoteControl(host, port) - add_entities([PanasonicVieraTVDevice(mac, name, remote, host, app_power)]) + add_entities( + [PanasonicVieraTVDevice(mac, name, remote, host, broadcast, app_power)] + ) return True class PanasonicVieraTVDevice(MediaPlayerDevice): """Representation of a Panasonic Viera TV.""" - def __init__(self, mac, name, remote, host, app_power, uuid=None): + def __init__(self, mac, name, remote, host, broadcast, app_power, uuid=None): """Initialize the Panasonic device.""" # Save a reference to the imported class self._wol = wakeonlan @@ -105,6 +113,7 @@ def __init__(self, mac, name, remote, host, app_power, uuid=None): self._state = None self._remote = remote self._host = host + self._broadcast = broadcast self._volume = 0 self._app_power = app_power @@ -162,7 +171,7 @@ def supported_features(self): def turn_on(self): """Turn on the media player.""" if self._mac: - self._wol.send_magic_packet(self._mac, ip_address=self._host) + self._wol.send_magic_packet(self._mac, ip_address=self._broadcast) self._state = STATE_ON elif self._app_power: self._remote.turn_on() diff --git a/homeassistant/components/pencom/switch.py b/homeassistant/components/pencom/switch.py index 60e7ef30837f..36266feaa6e9 100644 --- a/homeassistant/components/pencom/switch.py +++ b/homeassistant/components/pencom/switch.py @@ -5,10 +5,11 @@ """ import logging +from pencompy.pencompy import Pencompy import voluptuous as vol -from homeassistant.components.switch import SwitchDevice, PLATFORM_SCHEMA -from homeassistant.const import CONF_HOST, CONF_PORT, CONF_NAME +from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchDevice +from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv @@ -39,7 +40,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Pencom relay platform (pencompy).""" - from pencompy.pencompy import Pencompy # Assign configuration variables. host = config[CONF_HOST] diff --git a/homeassistant/components/philips_js/media_player.py b/homeassistant/components/philips_js/media_player.py index 579dc2536032..fe6d7edf8040 100644 --- a/homeassistant/components/philips_js/media_player.py +++ b/homeassistant/components/philips_js/media_player.py @@ -2,11 +2,14 @@ from datetime import timedelta import logging +from haphilipsjs import PhilipsTV import voluptuous as vol -from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA +from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice from homeassistant.components.media_player.const import ( + MEDIA_TYPE_CHANNEL, SUPPORT_NEXT_TRACK, + SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK, SUPPORT_SELECT_SOURCE, SUPPORT_TURN_OFF, @@ -14,8 +17,6 @@ SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, SUPPORT_VOLUME_STEP, - MEDIA_TYPE_CHANNEL, - SUPPORT_PLAY_MEDIA, ) from homeassistant.const import ( CONF_API_VERSION, @@ -70,20 +71,18 @@ def _inverted(data): def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Philips TV platform.""" - import haphilipsjs - name = config.get(CONF_NAME) host = config.get(CONF_HOST) api_version = config.get(CONF_API_VERSION) turn_on_action = config.get(CONF_ON_ACTION) - tvapi = haphilipsjs.PhilipsTV(host, api_version) + tvapi = PhilipsTV(host, api_version) on_script = Script(hass, turn_on_action) if turn_on_action else None - add_entities([PhilipsTV(tvapi, name, on_script)]) + add_entities([PhilipsTVMediaPlayer(tvapi, name, on_script)]) -class PhilipsTV(MediaPlayerDevice): +class PhilipsTVMediaPlayer(MediaPlayerDevice): """Representation of a Philips TV exposing the JointSpace API.""" def __init__(self, tv, name, on_script): diff --git a/homeassistant/components/pilight/__init__.py b/homeassistant/components/pilight/__init__.py index 2688b15e837c..e8cc0862a53f 100644 --- a/homeassistant/components/pilight/__init__.py +++ b/homeassistant/components/pilight/__init__.py @@ -7,6 +7,8 @@ import voluptuous as vol +from pilight import pilight + from homeassistant.helpers.event import track_point_in_utc_time from homeassistant.util import dt as dt_util import homeassistant.helpers.config_validation as cv @@ -59,7 +61,6 @@ def setup(hass, config): """Set up the Pilight component.""" - from pilight import pilight host = config[DOMAIN][CONF_HOST] port = config[DOMAIN][CONF_PORT] diff --git a/homeassistant/components/pilight/services.yaml b/homeassistant/components/pilight/services.yaml index e69de29bb2d1..cc6141fdd91e 100644 --- a/homeassistant/components/pilight/services.yaml +++ b/homeassistant/components/pilight/services.yaml @@ -0,0 +1,6 @@ +send: + description: Send RF code to Pilight device + fields: + protocol: + description: 'Protocol that Pilight recognizes. See https://manual.pilight.org/protocols/index.html for supported protocols and additional parameters that each protocol supports' + example: 'lirc' diff --git a/homeassistant/components/plaato/.translations/pl.json b/homeassistant/components/plaato/.translations/pl.json index c4402cb8f37f..dc931b6bd8c4 100644 --- a/homeassistant/components/plaato/.translations/pl.json +++ b/homeassistant/components/plaato/.translations/pl.json @@ -5,7 +5,7 @@ "one_instance_allowed": "Wymagana jest tylko jedna instancja." }, "create_entry": { - "default": "Aby wysy\u0142a\u0107 zdarzenia do Home Assistant'a, musisz skonfigurowa\u0107 webhook w Plaato Airlock. \n\n Wprowad\u017a nast\u0119puj\u0105ce dane:\n\n - URL: `{webhook_url}` \n - Metoda: POST \n\nZapoznaj si\u0119 z [dokumentacj\u0105]({docs_url}) by pozna\u0107 szczeg\u00f3\u0142y." + "default": "Aby wysy\u0142a\u0107 zdarzenia do Home Assistant'a, musisz skonfigurowa\u0107 webhook w Plaato Airlock. \n\n Wprowad\u017a nast\u0119puj\u0105ce dane:\n\n - URL: `{webhook_url}` \n - Metoda: POST \n\nZapoznaj si\u0119 z [dokumentacj\u0105]({docs_url}), by pozna\u0107 szczeg\u00f3\u0142y." }, "step": { "user": { diff --git a/homeassistant/components/plex/.translations/bg.json b/homeassistant/components/plex/.translations/bg.json index 9a2ffe299c8c..adfdd98ebaff 100644 --- a/homeassistant/components/plex/.translations/bg.json +++ b/homeassistant/components/plex/.translations/bg.json @@ -6,6 +6,7 @@ "already_in_progress": "Plex \u0441\u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430", "discovery_no_file": "\u041d\u0435 \u0435 \u043d\u0430\u043c\u0435\u0440\u0435\u043d \u0441\u0442\u0430\u0440 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u043e\u043d\u0435\u043d \u0444\u0430\u0439\u043b", "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", + "non-interactive": "\u041d\u0435\u0438\u043d\u0442\u0435\u0440\u0430\u043a\u0442\u0438\u0432\u0435\u043d \u0438\u043c\u043f\u043e\u0440\u0442", "token_request_timeout": "\u0418\u0437\u0442\u0435\u0447\u0435 \u0432\u0440\u0435\u043c\u0435\u0442\u043e \u0437\u0430 \u043f\u043e\u043b\u0443\u0447\u0430\u0432\u0430\u043d\u0435 \u043d\u0430 \u043a\u043e\u0434 \u0437\u0430 \u0434\u043e\u0441\u0442\u044a\u043f", "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" }, @@ -42,7 +43,7 @@ "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.", + "description": "\u041f\u0440\u043e\u0434\u044a\u043b\u0436\u0435\u0442\u0435 \u0441 \u043e\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u044f\u0442\u0430 \u043d\u0430 plex.tv \u0438\u043b\u0438 \u0440\u044a\u0447\u043d\u043e \u0434\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u0442\u0435 \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" } }, diff --git a/homeassistant/components/plex/.translations/ca.json b/homeassistant/components/plex/.translations/ca.json index 9eb6d16639f4..63cf65b8d6c1 100644 --- a/homeassistant/components/plex/.translations/ca.json +++ b/homeassistant/components/plex/.translations/ca.json @@ -6,6 +6,7 @@ "already_in_progress": "S\u2019est\u00e0 configurant Plex", "discovery_no_file": "No s'ha trobat cap fitxer de configuraci\u00f3 heretat", "invalid_import": "La configuraci\u00f3 importada \u00e9s inv\u00e0lida", + "non-interactive": "Importaci\u00f3 no interactiva", "token_request_timeout": "S'ha acabat el temps d'espera durant l'obtenci\u00f3 del testimoni.", "unknown": "Ha fallat per motiu desconegut" }, diff --git a/homeassistant/components/plex/.translations/en.json b/homeassistant/components/plex/.translations/en.json index bf927b7f1be0..31211182f471 100644 --- a/homeassistant/components/plex/.translations/en.json +++ b/homeassistant/components/plex/.translations/en.json @@ -6,6 +6,7 @@ "already_in_progress": "Plex is being configured", "discovery_no_file": "No legacy config file found", "invalid_import": "Imported configuration is invalid", + "non-interactive": "Non-interactive import", "token_request_timeout": "Timed out obtaining token", "unknown": "Failed for unknown reason" }, diff --git a/homeassistant/components/plex/.translations/fr.json b/homeassistant/components/plex/.translations/fr.json index 2eef7a5e9a2f..bcd53d2ffae4 100644 --- a/homeassistant/components/plex/.translations/fr.json +++ b/homeassistant/components/plex/.translations/fr.json @@ -6,6 +6,7 @@ "already_in_progress": "Plex en cours de configuration", "discovery_no_file": "Aucun fichier de configuration h\u00e9rit\u00e9 trouv\u00e9", "invalid_import": "La configuration import\u00e9e est invalide", + "non-interactive": "Importation non interactive", "token_request_timeout": "D\u00e9lai d'obtention du jeton", "unknown": "\u00c9chec pour une raison inconnue" }, diff --git a/homeassistant/components/plex/.translations/it.json b/homeassistant/components/plex/.translations/it.json index 8f61f968dba8..06c20660fef7 100644 --- a/homeassistant/components/plex/.translations/it.json +++ b/homeassistant/components/plex/.translations/it.json @@ -6,6 +6,7 @@ "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", + "non-interactive": "Importazione non interattiva", "token_request_timeout": "Timeout per l'ottenimento del token", "unknown": "Non riuscito per motivo sconosciuto" }, diff --git a/homeassistant/components/plex/.translations/lb.json b/homeassistant/components/plex/.translations/lb.json index 7b0f7232976a..c6fcabc40d7c 100644 --- a/homeassistant/components/plex/.translations/lb.json +++ b/homeassistant/components/plex/.translations/lb.json @@ -6,6 +6,7 @@ "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", + "non-interactive": "Net interaktiven Import", "token_request_timeout": "Z\u00e4it Iwwerschreidung beim kr\u00e9ien vum Jeton", "unknown": "Onbekannte Feeler opgetrueden" }, diff --git a/homeassistant/components/plex/.translations/no.json b/homeassistant/components/plex/.translations/no.json index 8ebd2b69bb93..cc6dac8a35bb 100644 --- a/homeassistant/components/plex/.translations/no.json +++ b/homeassistant/components/plex/.translations/no.json @@ -6,6 +6,7 @@ "already_in_progress": "Plex blir konfigurert", "discovery_no_file": "Ingen eldre konfigurasjonsfil ble funnet", "invalid_import": "Den importerte konfigurasjonen er ugyldig", + "non-interactive": "Ikke-interaktiv import", "token_request_timeout": "Tidsavbrudd ved innhenting av token", "unknown": "Mislyktes av ukjent \u00e5rsak" }, diff --git a/homeassistant/components/plex/.translations/pl.json b/homeassistant/components/plex/.translations/pl.json index b4ed6134106b..d752899b9f0e 100644 --- a/homeassistant/components/plex/.translations/pl.json +++ b/homeassistant/components/plex/.translations/pl.json @@ -6,6 +6,7 @@ "already_in_progress": "Plex jest konfigurowany", "discovery_no_file": "Nie znaleziono pliku konfiguracyjnego", "invalid_import": "Zaimportowana konfiguracja jest nieprawid\u0142owa", + "non-interactive": "Nieinteraktywny import", "token_request_timeout": "Przekroczono limit czasu na uzyskanie tokena.", "unknown": "Nieznany b\u0142\u0105d" }, diff --git a/homeassistant/components/plex/.translations/ru.json b/homeassistant/components/plex/.translations/ru.json index bce55d35baa4..334a4e353d4c 100644 --- a/homeassistant/components/plex/.translations/ru.json +++ b/homeassistant/components/plex/.translations/ru.json @@ -6,12 +6,13 @@ "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.", + "non-interactive": "\u041d\u0435\u0438\u043d\u0442\u0435\u0440\u0430\u043a\u0442\u0438\u0432\u043d\u044b\u0439 \u0438\u043c\u043f\u043e\u0440\u0442.", "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_servers": "\u041d\u0435\u0442 \u0441\u0435\u0440\u0432\u0435\u0440\u043e\u0432, \u0441\u0432\u044f\u0437\u0430\u043d\u043d\u044b\u0445 \u0441 \u0443\u0447\u0451\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u044c\u044e.", "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." }, diff --git a/homeassistant/components/plex/.translations/sl.json b/homeassistant/components/plex/.translations/sl.json index 7426e7f95edd..1ff93cff650b 100644 --- a/homeassistant/components/plex/.translations/sl.json +++ b/homeassistant/components/plex/.translations/sl.json @@ -6,6 +6,7 @@ "already_in_progress": "Plex se konfigurira", "discovery_no_file": "Podatkovne konfiguracijske datoteke ni bilo mogo\u010de najti", "invalid_import": "Uvo\u017eena konfiguracija ni veljavna", + "non-interactive": "Neinteraktivni uvoz", "token_request_timeout": "Potekla \u010dasovna omejitev za pridobitev \u017eetona", "unknown": "Ni uspelo iz neznanega razloga" }, diff --git a/homeassistant/components/plex/.translations/zh-Hant.json b/homeassistant/components/plex/.translations/zh-Hant.json index 2d4ce1ea6aa2..5c05d2104f9b 100644 --- a/homeassistant/components/plex/.translations/zh-Hant.json +++ b/homeassistant/components/plex/.translations/zh-Hant.json @@ -6,6 +6,7 @@ "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", + "non-interactive": "\u7121\u4e92\u52d5\u532f\u5165", "token_request_timeout": "\u53d6\u5f97\u5bc6\u9470\u903e\u6642", "unknown": "\u672a\u77e5\u539f\u56e0\u5931\u6557" }, diff --git a/homeassistant/components/plex/config_flow.py b/homeassistant/components/plex/config_flow.py index cb79c08b16e3..350f1b3d5773 100644 --- a/homeassistant/components/plex/config_flow.py +++ b/homeassistant/components/plex/config_flow.py @@ -80,12 +80,17 @@ async def async_step_server_validate(self, server_config): """Validate a provided configuration.""" errors = {} self.current_login = server_config + is_importing = ( + self.context["source"] # pylint: disable=no-member + == config_entries.SOURCE_IMPORT + ) plex_server = PlexServer(self.hass, server_config) try: await self.hass.async_add_executor_job(plex_server.connect) except NoServersFound: + _LOGGER.error("No servers linked to Plex account") errors["base"] = "no_servers" except (plexapi.exceptions.BadRequest, plexapi.exceptions.Unauthorized): _LOGGER.error("Invalid credentials provided, config not created") @@ -98,6 +103,11 @@ async def async_step_server_validate(self, server_config): errors["base"] = "not_found" except ServerNotSpecified as available_servers: + if is_importing: + _LOGGER.warning( + "Imported configuration has multiple available Plex servers. Specify server in configuration or add a new Integration." + ) + return self.async_abort(reason="non-interactive") self.available_servers = available_servers.args[0] return await self.async_step_select_server() @@ -106,12 +116,17 @@ async def async_step_server_validate(self, server_config): return self.async_abort(reason="unknown") if errors: + if is_importing: + return self.async_abort(reason="non-interactive") 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: + _LOGGER.debug( + "Plex server already configured: %s", entry.data[CONF_SERVER] + ) return self.async_abort(reason="already_configured") url = plex_server.url_in_use diff --git a/homeassistant/components/plex/manifest.json b/homeassistant/components/plex/manifest.json index 29bdbf34b60f..922a9c142884 100644 --- a/homeassistant/components/plex/manifest.json +++ b/homeassistant/components/plex/manifest.json @@ -6,7 +6,7 @@ "requirements": [ "plexapi==3.3.0", "plexauth==0.0.5", - "plexwebsocket==0.0.5" + "plexwebsocket==0.0.6" ], "dependencies": [ "http" diff --git a/homeassistant/components/plex/server.py b/homeassistant/components/plex/server.py index 69838fbf27fb..46602cf6552e 100644 --- a/homeassistant/components/plex/server.py +++ b/homeassistant/components/plex/server.py @@ -77,7 +77,7 @@ def _connect_with_token(): self.server_choice = ( self._server_name if self._server_name else available_servers[0][0] ) - self._plex_server = account.resource(self.server_choice).connect() + self._plex_server = account.resource(self.server_choice).connect(timeout=10) def _connect_with_url(): session = None @@ -192,7 +192,7 @@ def machine_identifier(self): @property def url_in_use(self): """Return URL used for connected Plex server.""" - return self._plex_server._baseurl # pylint: disable=W0212 + return self._plex_server._baseurl # pylint: disable=protected-access @property def use_episode_art(self): diff --git a/homeassistant/components/plex/strings.json b/homeassistant/components/plex/strings.json index aff79acc2ed4..b6491db350cf 100644 --- a/homeassistant/components/plex/strings.json +++ b/homeassistant/components/plex/strings.json @@ -25,6 +25,7 @@ "already_in_progress": "Plex is being configured", "discovery_no_file": "No legacy config file found", "invalid_import": "Imported configuration is invalid", + "non-interactive": "Non-interactive import", "token_request_timeout": "Timed out obtaining token", "unknown": "Failed for unknown reason" } diff --git a/homeassistant/components/plum_lightpad/__init__.py b/homeassistant/components/plum_lightpad/__init__.py index 67a3b60e8bad..bfdf67a0f400 100644 --- a/homeassistant/components/plum_lightpad/__init__.py +++ b/homeassistant/components/plum_lightpad/__init__.py @@ -2,6 +2,7 @@ import asyncio import logging +from plumlightpad import Plum import voluptuous as vol from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, EVENT_HOMEASSISTANT_STOP @@ -30,7 +31,6 @@ async def async_setup(hass, config): """Plum Lightpad Platform initialization.""" - from plumlightpad import Plum conf = config[DOMAIN] plum = Plum(conf[CONF_USERNAME], conf[CONF_PASSWORD]) diff --git a/homeassistant/components/point/.translations/pl.json b/homeassistant/components/point/.translations/pl.json index 40acc8b4e496..4de46c841379 100644 --- a/homeassistant/components/point/.translations/pl.json +++ b/homeassistant/components/point/.translations/pl.json @@ -5,7 +5,7 @@ "authorize_url_fail": "Nieznany b\u0142\u0105d podczas generowania url autoryzacji.", "authorize_url_timeout": "Przekroczono limit czasu generowania URL autoryzacji.", "external_setup": "Punkt pomy\u015blnie skonfigurowany.", - "no_flows": "Musisz skonfigurowa\u0107 Point, zanim b\u0119dziesz m\u00f3g\u0142 si\u0119 z nim uwierzytelni\u0107. [Przeczytaj instrukcj\u0119](https://www.home-assistant.io/components/point/)." + "no_flows": "Musisz skonfigurowa\u0107 Point, aby m\u00f3c si\u0119 z nim uwierzytelni\u0107. Zapoznaj si\u0119 z [instrukcj\u0105](https://www.home-assistant.io/components/point/)." }, "create_entry": { "default": "Pomy\u015blnie uwierzytelniono przy u\u017cyciu Minut dla urz\u0105dze\u0144 Point" diff --git a/homeassistant/components/point/.translations/ru.json b/homeassistant/components/point/.translations/ru.json index 487510969481..b0fc5a61f724 100644 --- a/homeassistant/components/point/.translations/ru.json +++ b/homeassistant/components/point/.translations/ru.json @@ -16,7 +16,7 @@ }, "step": { "auth": { - "description": "\u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043f\u043e [\u0441\u0441\u044b\u043b\u043a\u0435]({authorization_url}) \u0438 \u0420\u0430\u0437\u0440\u0435\u0448\u0438\u0442\u0435 \u0434\u043e\u0441\u0442\u0443\u043f \u043a \u0412\u0430\u0448\u0435\u0439 \u0443\u0447\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 Minut, \u0437\u0430\u0442\u0435\u043c \u0432\u0435\u0440\u043d\u0438\u0442\u0435\u0441\u044c \u0441\u044e\u0434\u0430 \u0438 \u043d\u0430\u0436\u043c\u0438\u0442\u0435 \u041f\u041e\u0414\u0422\u0412\u0415\u0420\u0414\u0418\u0422\u042c.", + "description": "\u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043f\u043e [\u0441\u0441\u044b\u043b\u043a\u0435]({authorization_url}) \u0438 \u0420\u0430\u0437\u0440\u0435\u0448\u0438\u0442\u0435 \u0434\u043e\u0441\u0442\u0443\u043f \u043a \u0412\u0430\u0448\u0435\u0439 \u0443\u0447\u0451\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 Minut, \u0437\u0430\u0442\u0435\u043c \u0432\u0435\u0440\u043d\u0438\u0442\u0435\u0441\u044c \u0441\u044e\u0434\u0430 \u0438 \u043d\u0430\u0436\u043c\u0438\u0442\u0435 \u041f\u041e\u0414\u0422\u0412\u0415\u0420\u0414\u0418\u0422\u042c.", "title": "Minut Point" }, "user": { diff --git a/homeassistant/components/point/__init__.py b/homeassistant/components/point/__init__.py index e9885891553b..9abae9ab0254 100644 --- a/homeassistant/components/point/__init__.py +++ b/homeassistant/components/point/__init__.py @@ -2,6 +2,7 @@ import asyncio import logging +from pypoint import PointSession import voluptuous as vol from homeassistant import config_entries @@ -17,7 +18,7 @@ from homeassistant.helpers.typing import HomeAssistantType from homeassistant.util.dt import as_local, parse_datetime, utc_from_timestamp -from . import config_flow # noqa pylint_disable=unused-import +from . import config_flow from .const import ( CONF_WEBHOOK_URL, DOMAIN, @@ -71,7 +72,6 @@ async def async_setup(hass, config): async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry): """Set up Point from a config entry.""" - from pypoint import PointSession def token_saver(token): _LOGGER.debug("Saving updated token") diff --git a/homeassistant/components/point/alarm_control_panel.py b/homeassistant/components/point/alarm_control_panel.py index f9e725f6c8e5..e86b3dd42e87 100644 --- a/homeassistant/components/point/alarm_control_panel.py +++ b/homeassistant/components/point/alarm_control_panel.py @@ -2,6 +2,7 @@ import logging from homeassistant.components.alarm_control_panel import DOMAIN, AlarmControlPanel +from homeassistant.components.alarm_control_panel.const import SUPPORT_ALARM_ARM_AWAY from homeassistant.const import ( STATE_ALARM_ARMED_AWAY, STATE_ALARM_DISARMED, @@ -88,6 +89,11 @@ def state(self): """Return state of the device.""" return EVENT_MAP.get(self._home["alarm_status"], STATE_ALARM_ARMED_AWAY) + @property + def supported_features(self) -> int: + """Return the list of supported features.""" + return SUPPORT_ALARM_ARM_AWAY + @property def changed_by(self): """Return the user the last change was triggered by.""" diff --git a/homeassistant/components/point/config_flow.py b/homeassistant/components/point/config_flow.py index f354411ab42d..3312931085ef 100644 --- a/homeassistant/components/point/config_flow.py +++ b/homeassistant/components/point/config_flow.py @@ -4,6 +4,7 @@ import logging import async_timeout +from pypoint import PointSession import voluptuous as vol from homeassistant import config_entries @@ -109,7 +110,6 @@ async def async_step_auth(self, user_input=None): async def _get_authorization_url(self): """Create Minut Point session and get authorization url.""" - from pypoint import PointSession flow = self.hass.data[DATA_FLOW_IMPL][self.flow_impl] client_id = flow[CLIENT_ID] @@ -138,7 +138,6 @@ async def async_step_code(self, code=None): async def _async_create_session(self, code): """Create point session and entries.""" - from pypoint import PointSession flow = self.hass.data[DATA_FLOW_IMPL][DOMAIN] client_id = flow[CLIENT_ID] diff --git a/homeassistant/components/postnl/sensor.py b/homeassistant/components/postnl/sensor.py index 6155f58519a6..2e1f8176835b 100644 --- a/homeassistant/components/postnl/sensor.py +++ b/homeassistant/components/postnl/sensor.py @@ -2,6 +2,7 @@ from datetime import timedelta import logging +from postnl_api import PostNL_API, UnauthorizedException import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -36,7 +37,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the PostNL sensor platform.""" - from postnl_api import PostNL_API, UnauthorizedException username = config.get(CONF_USERNAME) password = config.get(CONF_PASSWORD) diff --git a/homeassistant/components/prezzibenzina/sensor.py b/homeassistant/components/prezzibenzina/sensor.py index f1f41ba46bad..c985f96e6c60 100644 --- a/homeassistant/components/prezzibenzina/sensor.py +++ b/homeassistant/components/prezzibenzina/sensor.py @@ -3,6 +3,7 @@ from datetime import timedelta import logging +from prezzibenzina import PrezziBenzinaPy import voluptuous as vol from homeassistant.const import ATTR_ATTRIBUTION, ATTR_TIME, CONF_NAME @@ -43,7 +44,6 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the PrezziBenzina sensor platform.""" - from prezzibenzina import PrezziBenzinaPy station = config[CONF_STATION] name = config.get(CONF_NAME) diff --git a/homeassistant/components/prometheus/__init__.py b/homeassistant/components/prometheus/__init__.py index 8eeb9325bc0a..71d56cda18a1 100644 --- a/homeassistant/components/prometheus/__init__.py +++ b/homeassistant/components/prometheus/__init__.py @@ -284,15 +284,6 @@ def _handle_climate(self, state): ) metric.labels(**self._labels(state)).set(current_temp) - metric = self._metric( - "climate_state", self.prometheus_cli.Gauge, "State of the thermostat (0/1)" - ) - try: - value = self.state_as_number(state) - metric.labels(**self._labels(state)).set(value) - except ValueError: - pass - def _handle_sensor(self, state): unit = self._unit_string(state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)) diff --git a/homeassistant/components/proxmoxve/__init__.py b/homeassistant/components/proxmoxve/__init__.py new file mode 100644 index 000000000000..246dc2d48ade --- /dev/null +++ b/homeassistant/components/proxmoxve/__init__.py @@ -0,0 +1,154 @@ +"""Support for Proxmox VE.""" +from enum import Enum +import logging +import time + +from proxmoxer import ProxmoxAPI +from proxmoxer.backends.https import AuthenticationError +import voluptuous as vol + +from homeassistant.const import ( + CONF_HOST, + CONF_PASSWORD, + CONF_PORT, + CONF_USERNAME, + CONF_VERIFY_SSL, +) +import homeassistant.helpers.config_validation as cv + +_LOGGER = logging.getLogger(__name__) + +DOMAIN = "proxmoxve" +PROXMOX_CLIENTS = "proxmox_clients" +CONF_REALM = "realm" +CONF_NODE = "node" +CONF_NODES = "nodes" +CONF_VMS = "vms" +CONF_CONTAINERS = "containers" + +DEFAULT_PORT = 8006 +DEFAULT_REALM = "pam" +DEFAULT_VERIFY_SSL = True + +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: vol.All( + cv.ensure_list, + [ + vol.Schema( + { + vol.Required(CONF_HOST): cv.string, + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Optional(CONF_REALM, default=DEFAULT_REALM): cv.string, + vol.Optional( + CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL + ): cv.boolean, + vol.Required(CONF_NODES): vol.All( + cv.ensure_list, + [ + vol.Schema( + { + vol.Required(CONF_NODE): cv.string, + vol.Optional(CONF_VMS, default=[]): [ + cv.positive_int + ], + vol.Optional(CONF_CONTAINERS, default=[]): [ + cv.positive_int + ], + } + ) + ], + ), + } + ) + ], + ) + }, + extra=vol.ALLOW_EXTRA, +) + + +def setup(hass, config): + """Set up the component.""" + + # Create API Clients for later use + hass.data[PROXMOX_CLIENTS] = {} + for entry in config[DOMAIN]: + host = entry[CONF_HOST] + port = entry[CONF_PORT] + user = entry[CONF_USERNAME] + realm = entry[CONF_REALM] + password = entry[CONF_PASSWORD] + verify_ssl = entry[CONF_VERIFY_SSL] + + try: + # Construct an API client with the given data for the given host + proxmox_client = ProxmoxClient( + host, port, user, realm, password, verify_ssl + ) + proxmox_client.build_client() + except AuthenticationError: + _LOGGER.warning( + "Invalid credentials for proxmox instance %s:%d", host, port + ) + continue + + hass.data[PROXMOX_CLIENTS][f"{host}:{port}"] = proxmox_client + + if hass.data[PROXMOX_CLIENTS]: + hass.helpers.discovery.load_platform( + "binary_sensor", DOMAIN, {"entries": config[DOMAIN]}, config + ) + return True + + return False + + +class ProxmoxItemType(Enum): + """Represents the different types of machines in Proxmox.""" + + qemu = 0 + lxc = 1 + + +class ProxmoxClient: + """A wrapper for the proxmoxer ProxmoxAPI client.""" + + def __init__(self, host, port, user, realm, password, verify_ssl): + """Initialize the ProxmoxClient.""" + + self._host = host + self._port = port + self._user = user + self._realm = realm + self._password = password + self._verify_ssl = verify_ssl + + self._proxmox = None + self._connection_start_time = None + + def build_client(self): + """Construct the ProxmoxAPI client.""" + + self._proxmox = ProxmoxAPI( + self._host, + port=self._port, + user=f"{self._user}@{self._realm}", + password=self._password, + verify_ssl=self._verify_ssl, + ) + + self._connection_start_time = time.time() + + def get_api_client(self): + """Return the ProxmoxAPI client and rebuild it if necessary.""" + + connection_age = time.time() - self._connection_start_time + + # Workaround for the Proxmoxer bug where the connection stops working after some time + if connection_age > 30 * 60: + self.build_client() + + return self._proxmox diff --git a/homeassistant/components/proxmoxve/binary_sensor.py b/homeassistant/components/proxmoxve/binary_sensor.py new file mode 100644 index 000000000000..15b1f1483e1f --- /dev/null +++ b/homeassistant/components/proxmoxve/binary_sensor.py @@ -0,0 +1,112 @@ +"""Binary sensor to read Proxmox VE data.""" +import logging + +from homeassistant.components.binary_sensor import BinarySensorDevice +from homeassistant.const import ATTR_ATTRIBUTION, CONF_HOST, CONF_PORT + +from . import CONF_CONTAINERS, CONF_NODES, CONF_VMS, PROXMOX_CLIENTS, ProxmoxItemType + +ATTRIBUTION = "Data provided by Proxmox VE" +_LOGGER = logging.getLogger(__name__) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the sensor platform.""" + + sensors = [] + + for entry in discovery_info["entries"]: + port = entry[CONF_PORT] + + for node in entry[CONF_NODES]: + for virtual_machine in node[CONF_VMS]: + sensors.append( + ProxmoxBinarySensor( + hass.data[PROXMOX_CLIENTS][f"{entry[CONF_HOST]}:{port}"], + node["node"], + ProxmoxItemType.qemu, + virtual_machine, + ) + ) + + for container in node[CONF_CONTAINERS]: + sensors.append( + ProxmoxBinarySensor( + hass.data[PROXMOX_CLIENTS][f"{entry[CONF_HOST]}:{port}"], + node["node"], + ProxmoxItemType.lxc, + container, + ) + ) + + add_entities(sensors, True) + + +class ProxmoxBinarySensor(BinarySensorDevice): + """A binary sensor for reading Proxmox VE data.""" + + def __init__(self, proxmox_client, item_node, item_type, item_id): + """Initialize the binary sensor.""" + self._proxmox_client = proxmox_client + self._item_node = item_node + self._item_type = item_type + self._item_id = item_id + + self._vmname = None + self._name = None + + self._state = None + + @property + def name(self): + """Return the name of the entity.""" + return self._name + + @property + def is_on(self): + """Return true if VM/container is running.""" + return self._state + + @property + def device_state_attributes(self): + """Return device attributes of the entity.""" + return { + "node": self._item_node, + "vmid": self._item_id, + "vmname": self._vmname, + "type": self._item_type.name, + ATTR_ATTRIBUTION: ATTRIBUTION, + } + + def update(self): + """Check if the VM/Container is running.""" + item = self.poll_item() + + if item is None: + _LOGGER.warning("Failed to poll VM/container %s", self._item_id) + return + + self._state = item["status"] == "running" + + def poll_item(self): + """Find the VM/Container with the set item_id.""" + items = ( + self._proxmox_client.get_api_client() + .nodes(self._item_node) + .get(self._item_type.name) + ) + item = next( + (item for item in items if item["vmid"] == str(self._item_id)), None + ) + + if item is None: + _LOGGER.warning("Couldn't find VM/Container with the ID %s", self._item_id) + return None + + if self._vmname is None: + self._vmname = item["name"] + + if self._name is None: + self._name = f"{self._item_node} {self._vmname} running" + + return item diff --git a/homeassistant/components/proxmoxve/manifest.json b/homeassistant/components/proxmoxve/manifest.json new file mode 100644 index 000000000000..9c03038a6302 --- /dev/null +++ b/homeassistant/components/proxmoxve/manifest.json @@ -0,0 +1,8 @@ +{ + "domain": "proxmoxve", + "name": "Proxmox VE", + "documentation": "https://www.home-assistant.io/integrations/proxmoxve", + "dependencies": [], + "codeowners": ["@k4ds3"], + "requirements": ["proxmoxer==1.0.3"] + } \ No newline at end of file diff --git a/homeassistant/components/ps4/.translations/bg.json b/homeassistant/components/ps4/.translations/bg.json index 4bea40b206a6..fabd9032dc0c 100644 --- a/homeassistant/components/ps4/.translations/bg.json +++ b/homeassistant/components/ps4/.translations/bg.json @@ -4,8 +4,8 @@ "credential_error": "\u0413\u0440\u0435\u0448\u043a\u0430 \u043f\u0440\u0438 \u0438\u0437\u0432\u043b\u0438\u0447\u0430\u043d\u0435 \u043d\u0430 \u0438\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u043e\u043d\u043d\u0438 \u0434\u0430\u043d\u043d\u0438.", "devices_configured": "\u0412\u0441\u0438\u0447\u043a\u0438 \u043d\u0430\u043c\u0435\u0440\u0435\u043d\u0438 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0441\u0430 \u0432\u0435\u0447\u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0438.", "no_devices_found": "\u041d\u0435 \u0441\u0430 \u043d\u0430\u043c\u0435\u0440\u0435\u043d\u0438 PlayStation 4 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0432 \u043c\u0440\u0435\u0436\u0430\u0442\u0430.", - "port_987_bind_error": "\u041d\u0435\u0432\u044a\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442 \u0437\u0430 \u0440\u0435\u0437\u0435\u0440\u0438\u0432\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 \u043f\u043e\u0440\u0442 987.", - "port_997_bind_error": "\u041d\u0435\u0432\u044a\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442 \u0437\u0430 \u0440\u0435\u0437\u0435\u0440\u0438\u0432\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 \u043f\u043e\u0440\u0442 997." + "port_987_bind_error": "\u041d\u0435\u0432\u044a\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442 \u0437\u0430 \u0440\u0435\u0437\u0435\u0440\u0438\u0432\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 \u043f\u043e\u0440\u0442 987. \u041c\u043e\u043b\u044f, \u043f\u0440\u043e\u0447\u0435\u0442\u0435\u0442\u0435 [\u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u0438\u0442\u0435] (https://www.home-assistant.io/components/ps4/) \u0437\u0430 \u0434\u043e\u043f\u044a\u043b\u043d\u0438\u0442\u0435\u043b\u043d\u0430 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044f.", + "port_997_bind_error": "\u041d\u0435\u0432\u044a\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442 \u0437\u0430 \u0440\u0435\u0437\u0435\u0440\u0438\u0432\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 \u043f\u043e\u0440\u0442 997. \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](https://www.home-assistant.io/components/ps4/) \u0437\u0430 \u0434\u043e\u043f\u044a\u043b\u043d\u0438\u0442\u0435\u043b\u043d\u0430 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044f." }, "error": { "credential_timeout": "\u0412\u0440\u0435\u043c\u0435\u0442\u043e \u0437\u0430 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435 \u0441 \u0443\u0441\u043b\u0443\u0433\u0430\u0442\u0430 \u0437\u0430 \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u0438\u0437\u0442\u0435\u0447\u0435. \u041d\u0430\u0442\u0438\u0441\u043d\u0435\u0442\u0435 \"\u0418\u0437\u043f\u0440\u0430\u0449\u0430\u043d\u0435\" \u0437\u0430 \u0434\u0430 \u0440\u0435\u0441\u0442\u0430\u0440\u0442\u0438\u0440\u0430\u0442\u0435.", @@ -25,7 +25,7 @@ "name": "\u0418\u043c\u0435", "region": "\u0420\u0435\u0433\u0438\u043e\u043d" }, - "description": "\u0412\u044a\u0432\u0435\u0434\u0435\u0442\u0435 \u0432\u0430\u0448\u0430\u0442\u0430 PlayStation 4 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044f. \u0417\u0430 \u201ePIN\u201c \u043e\u0442\u0438\u0434\u0435\u0442\u0435 \u0432 \u201eSettings\u201c \u043d\u0430 \u0432\u0430\u0448\u0430\u0442\u0430 PlayStation 4 \u043a\u043e\u043d\u0437\u043e\u043b\u0430. \u0421\u043b\u0435\u0434 \u0442\u043e\u0432\u0430 \u043f\u0440\u0435\u043c\u0438\u043d\u0435\u0442\u0435 \u043a\u044a\u043c \u201eMobile App Connection Settings\u201c \u0438 \u0438\u0437\u0431\u0435\u0440\u0435\u0442\u0435 \u201eAdd Device\u201c. \u0412\u044a\u0432\u0435\u0434\u0435\u0442\u0435 \u043f\u043e\u043a\u0430\u0437\u0430\u043d\u0438\u044f PIN \u043a\u043e\u0434.", + "description": "\u0412\u044a\u0432\u0435\u0434\u0435\u0442\u0435 \u0432\u0430\u0448\u0430\u0442\u0430 PlayStation 4 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044f. \u0417\u0430 \u201ePIN\u201c \u043e\u0442\u0438\u0434\u0435\u0442\u0435 \u0432 \u201eSettings\u201c \u043d\u0430 \u0412\u0430\u0448\u0430\u0442\u0430 PlayStation 4 \u043a\u043e\u043d\u0437\u043e\u043b\u0430. \u0421\u043b\u0435\u0434 \u0442\u043e\u0432\u0430 \u043f\u0440\u0435\u043c\u0438\u043d\u0435\u0442\u0435 \u043a\u044a\u043c \u201eMobile App Connection Settings\u201c \u0438 \u0438\u0437\u0431\u0435\u0440\u0435\u0442\u0435 \u201eAdd Device\u201c. \u0412\u044a\u0432\u0435\u0434\u0435\u0442\u0435 \u043f\u043e\u043a\u0430\u0437\u0430\u043d\u0438\u044f PIN \u043a\u043e\u0434. \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](https://www.home-assistant.io/components/ps4/) \u0437\u0430 \u0434\u043e\u043f\u044a\u043b\u043d\u0438\u0442\u0435\u043b\u043d\u0430 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044f.", "title": "PlayStation 4" }, "mode": { diff --git a/homeassistant/components/ps4/.translations/ru.json b/homeassistant/components/ps4/.translations/ru.json index bf2484d02543..c7ac8d76cf16 100644 --- a/homeassistant/components/ps4/.translations/ru.json +++ b/homeassistant/components/ps4/.translations/ru.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "credential_error": "\u041e\u0448\u0438\u0431\u043a\u0430 \u043f\u0440\u0438 \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u0438 \u0443\u0447\u0435\u0442\u043d\u044b\u0445 \u0434\u0430\u043d\u043d\u044b\u0445.", + "credential_error": "\u041e\u0448\u0438\u0431\u043a\u0430 \u043f\u0440\u0438 \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u0438 \u0443\u0447\u0451\u0442\u043d\u044b\u0445 \u0434\u0430\u043d\u043d\u044b\u0445.", "devices_configured": "\u0412\u0441\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u043d\u044b\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u044b.", "no_devices_found": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 PlayStation 4 \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b \u0432 \u0441\u0435\u0442\u0438.", "port_987_bind_error": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u043f\u043e\u0440\u0442\u0443 987. \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/ps4/).", diff --git a/homeassistant/components/ps4/__init__.py b/homeassistant/components/ps4/__init__.py index 205059be608f..05e3422fe746 100644 --- a/homeassistant/components/ps4/__init__.py +++ b/homeassistant/components/ps4/__init__.py @@ -2,9 +2,9 @@ import logging import os -import voluptuous as vol from pyps4_2ndscreen.ddp import async_create_ddp_endpoint from pyps4_2ndscreen.media_art import COUNTRIES +import voluptuous as vol from homeassistant.components.media_player.const import ( ATTR_MEDIA_CONTENT_TYPE, @@ -20,7 +20,7 @@ ) from homeassistant.core import split_entity_id from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers import entity_registry, config_validation as cv +from homeassistant.helpers import config_validation as cv, entity_registry from homeassistant.helpers.typing import HomeAssistantType from homeassistant.util import location from homeassistant.util.json import load_json, save_json diff --git a/homeassistant/components/ps4/manifest.json b/homeassistant/components/ps4/manifest.json index 361711c400c7..add52231a074 100644 --- a/homeassistant/components/ps4/manifest.json +++ b/homeassistant/components/ps4/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/ps4", "requirements": [ - "pyps4-2ndscreen==1.0.1" + "pyps4-2ndscreen==1.0.3" ], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/ps4/media_player.py b/homeassistant/components/ps4/media_player.py index 91d3a5b13c72..35cdbab25340 100644 --- a/homeassistant/components/ps4/media_player.py +++ b/homeassistant/components/ps4/media_player.py @@ -1,19 +1,18 @@ """Support for PlayStation 4 consoles.""" -import logging import asyncio +import logging +from pyps4_2ndscreen.errors import NotReady, PSDataIncomplete import pyps4_2ndscreen.ps4 as pyps4 -from pyps4_2ndscreen.errors import NotReady -from homeassistant.core import callback from homeassistant.components.media_player import ENTITY_IMAGE_URL, MediaPlayerDevice from homeassistant.components.media_player.const import ( ATTR_MEDIA_CONTENT_TYPE, ATTR_MEDIA_TITLE, - MEDIA_TYPE_GAME, MEDIA_TYPE_APP, - SUPPORT_SELECT_SOURCE, + MEDIA_TYPE_GAME, SUPPORT_PAUSE, + SUPPORT_SELECT_SOURCE, SUPPORT_STOP, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, @@ -26,9 +25,10 @@ CONF_REGION, CONF_TOKEN, STATE_IDLE, - STATE_STANDBY, STATE_PLAYING, + STATE_STANDBY, ) +from homeassistant.core import callback from homeassistant.helpers import device_registry, entity_registry from .const import ( @@ -254,7 +254,6 @@ def reset_title(self): async def async_get_title_data(self, title_id, name): """Get PS Store Data.""" - from pyps4_2ndscreen.errors import PSDataIncomplete app_name = None art = None diff --git a/homeassistant/components/pulseaudio_loopback/switch.py b/homeassistant/components/pulseaudio_loopback/switch.py index b3bddbf12634..618d54ab3adb 100644 --- a/homeassistant/components/pulseaudio_loopback/switch.py +++ b/homeassistant/components/pulseaudio_loopback/switch.py @@ -22,7 +22,7 @@ DEFAULT_BUFFER_SIZE = 1024 DEFAULT_HOST = "localhost" DEFAULT_NAME = "paloopback" -DEFAULT_PORT = 4712 +DEFAULT_PORT = 4713 DEFAULT_TCP_TIMEOUT = 3 IGNORED_SWITCH_WARN = "Switch is already in the desired state. Ignoring." diff --git a/homeassistant/components/pushetta/notify.py b/homeassistant/components/pushetta/notify.py index b8911039f3f0..c9b008524d68 100644 --- a/homeassistant/components/pushetta/notify.py +++ b/homeassistant/components/pushetta/notify.py @@ -1,17 +1,17 @@ """Pushetta platform for notify component.""" import logging +from pushetta import Pushetta, exceptions as pushetta_exceptions import voluptuous as vol -from homeassistant.const import CONF_API_KEY -import homeassistant.helpers.config_validation as cv - from homeassistant.components.notify import ( ATTR_TITLE, ATTR_TITLE_DEFAULT, PLATFORM_SCHEMA, BaseNotificationService, ) +from homeassistant.const import CONF_API_KEY +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -44,7 +44,6 @@ class PushettaNotificationService(BaseNotificationService): def __init__(self, api_key, channel_name, send_test_msg): """Initialize the service.""" - from pushetta import Pushetta self._api_key = api_key self._channel_name = channel_name @@ -56,15 +55,14 @@ def __init__(self, api_key, channel_name, send_test_msg): def send_message(self, message="", **kwargs): """Send a message to a user.""" - from pushetta import exceptions title = kwargs.get(ATTR_TITLE, ATTR_TITLE_DEFAULT) try: self.pushetta.pushMessage(self._channel_name, f"{title} {message}") - except exceptions.TokenValidationError: + except pushetta_exceptions.TokenValidationError: _LOGGER.error("Please check your access token") self.is_valid = False - except exceptions.ChannelNotFoundError: + except pushetta_exceptions.ChannelNotFoundError: _LOGGER.error("Channel '%s' not found", self._channel_name) self.is_valid = False diff --git a/homeassistant/components/python_script/__init__.py b/homeassistant/components/python_script/__init__.py index af0865bc685a..e36ac397c0fa 100644 --- a/homeassistant/components/python_script/__init__.py +++ b/homeassistant/components/python_script/__init__.py @@ -5,6 +5,15 @@ import os import time +from RestrictedPython import compile_restricted_exec +from RestrictedPython.Eval import default_guarded_getitem +from RestrictedPython.Guards import ( + full_write_guard, + guarded_iter_unpack_sequence, + guarded_unpack_sequence, + safe_builtins, +) +from RestrictedPython.Utilities import utility_builtins import voluptuous as vol from homeassistant.const import SERVICE_RELOAD @@ -12,8 +21,8 @@ from homeassistant.helpers.service import async_set_service_schema from homeassistant.loader import bind_hass from homeassistant.util import sanitize_filename -from homeassistant.util.yaml.loader import load_yaml import homeassistant.util.dt as dt_util +from homeassistant.util.yaml.loader import load_yaml _LOGGER = logging.getLogger(__name__) @@ -122,15 +131,6 @@ def execute_script(hass, name, data=None): @bind_hass def execute(hass, filename, source, data=None): """Execute Python source.""" - from RestrictedPython import compile_restricted_exec - from RestrictedPython.Guards import ( - safe_builtins, - full_write_guard, - guarded_iter_unpack_sequence, - guarded_unpack_sequence, - ) - from RestrictedPython.Utilities import utility_builtins - from RestrictedPython.Eval import default_guarded_getitem compiled = compile_restricted_exec(source, filename=filename) @@ -147,7 +147,6 @@ def execute(hass, filename, source, data=None): def protected_getattr(obj, name, default=None): """Restricted method to get attributes.""" - # pylint: disable=too-many-boolean-expressions if name.startswith("async_"): raise ScriptError("Not allowed to access async methods") if ( diff --git a/homeassistant/components/qbittorrent/sensor.py b/homeassistant/components/qbittorrent/sensor.py index f00b392065cb..0299277059b4 100644 --- a/homeassistant/components/qbittorrent/sensor.py +++ b/homeassistant/components/qbittorrent/sensor.py @@ -1,9 +1,9 @@ """Support for monitoring the qBittorrent API.""" import logging -import voluptuous as vol - +from qbittorrent.client import Client, LoginRequired from requests.exceptions import RequestException +import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( @@ -13,9 +13,9 @@ CONF_USERNAME, STATE_IDLE, ) -from homeassistant.helpers.entity import Entity -import homeassistant.helpers.config_validation as cv from homeassistant.exceptions import PlatformNotReady +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -43,7 +43,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the qBittorrent sensors.""" - from qbittorrent.client import Client, LoginRequired try: client = Client(config[CONF_URL]) diff --git a/homeassistant/components/qnap/sensor.py b/homeassistant/components/qnap/sensor.py index efbb1ac26ca5..c3863bd0077c 100644 --- a/homeassistant/components/qnap/sensor.py +++ b/homeassistant/components/qnap/sensor.py @@ -1,26 +1,27 @@ """Support for QNAP NAS Sensors.""" -import logging from datetime import timedelta +import logging +from qnapstats import QNAPStats import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.helpers.entity import Entity from homeassistant.const import ( + ATTR_NAME, CONF_HOST, - CONF_USERNAME, + CONF_MONITORED_CONDITIONS, CONF_PASSWORD, CONF_PORT, CONF_SSL, - ATTR_NAME, - CONF_VERIFY_SSL, CONF_TIMEOUT, - CONF_MONITORED_CONDITIONS, + CONF_USERNAME, + CONF_VERIFY_SSL, TEMP_CELSIUS, ) -from homeassistant.util import Throttle from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity +from homeassistant.util import Throttle _LOGGER = logging.getLogger(__name__) @@ -170,7 +171,6 @@ class QNAPStatsAPI: def __init__(self, config): """Initialize the API wrapper.""" - from qnapstats import QNAPStats protocol = "https" if config.get(CONF_SSL) else "http" self._api = QNAPStats( diff --git a/homeassistant/components/quantum_gateway/device_tracker.py b/homeassistant/components/quantum_gateway/device_tracker.py index ea3979e77570..97eb8eedfd39 100644 --- a/homeassistant/components/quantum_gateway/device_tracker.py +++ b/homeassistant/components/quantum_gateway/device_tracker.py @@ -1,6 +1,7 @@ """Support for Verizon FiOS Quantum Gateways.""" import logging +from quantum_gateway import QuantumGatewayScanner from requests.exceptions import RequestException import voluptuous as vol @@ -37,7 +38,6 @@ class QuantumGatewayDeviceScanner(DeviceScanner): def __init__(self, config): """Initialize the scanner.""" - from quantum_gateway import QuantumGatewayScanner self.host = config[CONF_HOST] self.password = config[CONF_PASSWORD] diff --git a/homeassistant/components/qwikswitch/__init__.py b/homeassistant/components/qwikswitch/__init__.py index 1ae92b0a18ad..33392c51be8e 100644 --- a/homeassistant/components/qwikswitch/__init__.py +++ b/homeassistant/components/qwikswitch/__init__.py @@ -1,6 +1,8 @@ """Support for Qwikswitch devices.""" import logging +from pyqwikswitch.async_ import QSUsb +from pyqwikswitch.qwikswitch import CMD_BUTTONS, QS_CMD, QS_ID, SENSORS, QSType import voluptuous as vol from homeassistant.components.binary_sensor import DEVICE_CLASSES_SCHEMA @@ -128,8 +130,6 @@ async def async_turn_off(self, **_): async def async_setup(hass, config): """Qwiskswitch component setup.""" - from pyqwikswitch.async_ import QSUsb - from pyqwikswitch.qwikswitch import CMD_BUTTONS, QS_CMD, QS_ID, QSType, SENSORS # Add cmd's to in /&listen packets will fire events # By default only buttons of type [TOGGLE,SCENE EXE,LEVEL] diff --git a/homeassistant/components/qwikswitch/binary_sensor.py b/homeassistant/components/qwikswitch/binary_sensor.py index a5b142e19aed..054028b5629e 100644 --- a/homeassistant/components/qwikswitch/binary_sensor.py +++ b/homeassistant/components/qwikswitch/binary_sensor.py @@ -1,6 +1,8 @@ """Support for Qwikswitch Binary Sensors.""" import logging +from pyqwikswitch.qwikswitch import SENSORS + from homeassistant.components.binary_sensor import BinarySensorDevice from homeassistant.core import callback @@ -27,7 +29,6 @@ class QSBinarySensor(QSEntity, BinarySensorDevice): def __init__(self, sensor): """Initialize the sensor.""" - from pyqwikswitch.qwikswitch import SENSORS super().__init__(sensor["id"], sensor["name"]) self.channel = sensor["channel"] diff --git a/homeassistant/components/qwikswitch/sensor.py b/homeassistant/components/qwikswitch/sensor.py index 01964fc7831e..4674da420b22 100644 --- a/homeassistant/components/qwikswitch/sensor.py +++ b/homeassistant/components/qwikswitch/sensor.py @@ -1,6 +1,8 @@ """Support for Qwikswitch Sensors.""" import logging +from pyqwikswitch.qwikswitch import SENSORS + from homeassistant.core import callback from . import DOMAIN as QWIKSWITCH, QSEntity @@ -26,7 +28,6 @@ class QSSensor(QSEntity): def __init__(self, sensor): """Initialize the sensor.""" - from pyqwikswitch.qwikswitch import SENSORS super().__init__(sensor["id"], sensor["name"]) self.channel = sensor["channel"] diff --git a/homeassistant/components/rachio/__init__.py b/homeassistant/components/rachio/__init__.py index 2030512ab313..4d67175ecd56 100644 --- a/homeassistant/components/rachio/__init__.py +++ b/homeassistant/components/rachio/__init__.py @@ -4,11 +4,13 @@ from typing import Optional from aiohttp import web +from rachiopy import Rachio import voluptuous as vol + from homeassistant.auth.util import generate_secret from homeassistant.components.http import HomeAssistantView from homeassistant.const import CONF_API_KEY, EVENT_HOMEASSISTANT_STOP, URL_API -from homeassistant.helpers import discovery, config_validation as cv +from homeassistant.helpers import config_validation as cv, discovery from homeassistant.helpers.dispatcher import async_dispatcher_send _LOGGER = logging.getLogger(__name__) @@ -102,7 +104,6 @@ def setup(hass, config) -> bool: """Set up the Rachio component.""" - from rachiopy import Rachio # Listen for incoming webhook connections hass.http.register_view(RachioWebhookView()) @@ -284,7 +285,6 @@ class RachioWebhookView(HomeAssistantView): url = WEBHOOK_PATH name = url[1:].replace("/", ":") - # pylint: disable=no-self-use @asyncio.coroutine async def post(self, request) -> web.Response: """Handle webhook calls from the server.""" diff --git a/homeassistant/components/radarr/sensor.py b/homeassistant/components/radarr/sensor.py index 0dcdcf1514fd..79e45ffd9a82 100644 --- a/homeassistant/components/radarr/sensor.py +++ b/homeassistant/components/radarr/sensor.py @@ -1,21 +1,22 @@ """Support for Radarr.""" +from datetime import datetime, timedelta import logging import time -from datetime import datetime, timedelta +from pytz import timezone import requests import voluptuous as vol -import homeassistant.helpers.config_validation as cv +from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( CONF_API_KEY, CONF_HOST, - CONF_PORT, CONF_MONITORED_CONDITIONS, + CONF_PORT, CONF_SSL, ) +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity -from homeassistant.components.sensor import PLATFORM_SCHEMA _LOGGER = logging.getLogger(__name__) @@ -79,7 +80,6 @@ class RadarrSensor(Entity): def __init__(self, hass, conf, sensor_type): """Create Radarr entity.""" - from pytz import timezone self.conf = conf self.host = conf.get(CONF_HOST) diff --git a/homeassistant/components/rainbird/__init__.py b/homeassistant/components/rainbird/__init__.py index 0b51be1f2580..83c358c480b0 100644 --- a/homeassistant/components/rainbird/__init__.py +++ b/homeassistant/components/rainbird/__init__.py @@ -75,7 +75,7 @@ def _setup_controller(hass, controller_config, config): position = len(hass.data[DATA_RAINBIRD]) try: controller.get_serial_number() - except Exception as exc: # pylint: disable=W0703 + except Exception as exc: # pylint: disable=broad-except _LOGGER.error("Unable to setup controller: %s", exc) return False hass.data[DATA_RAINBIRD].append(controller) diff --git a/homeassistant/components/raincloud/__init__.py b/homeassistant/components/raincloud/__init__.py index 77bdcc5aa2f9..dd851c0b3e30 100644 --- a/homeassistant/components/raincloud/__init__.py +++ b/homeassistant/components/raincloud/__init__.py @@ -2,6 +2,7 @@ from datetime import timedelta import logging +from raincloudy.core import RainCloudy from requests.exceptions import ConnectTimeout, HTTPError import voluptuous as vol @@ -96,8 +97,6 @@ def setup(hass, config): scan_interval = conf.get(CONF_SCAN_INTERVAL) try: - from raincloudy.core import RainCloudy - raincloud = RainCloudy(username=username, password=password) if not raincloud.is_connected: raise HTTPError diff --git a/homeassistant/components/rainmachine/.translations/ru.json b/homeassistant/components/rainmachine/.translations/ru.json index afaa55424d25..ca535663f543 100644 --- a/homeassistant/components/rainmachine/.translations/ru.json +++ b/homeassistant/components/rainmachine/.translations/ru.json @@ -1,8 +1,8 @@ { "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.", - "invalid_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435." + "identifier_exists": "\u0423\u0447\u0451\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\u0451\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435." }, "step": { "user": { diff --git a/homeassistant/components/rainmachine/__init__.py b/homeassistant/components/rainmachine/__init__.py index 183872087a72..5dffecb0488e 100644 --- a/homeassistant/components/rainmachine/__init__.py +++ b/homeassistant/components/rainmachine/__init__.py @@ -1,8 +1,10 @@ """Support for RainMachine devices.""" import asyncio -import logging from datetime import timedelta +import logging +from regenmaschine import login +from regenmaschine.errors import RainMachineError import voluptuous as vol from homeassistant.config_entries import SOURCE_IMPORT @@ -10,12 +12,12 @@ ATTR_ATTRIBUTION, CONF_BINARY_SENSORS, CONF_IP_ADDRESS, + CONF_MONITORED_CONDITIONS, CONF_PASSWORD, CONF_PORT, CONF_SCAN_INTERVAL, CONF_SENSORS, CONF_SSL, - CONF_MONITORED_CONDITIONS, CONF_SWITCHES, ) from homeassistant.exceptions import ConfigEntryNotReady @@ -211,8 +213,6 @@ async def async_setup(hass, config): async def async_setup_entry(hass, config_entry): """Set up RainMachine as config entry.""" - from regenmaschine import login - from regenmaschine.errors import RainMachineError _verify_domain_control = verify_domain_control(hass, DOMAIN) @@ -351,8 +351,12 @@ async def async_unload_entry(hass, config_entry): remove_listener = hass.data[DOMAIN][DATA_LISTENER].pop(config_entry.entry_id) remove_listener() - for component in ("binary_sensor", "sensor", "switch"): - await hass.config_entries.async_forward_entry_unload(config_entry, component) + tasks = [ + hass.config_entries.async_forward_entry_unload(config_entry, component) + for component in ("binary_sensor", "sensor", "switch") + ] + + await asyncio.gather(*tasks) return True @@ -373,7 +377,6 @@ def __init__( async def async_update(self): """Update sensor/binary sensor data.""" - from regenmaschine.errors import RainMachineError tasks = {} diff --git a/homeassistant/components/rainmachine/config_flow.py b/homeassistant/components/rainmachine/config_flow.py index 3600324cb120..4753335da783 100644 --- a/homeassistant/components/rainmachine/config_flow.py +++ b/homeassistant/components/rainmachine/config_flow.py @@ -2,10 +2,11 @@ from collections import OrderedDict +from regenmaschine import login +from regenmaschine.errors import RainMachineError import voluptuous as vol from homeassistant import config_entries -from homeassistant.core import callback from homeassistant.const import ( CONF_IP_ADDRESS, CONF_PASSWORD, @@ -13,6 +14,7 @@ CONF_SCAN_INTERVAL, CONF_SSL, ) +from homeassistant.core import callback from homeassistant.helpers import aiohttp_client from .const import DEFAULT_PORT, DEFAULT_SCAN_INTERVAL, DEFAULT_SSL, DOMAIN @@ -55,8 +57,6 @@ async def async_step_import(self, import_config): async def async_step_user(self, user_input=None): """Handle the start of the config flow.""" - from regenmaschine import login - from regenmaschine.errors import RainMachineError if not user_input: return await self._show_form() diff --git a/homeassistant/components/rainmachine/switch.py b/homeassistant/components/rainmachine/switch.py index 870c7317f257..69c0e4da52dc 100644 --- a/homeassistant/components/rainmachine/switch.py +++ b/homeassistant/components/rainmachine/switch.py @@ -2,6 +2,8 @@ from datetime import datetime import logging +from regenmaschine.errors import RequestError + from homeassistant.components.switch import SwitchDevice from homeassistant.const import ATTR_ID from homeassistant.core import callback @@ -181,7 +183,6 @@ async def async_added_to_hass(self): async def async_turn_off(self, **kwargs) -> None: """Turn the program off.""" - from regenmaschine.errors import RequestError try: await self.rainmachine.client.programs.stop(self._rainmachine_entity_id) @@ -193,7 +194,6 @@ async def async_turn_off(self, **kwargs) -> None: async def async_turn_on(self, **kwargs) -> None: """Turn the program on.""" - from regenmaschine.errors import RequestError try: await self.rainmachine.client.programs.start(self._rainmachine_entity_id) @@ -205,7 +205,6 @@ async def async_turn_on(self, **kwargs) -> None: async def async_update(self) -> None: """Update info for the program.""" - from regenmaschine.errors import RequestError try: self._obj = await self.rainmachine.client.programs.get( @@ -265,7 +264,6 @@ async def async_added_to_hass(self): async def async_turn_off(self, **kwargs) -> None: """Turn the zone off.""" - from regenmaschine.errors import RequestError try: await self.rainmachine.client.zones.stop(self._rainmachine_entity_id) @@ -274,7 +272,6 @@ async def async_turn_off(self, **kwargs) -> None: async def async_turn_on(self, **kwargs) -> None: """Turn the zone on.""" - from regenmaschine.errors import RequestError try: await self.rainmachine.client.zones.start( @@ -285,7 +282,6 @@ async def async_turn_on(self, **kwargs) -> None: async def async_update(self) -> None: """Update info for the zone.""" - from regenmaschine.errors import RequestError try: self._obj = await self.rainmachine.client.zones.get( diff --git a/homeassistant/components/random/binary_sensor.py b/homeassistant/components/random/binary_sensor.py index 8c2608ad81dd..e502439b28c3 100644 --- a/homeassistant/components/random/binary_sensor.py +++ b/homeassistant/components/random/binary_sensor.py @@ -1,15 +1,16 @@ """Support for showing random states.""" import logging +from random import getrandbits import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.binary_sensor import ( - BinarySensorDevice, - PLATFORM_SCHEMA, DEVICE_CLASSES_SCHEMA, + PLATFORM_SCHEMA, + BinarySensorDevice, ) -from homeassistant.const import CONF_NAME, CONF_DEVICE_CLASS +from homeassistant.const import CONF_DEVICE_CLASS, CONF_NAME +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -57,6 +58,5 @@ def device_class(self): async def async_update(self): """Get new state and update the sensor's state.""" - from random import getrandbits self._state = bool(getrandbits(1)) diff --git a/homeassistant/components/random/sensor.py b/homeassistant/components/random/sensor.py index 5d4e3d0d57a0..4ebd710f1031 100644 --- a/homeassistant/components/random/sensor.py +++ b/homeassistant/components/random/sensor.py @@ -1,5 +1,6 @@ """Support for showing random numbers.""" import logging +from random import randrange import voluptuous as vol @@ -82,6 +83,5 @@ def device_state_attributes(self): async def async_update(self): """Get a new number and updates the states.""" - from random import randrange self._state = randrange(self._minimum, self._maximum + 1) diff --git a/homeassistant/components/raspyrfm/switch.py b/homeassistant/components/raspyrfm/switch.py index 53cb1dbdcb50..ec07119b96a3 100644 --- a/homeassistant/components/raspyrfm/switch.py +++ b/homeassistant/components/raspyrfm/switch.py @@ -1,6 +1,15 @@ """Support for switches that can be controlled using the RaspyRFM rc module.""" import logging +from raspyrfm_client import RaspyRFMClient +from raspyrfm_client.device_implementations.controlunit.actions import Action +from raspyrfm_client.device_implementations.controlunit.controlunit_constants import ( + ControlUnitModel, +) +from raspyrfm_client.device_implementations.gateway.manufacturer.gateway_constants import ( + GatewayModel, +) +from raspyrfm_client.device_implementations.manufacturer_constants import Manufacturer import voluptuous as vol from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchDevice @@ -46,16 +55,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the RaspyRFM switch.""" - from raspyrfm_client import RaspyRFMClient - from raspyrfm_client.device_implementations.controlunit.controlunit_constants import ( - ControlUnitModel, - ) - from raspyrfm_client.device_implementations.gateway.manufacturer.gateway_constants import ( - GatewayModel, - ) - from raspyrfm_client.device_implementations.manufacturer_constants import ( - Manufacturer, - ) gateway_manufacturer = config.get( CONF_GATEWAY_MANUFACTURER, Manufacturer.SEEGEL_SYSTEME.value @@ -123,7 +122,6 @@ def is_on(self): def turn_on(self, **kwargs): """Turn the switch on.""" - from raspyrfm_client.device_implementations.controlunit.actions import Action self._raspyrfm_client.send(self._gateway, self._controlunit, Action.ON) self._state = True @@ -131,7 +129,6 @@ def turn_on(self, **kwargs): def turn_off(self, **kwargs): """Turn the switch off.""" - from raspyrfm_client.device_implementations.controlunit.actions import Action if Action.OFF in self._controlunit.get_supported_actions(): self._raspyrfm_client.send(self._gateway, self._controlunit, Action.OFF) diff --git a/homeassistant/components/recorder/manifest.json b/homeassistant/components/recorder/manifest.json index 1f00cf89f158..4ad000866db0 100644 --- a/homeassistant/components/recorder/manifest.json +++ b/homeassistant/components/recorder/manifest.json @@ -3,7 +3,7 @@ "name": "Recorder", "documentation": "https://www.home-assistant.io/integrations/recorder", "requirements": [ - "sqlalchemy==1.3.10" + "sqlalchemy==1.3.11" ], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/recorder/util.py b/homeassistant/components/recorder/util.py index 8cfcafea79d8..693d88ae7953 100644 --- a/homeassistant/components/recorder/util.py +++ b/homeassistant/components/recorder/util.py @@ -28,7 +28,7 @@ def session_scope(*, hass=None, session=None): if session.transaction: need_rollback = True session.commit() - except Exception as err: # pylint: disable=broad-except + except Exception as err: _LOGGER.error("Error executing query: %s", err) if need_rollback: session.rollback() diff --git a/homeassistant/components/recswitch/switch.py b/homeassistant/components/recswitch/switch.py index aa93693a36d4..c242f23dfddb 100644 --- a/homeassistant/components/recswitch/switch.py +++ b/homeassistant/components/recswitch/switch.py @@ -2,13 +2,13 @@ import logging +from pyrecswitch import RSNetwork, RSNetworkError import voluptuous as vol from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchDevice from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME import homeassistant.helpers.config_validation as cv - _LOGGER = logging.getLogger(__name__) DEFAULT_NAME = "RecSwitch {0}" @@ -26,7 +26,6 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the device.""" - from pyrecswitch import RSNetwork host = config[CONF_HOST] mac_address = config[CONF_MAC] @@ -78,7 +77,6 @@ async def async_turn_off(self, **kwargs): async def async_set_gpio_status(self, status): """Set the switch status.""" - from pyrecswitch import RSNetworkError try: ret = await self.device.set_gpio_status(status) @@ -88,7 +86,6 @@ async def async_set_gpio_status(self, status): async def async_update(self): """Update the current switch status.""" - from pyrecswitch import RSNetworkError try: ret = await self.device.get_gpio_status() diff --git a/homeassistant/components/remote/__init__.py b/homeassistant/components/remote/__init__.py index 450b1c123c32..af653165ee38 100644 --- a/homeassistant/components/remote/__init__.py +++ b/homeassistant/components/remote/__init__.py @@ -16,8 +16,8 @@ SERVICE_TOGGLE, ) from homeassistant.components import group -from homeassistant.helpers.config_validation import ( # noqa - ENTITY_SERVICE_SCHEMA, +from homeassistant.helpers.config_validation import ( # noqa: F401 + make_entity_service_schema, PLATFORM_SCHEMA, PLATFORM_SCHEMA_BASE, ) @@ -56,29 +56,10 @@ SUPPORT_LEARN_COMMAND = 1 -REMOTE_SERVICE_ACTIVITY_SCHEMA = ENTITY_SERVICE_SCHEMA.extend( +REMOTE_SERVICE_ACTIVITY_SCHEMA = make_entity_service_schema( {vol.Optional(ATTR_ACTIVITY): cv.string} ) -REMOTE_SERVICE_SEND_COMMAND_SCHEMA = ENTITY_SERVICE_SCHEMA.extend( - { - vol.Required(ATTR_COMMAND): vol.All(cv.ensure_list, [cv.string]), - vol.Optional(ATTR_DEVICE): cv.string, - vol.Optional(ATTR_NUM_REPEATS, default=DEFAULT_NUM_REPEATS): cv.positive_int, - vol.Optional(ATTR_DELAY_SECS): vol.Coerce(float), - vol.Optional(ATTR_HOLD_SECS, default=DEFAULT_HOLD_SECS): vol.Coerce(float), - } -) - -REMOTE_SERVICE_LEARN_COMMAND_SCHEMA = ENTITY_SERVICE_SCHEMA.extend( - { - vol.Optional(ATTR_DEVICE): cv.string, - vol.Optional(ATTR_COMMAND): vol.All(cv.ensure_list, [cv.string]), - vol.Optional(ATTR_ALTERNATIVE): cv.boolean, - vol.Optional(ATTR_TIMEOUT): cv.positive_int, - } -) - @bind_hass def is_on(hass, entity_id=None): @@ -107,12 +88,27 @@ async def async_setup(hass, config): ) component.async_register_entity_service( - SERVICE_SEND_COMMAND, REMOTE_SERVICE_SEND_COMMAND_SCHEMA, "async_send_command" + SERVICE_SEND_COMMAND, + { + vol.Required(ATTR_COMMAND): vol.All(cv.ensure_list, [cv.string]), + vol.Optional(ATTR_DEVICE): cv.string, + vol.Optional( + ATTR_NUM_REPEATS, default=DEFAULT_NUM_REPEATS + ): cv.positive_int, + vol.Optional(ATTR_DELAY_SECS): vol.Coerce(float), + vol.Optional(ATTR_HOLD_SECS, default=DEFAULT_HOLD_SECS): vol.Coerce(float), + }, + "async_send_command", ) component.async_register_entity_service( SERVICE_LEARN_COMMAND, - REMOTE_SERVICE_LEARN_COMMAND_SCHEMA, + { + vol.Optional(ATTR_DEVICE): cv.string, + vol.Optional(ATTR_COMMAND): vol.All(cv.ensure_list, [cv.string]), + vol.Optional(ATTR_ALTERNATIVE): cv.boolean, + vol.Optional(ATTR_TIMEOUT): cv.positive_int, + }, "async_learn_command", ) diff --git a/homeassistant/components/remote/services.yaml b/homeassistant/components/remote/services.yaml index a551ba18ed45..1d712a8f2850 100644 --- a/homeassistant/components/remote/services.yaml +++ b/homeassistant/components/remote/services.yaml @@ -64,34 +64,3 @@ learn_command: timeout: description: Timeout, in seconds, for the command to be learned. example: '30' - - -harmony_sync: - description: Syncs the remote's configuration. - fields: - entity_id: - description: Name(s) of entities to sync. - example: 'remote.family_room' - -harmony_change_channel: - description: Sends change channel command to the Harmony HUB - fields: - entity_id: - description: Name(s) of Harmony remote entities to send change channel command to - example: 'remote.family_room' - channel: - description: Channel number to change to - example: '200' - -xiaomi_miio_learn_command: - description: 'Learn an IR command, press "Call Service", point the remote at the IR device, and the learned command will be shown as a notification in Overview.' - fields: - entity_id: - description: 'Name of the entity to learn command from.' - example: 'remote.xiaomi_miio' - slot: - description: 'Define the slot used to save the IR command (Value from 1 to 1000000)' - example: '1' - timeout: - description: 'Define the timeout in seconds, before which the command must be learned.' - example: '30' diff --git a/homeassistant/components/remote_rpi_gpio/__init__.py b/homeassistant/components/remote_rpi_gpio/__init__.py index 33356d0e3b82..e1b66128e3f4 100644 --- a/homeassistant/components/remote_rpi_gpio/__init__.py +++ b/homeassistant/components/remote_rpi_gpio/__init__.py @@ -1,6 +1,9 @@ """Support for controlling GPIO pins of a Raspberry Pi.""" import logging +from gpiozero import LED, Button +from gpiozero.pins.pigpio import PiGPIOFactory + _LOGGER = logging.getLogger(__name__) CONF_BOUNCETIME = "bouncetime" @@ -21,8 +24,6 @@ def setup(hass, config): def setup_output(address, port, invert_logic): """Set up a GPIO as output.""" - from gpiozero import LED - from gpiozero.pins.pigpio import PiGPIOFactory try: return LED(port, active_high=invert_logic, pin_factory=PiGPIOFactory(address)) @@ -32,8 +33,6 @@ def setup_output(address, port, invert_logic): def setup_input(address, port, pull_mode, bouncetime): """Set up a GPIO as input.""" - from gpiozero import Button - from gpiozero.pins.pigpio import PiGPIOFactory if pull_mode == "UP": pull_gpio_up = True diff --git a/homeassistant/components/remote_rpi_gpio/binary_sensor.py b/homeassistant/components/remote_rpi_gpio/binary_sensor.py index e12d83324fd7..862bd30ae432 100644 --- a/homeassistant/components/remote_rpi_gpio/binary_sensor.py +++ b/homeassistant/components/remote_rpi_gpio/binary_sensor.py @@ -1,19 +1,17 @@ """Support for binary sensor using RPi GPIO.""" import logging -import voluptuous as vol - import requests +import voluptuous as vol +from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorDevice from homeassistant.const import CONF_HOST -from homeassistant.components.binary_sensor import BinarySensorDevice, PLATFORM_SCHEMA - import homeassistant.helpers.config_validation as cv from . import ( CONF_BOUNCETIME, - CONF_PULL_MODE, CONF_INVERT_LOGIC, + CONF_PULL_MODE, DEFAULT_BOUNCETIME, DEFAULT_INVERT_LOGIC, DEFAULT_PULL_MODE, diff --git a/homeassistant/components/remote_rpi_gpio/switch.py b/homeassistant/components/remote_rpi_gpio/switch.py index 8240de7951d7..a5b255179cde 100644 --- a/homeassistant/components/remote_rpi_gpio/switch.py +++ b/homeassistant/components/remote_rpi_gpio/switch.py @@ -3,9 +3,8 @@ import voluptuous as vol -from homeassistant.components.switch import SwitchDevice, PLATFORM_SCHEMA -from homeassistant.const import DEVICE_DEFAULT_NAME, CONF_HOST - +from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchDevice +from homeassistant.const import CONF_HOST, DEVICE_DEFAULT_NAME import homeassistant.helpers.config_validation as cv from . import CONF_INVERT_LOGIC, DEFAULT_INVERT_LOGIC diff --git a/homeassistant/components/rest/sensor.py b/homeassistant/components/rest/sensor.py index 41adb8559036..6fdf5ce7221c 100644 --- a/homeassistant/components/rest/sensor.py +++ b/homeassistant/components/rest/sensor.py @@ -197,13 +197,18 @@ def update(self): if value: try: json_dict = json.loads(value) + if isinstance(json_dict, list): + json_dict = json_dict[0] if isinstance(json_dict, dict): attrs = { k: json_dict[k] for k in self._json_attrs if k in json_dict } self._attributes = attrs else: - _LOGGER.warning("JSON result was not a dictionary") + _LOGGER.warning( + "JSON result was not a dictionary" + " or list with 0th element a dictionary" + ) except ValueError: _LOGGER.warning("REST result could not be parsed as JSON") _LOGGER.debug("Erroneous JSON: %s", value) @@ -227,32 +232,33 @@ def __init__( self, method, resource, auth, headers, data, verify_ssl, timeout=DEFAULT_TIMEOUT ): """Initialize the data object.""" - self._request = requests.Request( - method, resource, headers=headers, auth=auth, data=data - ).prepare() + self._method = method + self._resource = resource + self._auth = auth + self._headers = headers + self._request_data = data self._verify_ssl = verify_ssl self._timeout = timeout self.data = None def set_url(self, url): """Set url.""" - self._request.prepare_url(url, None) + self._resource = url def update(self): """Get the latest data from REST service with provided method.""" - _LOGGER.debug("Updating from %s", self._request.url) + _LOGGER.debug("Updating from %s", self._resource) try: - with requests.Session() as sess: - response = sess.send( - self._request, timeout=self._timeout, verify=self._verify_ssl - ) - + response = requests.request( + self._method, + self._resource, + headers=self._headers, + auth=self._auth, + data=self._request_data, + timeout=self._timeout, + verify=self._verify_ssl, + ) self.data = response.text except requests.exceptions.RequestException as ex: - _LOGGER.error( - "Error fetching data: %s from %s failed with %s", - self._request, - self._request.url, - ex, - ) + _LOGGER.error("Error fetching data: %s failed with %s", self._resource, ex) self.data = None diff --git a/homeassistant/components/rest_command/__init__.py b/homeassistant/components/rest_command/__init__.py index 223dc7da7cc8..7dfbb964167e 100644 --- a/homeassistant/components/rest_command/__init__.py +++ b/homeassistant/components/rest_command/__init__.py @@ -4,17 +4,16 @@ import aiohttp from aiohttp import hdrs -import async_timeout import voluptuous as vol from homeassistant.const import ( - CONF_TIMEOUT, - CONF_USERNAME, + CONF_HEADERS, + CONF_METHOD, CONF_PASSWORD, - CONF_URL, CONF_PAYLOAD, - CONF_METHOD, - CONF_HEADERS, + CONF_TIMEOUT, + CONF_URL, + CONF_USERNAME, CONF_VERIFY_SSL, ) from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -38,7 +37,7 @@ vol.Optional(CONF_METHOD, default=DEFAULT_METHOD): vol.All( vol.Lower, vol.In(SUPPORT_REST_METHODS) ), - vol.Optional(CONF_HEADERS): vol.Schema({cv.string: cv.string}), + vol.Optional(CONF_HEADERS): vol.Schema({cv.string: cv.template}), vol.Inclusive(CONF_USERNAME, "authentication"): cv.string, vol.Inclusive(CONF_PASSWORD, "authentication"): cv.string, vol.Optional(CONF_PAYLOAD): cv.template, @@ -76,15 +75,15 @@ def async_register_rest_command(name, command_config): template_payload = command_config[CONF_PAYLOAD] template_payload.hass = hass - headers = None + template_headers = None if CONF_HEADERS in command_config: - headers = command_config[CONF_HEADERS] + template_headers = command_config[CONF_HEADERS] + for template_header in template_headers.values(): + template_header.hass = hass + content_type = None if CONF_CONTENT_TYPE in command_config: content_type = command_config[CONF_CONTENT_TYPE] - if headers is None: - headers = {} - headers[hdrs.CONTENT_TYPE] = content_type async def async_service_handler(service): """Execute a shell command service.""" @@ -95,22 +94,47 @@ async def async_service_handler(service): ) request_url = template_url.async_render(variables=service.data) - try: - with async_timeout.timeout(timeout): - request = await getattr(websession, method)( - request_url, data=payload, auth=auth, headers=headers + + headers = None + if template_headers: + headers = {} + for header_name, template_header in template_headers.items(): + headers[header_name] = template_header.async_render( + variables=service.data ) - if request.status < 400: - _LOGGER.info("Success call %s.", request.url) - else: - _LOGGER.warning("Error %d on call %s.", request.status, request.url) + if content_type: + if headers is None: + headers = {} + headers[hdrs.CONTENT_TYPE] = content_type + + try: + async with getattr(websession, method)( + request_url, + data=payload, + auth=auth, + headers=headers, + timeout=timeout, + ) as response: + + if response.status < 400: + _LOGGER.debug( + "Success. Url: %s. Status code: %d.", + response.url, + response.status, + ) + else: + _LOGGER.warning( + "Error. Url: %s. Status code %d.", + response.url, + response.status, + ) except asyncio.TimeoutError: - _LOGGER.warning("Timeout call %s.", request.url) + _LOGGER.warning("Timeout call %s.", response.url, exc_info=1) except aiohttp.ClientError: - _LOGGER.error("Client error %s.", request_url) + _LOGGER.error("Client error %s.", request_url, exc_info=1) # register services hass.services.async_register(DOMAIN, name, async_service_handler) diff --git a/homeassistant/components/rfxtrx/__init__.py b/homeassistant/components/rfxtrx/__init__.py index 1515ce33c6e6..ceba82cf544d 100644 --- a/homeassistant/components/rfxtrx/__init__.py +++ b/homeassistant/components/rfxtrx/__init__.py @@ -12,6 +12,8 @@ ATTR_STATE, CONF_DEVICE, CONF_DEVICES, + CONF_HOST, + CONF_PORT, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, POWER_WATT, @@ -80,17 +82,21 @@ _LOGGER = logging.getLogger(__name__) DATA_RFXOBJECT = "rfxobject" -CONFIG_SCHEMA = vol.Schema( +BASE_SCHEMA = vol.Schema( { - DOMAIN: vol.Schema( - { - vol.Required(CONF_DEVICE): cv.string, - vol.Optional(CONF_DEBUG, default=False): cv.boolean, - vol.Optional(CONF_DUMMY, default=False): cv.boolean, - } - ) - }, - extra=vol.ALLOW_EXTRA, + vol.Optional(CONF_DEBUG, default=False): cv.boolean, + vol.Optional(CONF_DUMMY, default=False): cv.boolean, + } +) + +DEVICE_SCHEMA = BASE_SCHEMA.extend({vol.Required(CONF_DEVICE): cv.string}) + +PORT_SCHEMA = BASE_SCHEMA.extend( + {vol.Required(CONF_PORT): cv.port, vol.Optional(CONF_HOST): cv.string} +) + +CONFIG_SCHEMA = vol.Schema( + {DOMAIN: vol.Any(DEVICE_SCHEMA, PORT_SCHEMA)}, extra=vol.ALLOW_EXTRA ) @@ -115,7 +121,9 @@ def handle_receive(event): for subscriber in RECEIVED_EVT_SUBSCRIBERS: subscriber(event) - device = config[DOMAIN][ATTR_DEVICE] + device = config[DOMAIN].get(ATTR_DEVICE) + host = config[DOMAIN].get(CONF_HOST) + port = config[DOMAIN].get(CONF_PORT) debug = config[DOMAIN][ATTR_DEBUG] dummy_connection = config[DOMAIN][ATTR_DUMMY] @@ -123,6 +131,14 @@ def handle_receive(event): rfx_object = rfxtrxmod.Connect( device, None, debug=debug, transport_protocol=rfxtrxmod.DummyTransport2 ) + elif port is not None: + # If port is set then we create a TCP connection + rfx_object = rfxtrxmod.Connect( + (host, port), + None, + debug=debug, + transport_protocol=rfxtrxmod.PyNetworkTransport, + ) else: rfx_object = rfxtrxmod.Connect(device, None, debug=debug) diff --git a/homeassistant/components/rfxtrx/manifest.json b/homeassistant/components/rfxtrx/manifest.json index a75a8ba9eb1d..e26ceb7ef57c 100644 --- a/homeassistant/components/rfxtrx/manifest.json +++ b/homeassistant/components/rfxtrx/manifest.json @@ -3,7 +3,7 @@ "name": "Rfxtrx", "documentation": "https://www.home-assistant.io/integrations/rfxtrx", "requirements": [ - "pyRFXtrx==0.23" + "pyRFXtrx==0.24" ], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/ring/__init__.py b/homeassistant/components/ring/__init__.py index 006b3ae3e9ab..a68749b2c67a 100644 --- a/homeassistant/components/ring/__init__.py +++ b/homeassistant/components/ring/__init__.py @@ -1,14 +1,15 @@ """Support for Ring Doorbell/Chimes.""" +from datetime import timedelta import logging -from datetime import timedelta from requests.exceptions import ConnectTimeout, HTTPError +from ring_doorbell import Ring import voluptuous as vol -from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, CONF_SCAN_INTERVAL -from homeassistant.helpers.event import track_time_interval -from homeassistant.helpers.dispatcher import dispatcher_send +from homeassistant.const import CONF_PASSWORD, CONF_SCAN_INTERVAL, CONF_USERNAME import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.dispatcher import dispatcher_send +from homeassistant.helpers.event import track_time_interval _LOGGER = logging.getLogger(__name__) @@ -50,8 +51,6 @@ def setup(hass, config): scan_interval = conf[CONF_SCAN_INTERVAL] try: - from ring_doorbell import Ring - cache = hass.config.path(DEFAULT_CACHEDB) ring = Ring(username=username, password=password, cache_file=cache) if not ring.is_connected: diff --git a/homeassistant/components/ring/camera.py b/homeassistant/components/ring/camera.py index 461b3a199d77..1d2fe6ff67b0 100644 --- a/homeassistant/components/ring/camera.py +++ b/homeassistant/components/ring/camera.py @@ -3,16 +3,18 @@ from datetime import timedelta import logging +from haffmpeg.camera import CameraMjpeg +from haffmpeg.tools import IMAGE_JPEG, ImageFrame import voluptuous as vol from homeassistant.components.camera import PLATFORM_SCHEMA, Camera from homeassistant.components.ffmpeg import DATA_FFMPEG from homeassistant.const import ATTR_ATTRIBUTION +from homeassistant.core import callback from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_aiohttp_proxy_stream -from homeassistant.util import dt as dt_util from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.core import callback +from homeassistant.util import dt as dt_util from . import ( ATTRIBUTION, @@ -122,7 +124,6 @@ def device_state_attributes(self): async def async_camera_image(self): """Return a still image response from the camera.""" - from haffmpeg.tools import ImageFrame, IMAGE_JPEG ffmpeg = ImageFrame(self._ffmpeg.binary, loop=self.hass.loop) @@ -140,7 +141,6 @@ async def async_camera_image(self): async def handle_async_mjpeg_stream(self, request): """Generate an HTTP MJPEG stream from the camera.""" - from haffmpeg.camera import CameraMjpeg if self._video_url is None: return diff --git a/homeassistant/components/ring/light.py b/homeassistant/components/ring/light.py index 697be4d15792..bc86e5b5fd19 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 timedelta +import logging + from homeassistant.components.light import Light -from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect import homeassistant.util.dt as dt_util from . import DATA_RING_STICKUP_CAMS, SIGNAL_UPDATE_RING diff --git a/homeassistant/components/ring/sensor.py b/homeassistant/components/ring/sensor.py index 6a64226a053e..b54c750664ef 100644 --- a/homeassistant/components/ring/sensor.py +++ b/homeassistant/components/ring/sensor.py @@ -9,11 +9,11 @@ CONF_ENTITY_NAMESPACE, CONF_MONITORED_CONDITIONS, ) +from homeassistant.core import callback import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import Entity from homeassistant.helpers.icon import icon_for_battery_level -from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.core import callback from . import ( ATTRIBUTION, diff --git a/homeassistant/components/ring/switch.py b/homeassistant/components/ring/switch.py index 413d2a70aae2..16fc4a6717fe 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 timedelta +import logging + from homeassistant.components.switch import SwitchDevice -from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect import homeassistant.util.dt as dt_util from . import DATA_RING_STICKUP_CAMS, SIGNAL_UPDATE_RING diff --git a/homeassistant/components/ripple/sensor.py b/homeassistant/components/ripple/sensor.py index ebbcec708c3a..ab0da77b173b 100644 --- a/homeassistant/components/ripple/sensor.py +++ b/homeassistant/components/ripple/sensor.py @@ -1,6 +1,7 @@ """Support for Ripple sensors.""" from datetime import timedelta +from pyripple import get_balance import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -62,7 +63,6 @@ def device_state_attributes(self): def update(self): """Get the latest state of the sensor.""" - from pyripple import get_balance balance = get_balance(self.address) if balance is not None: diff --git a/homeassistant/components/rocketchat/notify.py b/homeassistant/components/rocketchat/notify.py index 8657d0f94505..2b5b8dcd235f 100644 --- a/homeassistant/components/rocketchat/notify.py +++ b/homeassistant/components/rocketchat/notify.py @@ -1,16 +1,20 @@ """Rocket.Chat notification service.""" import logging +from rocketchat_API.APIExceptions.RocketExceptions import ( + RocketAuthenticationException, + RocketConnectionException, +) +from rocketchat_API.rocketchat import RocketChat import voluptuous as vol -from homeassistant.const import CONF_PASSWORD, CONF_ROOM, CONF_URL, CONF_USERNAME -import homeassistant.helpers.config_validation as cv - from homeassistant.components.notify import ( ATTR_DATA, PLATFORM_SCHEMA, BaseNotificationService, ) +from homeassistant.const import CONF_PASSWORD, CONF_ROOM, CONF_URL, CONF_USERNAME +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -27,10 +31,6 @@ def get_service(hass, config, discovery_info=None): """Return the notify service.""" - from rocketchat_API.APIExceptions.RocketExceptions import ( - RocketConnectionException, - RocketAuthenticationException, - ) username = config.get(CONF_USERNAME) password = config.get(CONF_PASSWORD) @@ -54,7 +54,6 @@ class RocketChatNotificationService(BaseNotificationService): def __init__(self, url, username, password, room): """Initialize the service.""" - from rocketchat_API.rocketchat import RocketChat self._room = room self._server = RocketChat(username, password, server_url=url) diff --git a/homeassistant/components/roku/__init__.py b/homeassistant/components/roku/__init__.py index aa13814ee6b7..b84b6dd1e63f 100644 --- a/homeassistant/components/roku/__init__.py +++ b/homeassistant/components/roku/__init__.py @@ -1,6 +1,7 @@ """Support for Roku.""" import logging +from roku import Roku, RokuException import voluptuous as vol from homeassistant.components.discovery import SERVICE_ROKU @@ -64,7 +65,6 @@ def roku_discovered(service, info): def scan_for_rokus(hass): """Scan for devices and present a notification of the ones found.""" - from roku import Roku, RokuException rokus = Roku.discover() @@ -94,7 +94,6 @@ def scan_for_rokus(hass): def _setup_roku(hass, hass_config, roku_config): """Set up a Roku.""" - from roku import Roku host = roku_config[CONF_HOST] diff --git a/homeassistant/components/roku/manifest.json b/homeassistant/components/roku/manifest.json index f2639b31d158..1b5f07eb87a2 100644 --- a/homeassistant/components/roku/manifest.json +++ b/homeassistant/components/roku/manifest.json @@ -3,7 +3,7 @@ "name": "Roku", "documentation": "https://www.home-assistant.io/integrations/roku", "requirements": [ - "roku==3.1" + "roku==4.0.0" ], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/roku/media_player.py b/homeassistant/components/roku/media_player.py index 12aca1415105..a785d7b18ff9 100644 --- a/homeassistant/components/roku/media_player.py +++ b/homeassistant/components/roku/media_player.py @@ -1,6 +1,8 @@ """Support for the Roku media player.""" import logging + import requests.exceptions +from roku import Roku from homeassistant.components.media_player import MediaPlayerDevice from homeassistant.components.media_player.const import ( @@ -10,10 +12,10 @@ SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK, SUPPORT_SELECT_SOURCE, + SUPPORT_TURN_OFF, + SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, - SUPPORT_TURN_ON, - SUPPORT_TURN_OFF, ) from homeassistant.const import ( CONF_HOST, @@ -54,7 +56,6 @@ class RokuDevice(MediaPlayerDevice): def __init__(self, host): """Initialize the Roku device.""" - from roku import Roku self.roku = Roku(host) self.ip_address = host @@ -174,7 +175,7 @@ def source_list(self): def turn_on(self): """Turn on the Roku.""" - self.roku.power() + self.roku.poweron() def turn_off(self): """Turn off the Roku.""" diff --git a/homeassistant/components/roku/remote.py b/homeassistant/components/roku/remote.py index f443b7e8e74e..c953d9ba734e 100644 --- a/homeassistant/components/roku/remote.py +++ b/homeassistant/components/roku/remote.py @@ -1,5 +1,6 @@ """Support for the Roku remote.""" import requests.exceptions +from roku import Roku from homeassistant.components import remote from homeassistant.const import CONF_HOST @@ -19,7 +20,6 @@ class RokuRemote(remote.RemoteDevice): def __init__(self, host): """Initialize the Roku device.""" - from roku import Roku self.roku = Roku(host) self._device_info = {} diff --git a/homeassistant/components/roku/services.yaml b/homeassistant/components/roku/services.yaml index e69de29bb2d1..956ecb0dd2d2 100644 --- a/homeassistant/components/roku/services.yaml +++ b/homeassistant/components/roku/services.yaml @@ -0,0 +1,2 @@ +roku_scan: + description: Scans the local network for Rokus. All found devices are presented as a persistent notification. diff --git a/homeassistant/components/roomba/manifest.json b/homeassistant/components/roomba/manifest.json index 5064357a7df4..92ed16406db6 100644 --- a/homeassistant/components/roomba/manifest.json +++ b/homeassistant/components/roomba/manifest.json @@ -3,7 +3,7 @@ "name": "Roomba", "documentation": "https://www.home-assistant.io/integrations/roomba", "requirements": [ - "roombapy==1.3.1" + "roombapy==1.4.2" ], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/roomba/vacuum.py b/homeassistant/components/roomba/vacuum.py index 291658e19f4b..172a494b602e 100644 --- a/homeassistant/components/roomba/vacuum.py +++ b/homeassistant/components/roomba/vacuum.py @@ -3,12 +3,14 @@ import logging import async_timeout +from roomba import Roomba import voluptuous as vol from homeassistant.components.vacuum import ( PLATFORM_SCHEMA, SUPPORT_BATTERY, SUPPORT_FAN_SPEED, + SUPPORT_LOCATE, SUPPORT_PAUSE, SUPPORT_RETURN_HOME, SUPPORT_SEND_COMMAND, @@ -16,7 +18,6 @@ SUPPORT_STOP, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, - SUPPORT_LOCATE, VacuumDevice, ) from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_USERNAME @@ -33,15 +34,16 @@ ATTR_POSITION = "position" ATTR_SOFTWARE_VERSION = "software_version" -CAP_BIN_FULL = "bin_full" CAP_POSITION = "position" CAP_CARPET_BOOST = "carpet_boost" CONF_CERT = "certificate" CONF_CONTINUOUS = "continuous" +CONF_DELAY = "delay" DEFAULT_CERT = "/etc/ssl/certs/ca-certificates.crt" DEFAULT_CONTINUOUS = True +DEFAULT_DELAY = 1 DEFAULT_NAME = "Roomba" PLATFORM = "roomba" @@ -59,6 +61,7 @@ vol.Required(CONF_PASSWORD): cv.string, vol.Optional(CONF_CERT, default=DEFAULT_CERT): cv.string, vol.Optional(CONF_CONTINUOUS, default=DEFAULT_CONTINUOUS): cv.boolean, + vol.Optional(CONF_DELAY, default=DEFAULT_DELAY): cv.positive_int, }, extra=vol.ALLOW_EXTRA, ) @@ -82,7 +85,6 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the iRobot Roomba vacuum cleaner platform.""" - from roomba import Roomba if PLATFORM not in hass.data: hass.data[PLATFORM] = {} @@ -93,6 +95,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= password = config.get(CONF_PASSWORD) certificate = config.get(CONF_CERT) continuous = config.get(CONF_CONTINUOUS) + delay = config.get(CONF_DELAY) roomba = Roomba( address=host, @@ -100,6 +103,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= password=password, cert_name=certificate, continuous=continuous, + delay=delay, ) _LOGGER.debug("Initializing communication with host %s", host) @@ -271,18 +275,14 @@ async def async_update(self): # Get the capabilities of our unit capabilities = state.get("cap", {}) - cap_bin_full = capabilities.get("binFullDetect") cap_carpet_boost = capabilities.get("carpetBoost") cap_pos = capabilities.get("pose") # Store capabilities self._capabilities = { - CAP_BIN_FULL: cap_bin_full == 1, CAP_CARPET_BOOST: cap_carpet_boost == 1, CAP_POSITION: cap_pos == 1, } - bin_state = state.get("bin", {}) - # Roomba software version software_version = state.get("softwareVer") @@ -296,10 +296,11 @@ async def async_update(self): self._is_on = self._status in ["Running"] # Set properties that are to appear in the GUI - self._state_attrs = { - ATTR_BIN_PRESENT: bin_state.get("present"), - ATTR_SOFTWARE_VERSION: software_version, - } + self._state_attrs = {ATTR_SOFTWARE_VERSION: software_version} + + # Get bin state + bin_state = self._get_bin_state(state) + self._state_attrs.update(bin_state) # Only add cleaning time and cleaned area attrs when the vacuum is # currently on @@ -330,10 +331,6 @@ async def async_update(self): position = f"({pos_x}, {pos_y}, {theta})" self._state_attrs[ATTR_POSITION] = position - # Not all Roombas have a bin full sensor - if self._capabilities[CAP_BIN_FULL]: - self._state_attrs[ATTR_BIN_FULL] = bin_state.get("full") - # Fan speed mode (Performance, Automatic or Eco) # Not all Roombas expose carpet boost if self._capabilities[CAP_CARPET_BOOST]: @@ -350,3 +347,16 @@ async def async_update(self): fan_speed = FAN_SPEED_ECO self._fan_speed = fan_speed + + @staticmethod + def _get_bin_state(state): + bin_raw_state = state.get("bin", {}) + bin_state = {} + + if bin_raw_state.get("present") is not None: + bin_state[ATTR_BIN_PRESENT] = bin_raw_state.get("present") + + if bin_raw_state.get("full") is not None: + bin_state[ATTR_BIN_FULL] = bin_raw_state.get("full") + + return bin_state diff --git a/homeassistant/components/route53/__init__.py b/homeassistant/components/route53/__init__.py index 3dffc3ffd9e3..a84475ab8a19 100644 --- a/homeassistant/components/route53/__init__.py +++ b/homeassistant/components/route53/__init__.py @@ -3,6 +3,8 @@ import logging from typing import List +import boto3 +from ipify import exceptions, get_ip import voluptuous as vol from homeassistant.const import CONF_DOMAIN, CONF_TTL, CONF_ZONE @@ -72,10 +74,6 @@ def _update_route53( records: List[str], ttl: int, ): - import boto3 - from ipify import get_ip - from ipify import exceptions - _LOGGER.debug("Starting update for zone %s", zone) client = boto3.client( diff --git a/homeassistant/components/rova/sensor.py b/homeassistant/components/rova/sensor.py index fe0b5dead84d..86a04829c750 100644 --- a/homeassistant/components/rova/sensor.py +++ b/homeassistant/components/rova/sensor.py @@ -3,6 +3,8 @@ from datetime import datetime, timedelta import logging +from requests.exceptions import ConnectTimeout, HTTPError +from rova.rova import Rova import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -49,8 +51,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Create the Rova data service and sensors.""" - from rova.rova import Rova - from requests.exceptions import HTTPError, ConnectTimeout zip_code = config[CONF_ZIP_CODE] house_number = config[CONF_HOUSE_NUMBER] @@ -132,7 +132,6 @@ def __init__(self, api): @Throttle(UPDATE_DELAY) def update(self): """Update the data from the Rova API.""" - from requests.exceptions import HTTPError, ConnectTimeout try: items = self.api.get_calendar_items() diff --git a/homeassistant/components/rpi_gpio_pwm/light.py b/homeassistant/components/rpi_gpio_pwm/light.py index 27dd4da80ac2..aededbc676cf 100644 --- a/homeassistant/components/rpi_gpio_pwm/light.py +++ b/homeassistant/components/rpi_gpio_pwm/light.py @@ -1,22 +1,28 @@ """Support for LED lights that can be controlled using PWM.""" import logging +from pwmled import Color +from pwmled.driver.gpio import GpioDriver +from pwmled.driver.pca9685 import Pca9685Driver +from pwmled.led import SimpleLed +from pwmled.led.rgb import RgbLed +from pwmled.led.rgbw import RgbwLed import voluptuous as vol -from homeassistant.const import CONF_NAME, CONF_TYPE, STATE_ON, CONF_ADDRESS from homeassistant.components.light import ( - Light, ATTR_BRIGHTNESS, ATTR_HS_COLOR, ATTR_TRANSITION, + PLATFORM_SCHEMA, SUPPORT_BRIGHTNESS, SUPPORT_COLOR, SUPPORT_TRANSITION, - PLATFORM_SCHEMA, + Light, ) +from homeassistant.const import CONF_ADDRESS, CONF_NAME, CONF_TYPE, STATE_ON import homeassistant.helpers.config_validation as cv -import homeassistant.util.color as color_util from homeassistant.helpers.restore_state import RestoreEntity +import homeassistant.util.color as color_util _LOGGER = logging.getLogger(__name__) @@ -61,11 +67,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the PWM LED lights.""" - from pwmled.led import SimpleLed - from pwmled.led.rgb import RgbLed - from pwmled.led.rgbw import RgbwLed - from pwmled.driver.gpio import GpioDriver - from pwmled.driver.pca9685 import Pca9685Driver leds = [] for led_conf in config[CONF_LEDS]: @@ -240,7 +241,6 @@ def _from_hass_brightness(brightness): def _from_hass_color(color): """Convert Home Assistant RGB list to Color tuple.""" - from pwmled import Color rgb = color_util.color_hs_to_RGB(*color) return Color(*tuple(rgb)) diff --git a/homeassistant/components/rpi_rf/switch.py b/homeassistant/components/rpi_rf/switch.py index 18e4a28d5c86..5c09111c1cbc 100644 --- a/homeassistant/components/rpi_rf/switch.py +++ b/homeassistant/components/rpi_rf/switch.py @@ -1,10 +1,11 @@ """Support for a switch using a 433MHz module via GPIO on a Raspberry Pi.""" import importlib import logging +from threading import RLock import voluptuous as vol -from homeassistant.components.switch import SwitchDevice, PLATFORM_SCHEMA +from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchDevice from homeassistant.const import CONF_NAME, CONF_SWITCHES, EVENT_HOMEASSISTANT_STOP import homeassistant.helpers.config_validation as cv @@ -44,7 +45,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Find and return switches controlled by a generic RF device via GPIO.""" rpi_rf = importlib.import_module("rpi_rf") - from threading import RLock gpio = config.get(CONF_GPIO) rfdevice = rpi_rf.RFDevice(gpio) diff --git a/homeassistant/components/russound_rio/media_player.py b/homeassistant/components/russound_rio/media_player.py index 69967c21fd59..fdcd308618ad 100644 --- a/homeassistant/components/russound_rio/media_player.py +++ b/homeassistant/components/russound_rio/media_player.py @@ -1,6 +1,7 @@ """Support for Russound multizone controllers using RIO Protocol.""" import logging +from russound_rio import Russound import voluptuous as vol from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA @@ -44,7 +45,6 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the Russound RIO platform.""" - from russound_rio import Russound host = config.get(CONF_HOST) port = config.get(CONF_PORT) diff --git a/homeassistant/components/russound_rnet/media_player.py b/homeassistant/components/russound_rnet/media_player.py index e62e0a6b3af0..70ed12123638 100644 --- a/homeassistant/components/russound_rnet/media_player.py +++ b/homeassistant/components/russound_rnet/media_player.py @@ -1,9 +1,10 @@ """Support for interfacing with Russound via RNET Protocol.""" import logging +from russound import russound import voluptuous as vol -from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA +from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice from homeassistant.components.media_player.const import ( SUPPORT_SELECT_SOURCE, SUPPORT_TURN_OFF, @@ -51,8 +52,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): _LOGGER.error("Invalid config. Expected %s and %s", CONF_HOST, CONF_PORT) return False - from russound import russound - russ = russound.Russound(host, port) russ.connect() diff --git a/homeassistant/components/sabnzbd/__init__.py b/homeassistant/components/sabnzbd/__init__.py index bf5e90e21f14..f436bcb8a722 100644 --- a/homeassistant/components/sabnzbd/__init__.py +++ b/homeassistant/components/sabnzbd/__init__.py @@ -1,14 +1,14 @@ """Support for monitoring an SABnzbd NZB client.""" -import logging from datetime import timedelta +import logging +from pysabnzbd import SabnzbdApi, SabnzbdApiException import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.discovery import SERVICE_SABNZBD from homeassistant.const import ( - CONF_HOST, CONF_API_KEY, + CONF_HOST, CONF_NAME, CONF_PATH, CONF_PORT, @@ -18,6 +18,7 @@ from homeassistant.core import callback from homeassistant.helpers import discovery from homeassistant.helpers.aiohttp_client import async_get_clientsession +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.event import async_track_time_interval from homeassistant.util.json import load_json, save_json @@ -86,7 +87,6 @@ async def async_check_sabnzbd(sab_api): """Check if we can reach SABnzbd.""" - from pysabnzbd import SabnzbdApiException try: await sab_api.check_available() @@ -100,7 +100,6 @@ async def async_configure_sabnzbd( hass, config, use_ssl, name=DEFAULT_NAME, api_key=None ): """Try to configure Sabnzbd and request api key if configuration fails.""" - from pysabnzbd import SabnzbdApi host = config[CONF_HOST] port = config[CONF_PORT] @@ -174,7 +173,6 @@ async def async_service_handler(service): async def async_update_sabnzbd(now): """Refresh SABnzbd queue data.""" - from pysabnzbd import SabnzbdApiException try: await sab_api.refresh_data() @@ -188,7 +186,6 @@ async def async_update_sabnzbd(now): @callback def async_request_configuration(hass, config, host, web_root): """Request configuration steps from the user.""" - from pysabnzbd import SabnzbdApi configurator = hass.components.configurator # We got an error if this method is called while we are configuring @@ -239,7 +236,6 @@ def __init__(self, sab_api, name, sensors): async def async_pause_queue(self): """Pause Sabnzbd queue.""" - from pysabnzbd import SabnzbdApiException try: return await self.sab_api.pause_queue() @@ -249,7 +245,6 @@ async def async_pause_queue(self): async def async_resume_queue(self): """Resume Sabnzbd queue.""" - from pysabnzbd import SabnzbdApiException try: return await self.sab_api.resume_queue() @@ -259,7 +254,6 @@ async def async_resume_queue(self): async def async_set_queue_speed(self, limit): """Set speed limit for the Sabnzbd queue.""" - from pysabnzbd import SabnzbdApiException try: return await self.sab_api.set_speed_limit(limit) diff --git a/homeassistant/components/saj/sensor.py b/homeassistant/components/saj/sensor.py index 2a17d110c6e4..52ae3640a7f6 100644 --- a/homeassistant/components/saj/sensor.py +++ b/homeassistant/components/saj/sensor.py @@ -24,6 +24,7 @@ TEMP_FAHRENHEIT, ) from homeassistant.core import CALLBACK_TYPE, callback +from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import async_call_later @@ -38,12 +39,12 @@ INVERTER_TYPES = ["ethernet", "wifi"] SAJ_UNIT_MAPPINGS = { - "W": POWER_WATT, - "kWh": ENERGY_KILO_WATT_HOUR, + "": None, "h": UNIT_OF_MEASUREMENT_HOURS, "kg": MASS_KILOGRAMS, + "kWh": ENERGY_KILO_WATT_HOUR, + "W": POWER_WATT, "°C": TEMP_CELSIUS, - "": None, } PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( @@ -58,7 +59,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Set up SAJ sensors.""" + """Set up the SAJ sensors.""" remove_interval_update = None wifi = config[CONF_TYPE] == INVERTER_TYPES[1] @@ -80,7 +81,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= saj = pysaj.SAJ(config[CONF_HOST], **kwargs) done = await saj.read(sensor_def) except pysaj.UnauthorizedException: - _LOGGER.error("Username and/or password is wrong.") + _LOGGER.error("Username and/or password is wrong") return except pysaj.UnexpectedResponseException as err: _LOGGER.error( @@ -88,13 +89,15 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= ) return - if done: - for sensor in sensor_def: - hass_sensors.append( - SAJsensor(saj.serialnumber, sensor, inverter_name=config.get(CONF_NAME)) - ) + if not done: + raise PlatformNotReady + + for sensor in sensor_def: + hass_sensors.append( + SAJsensor(saj.serialnumber, sensor, inverter_name=config.get(CONF_NAME)) + ) - async_add_entities(hass_sensors) + async_add_entities(hass_sensors) async def async_saj(): """Update all the SAJ sensors.""" @@ -167,7 +170,7 @@ class SAJsensor(Entity): """Representation of a SAJ sensor.""" def __init__(self, serialnumber, pysaj_sensor, inverter_name=None): - """Initialize the sensor.""" + """Initialize the SAJ sensor.""" self._sensor = pysaj_sensor self._inverter_name = inverter_name self._serialnumber = serialnumber diff --git a/homeassistant/components/samsungtv/media_player.py b/homeassistant/components/samsungtv/media_player.py index aa6e3ae62d12..2488d5ab9132 100644 --- a/homeassistant/components/samsungtv/media_player.py +++ b/homeassistant/components/samsungtv/media_player.py @@ -6,6 +6,7 @@ from samsungctl import exceptions as samsung_exceptions, Remote as SamsungRemote import voluptuous as vol import wakeonlan +from websocket import WebSocketException from homeassistant.components.media_player import ( MediaPlayerDevice, @@ -26,6 +27,7 @@ SUPPORT_VOLUME_STEP, ) from homeassistant.const import ( + CONF_BROADCAST_ADDRESS, CONF_HOST, CONF_MAC, CONF_NAME, @@ -41,6 +43,7 @@ DEFAULT_NAME = "Samsung TV Remote" DEFAULT_TIMEOUT = 1 +DEFAULT_BROADCAST_ADDRESS = "255.255.255.255" KEY_PRESS_TIMEOUT = 1.2 KNOWN_DEVICES_KEY = "samsungtv_known_devices" @@ -65,6 +68,9 @@ vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_PORT): cv.port, vol.Optional(CONF_MAC): cv.string, + vol.Optional( + CONF_BROADCAST_ADDRESS, default=DEFAULT_BROADCAST_ADDRESS + ): cv.string, vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int, } ) @@ -84,6 +90,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): port = config.get(CONF_PORT) name = config.get(CONF_NAME) mac = config.get(CONF_MAC) + broadcast = config.get(CONF_BROADCAST_ADDRESS) timeout = config.get(CONF_TIMEOUT) elif discovery_info is not None: tv_name = discovery_info.get("name") @@ -95,6 +102,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): port = None timeout = DEFAULT_TIMEOUT mac = None + broadcast = DEFAULT_BROADCAST_ADDRESS uuid = discovery_info.get("udn") if uuid and uuid.startswith("uuid:"): uuid = uuid[len("uuid:") :] @@ -104,7 +112,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): ip_addr = socket.gethostbyname(host) if ip_addr not in known_devices: known_devices.add(ip_addr) - add_entities([SamsungTVDevice(host, port, name, timeout, mac, uuid)]) + add_entities([SamsungTVDevice(host, port, name, timeout, mac, broadcast, uuid)]) LOGGER.info("Samsung TV %s added as '%s'", host, name) else: LOGGER.info("Ignoring duplicate Samsung TV %s", host) @@ -113,12 +121,13 @@ def setup_platform(hass, config, add_entities, discovery_info=None): class SamsungTVDevice(MediaPlayerDevice): """Representation of a Samsung TV.""" - def __init__(self, host, port, name, timeout, mac, uuid): + def __init__(self, host, port, name, timeout, mac, broadcast, uuid): """Initialize the Samsung device.""" # Save a reference to the imported classes self._name = name self._mac = mac + self._broadcast = broadcast self._uuid = uuid # Assume that the TV is not muted self._muted = False @@ -201,23 +210,26 @@ def send_key(self, key): try: self.get_remote().control(key) break - except (samsung_exceptions.ConnectionClosed, BrokenPipeError): + except ( + samsung_exceptions.ConnectionClosed, + BrokenPipeError, + WebSocketException, + ): # BrokenPipe can occur when the commands is sent to fast + # WebSocketException can occur when timed out self._remote = None self._state = STATE_ON except AttributeError: # Auto-detect could not find working config yet pass - except ( - samsung_exceptions.UnhandledResponse, - samsung_exceptions.AccessDenied, - ): + except (samsung_exceptions.UnhandledResponse, samsung_exceptions.AccessDenied): # We got a response so it's on. self._state = STATE_ON self._remote = None LOGGER.debug("Failed sending command %s", key, exc_info=True) return except OSError: + # Different reasons, e.g. hostname not resolveable self._state = STATE_OFF self._remote = None if self._power_off_in_progress(): @@ -312,11 +324,11 @@ def media_pause(self): def media_next_track(self): """Send next track command.""" - self.send_key("KEY_FF") + self.send_key("KEY_CHUP") def media_previous_track(self): """Send the previous track command.""" - self.send_key("KEY_REWIND") + self.send_key("KEY_CHDOWN") async def async_play_media(self, media_type, media_id, **kwargs): """Support changing a channel.""" @@ -339,7 +351,7 @@ async def async_play_media(self, media_type, media_id, **kwargs): def turn_on(self): """Turn the media player on.""" if self._mac: - wakeonlan.send_magic_packet(self._mac) + wakeonlan.send_magic_packet(self._mac, ip_address=self._broadcast) else: self.send_key("KEY_POWERON") diff --git a/homeassistant/components/satel_integra/__init__.py b/homeassistant/components/satel_integra/__init__.py index a657f6239d1e..1972eefd6b50 100644 --- a/homeassistant/components/satel_integra/__init__.py +++ b/homeassistant/components/satel_integra/__init__.py @@ -2,9 +2,10 @@ import collections import logging +from satel_integra.satel_integra import AsyncSatel import voluptuous as vol -from homeassistant.const import EVENT_HOMEASSISTANT_STOP, CONF_HOST, CONF_PORT +from homeassistant.const import CONF_HOST, CONF_PORT, EVENT_HOMEASSISTANT_STOP from homeassistant.core import callback from homeassistant.helpers import config_validation as cv from homeassistant.helpers.discovery import async_load_platform @@ -102,8 +103,6 @@ async def async_setup(hass, config): port = conf.get(CONF_PORT) partitions = conf.get(CONF_DEVICE_PARTITIONS) - from satel_integra.satel_integra import AsyncSatel - monitored_outputs = collections.OrderedDict( list(outputs.items()) + list(switchable_outputs.items()) ) diff --git a/homeassistant/components/satel_integra/alarm_control_panel.py b/homeassistant/components/satel_integra/alarm_control_panel.py index 2f0e165f21f1..d4294788fdda 100644 --- a/homeassistant/components/satel_integra/alarm_control_panel.py +++ b/homeassistant/components/satel_integra/alarm_control_panel.py @@ -1,9 +1,15 @@ """Support for Satel Integra alarm, using ETHM module.""" import asyncio -import logging from collections import OrderedDict +import logging + +from satel_integra.satel_integra import AlarmState import homeassistant.components.alarm_control_panel as alarm +from homeassistant.components.alarm_control_panel.const import ( + SUPPORT_ALARM_ARM_AWAY, + SUPPORT_ALARM_ARM_HOME, +) from homeassistant.const import ( STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, @@ -17,8 +23,8 @@ from . import ( CONF_ARM_HOME_MODE, CONF_DEVICE_PARTITIONS, - DATA_SATEL, CONF_ZONE_NAME, + DATA_SATEL, SIGNAL_PANEL_MESSAGE, ) @@ -78,7 +84,6 @@ def _update_alarm_status(self): def _read_alarm_state(self): """Read current status of the alarm and translate it into HA status.""" - from satel_integra.satel_integra import AlarmState # Default - disarmed: hass_alarm_status = STATE_ALARM_DISARMED @@ -131,6 +136,11 @@ def state(self): """Return the state of the device.""" return self._state + @property + def supported_features(self) -> int: + """Return the list of supported features.""" + return SUPPORT_ALARM_ARM_HOME | SUPPORT_ALARM_ARM_AWAY + async def async_alarm_disarm(self, code=None): """Send disarm command.""" if not code: diff --git a/homeassistant/components/satel_integra/binary_sensor.py b/homeassistant/components/satel_integra/binary_sensor.py index 1e4877229b99..cbe760c06bf1 100644 --- a/homeassistant/components/satel_integra/binary_sensor.py +++ b/homeassistant/components/satel_integra/binary_sensor.py @@ -10,9 +10,9 @@ CONF_ZONE_NAME, CONF_ZONE_TYPE, CONF_ZONES, + DATA_SATEL, SIGNAL_OUTPUTS_UPDATED, SIGNAL_ZONES_UPDATED, - DATA_SATEL, ) _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/satel_integra/switch.py b/homeassistant/components/satel_integra/switch.py index 5b5e4f3095bb..c20f30cf8715 100644 --- a/homeassistant/components/satel_integra/switch.py +++ b/homeassistant/components/satel_integra/switch.py @@ -9,8 +9,8 @@ CONF_DEVICE_CODE, CONF_SWITCHABLE_OUTPUTS, CONF_ZONE_NAME, - SIGNAL_OUTPUTS_UPDATED, DATA_SATEL, + SIGNAL_OUTPUTS_UPDATED, ) _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/scene/services.yaml b/homeassistant/components/scene/services.yaml index 9cf1b9010a80..0c261ed60b50 100644 --- a/homeassistant/components/scene/services.yaml +++ b/homeassistant/components/scene/services.yaml @@ -34,3 +34,8 @@ create: light.ceiling: state: "on" brightness: 200 + snapshot_entities: + description: The entities of which a snapshot is to be taken + example: + - light.ceiling + - light.kitchen diff --git a/homeassistant/components/script/__init__.py b/homeassistant/components/script/__init__.py index 5a3223a8508f..cb9cb5194ba1 100644 --- a/homeassistant/components/script/__init__.py +++ b/homeassistant/components/script/__init__.py @@ -19,7 +19,7 @@ from homeassistant.helpers.entity import ToggleEntity from homeassistant.helpers.entity_component import EntityComponent import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.config_validation import ENTITY_SERVICE_SCHEMA +from homeassistant.helpers.config_validation import make_entity_service_schema from homeassistant.helpers.service import async_set_service_schema from homeassistant.helpers.script import Script @@ -60,7 +60,7 @@ ) SCRIPT_SERVICE_SCHEMA = vol.Schema(dict) -SCRIPT_TURN_ONOFF_SCHEMA = ENTITY_SERVICE_SCHEMA.extend( +SCRIPT_TURN_ONOFF_SCHEMA = make_entity_service_schema( {vol.Optional(ATTR_VARIABLES): dict} ) RELOAD_SERVICE_SCHEMA = vol.Schema({}) @@ -207,7 +207,7 @@ async def async_turn_on(self, **kwargs): ) try: await self.script.async_run(kwargs.get(ATTR_VARIABLES), context) - except Exception as err: # pylint: disable=broad-except + except Exception as err: self.script.async_log_exception( _LOGGER, f"Error executing script {self.entity_id}", err ) diff --git a/homeassistant/components/scsgate/__init__.py b/homeassistant/components/scsgate/__init__.py index 739a2949d17a..21e3608a51b5 100644 --- a/homeassistant/components/scsgate/__init__.py +++ b/homeassistant/components/scsgate/__init__.py @@ -2,6 +2,10 @@ import logging from threading import Lock +from scsgate.connection import Connection +from scsgate.messages import ScenarioTriggeredMessage, StateMessage +from scsgate.reactor import Reactor +from scsgate.tasks import GetStatusTask import voluptuous as vol from homeassistant.const import CONF_DEVICE, CONF_NAME @@ -61,12 +65,8 @@ def __init__(self, device, logger): self._device_being_registered = None self._device_being_registered_lock = Lock() - from scsgate.connection import Connection - connection = Connection(device=device, logger=self._logger) - from scsgate.reactor import Reactor - self._reactor = Reactor( connection=connection, logger=self._logger, @@ -75,7 +75,6 @@ def __init__(self, device, logger): def handle_message(self, message): """Handle a messages seen on the bus.""" - from scsgate.messages import StateMessage, ScenarioTriggeredMessage self._logger.debug(f"Received message {message}") if not isinstance(message, StateMessage) and not isinstance( @@ -132,7 +131,6 @@ def add_devices_to_register(self, devices): def _activate_next_device(self): """Start the activation of the first device.""" - from scsgate.tasks import GetStatusTask with self._devices_to_register_lock: while self._devices_to_register: diff --git a/homeassistant/components/scsgate/cover.py b/homeassistant/components/scsgate/cover.py index 9aa19e3f6681..9d034c146ee2 100644 --- a/homeassistant/components/scsgate/cover.py +++ b/homeassistant/components/scsgate/cover.py @@ -1,10 +1,15 @@ """Support for SCSGate covers.""" import logging +from scsgate.tasks import ( + HaltRollerShutterTask, + LowerRollerShutterTask, + RaiseRollerShutterTask, +) import voluptuous as vol from homeassistant.components import scsgate -from homeassistant.components.cover import CoverDevice, PLATFORM_SCHEMA +from homeassistant.components.cover import PLATFORM_SCHEMA, CoverDevice from homeassistant.const import CONF_DEVICES, CONF_NAME import homeassistant.helpers.config_validation as cv @@ -69,20 +74,14 @@ def is_closed(self): def open_cover(self, **kwargs): """Move the cover.""" - from scsgate.tasks import RaiseRollerShutterTask - scsgate.SCSGATE.append_task(RaiseRollerShutterTask(target=self._scs_id)) def close_cover(self, **kwargs): """Move the cover down.""" - from scsgate.tasks import LowerRollerShutterTask - scsgate.SCSGATE.append_task(LowerRollerShutterTask(target=self._scs_id)) def stop_cover(self, **kwargs): """Stop the cover.""" - from scsgate.tasks import HaltRollerShutterTask - scsgate.SCSGATE.append_task(HaltRollerShutterTask(target=self._scs_id)) def process_event(self, message): diff --git a/homeassistant/components/scsgate/light.py b/homeassistant/components/scsgate/light.py index c183fc6a3f88..a04dfdc7e7ae 100644 --- a/homeassistant/components/scsgate/light.py +++ b/homeassistant/components/scsgate/light.py @@ -1,10 +1,11 @@ """Support for SCSGate lights.""" import logging +from scsgate.tasks import ToggleStatusTask import voluptuous as vol from homeassistant.components import scsgate -from homeassistant.components.light import Light, PLATFORM_SCHEMA +from homeassistant.components.light import PLATFORM_SCHEMA, Light from homeassistant.const import ATTR_ENTITY_ID, ATTR_STATE, CONF_DEVICES, CONF_NAME import homeassistant.helpers.config_validation as cv @@ -70,7 +71,6 @@ def is_on(self): def turn_on(self, **kwargs): """Turn the device on.""" - from scsgate.tasks import ToggleStatusTask scsgate.SCSGATE.append_task(ToggleStatusTask(target=self._scs_id, toggled=True)) @@ -79,7 +79,6 @@ def turn_on(self, **kwargs): def turn_off(self, **kwargs): """Turn the device off.""" - from scsgate.tasks import ToggleStatusTask scsgate.SCSGATE.append_task( ToggleStatusTask(target=self._scs_id, toggled=False) diff --git a/homeassistant/components/scsgate/switch.py b/homeassistant/components/scsgate/switch.py index 75e55e259a6b..b2043d3a4c39 100644 --- a/homeassistant/components/scsgate/switch.py +++ b/homeassistant/components/scsgate/switch.py @@ -1,11 +1,13 @@ """Support for SCSGate switches.""" import logging +from scsgate.messages import ScenarioTriggeredMessage, StateMessage +from scsgate.tasks import ToggleStatusTask import voluptuous as vol from homeassistant.components import scsgate -from homeassistant.components.switch import SwitchDevice, PLATFORM_SCHEMA -from homeassistant.const import ATTR_ENTITY_ID, ATTR_STATE, CONF_NAME, CONF_DEVICES +from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchDevice +from homeassistant.const import ATTR_ENTITY_ID, ATTR_STATE, CONF_DEVICES, CONF_NAME import homeassistant.helpers.config_validation as cv ATTR_SCENARIO_ID = "scenario_id" @@ -105,7 +107,6 @@ def is_on(self): def turn_on(self, **kwargs): """Turn the device on.""" - from scsgate.tasks import ToggleStatusTask scsgate.SCSGATE.append_task(ToggleStatusTask(target=self._scs_id, toggled=True)) @@ -114,7 +115,6 @@ def turn_on(self, **kwargs): def turn_off(self, **kwargs): """Turn the device off.""" - from scsgate.tasks import ToggleStatusTask scsgate.SCSGATE.append_task( ToggleStatusTask(target=self._scs_id, toggled=False) @@ -172,7 +172,6 @@ def name(self): def process_event(self, message): """Handle a SCSGate message related with this switch.""" - from scsgate.messages import StateMessage, ScenarioTriggeredMessage if isinstance(message, StateMessage): scenario_id = message.bytes[4] diff --git a/homeassistant/components/season/manifest.json b/homeassistant/components/season/manifest.json index 528e9ef35f1f..445c2bc38273 100644 --- a/homeassistant/components/season/manifest.json +++ b/homeassistant/components/season/manifest.json @@ -3,7 +3,7 @@ "name": "Season", "documentation": "https://www.home-assistant.io/integrations/season", "requirements": [ - "ephem==3.7.6.0" + "ephem==3.7.7.0" ], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/season/sensor.py b/homeassistant/components/season/sensor.py index 46d2291cf812..8c237c1da19f 100644 --- a/homeassistant/components/season/sensor.py +++ b/homeassistant/components/season/sensor.py @@ -1,27 +1,34 @@ """Support for tracking which astronomical or meteorological season it is.""" -import logging from datetime import datetime +import logging import ephem import voluptuous as vol +from homeassistant import util from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import CONF_TYPE +from homeassistant.const import CONF_NAME, CONF_TYPE +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity -from homeassistant import util import homeassistant.util.dt as dt_util _LOGGER = logging.getLogger(__name__) +DEFAULT_NAME = "Season" + +EQUATOR = "equator" + NORTHERN = "northern" + SOUTHERN = "southern" -EQUATOR = "equator" +STATE_AUTUMN = "autumn" STATE_SPRING = "spring" STATE_SUMMER = "summer" -STATE_AUTUMN = "autumn" STATE_WINTER = "winter" + TYPE_ASTRONOMICAL = "astronomical" TYPE_METEOROLOGICAL = "meteorological" + VALID_TYPES = [TYPE_ASTRONOMICAL, TYPE_METEOROLOGICAL] HEMISPHERE_SEASON_SWAP = { @@ -40,7 +47,10 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - {vol.Optional(CONF_TYPE, default=TYPE_ASTRONOMICAL): vol.In(VALID_TYPES)} + { + vol.Optional(CONF_TYPE, default=TYPE_ASTRONOMICAL): vol.In(VALID_TYPES), + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + } ) @@ -52,6 +62,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): latitude = util.convert(hass.config.latitude, float) _type = config.get(CONF_TYPE) + name = config.get(CONF_NAME) if latitude < 0: hemisphere = SOUTHERN @@ -61,7 +72,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): hemisphere = EQUATOR _LOGGER.debug(_type) - add_entities([Season(hass, hemisphere, _type)]) + add_entities([Season(hass, hemisphere, _type, name)]) return True @@ -101,9 +112,10 @@ def get_season(date, hemisphere, season_tracking_type): class Season(Entity): """Representation of the current season.""" - def __init__(self, hass, hemisphere, season_tracking_type): + def __init__(self, hass, hemisphere, season_tracking_type, name): """Initialize the season.""" self.hass = hass + self._name = name self.hemisphere = hemisphere self.datetime = dt_util.utcnow().replace(tzinfo=None) self.type = season_tracking_type @@ -112,7 +124,7 @@ def __init__(self, hass, hemisphere, season_tracking_type): @property def name(self): """Return the name.""" - return "Season" + return self._name @property def state(self): diff --git a/homeassistant/components/sense/__init__.py b/homeassistant/components/sense/__init__.py index f905c369d723..ce0d3bce5dc9 100644 --- a/homeassistant/components/sense/__init__.py +++ b/homeassistant/components/sense/__init__.py @@ -1,7 +1,12 @@ """Support for monitoring a Sense energy sensor.""" -import logging from datetime import timedelta +import logging +from sense_energy import ( + ASyncSenseable, + SenseAPITimeoutException, + SenseAuthenticationException, +) import voluptuous as vol from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, CONF_TIMEOUT @@ -36,11 +41,6 @@ async def async_setup(hass, config): """Set up the Sense sensor.""" - from sense_energy import ( - ASyncSenseable, - SenseAuthenticationException, - SenseAPITimeoutException, - ) username = config[DOMAIN][CONF_EMAIL] password = config[DOMAIN][CONF_PASSWORD] diff --git a/homeassistant/components/sense/binary_sensor.py b/homeassistant/components/sense/binary_sensor.py index ffc3ecb3cab0..81f1b64c8643 100644 --- a/homeassistant/components/sense/binary_sensor.py +++ b/homeassistant/components/sense/binary_sensor.py @@ -2,8 +2,8 @@ import logging from homeassistant.components.binary_sensor import BinarySensorDevice -from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect from . import SENSE_DATA, SENSE_DEVICE_UPDATE diff --git a/homeassistant/components/sense/sensor.py b/homeassistant/components/sense/sensor.py index 36474620b03f..d177a480ddf2 100644 --- a/homeassistant/components/sense/sensor.py +++ b/homeassistant/components/sense/sensor.py @@ -2,6 +2,8 @@ from datetime import timedelta import logging +from sense_energy import SenseAPITimeoutException + from homeassistant.const import ENERGY_KILO_WATT_HOUR, POWER_WATT from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle @@ -114,7 +116,6 @@ def icon(self): async def async_update(self): """Get the latest data, update state.""" - from sense_energy import SenseAPITimeoutException try: await self.update_sensor() diff --git a/homeassistant/components/sensehat/light.py b/homeassistant/components/sensehat/light.py index dcfc9b925e57..462c4245cd4a 100644 --- a/homeassistant/components/sensehat/light.py +++ b/homeassistant/components/sensehat/light.py @@ -1,18 +1,19 @@ """Support for Sense Hat LEDs.""" import logging +from sense_hat import SenseHat import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.light import ( ATTR_BRIGHTNESS, - SUPPORT_BRIGHTNESS, ATTR_HS_COLOR, + PLATFORM_SCHEMA, + SUPPORT_BRIGHTNESS, SUPPORT_COLOR, Light, - PLATFORM_SCHEMA, ) from homeassistant.const import CONF_NAME +import homeassistant.helpers.config_validation as cv import homeassistant.util.color as color_util _LOGGER = logging.getLogger(__name__) @@ -28,7 +29,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Sense Hat Light platform.""" - from sense_hat import SenseHat sensehat = SenseHat() diff --git a/homeassistant/components/sensehat/sensor.py b/homeassistant/components/sensehat/sensor.py index 7a7af09b4eb0..980c23f8555e 100644 --- a/homeassistant/components/sensehat/sensor.py +++ b/homeassistant/components/sensehat/sensor.py @@ -1,12 +1,13 @@ """Support for Sense HAT sensors.""" -import os -import logging from datetime import timedelta +import logging +import os +from sense_hat import SenseHat import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import TEMP_CELSIUS, CONF_DISPLAY_OPTIONS, CONF_NAME +from homeassistant.const import CONF_DISPLAY_OPTIONS, CONF_NAME, TEMP_CELSIUS import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle @@ -117,7 +118,6 @@ def __init__(self, is_hat_attached): @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): """Get the latest data from Sense HAT.""" - from sense_hat import SenseHat sense = SenseHat() temp_from_h = sense.get_temperature_from_humidity() diff --git a/homeassistant/components/sensor/.translations/es.json b/homeassistant/components/sensor/.translations/es.json index c5641d38cc0c..7b8ef36efe12 100644 --- a/homeassistant/components/sensor/.translations/es.json +++ b/homeassistant/components/sensor/.translations/es.json @@ -1,22 +1,22 @@ { "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" + "is_battery_level": "Nivel de bater\u00eda actual de {entity_name}", + "is_humidity": "Humedad actual de {entity_name}", + "is_illuminance": "Luminosidad actual de {entity_name}", + "is_power": "Potencia actual de {entity_name}", + "is_pressure": "Presi\u00f3n actual de {entity_name}", + "is_signal_strength": "Intensidad de la se\u00f1al actual de {entity_name}", + "is_temperature": "Temperatura actual de {entity_name}", + "is_timestamp": "Marca de tiempo actual de {entity_name}", + "is_value": "Valor actual de {entity_name}" }, "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", + "battery_level": "Cambios de nivel de bater\u00eda de {entity_name}", + "humidity": "Cambios de humedad de {entity_name}", + "illuminance": "Cambios de luminosidad de {entity_name}", + "power": "Cambios de potencia de {entity_name}", + "pressure": "Cambios de presi\u00f3n de {entity_name}", "signal_strength": "cambios de la intensidad de se\u00f1al de {entity_name} ", "temperature": "{entity_name} cambios de temperatura", "timestamp": "{entity_name} cambios de fecha y hora", diff --git a/homeassistant/components/sensor/.translations/hu.json b/homeassistant/components/sensor/.translations/hu.json index 78ea3e5e89be..a83db67b3e15 100644 --- a/homeassistant/components/sensor/.translations/hu.json +++ b/homeassistant/components/sensor/.translations/hu.json @@ -1,26 +1,26 @@ { "device_automation": { "condition_type": { - "is_battery_level": "{entity_name} akku szint", - "is_humidity": "{entity_name} p\u00e1ratartalom", - "is_illuminance": "{entity_name} megvil\u00e1g\u00edt\u00e1s", - "is_power": "{entity_name} teljes\u00edtm\u00e9ny", - "is_pressure": "{entity_name} nyom\u00e1s", - "is_signal_strength": "{entity_name} jeler\u0151ss\u00e9g", - "is_temperature": "{entity_name} h\u0151m\u00e9rs\u00e9klet", - "is_timestamp": "{entity_name} id\u0151b\u00e9lyeg", - "is_value": "{entity_name} \u00e9rt\u00e9k" + "is_battery_level": "{entity_name} aktu\u00e1lis akku szintje", + "is_humidity": "{entity_name} aktu\u00e1lis p\u00e1ratartalma", + "is_illuminance": "{entity_name} aktu\u00e1lis megvil\u00e1g\u00edt\u00e1sa", + "is_power": "{entity_name} aktu\u00e1lis teljes\u00edtm\u00e9nye", + "is_pressure": "{entity_name} aktu\u00e1lis nyom\u00e1sa", + "is_signal_strength": "{entity_name} aktu\u00e1lis jeler\u0151ss\u00e9ge", + "is_temperature": "{entity_name} aktu\u00e1lis h\u0151m\u00e9rs\u00e9klete", + "is_timestamp": "{entity_name} aktu\u00e1lis id\u0151b\u00e9lyege", + "is_value": "{entity_name} aktu\u00e1lis \u00e9rt\u00e9ke" }, "trigger_type": { - "battery_level": "{entity_name} akku szint", - "humidity": "{entity_name} p\u00e1ratartalom", - "illuminance": "{entity_name} megvil\u00e1g\u00edt\u00e1s", - "power": "{entity_name} teljes\u00edtm\u00e9ny", - "pressure": "{entity_name} nyom\u00e1s", - "signal_strength": "{entity_name} jeler\u0151ss\u00e9g", - "temperature": "{entity_name} h\u0151m\u00e9rs\u00e9klet", - "timestamp": "{entity_name} id\u0151b\u00e9lyeg", - "value": "{entity_name} \u00e9rt\u00e9k" + "battery_level": "{entity_name} akku szintje v\u00e1ltozik", + "humidity": "{entity_name} p\u00e1ratartalma v\u00e1ltozik", + "illuminance": "{entity_name} megvil\u00e1g\u00edt\u00e1sa v\u00e1ltozik", + "power": "{entity_name} teljes\u00edtm\u00e9nye v\u00e1ltozik", + "pressure": "{entity_name} nyom\u00e1sa v\u00e1ltozik", + "signal_strength": "{entity_name} jeler\u0151ss\u00e9ge v\u00e1ltozik", + "temperature": "{entity_name} h\u0151m\u00e9rs\u00e9klete v\u00e1ltozik", + "timestamp": "{entity_name} id\u0151b\u00e9lyege v\u00e1ltozik", + "value": "{entity_name} \u00e9rt\u00e9ke v\u00e1ltozik" } } } \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/no.json b/homeassistant/components/sensor/.translations/no.json index 6709e4eb28c4..1c0fc108510b 100644 --- a/homeassistant/components/sensor/.translations/no.json +++ b/homeassistant/components/sensor/.translations/no.json @@ -1,26 +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} effekt", - "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" + "is_battery_level": "Gjeldende {entity_name} batteriniv\u00e5", + "is_humidity": "Gjeldende {entity_name} fuktighet", + "is_illuminance": "Gjeldende {entity_name} belysningsstyrke", + "is_power": "Gjeldende {entity_name} str\u00f8m", + "is_pressure": "Gjeldende {entity_name} trykk", + "is_signal_strength": "Gjeldende {entity_name} signalstyrke", + "is_temperature": "Gjeldende {entity_name} temperatur", + "is_timestamp": "Gjeldende {entity_name} tidsstempel", + "is_value": "Gjeldende {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" + "battery_level": "{entity_name} batteriniv\u00e5 endres", + "humidity": "{entity_name} fuktighets endringer", + "illuminance": "{entity_name} belysningsstyrke endringer", + "power": "{entity_name} str\u00f8m endringer", + "pressure": "{entity_name} trykk endringer", + "signal_strength": "{entity_name} signalstyrkeendringer", + "temperature": "{entity_name} temperaturendringer", + "timestamp": "{entity_name} tidsstempel endringer", + "value": "{entity_name} verdi endringer" } } } \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/pt.json b/homeassistant/components/sensor/.translations/pt.json index 801b22f0c454..032d88e02d9e 100644 --- a/homeassistant/components/sensor/.translations/pt.json +++ b/homeassistant/components/sensor/.translations/pt.json @@ -1,8 +1,13 @@ { "device_automation": { "condition_type": { + "is_battery_level": "N\u00edvel de bateria atual de {entity_name}", "is_humidity": "humidade {entity_name}", - "is_power": "pot\u00eancia {entity_name}", + "is_illuminance": "Luminancia atual de {entity_name}", + "is_power": "Pot\u00eancia atual de {entity_name}", + "is_pressure": "Press\u00e3o atual de {entity_name}", + "is_signal_strength": "Intensidade atual do sinal de {entity_name}", + "is_temperature": "Temperatura atual de {entity_name}", "is_timestamp": "momento temporal de {entity_name}", "is_value": "valor {entity_name}" }, diff --git a/homeassistant/components/sensor/__init__.py b/homeassistant/components/sensor/__init__.py index 9ca11b5266ad..53e4b0ffcf71 100644 --- a/homeassistant/components/sensor/__init__.py +++ b/homeassistant/components/sensor/__init__.py @@ -15,7 +15,7 @@ DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_TIMESTAMP, ) -from homeassistant.helpers.config_validation import ( # noqa +from homeassistant.helpers.config_validation import ( # noqa: F401 PLATFORM_SCHEMA, PLATFORM_SCHEMA_BASE, ) diff --git a/homeassistant/components/serial_pm/sensor.py b/homeassistant/components/serial_pm/sensor.py index 1d46b05d46e0..75587e4eab79 100644 --- a/homeassistant/components/serial_pm/sensor.py +++ b/homeassistant/components/serial_pm/sensor.py @@ -1,6 +1,7 @@ """Support for particulate matter sensors connected to a serial port.""" import logging +from pmsensor import serial_pm as pm import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -24,8 +25,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the available PM sensors.""" - from pmsensor import serial_pm as pm - try: coll = pm.PMDataCollector( config.get(CONF_SERIAL_DEVICE), pm.SUPPORTED_SENSORS[config.get(CONF_BRAND)] diff --git a/homeassistant/components/seventeentrack/sensor.py b/homeassistant/components/seventeentrack/sensor.py index 33abe2f1f861..167f4347c0cc 100644 --- a/homeassistant/components/seventeentrack/sensor.py +++ b/homeassistant/components/seventeentrack/sensor.py @@ -1,7 +1,9 @@ """Support for package tracking sensors from 17track.net.""" -import logging from datetime import timedelta +import logging +from py17track import Client as SeventeenTrackClient +from py17track.errors import SeventeenTrackError import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -61,12 +63,10 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Configure the platform and add the sensors.""" - from py17track import Client - from py17track.errors import SeventeenTrackError websession = aiohttp_client.async_get_clientsession(hass) - client = Client(websession) + client = SeventeenTrackClient(websession) try: login_result = await client.profile.login( @@ -290,7 +290,6 @@ def __init__( async def _async_update(self): """Get updated data from 17track.net.""" - from py17track.errors import SeventeenTrackError try: packages = await self._client.profile.packages( diff --git a/homeassistant/components/shodan/manifest.json b/homeassistant/components/shodan/manifest.json index 3ef01a44315a..36af63da9f89 100644 --- a/homeassistant/components/shodan/manifest.json +++ b/homeassistant/components/shodan/manifest.json @@ -3,7 +3,7 @@ "name": "Shodan", "documentation": "https://www.home-assistant.io/integrations/shodan", "requirements": [ - "shodan==1.19.0" + "shodan==1.20.0" ], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/shopping_list/__init__.py b/homeassistant/components/shopping_list/__init__.py index a5e901b8c6e7..850b06332f88 100644 --- a/homeassistant/components/shopping_list/__init__.py +++ b/homeassistant/components/shopping_list/__init__.py @@ -9,7 +9,6 @@ from homeassistant.core import callback from homeassistant.components import http from homeassistant.components.http.data_validator import RequestDataValidator -from homeassistant.helpers import intent import homeassistant.helpers.config_validation as cv from homeassistant.util.json import load_json, save_json from homeassistant.components import websocket_api @@ -20,8 +19,6 @@ _LOGGER = logging.getLogger(__name__) CONFIG_SCHEMA = vol.Schema({DOMAIN: {}}, extra=vol.ALLOW_EXTRA) EVENT = "shopping_list_updated" -INTENT_ADD_ITEM = "HassShoppingListAddItem" -INTENT_LAST_ITEMS = "HassShoppingListLastItems" ITEM_UPDATE_SCHEMA = vol.Schema({"complete": bool, ATTR_NAME: str}) PERSISTENCE = ".shopping_list.json" @@ -86,9 +83,6 @@ def complete_item_service(call): data = hass.data[DOMAIN] = ShoppingData(hass) yield from data.async_load() - intent.async_register(hass, AddItemIntent()) - intent.async_register(hass, ListTopItemsIntent()) - hass.services.async_register( DOMAIN, SERVICE_ADD_ITEM, add_item_service, schema=SERVICE_ITEM_SCHEMA ) @@ -175,49 +169,6 @@ def save(self): save_json(self.hass.config.path(PERSISTENCE), self.items) -class AddItemIntent(intent.IntentHandler): - """Handle AddItem intents.""" - - intent_type = INTENT_ADD_ITEM - slot_schema = {"item": cv.string} - - @asyncio.coroutine - def async_handle(self, intent_obj): - """Handle the intent.""" - slots = self.async_validate_slots(intent_obj.slots) - item = slots["item"]["value"] - intent_obj.hass.data[DOMAIN].async_add(item) - - response = intent_obj.create_response() - response.async_set_speech(f"I've added {item} to your shopping list") - intent_obj.hass.bus.async_fire(EVENT) - return response - - -class ListTopItemsIntent(intent.IntentHandler): - """Handle AddItem intents.""" - - intent_type = INTENT_LAST_ITEMS - slot_schema = {"item": cv.string} - - @asyncio.coroutine - def async_handle(self, intent_obj): - """Handle the intent.""" - items = intent_obj.hass.data[DOMAIN].items[-5:] - response = intent_obj.create_response() - - if not items: - response.async_set_speech("There are no items on your shopping list") - else: - response.async_set_speech( - "These are the top {} items on your shopping list: {}".format( - min(len(items), 5), - ", ".join(itm["name"] for itm in reversed(items)), - ) - ) - return response - - class ShoppingListView(http.HomeAssistantView): """View to retrieve shopping list content.""" diff --git a/homeassistant/components/shopping_list/intent.py b/homeassistant/components/shopping_list/intent.py new file mode 100644 index 000000000000..21ae7181e895 --- /dev/null +++ b/homeassistant/components/shopping_list/intent.py @@ -0,0 +1,55 @@ +"""Intents for the Shopping List integration.""" +from homeassistant.helpers import intent +import homeassistant.helpers.config_validation as cv + +from . import DOMAIN, EVENT + +INTENT_ADD_ITEM = "HassShoppingListAddItem" +INTENT_LAST_ITEMS = "HassShoppingListLastItems" + + +async def async_setup_intents(hass): + """Set up the Shopping List intents.""" + intent.async_register(hass, AddItemIntent()) + intent.async_register(hass, ListTopItemsIntent()) + + +class AddItemIntent(intent.IntentHandler): + """Handle AddItem intents.""" + + intent_type = INTENT_ADD_ITEM + slot_schema = {"item": cv.string} + + async def async_handle(self, intent_obj): + """Handle the intent.""" + slots = self.async_validate_slots(intent_obj.slots) + item = slots["item"]["value"] + intent_obj.hass.data[DOMAIN].async_add(item) + + response = intent_obj.create_response() + response.async_set_speech(f"I've added {item} to your shopping list") + intent_obj.hass.bus.async_fire(EVENT) + return response + + +class ListTopItemsIntent(intent.IntentHandler): + """Handle AddItem intents.""" + + intent_type = INTENT_LAST_ITEMS + slot_schema = {"item": cv.string} + + async def async_handle(self, intent_obj): + """Handle the intent.""" + items = intent_obj.hass.data[DOMAIN].items[-5:] + response = intent_obj.create_response() + + if not items: + response.async_set_speech("There are no items on your shopping list") + else: + response.async_set_speech( + "These are the top {} items on your shopping list: {}".format( + min(len(items), 5), + ", ".join(itm["name"] for itm in reversed(items)), + ) + ) + return response diff --git a/homeassistant/components/sht31/sensor.py b/homeassistant/components/sht31/sensor.py index 7f8b6ecdc521..8a5203778960 100644 --- a/homeassistant/components/sht31/sensor.py +++ b/homeassistant/components/sht31/sensor.py @@ -4,17 +4,21 @@ import logging import math +from Adafruit_SHT31 import SHT31 import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import TEMP_CELSIUS, CONF_NAME, CONF_MONITORED_CONDITIONS +from homeassistant.const import ( + CONF_MONITORED_CONDITIONS, + CONF_NAME, + PRECISION_TENTHS, + TEMP_CELSIUS, +) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.helpers.temperature import display_temp -from homeassistant.const import PRECISION_TENTHS from homeassistant.util import Throttle - _LOGGER = logging.getLogger(__name__) CONF_I2C_ADDRESS = "i2c_address" @@ -43,7 +47,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the sensor platform.""" - from Adafruit_SHT31 import SHT31 i2c_address = config.get(CONF_I2C_ADDRESS) sensor = SHT31(address=i2c_address) diff --git a/homeassistant/components/simplepush/notify.py b/homeassistant/components/simplepush/notify.py index 0b3b09fe11b1..63bcd31935eb 100644 --- a/homeassistant/components/simplepush/notify.py +++ b/homeassistant/components/simplepush/notify.py @@ -1,17 +1,17 @@ """Simplepush notification service.""" import logging +from simplepush import send, send_encrypted import voluptuous as vol -from homeassistant.const import CONF_PASSWORD -import homeassistant.helpers.config_validation as cv - from homeassistant.components.notify import ( ATTR_TITLE, ATTR_TITLE_DEFAULT, PLATFORM_SCHEMA, BaseNotificationService, ) +from homeassistant.const import CONF_PASSWORD +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -48,7 +48,6 @@ def __init__(self, config): def send_message(self, message="", **kwargs): """Send a message to a Simplepush user.""" - from simplepush import send, send_encrypted title = kwargs.get(ATTR_TITLE, ATTR_TITLE_DEFAULT) diff --git a/homeassistant/components/simplisafe/.translations/ru.json b/homeassistant/components/simplisafe/.translations/ru.json index 721ba69d67ef..301eed6d1c1a 100644 --- a/homeassistant/components/simplisafe/.translations/ru.json +++ b/homeassistant/components/simplisafe/.translations/ru.json @@ -2,7 +2,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.", - "invalid_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435." + "invalid_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0443\u0447\u0451\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435." }, "step": { "user": { diff --git a/homeassistant/components/simplisafe/__init__.py b/homeassistant/components/simplisafe/__init__.py index 2514c5e9ed9b..63ac0ca973c6 100644 --- a/homeassistant/components/simplisafe/__init__.py +++ b/homeassistant/components/simplisafe/__init__.py @@ -1,10 +1,11 @@ """Support for SimpliSafe alarm systems.""" import asyncio -import logging from datetime import timedelta +import logging from simplipy import API from simplipy.errors import InvalidCredentialsError, SimplipyError +from simplipy.system.v3 import LevelMap as V3Volume import voluptuous as vol from homeassistant.config_entries import SOURCE_IMPORT @@ -14,6 +15,7 @@ CONF_SCAN_INTERVAL, CONF_TOKEN, CONF_USERNAME, + STATE_HOME, ) from homeassistant.core import callback from homeassistant.exceptions import ConfigEntryNotReady @@ -35,27 +37,57 @@ _LOGGER = logging.getLogger(__name__) +CONF_ACCOUNTS = "accounts" + +DATA_LISTENER = "listener" + +ATTR_ARMED_LIGHT_STATE = "armed_light_state" +ATTR_ARRIVAL_STATE = "arrival_state" ATTR_PIN_LABEL = "label" ATTR_PIN_LABEL_OR_VALUE = "label_or_pin" ATTR_PIN_VALUE = "pin" +ATTR_SECONDS = "seconds" ATTR_SYSTEM_ID = "system_id" +ATTR_TRANSITION = "transition" +ATTR_VOLUME = "volume" +ATTR_VOLUME_PROPERTY = "volume_property" -CONF_ACCOUNTS = "accounts" +STATE_AWAY = "away" +STATE_ENTRY = "entry" +STATE_EXIT = "exit" -DATA_LISTENER = "listener" +VOLUME_PROPERTY_ALARM = "alarm" +VOLUME_PROPERTY_CHIME = "chime" +VOLUME_PROPERTY_VOICE_PROMPT = "voice_prompt" + +SERVICE_BASE_SCHEMA = vol.Schema({vol.Required(ATTR_SYSTEM_ID): cv.positive_int}) + +SERVICE_REMOVE_PIN_SCHEMA = SERVICE_BASE_SCHEMA.extend( + {vol.Required(ATTR_PIN_LABEL_OR_VALUE): cv.string} +) -SERVICE_REMOVE_PIN_SCHEMA = vol.Schema( +SERVICE_SET_DELAY_SCHEMA = SERVICE_BASE_SCHEMA.extend( { - vol.Required(ATTR_SYSTEM_ID): cv.string, - vol.Required(ATTR_PIN_LABEL_OR_VALUE): cv.string, + vol.Required(ATTR_ARRIVAL_STATE): vol.In((STATE_AWAY, STATE_HOME)), + vol.Required(ATTR_TRANSITION): vol.In((STATE_ENTRY, STATE_EXIT)), + vol.Required(ATTR_SECONDS): cv.positive_int, } ) -SERVICE_SET_PIN_SCHEMA = vol.Schema( +SERVICE_SET_LIGHT_SCHEMA = SERVICE_BASE_SCHEMA.extend( + {vol.Required(ATTR_ARMED_LIGHT_STATE): cv.boolean} +) + +SERVICE_SET_PIN_SCHEMA = SERVICE_BASE_SCHEMA.extend( + {vol.Required(ATTR_PIN_LABEL): cv.string, vol.Required(ATTR_PIN_VALUE): cv.string} +) + +SERVICE_SET_VOLUME_SCHEMA = SERVICE_BASE_SCHEMA.extend( { - vol.Required(ATTR_SYSTEM_ID): cv.string, - vol.Required(ATTR_PIN_LABEL): cv.string, - vol.Required(ATTR_PIN_VALUE): cv.string, + vol.Required(ATTR_VOLUME_PROPERTY): vol.In( + (VOLUME_PROPERTY_ALARM, VOLUME_PROPERTY_CHIME, VOLUME_PROPERTY_VOICE_PROMPT) + ), + vol.Required(ATTR_VOLUME): cv.string, } ) @@ -150,15 +182,14 @@ async def async_setup_entry(hass, config_entry): _async_save_refresh_token(hass, config_entry, api.refresh_token) systems = await api.get_systems() - simplisafe = SimpliSafe(hass, config_entry, systems) + simplisafe = SimpliSafe(hass, api, systems, config_entry) await simplisafe.async_update() hass.data[DOMAIN][DATA_CLIENT][config_entry.entry_id] = simplisafe - hass.async_create_task( - hass.config_entries.async_forward_entry_setup( - config_entry, "alarm_control_panel" + for component in ("alarm_control_panel", "lock"): + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(config_entry, component) ) - ) async def refresh(event_time): """Refresh data from the SimpliSafe account.""" @@ -176,21 +207,122 @@ async def refresh(event_time): async_register_base_station(hass, system, config_entry.entry_id) ) + @callback + def verify_system_exists(coro): + """Log an error if a service call uses an invalid system ID.""" + + async def decorator(call): + """Decorate.""" + system_id = int(call.data[ATTR_SYSTEM_ID]) + if system_id not in systems: + _LOGGER.error("Unknown system ID in service call: %s", system_id) + return + await coro(call) + + return decorator + + @callback + def v3_only(coro): + """Log an error if the decorated coroutine is called with a v2 system.""" + + async def decorator(call): + """Decorate.""" + system = systems[int(call.data[ATTR_SYSTEM_ID])] + if system.version != 3: + _LOGGER.error("Service only available on V3 systems") + return + await coro(call) + + return decorator + + @verify_system_exists @_verify_domain_control async def remove_pin(call): """Remove a PIN.""" - system = systems[int(call.data[ATTR_SYSTEM_ID])] - await system.remove_pin(call.data[ATTR_PIN_LABEL_OR_VALUE]) + system = systems[call.data[ATTR_SYSTEM_ID]] + try: + await system.remove_pin(call.data[ATTR_PIN_LABEL_OR_VALUE]) + except SimplipyError as err: + _LOGGER.error("Error during service call: %s", err) + return + + @verify_system_exists + @v3_only + @_verify_domain_control + async def set_alarm_duration(call): + """Set the duration of a running alarm.""" + system = systems[call.data[ATTR_SYSTEM_ID]] + try: + await system.set_alarm_duration(call.data[ATTR_SECONDS]) + except SimplipyError as err: + _LOGGER.error("Error during service call: %s", err) + return + + @verify_system_exists + @v3_only + @_verify_domain_control + async def set_delay(call): + """Set the delay duration for entry/exit, away/home (any combo).""" + system = systems[call.data[ATTR_SYSTEM_ID]] + coro = getattr( + system, + f"set_{call.data[ATTR_TRANSITION]}_delay_{call.data[ATTR_ARRIVAL_STATE]}", + ) + try: + await coro(call.data[ATTR_SECONDS]) + except SimplipyError as err: + _LOGGER.error("Error during service call: %s", err) + return + + @verify_system_exists + @v3_only + @_verify_domain_control + async def set_armed_light(call): + """Turn the base station light on/off.""" + system = systems[call.data[ATTR_SYSTEM_ID]] + try: + await system.set_light(call.data[ATTR_ARMED_LIGHT_STATE]) + except SimplipyError as err: + _LOGGER.error("Error during service call: %s", err) + return + + @verify_system_exists @_verify_domain_control async def set_pin(call): """Set a PIN.""" - system = systems[int(call.data[ATTR_SYSTEM_ID])] - await system.set_pin(call.data[ATTR_PIN_LABEL], call.data[ATTR_PIN_VALUE]) + system = systems[call.data[ATTR_SYSTEM_ID]] + try: + await system.set_pin(call.data[ATTR_PIN_LABEL], call.data[ATTR_PIN_VALUE]) + except SimplipyError as err: + _LOGGER.error("Error during service call: %s", err) + return + + @verify_system_exists + @v3_only + @_verify_domain_control + async def set_volume_property(call): + """Set a volume parameter in an appropriate service call.""" + system = systems[call.data[ATTR_SYSTEM_ID]] + try: + volume = V3Volume[call.data[ATTR_VOLUME]] + except KeyError: + _LOGGER.error("Unknown volume string: %s", call.data[ATTR_VOLUME]) + return + except SimplipyError as err: + _LOGGER.error("Error during service call: %s", err) + return + else: + coro = getattr(system, f"set_{call.data[ATTR_VOLUME_PROPERTY]}_volume") + await coro(volume) for service, method, schema in [ ("remove_pin", remove_pin, SERVICE_REMOVE_PIN_SCHEMA), + ("set_alarm_duration", set_alarm_duration, SERVICE_SET_DELAY_SCHEMA), + ("set_delay", set_delay, SERVICE_SET_DELAY_SCHEMA), + ("set_armed_light", set_armed_light, SERVICE_SET_LIGHT_SCHEMA), ("set_pin", set_pin, SERVICE_SET_PIN_SCHEMA), + ("set_volume_property", set_volume_property, SERVICE_SET_VOLUME_SCHEMA), ]: hass.services.async_register(DOMAIN, service, method, schema=schema) @@ -199,7 +331,12 @@ async def set_pin(call): async def async_unload_entry(hass, entry): """Unload a SimpliSafe config entry.""" - await hass.config_entries.async_forward_entry_unload(entry, "alarm_control_panel") + tasks = [ + hass.config_entries.async_forward_entry_unload(entry, component) + for component in ("alarm_control_panel", "lock") + ] + + await asyncio.gather(*tasks) hass.data[DOMAIN][DATA_CLIENT].pop(entry.entry_id) remove_listener = hass.data[DOMAIN][DATA_LISTENER].pop(entry.entry_id) @@ -211,8 +348,9 @@ async def async_unload_entry(hass, entry): class SimpliSafe: """Define a SimpliSafe API object.""" - def __init__(self, hass, config_entry, systems): + def __init__(self, hass, api, systems, config_entry): """Initialize.""" + self._api = api self._config_entry = config_entry self._hass = hass self.last_event_data = {} @@ -234,9 +372,9 @@ async def _update_system(self, system): self.last_event_data[system.system_id] = latest_event - if system.api.refresh_token_dirty: + if self._api.refresh_token_dirty: _async_save_refresh_token( - self._hass, self._config_entry, system.api.refresh_token + self._hass, self._config_entry, self._api.refresh_token ) async def async_update(self): diff --git a/homeassistant/components/simplisafe/alarm_control_panel.py b/homeassistant/components/simplisafe/alarm_control_panel.py index a63a077ed15b..9671d56c8734 100644 --- a/homeassistant/components/simplisafe/alarm_control_panel.py +++ b/homeassistant/components/simplisafe/alarm_control_panel.py @@ -10,6 +10,10 @@ FORMAT_TEXT, AlarmControlPanel, ) +from homeassistant.components.alarm_control_panel.const import ( + SUPPORT_ALARM_ARM_AWAY, + SUPPORT_ALARM_ARM_HOME, +) from homeassistant.const import ( CONF_CODE, STATE_ALARM_ARMED_AWAY, @@ -24,14 +28,23 @@ _LOGGER = logging.getLogger(__name__) ATTR_ALARM_ACTIVE = "alarm_active" +ATTR_ALARM_DURATION = "alarm_duration" +ATTR_ALARM_VOLUME = "alarm_volume" ATTR_BATTERY_BACKUP_POWER_LEVEL = "battery_backup_power_level" +ATTR_CHIME_VOLUME = "chime_volume" +ATTR_ENTRY_DELAY_AWAY = "entry_delay_away" +ATTR_ENTRY_DELAY_HOME = "entry_delay_home" +ATTR_EXIT_DELAY_AWAY = "exit_delay_away" +ATTR_EXIT_DELAY_HOME = "exit_delay_home" ATTR_GSM_STRENGTH = "gsm_strength" ATTR_LAST_EVENT_INFO = "last_event_info" ATTR_LAST_EVENT_SENSOR_NAME = "last_event_sensor_name" ATTR_LAST_EVENT_SENSOR_TYPE = "last_event_sensor_type" ATTR_LAST_EVENT_TIMESTAMP = "last_event_timestamp" ATTR_LAST_EVENT_TYPE = "last_event_type" +ATTR_LIGHT = "light" ATTR_RF_JAMMING = "rf_jamming" +ATTR_VOICE_PROMPT_VOLUME = "voice_prompt_volume" ATTR_WALL_POWER_LEVEL = "wall_power_level" ATTR_WIFI_STRENGTH = "wifi_strength" @@ -64,16 +77,26 @@ def __init__(self, simplisafe, system, code): self._simplisafe = simplisafe self._state = None - # Some properties only exist for V2 or V3 systems: - for prop in ( - ATTR_BATTERY_BACKUP_POWER_LEVEL, - ATTR_GSM_STRENGTH, - ATTR_RF_JAMMING, - ATTR_WALL_POWER_LEVEL, - ATTR_WIFI_STRENGTH, - ): - if hasattr(system, prop): - self._attrs[prop] = getattr(system, prop) + self._attrs.update({ATTR_ALARM_ACTIVE: self._system.alarm_going_off}) + if self._system.version == 3: + self._attrs.update( + { + ATTR_ALARM_DURATION: self._system.alarm_duration, + ATTR_ALARM_VOLUME: self._system.alarm_volume.name, + ATTR_BATTERY_BACKUP_POWER_LEVEL: self._system.battery_backup_power_level, + ATTR_CHIME_VOLUME: self._system.chime_volume.name, + ATTR_ENTRY_DELAY_AWAY: self._system.entry_delay_away, + ATTR_ENTRY_DELAY_HOME: self._system.entry_delay_home, + ATTR_EXIT_DELAY_AWAY: self._system.exit_delay_away, + ATTR_EXIT_DELAY_HOME: self._system.exit_delay_home, + ATTR_GSM_STRENGTH: self._system.gsm_strength, + ATTR_LIGHT: self._system.light, + ATTR_RF_JAMMING: self._system.rf_jamming, + ATTR_VOICE_PROMPT_VOLUME: self._system.voice_prompt_volume.name, + ATTR_WALL_POWER_LEVEL: self._system.wall_power_level, + ATTR_WIFI_STRENGTH: self._system.wifi_strength, + } + ) @property def changed_by(self): @@ -94,6 +117,11 @@ def state(self): """Return the state of the entity.""" return self._state + @property + def supported_features(self) -> int: + """Return the list of supported features.""" + return SUPPORT_ALARM_ARM_HOME | SUPPORT_ALARM_ARM_AWAY + def _validate_code(self, code, state): """Validate given code.""" check = self._code is None or code == self._code @@ -151,7 +179,6 @@ async def async_update(self): last_event = self._simplisafe.last_event_data[self._system.system_id] self._attrs.update( { - ATTR_ALARM_ACTIVE: self._system.alarm_going_off, ATTR_LAST_EVENT_INFO: last_event["info"], ATTR_LAST_EVENT_SENSOR_NAME: last_event["sensorName"], ATTR_LAST_EVENT_SENSOR_TYPE: EntityTypes(last_event["sensorType"]).name, diff --git a/homeassistant/components/simplisafe/config_flow.py b/homeassistant/components/simplisafe/config_flow.py index 0e3af0ae03c8..6e1082948d35 100644 --- a/homeassistant/components/simplisafe/config_flow.py +++ b/homeassistant/components/simplisafe/config_flow.py @@ -1,10 +1,11 @@ """Config flow to configure the SimpliSafe component.""" from collections import OrderedDict +from simplipy import API +from simplipy.errors import SimplipyError import voluptuous as vol from homeassistant import config_entries -from homeassistant.core import callback from homeassistant.const import ( CONF_CODE, CONF_PASSWORD, @@ -12,6 +13,7 @@ CONF_TOKEN, CONF_USERNAME, ) +from homeassistant.core import callback from homeassistant.helpers import aiohttp_client from .const import DEFAULT_SCAN_INTERVAL, DOMAIN @@ -53,8 +55,6 @@ async def async_step_import(self, import_config): async def async_step_user(self, user_input=None): """Handle the start of the config flow.""" - from simplipy import API - from simplipy.errors import SimplipyError if not user_input: return await self._show_form() diff --git a/homeassistant/components/simplisafe/lock.py b/homeassistant/components/simplisafe/lock.py new file mode 100644 index 000000000000..10c5d310e73e --- /dev/null +++ b/homeassistant/components/simplisafe/lock.py @@ -0,0 +1,72 @@ +"""Support for SimpliSafe locks.""" +import logging + +from simplipy.lock import LockStates + +from homeassistant.components.lock import LockDevice +from homeassistant.const import STATE_LOCKED, STATE_UNKNOWN, STATE_UNLOCKED + +from . import SimpliSafeEntity +from .const import DATA_CLIENT, DOMAIN + +_LOGGER = logging.getLogger(__name__) + +ATTR_LOCK_LOW_BATTERY = "lock_low_battery" +ATTR_JAMMED = "jammed" +ATTR_PIN_PAD_LOW_BATTERY = "pin_pad_low_battery" + +STATE_MAP = { + LockStates.locked: STATE_LOCKED, + LockStates.unknown: STATE_UNKNOWN, + LockStates.unlocked: STATE_UNLOCKED, +} + + +async def async_setup_entry(hass, entry, async_add_entities): + """Set up SimpliSafe locks based on a config entry.""" + simplisafe = hass.data[DOMAIN][DATA_CLIENT][entry.entry_id] + async_add_entities( + [ + SimpliSafeLock(system, lock) + for system in simplisafe.systems.values() + for lock in system.locks.values() + ] + ) + + +class SimpliSafeLock(SimpliSafeEntity, LockDevice): + """Define a SimpliSafe lock.""" + + def __init__(self, system, lock): + """Initialize.""" + super().__init__(system, lock.name, serial=lock.serial) + self._lock = lock + + @property + def is_locked(self): + """Return true if the lock is locked.""" + return STATE_MAP.get(self._lock.state) == STATE_LOCKED + + async def async_lock(self, **kwargs): + """Lock the lock.""" + await self._lock.lock() + + async def async_unlock(self, **kwargs): + """Unlock the lock.""" + await self._lock.unlock() + + async def async_update(self): + """Update lock status.""" + if self._lock.offline or self._lock.disabled: + self._online = False + return + + self._online = True + + self._attrs.update( + { + ATTR_LOCK_LOW_BATTERY: self._lock.lock_low_battery, + ATTR_JAMMED: self._lock.state == LockStates.jammed, + ATTR_PIN_PAD_LOW_BATTERY: self._lock.pin_pad_low_battery, + } + ) diff --git a/homeassistant/components/simplisafe/manifest.json b/homeassistant/components/simplisafe/manifest.json index 254e947ed259..4115ce455b5d 100644 --- a/homeassistant/components/simplisafe/manifest.json +++ b/homeassistant/components/simplisafe/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/simplisafe", "requirements": [ - "simplisafe-python==5.1.0" + "simplisafe-python==5.3.5" ], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/simplisafe/services.yaml b/homeassistant/components/simplisafe/services.yaml index 52e66a435c66..d8a4973b49ed 100644 --- a/homeassistant/components/simplisafe/services.yaml +++ b/homeassistant/components/simplisafe/services.yaml @@ -10,11 +10,46 @@ remove_pin: label_or_pin: description: The label/value to remove. example: Test PIN +set_alarm_duration: + description: "Set the duration (in seconds) of an active alarm" + fields: + system_id: + description: The SimpliSafe system ID to affect + example: 123987 + seconds: + description: The number of seconds to sound the alarm + example: 120 +set_delay: + description: > + Set a duration for how long the base station should delay when transitioning + between states + fields: + system_id: + description: The SimpliSafe system ID to affect + example: 123987 + arrival_state: + description: The target "arrival" state (away, home) + example: away + transition: + description: The system state transition to affect (entry, exit) + example: exit + seconds: + description: "The number of seconds to delay" + example: 120 +set_light: + description: "Turn the base station light on/off" + fields: + system_id: + description: The SimpliSafe system ID to affect + example: 123987 + armed_light_state: + description: "True for on, False for off" + example: "True" set_pin: description: Set/update a PIN fields: system_id: - description: The SimpliSafe system ID to affect. + description: The SimpliSafe system ID to affect example: 123987 label: description: The label of the PIN @@ -22,3 +57,15 @@ set_pin: pin: description: The value of the PIN example: 1256 +set_volume_property: + description: Set a level for one of the base station's various volumes + fields: + system_id: + description: The SimpliSafe system ID to affect + example: 123987 + volume_property: + description: The volume property to set (alarm, chime, voice_prompt) + example: voice_prompt + volume: + description: "A volume (off, low, medium, high)" + example: low diff --git a/homeassistant/components/sinch/notify.py b/homeassistant/components/sinch/notify.py index 173873c0a6c2..d7d1f242c67c 100644 --- a/homeassistant/components/sinch/notify.py +++ b/homeassistant/components/sinch/notify.py @@ -61,7 +61,7 @@ def __init__(self, config): def send_message(self, message="", **kwargs): """Send a message to a user.""" targets = kwargs.get(ATTR_TARGET, self.default_recipients) - data = kwargs.get(ATTR_DATA, {}) + data = kwargs.get(ATTR_DATA) or {} clx_args = {ATTR_MESSAGE: message, ATTR_SENDER: self.sender} diff --git a/homeassistant/components/sisyphus/__init__.py b/homeassistant/components/sisyphus/__init__.py index 771641c9b1dd..5ad59da5dee3 100644 --- a/homeassistant/components/sisyphus/__init__.py +++ b/homeassistant/components/sisyphus/__init__.py @@ -2,6 +2,7 @@ import asyncio import logging +from sisyphus_control import Table import voluptuous as vol from homeassistant.const import CONF_HOST, CONF_NAME, EVENT_HOMEASSISTANT_STOP @@ -29,7 +30,6 @@ async def async_setup(hass, config): """Set up the sisyphus component.""" - from sisyphus_control import Table class SocketIONoiseFilter(logging.Filter): """Filters out excessively verbose logs from SocketIO.""" @@ -105,7 +105,6 @@ async def get_table(self): return await self._table_task async def _connect_table(self): - from sisyphus_control import Table self._table = await Table.connect(self._host, self._session) if self._name is None: diff --git a/homeassistant/components/sisyphus/media_player.py b/homeassistant/components/sisyphus/media_player.py index e06b84b4ac58..e708504ff7ed 100644 --- a/homeassistant/components/sisyphus/media_player.py +++ b/homeassistant/components/sisyphus/media_player.py @@ -2,6 +2,7 @@ import logging import aiohttp +from sisyphus_control import Track from homeassistant.components.media_player import MediaPlayerDevice from homeassistant.components.media_player.const import ( @@ -43,7 +44,6 @@ ) -# pylint: disable=unused-argument async def async_setup_platform(hass, config, add_entities, discovery_info=None): """Set up a media player entity for a Sisyphus table.""" host = discovery_info[CONF_HOST] @@ -141,7 +141,6 @@ def supported_features(self): @property def media_image_url(self): """Return the URL for a thumbnail image of the current track.""" - from sisyphus_control import Track if self._table.active_track: return self._table.active_track.get_thumbnail_url(Track.ThumbnailSize.LARGE) diff --git a/homeassistant/components/skybell/__init__.py b/homeassistant/components/skybell/__init__.py index fd01b6d22c9d..a4e4263d3602 100644 --- a/homeassistant/components/skybell/__init__.py +++ b/homeassistant/components/skybell/__init__.py @@ -1,10 +1,11 @@ """Support for the Skybell HD Doorbell.""" import logging -from requests.exceptions import HTTPError, ConnectTimeout +from requests.exceptions import ConnectTimeout, HTTPError +from skybellpy import Skybell import voluptuous as vol -from homeassistant.const import ATTR_ATTRIBUTION, CONF_USERNAME, CONF_PASSWORD +from homeassistant.const import ATTR_ATTRIBUTION, CONF_PASSWORD, CONF_USERNAME import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity @@ -39,8 +40,6 @@ def setup(hass, config): password = conf.get(CONF_PASSWORD) try: - from skybellpy import Skybell - cache = hass.config.path(DEFAULT_CACHEDB) skybell = Skybell( username=username, password=password, get_devices=True, cache_path=cache diff --git a/homeassistant/components/smartthings/__init__.py b/homeassistant/components/smartthings/__init__.py index 93f7cbb8f329..9787fb53917c 100644 --- a/homeassistant/components/smartthings/__init__.py +++ b/homeassistant/components/smartthings/__init__.py @@ -20,7 +20,7 @@ from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.typing import ConfigType, HomeAssistantType -from .config_flow import SmartThingsFlowHandler # noqa +from .config_flow import SmartThingsFlowHandler # noqa: F401 from .const import ( CONF_APP_ID, CONF_INSTALLED_APP_ID, diff --git a/homeassistant/components/smarty/__init__.py b/homeassistant/components/smarty/__init__.py index ad824055126e..22987673005b 100644 --- a/homeassistant/components/smarty/__init__.py +++ b/homeassistant/components/smarty/__init__.py @@ -1,12 +1,13 @@ """Support to control a Salda Smarty XP/XV ventilation unit.""" from datetime import timedelta - import ipaddress import logging + +from pysmarty import Smarty import voluptuous as vol -from homeassistant.const import CONF_NAME, CONF_HOST +from homeassistant.const import CONF_HOST, CONF_NAME from homeassistant.helpers import discovery import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import dispatcher_send @@ -36,7 +37,6 @@ def setup(hass, config): """Set up the smarty environment.""" - from pysmarty import Smarty conf = config[DOMAIN] diff --git a/homeassistant/components/smarty/binary_sensor.py b/homeassistant/components/smarty/binary_sensor.py index 8723f0248d34..a86b3548e959 100644 --- a/homeassistant/components/smarty/binary_sensor.py +++ b/homeassistant/components/smarty/binary_sensor.py @@ -2,9 +2,10 @@ import logging -from homeassistant.core import callback from homeassistant.components.binary_sensor import BinarySensorDevice +from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect + from . import DOMAIN, SIGNAL_UPDATE_SMARTY _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/smarty/fan.py b/homeassistant/components/smarty/fan.py index 81edac80cb04..bb6b76237791 100644 --- a/homeassistant/components/smarty/fan.py +++ b/homeassistant/components/smarty/fan.py @@ -2,7 +2,6 @@ import logging -from homeassistant.core import callback from homeassistant.components.fan import ( SPEED_HIGH, SPEED_LOW, @@ -11,6 +10,7 @@ SUPPORT_SET_SPEED, FanEntity, ) +from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from . import DOMAIN, SIGNAL_UPDATE_SMARTY diff --git a/homeassistant/components/smarty/sensor.py b/homeassistant/components/smarty/sensor.py index bf647777b528..f5cd1fbb404d 100644 --- a/homeassistant/components/smarty/sensor.py +++ b/homeassistant/components/smarty/sensor.py @@ -3,15 +3,16 @@ import datetime as dt import logging -from homeassistant.core import callback from homeassistant.const import ( - TEMP_CELSIUS, DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_TIMESTAMP, + TEMP_CELSIUS, ) -import homeassistant.util.dt as dt_util +from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import Entity +import homeassistant.util.dt as dt_util + from . import DOMAIN, SIGNAL_UPDATE_SMARTY _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/snips/__init__.py b/homeassistant/components/snips/__init__.py index 441104211cf2..93e445e8cedf 100644 --- a/homeassistant/components/snips/__init__.py +++ b/homeassistant/components/snips/__init__.py @@ -135,7 +135,6 @@ async def message_received(msg): intent_type = request["intent"]["intentName"].split("__")[-1] else: intent_type = request["intent"]["intentName"].split(":")[-1] - snips_response = None slots = {} for slot in request.get("slots", []): slots[slot["slotName"]] = {"value": resolve_slot_values(slot)} @@ -148,8 +147,15 @@ async def message_received(msg): intent_response = await intent.async_handle( hass, DOMAIN, intent_type, slots, request["input"] ) + notification = {"sessionId": request.get("sessionId", "default")} + if "plain" in intent_response.speech: - snips_response = intent_response.speech["plain"]["speech"] + notification["text"] = intent_response.speech["plain"]["speech"] + + _LOGGER.debug("send_response %s", json.dumps(notification)) + mqtt.async_publish( + hass, "hermes/dialogueManager/endSession", json.dumps(notification) + ) except intent.UnknownIntent: _LOGGER.warning( "Received unknown intent %s", request["intent"]["intentName"] @@ -157,17 +163,6 @@ async def message_received(msg): except intent.IntentError: _LOGGER.exception("Error while handling intent: %s.", intent_type) - if snips_response: - notification = { - "sessionId": request.get("sessionId", "default"), - "text": snips_response, - } - - _LOGGER.debug("send_response %s", json.dumps(notification)) - mqtt.async_publish( - hass, "hermes/dialogueManager/endSession", json.dumps(notification) - ) - await hass.components.mqtt.async_subscribe(INTENT_TOPIC, message_received) async def snips_say(call): diff --git a/homeassistant/components/sochain/sensor.py b/homeassistant/components/sochain/sensor.py index cc5a477652be..608405dd1b40 100644 --- a/homeassistant/components/sochain/sensor.py +++ b/homeassistant/components/sochain/sensor.py @@ -2,6 +2,7 @@ from datetime import timedelta import logging +from pysochain import ChainSo import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -31,7 +32,6 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the sochain sensors.""" - from pysochain import ChainSo address = config.get(CONF_ADDRESS) network = config.get(CONF_NETWORK) diff --git a/homeassistant/components/solaredge/__init__.py b/homeassistant/components/solaredge/__init__.py index 8909b970aafd..bafc6b67f1c8 100644 --- a/homeassistant/components/solaredge/__init__.py +++ b/homeassistant/components/solaredge/__init__.py @@ -6,7 +6,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.typing import HomeAssistantType -from .const import DEFAULT_NAME, DOMAIN, CONF_SITE_ID +from .const import CONF_SITE_ID, DEFAULT_NAME, DOMAIN CONFIG_SCHEMA = vol.Schema( { diff --git a/homeassistant/components/solaredge/config_flow.py b/homeassistant/components/solaredge/config_flow.py index 67f05d83aa0f..7c8c9380522b 100644 --- a/homeassistant/components/solaredge/config_flow.py +++ b/homeassistant/components/solaredge/config_flow.py @@ -1,14 +1,14 @@ """Config flow for the SolarEdge platform.""" +from requests.exceptions import ConnectTimeout, HTTPError import solaredge import voluptuous as vol -from requests.exceptions import HTTPError, ConnectTimeout from homeassistant import config_entries from homeassistant.const import CONF_API_KEY, CONF_NAME from homeassistant.core import HomeAssistant, callback from homeassistant.util import slugify -from .const import DOMAIN, DEFAULT_NAME, CONF_SITE_ID +from .const import CONF_SITE_ID, DEFAULT_NAME, DOMAIN @callback diff --git a/homeassistant/components/solaredge/const.py b/homeassistant/components/solaredge/const.py index 0d3d1a0cb5ff..6fec88c42d55 100644 --- a/homeassistant/components/solaredge/const.py +++ b/homeassistant/components/solaredge/const.py @@ -1,7 +1,7 @@ """Constants for the SolarEdge Monitoring API.""" from datetime import timedelta -from homeassistant.const import POWER_WATT, ENERGY_WATT_HOUR +from homeassistant.const import ENERGY_WATT_HOUR, POWER_WATT DOMAIN = "solaredge" diff --git a/homeassistant/components/solaredge/sensor.py b/homeassistant/components/solaredge/sensor.py index 896596a2a34d..f0f1660a821a 100644 --- a/homeassistant/components/solaredge/sensor.py +++ b/homeassistant/components/solaredge/sensor.py @@ -1,17 +1,19 @@ """Support for SolarEdge Monitoring API.""" import logging + +from requests.exceptions import ConnectTimeout, HTTPError import solaredge +from stringcase import snakecase -from requests.exceptions import HTTPError, ConnectTimeout from homeassistant.const import CONF_API_KEY from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle from .const import ( CONF_SITE_ID, - OVERVIEW_UPDATE_DELAY, DETAILS_UPDATE_DELAY, INVENTORY_UPDATE_DELAY, + OVERVIEW_UPDATE_DELAY, POWER_FLOW_UPDATE_DELAY, SENSOR_TYPES, ) @@ -262,7 +264,6 @@ def __init__(self, api, site_id): @Throttle(DETAILS_UPDATE_DELAY) def update(self): """Update the data from the SolarEdge Monitoring API.""" - from stringcase import snakecase try: data = self.api.get_details(self.site_id) diff --git a/homeassistant/components/solarlog/.translations/nn.json b/homeassistant/components/solarlog/.translations/nn.json new file mode 100644 index 000000000000..3ce86b4e10a9 --- /dev/null +++ b/homeassistant/components/solarlog/.translations/nn.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Solar-Log" + } +} \ No newline at end of file diff --git a/homeassistant/components/somfy/.translations/ca.json b/homeassistant/components/somfy/.translations/ca.json index 0ca526fde69c..b3095cd4e9c4 100644 --- a/homeassistant/components/somfy/.translations/ca.json +++ b/homeassistant/components/somfy/.translations/ca.json @@ -10,7 +10,7 @@ }, "step": { "pick_implementation": { - "title": "Tria del m\u00e8tode d'autenticaci\u00f3" + "title": "Selecci\u00f3 del m\u00e8tode d'autenticaci\u00f3" } }, "title": "Somfy" diff --git a/homeassistant/components/somfy/__init__.py b/homeassistant/components/somfy/__init__.py index b767ea834317..1368725777bf 100644 --- a/homeassistant/components/somfy/__init__.py +++ b/homeassistant/components/somfy/__init__.py @@ -26,7 +26,7 @@ _LOGGER = logging.getLogger(__name__) -SCAN_INTERVAL = timedelta(seconds=10) +SCAN_INTERVAL = timedelta(seconds=30) DOMAIN = "somfy" @@ -48,7 +48,7 @@ extra=vol.ALLOW_EXTRA, ) -SOMFY_COMPONENTS = ["cover"] +SOMFY_COMPONENTS = ["cover", "switch"] async def async_setup(hass, config): diff --git a/homeassistant/components/somfy/cover.py b/homeassistant/components/somfy/cover.py index d54e7c990018..12f12676f924 100644 --- a/homeassistant/components/somfy/cover.py +++ b/homeassistant/components/somfy/cover.py @@ -1,9 +1,4 @@ -""" -Support for Somfy Covers. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/cover.somfy/ -""" +"""Support for Somfy Covers.""" from pymfy.api.devices.category import Category from pymfy.api.devices.blind import Blind @@ -12,7 +7,7 @@ ATTR_POSITION, ATTR_TILT_POSITION, ) -from homeassistant.components.somfy import DOMAIN, SomfyEntity, DEVICES, API +from . import DOMAIN, SomfyEntity, DEVICES, API async def async_setup_entry(hass, config_entry, async_add_entities): @@ -37,15 +32,6 @@ def get_covers(): async_add_entities(await hass.async_add_executor_job(get_covers), 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 SomfyCover(SomfyEntity, CoverDevice): """Representation of a Somfy cover device.""" diff --git a/homeassistant/components/somfy/manifest.json b/homeassistant/components/somfy/manifest.json index f5a17275bcb3..82e62e7dd08e 100644 --- a/homeassistant/components/somfy/manifest.json +++ b/homeassistant/components/somfy/manifest.json @@ -5,5 +5,5 @@ "documentation": "https://www.home-assistant.io/integrations/somfy", "dependencies": ["http"], "codeowners": ["@tetienne"], - "requirements": ["pymfy==0.6.1"] + "requirements": ["pymfy==0.7.1"] } diff --git a/homeassistant/components/somfy/switch.py b/homeassistant/components/somfy/switch.py new file mode 100644 index 000000000000..58ad2e5905d3 --- /dev/null +++ b/homeassistant/components/somfy/switch.py @@ -0,0 +1,49 @@ +"""Support for Somfy Camera Shutter.""" +from pymfy.api.devices.camera_protect import CameraProtect +from pymfy.api.devices.category import Category + +from homeassistant.components.switch import SwitchDevice +from . import DOMAIN, SomfyEntity, DEVICES, API + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the Somfy switch platform.""" + + def get_shutters(): + """Retrieve switches.""" + devices = hass.data[DOMAIN][DEVICES] + + return [ + SomfyCameraShutter(device, hass.data[DOMAIN][API]) + for device in devices + if Category.CAMERA.value in device.categories + ] + + async_add_entities(await hass.async_add_executor_job(get_shutters), True) + + +class SomfyCameraShutter(SomfyEntity, SwitchDevice): + """Representation of a Somfy Camera Shutter device.""" + + def __init__(self, device, api): + """Initialize the Somfy device.""" + super().__init__(device, api) + self.shutter = CameraProtect(self.device, self.api) + + async def async_update(self): + """Update the device with the latest data.""" + await super().async_update() + self.shutter = CameraProtect(self.device, self.api) + + def turn_on(self, **kwargs) -> None: + """Turn the entity on.""" + self.shutter.open_shutter() + + def turn_off(self, **kwargs): + """Turn the entity off.""" + self.shutter.close_shutter() + + @property + def is_on(self) -> bool: + """Return True if entity is on.""" + return self.shutter.get_shutter_position() == "opened" diff --git a/homeassistant/components/somfy_mylink/__init__.py b/homeassistant/components/somfy_mylink/__init__.py index 394de5980ea4..6c6333af5a66 100644 --- a/homeassistant/components/somfy_mylink/__init__.py +++ b/homeassistant/components/somfy_mylink/__init__.py @@ -1,6 +1,7 @@ """Component for the Somfy MyLink device supporting the Synergy API.""" import logging +from somfy_mylink_synergy import SomfyMyLinkSynergy import voluptuous as vol from homeassistant.const import CONF_HOST, CONF_PORT @@ -48,7 +49,6 @@ def validate_entity_config(values): async def async_setup(hass, config): """Set up the MyLink platform.""" - from somfy_mylink_synergy import SomfyMyLinkSynergy host = config[DOMAIN][CONF_HOST] port = config[DOMAIN][CONF_PORT] diff --git a/homeassistant/components/sonarr/sensor.py b/homeassistant/components/sonarr/sensor.py index 47738521bf06..82bcdad6ef45 100644 --- a/homeassistant/components/sonarr/sensor.py +++ b/homeassistant/components/sonarr/sensor.py @@ -1,20 +1,21 @@ """Support for Sonarr.""" +from datetime import datetime import logging import time -from datetime import datetime +from pytz import timezone import requests import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( CONF_API_KEY, CONF_HOST, - CONF_PORT, CONF_MONITORED_CONDITIONS, + CONF_PORT, CONF_SSL, ) +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -80,7 +81,6 @@ class SonarrSensor(Entity): def __init__(self, hass, conf, sensor_type): """Create Sonarr entity.""" - from pytz import timezone self.conf = conf self.host = conf.get(CONF_HOST) diff --git a/homeassistant/components/songpal/const.py b/homeassistant/components/songpal/const.py new file mode 100644 index 000000000000..6a19e316a9f3 --- /dev/null +++ b/homeassistant/components/songpal/const.py @@ -0,0 +1,3 @@ +"""Constants for the Songpal component.""" +DOMAIN = "songpal" +SET_SOUND_SETTING = "set_sound_setting" diff --git a/homeassistant/components/songpal/media_player.py b/homeassistant/components/songpal/media_player.py index 0567cd0ea6a2..681c97a77107 100644 --- a/homeassistant/components/songpal/media_player.py +++ b/homeassistant/components/songpal/media_player.py @@ -15,7 +15,6 @@ from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA from homeassistant.components.media_player.const import ( - DOMAIN, SUPPORT_SELECT_SOURCE, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, @@ -33,6 +32,8 @@ from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv +from .const import DOMAIN, SET_SOUND_SETTING + _LOGGER = logging.getLogger(__name__) CONF_ENDPOINT = "endpoint" @@ -42,8 +43,6 @@ PLATFORM = "songpal" -SET_SOUND_SETTING = "songpal_set_sound_setting" - SUPPORT_SONGPAL = ( SUPPORT_VOLUME_SET | SUPPORT_VOLUME_STEP diff --git a/homeassistant/components/songpal/services.yaml b/homeassistant/components/songpal/services.yaml index e69de29bb2d1..8cf1a664276e 100644 --- a/homeassistant/components/songpal/services.yaml +++ b/homeassistant/components/songpal/services.yaml @@ -0,0 +1,13 @@ +set_sound_setting: + description: Change sound setting. + + fields: + entity_id: + description: Target device. + example: 'media_player.my_soundbar' + name: + description: Name of the setting. + example: 'nightMode' + value: + description: Value to set. + example: 'on' diff --git a/homeassistant/components/sonos/media_player.py b/homeassistant/components/sonos/media_player.py index 2baa02d0a5df..9ce72d87dfeb 100644 --- a/homeassistant/components/sonos/media_player.py +++ b/homeassistant/components/sonos/media_player.py @@ -1056,7 +1056,6 @@ def _snapshot_all(entities): def restore(self): """Restore a snapshotted state to a player.""" try: - # pylint: disable=protected-access self._soco_snapshot.restore() except (TypeError, AttributeError, SoCoException) as ex: # Can happen if restoring a coordinator onto a current slave diff --git a/homeassistant/components/soundtouch/const.py b/homeassistant/components/soundtouch/const.py new file mode 100644 index 000000000000..37bf1d8cc2b9 --- /dev/null +++ b/homeassistant/components/soundtouch/const.py @@ -0,0 +1,6 @@ +"""Constants for the Bose Soundtouch component.""" +DOMAIN = "soundtouch" +SERVICE_PLAY_EVERYWHERE = "play_everywhere" +SERVICE_CREATE_ZONE = "create_zone" +SERVICE_ADD_ZONE_SLAVE = "add_zone_slave" +SERVICE_REMOVE_ZONE_SLAVE = "remove_zone_slave" diff --git a/homeassistant/components/soundtouch/media_player.py b/homeassistant/components/soundtouch/media_player.py index f613ba22dfa4..a9f6e05011fc 100644 --- a/homeassistant/components/soundtouch/media_player.py +++ b/homeassistant/components/soundtouch/media_player.py @@ -6,7 +6,6 @@ from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice from homeassistant.components.media_player.const import ( - DOMAIN, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY, @@ -29,12 +28,15 @@ ) import homeassistant.helpers.config_validation as cv -_LOGGER = logging.getLogger(__name__) +from .const import ( + DOMAIN, + SERVICE_ADD_ZONE_SLAVE, + SERVICE_CREATE_ZONE, + SERVICE_PLAY_EVERYWHERE, + SERVICE_REMOVE_ZONE_SLAVE, +) -SERVICE_PLAY_EVERYWHERE = "soundtouch_play_everywhere" -SERVICE_CREATE_ZONE = "soundtouch_create_zone" -SERVICE_ADD_ZONE_SLAVE = "soundtouch_add_zone_slave" -SERVICE_REMOVE_ZONE_SLAVE = "soundtouch_remove_zone_slave" +_LOGGER = logging.getLogger(__name__) MAP_STATUS = { "PLAY_STATE": STATE_PLAYING, diff --git a/homeassistant/components/soundtouch/services.yaml b/homeassistant/components/soundtouch/services.yaml index e69de29bb2d1..fd848b76b2db 100644 --- a/homeassistant/components/soundtouch/services.yaml +++ b/homeassistant/components/soundtouch/services.yaml @@ -0,0 +1,36 @@ +play_everywhere: + description: Play on all Bose Soundtouch devices. + fields: + master: + description: Name of the master entity that will coordinate the grouping. Platform dependent. It is a shortcut for creating a multi-room zone with all devices + example: 'media_player.soundtouch_home' + +create_zone: + description: Create a Sountouch multi-room zone. + fields: + master: + description: Name of the master entity that will coordinate the multi-room zone. Platform dependent. + example: 'media_player.soundtouch_home' + slaves: + description: Name of slaves entities to add to the new zone. + example: 'media_player.soundtouch_bedroom' + +add_zone_slave: + description: Add a slave to a Sountouch multi-room zone. + fields: + master: + description: Name of the master entity that is coordinating the multi-room zone. Platform dependent. + example: 'media_player.soundtouch_home' + slaves: + description: Name of slaves entities to add to the existing zone. + example: 'media_player.soundtouch_bedroom' + +remove_zone_slave: + description: Remove a slave from the Sounttouch multi-room zone. + fields: + master: + description: Name of the master entity that is coordinating the multi-room zone. Platform dependent. + example: 'media_player.soundtouch_home' + slaves: + description: Name of slaves entities to remove from the existing zone. + example: 'media_player.soundtouch_bedroom' diff --git a/homeassistant/components/spc/alarm_control_panel.py b/homeassistant/components/spc/alarm_control_panel.py index 8eeccc06515c..fa9a9681fff0 100644 --- a/homeassistant/components/spc/alarm_control_panel.py +++ b/homeassistant/components/spc/alarm_control_panel.py @@ -2,6 +2,11 @@ import logging import homeassistant.components.alarm_control_panel as alarm +from homeassistant.components.alarm_control_panel.const import ( + SUPPORT_ALARM_ARM_AWAY, + SUPPORT_ALARM_ARM_HOME, + SUPPORT_ALARM_ARM_NIGHT, +) from homeassistant.const import ( STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, @@ -80,6 +85,11 @@ def state(self): """Return the state of the device.""" return _get_alarm_state(self._area) + @property + def supported_features(self) -> int: + """Return the list of supported features.""" + return SUPPORT_ALARM_ARM_HOME | SUPPORT_ALARM_ARM_AWAY | SUPPORT_ALARM_ARM_NIGHT + async def async_alarm_disarm(self, code=None): """Send disarm command.""" from pyspcwebgw.const import AreaMode diff --git a/homeassistant/components/speedtestdotnet/manifest.json b/homeassistant/components/speedtestdotnet/manifest.json index b32026d86ef7..821d4158f571 100644 --- a/homeassistant/components/speedtestdotnet/manifest.json +++ b/homeassistant/components/speedtestdotnet/manifest.json @@ -6,5 +6,7 @@ "speedtest-cli==2.1.2" ], "dependencies": [], - "codeowners": [] + "codeowners": [ + "@rohankapoorcom" + ] } diff --git a/homeassistant/components/spider/__init__.py b/homeassistant/components/spider/__init__.py index 0d5e1606b53c..125799b394a4 100644 --- a/homeassistant/components/spider/__init__.py +++ b/homeassistant/components/spider/__init__.py @@ -2,6 +2,7 @@ from datetime import timedelta import logging +from spiderpy.spiderapi import SpiderApi, UnauthorizedException import voluptuous as vol from homeassistant.const import CONF_PASSWORD, CONF_SCAN_INTERVAL, CONF_USERNAME @@ -32,8 +33,6 @@ def setup(hass, config): """Set up Spider Component.""" - from spiderpy.spiderapi import SpiderApi - from spiderpy.spiderapi import UnauthorizedException username = config[DOMAIN][CONF_USERNAME] password = config[DOMAIN][CONF_PASSWORD] diff --git a/homeassistant/components/spotify/media_player.py b/homeassistant/components/spotify/media_player.py index 236c8b8db896..ec21a5d7822e 100644 --- a/homeassistant/components/spotify/media_player.py +++ b/homeassistant/components/spotify/media_player.py @@ -307,7 +307,7 @@ def play_media(self, media_type, media_id, **kwargs): def play_playlist(self, media_id, random_song): """Play random music in a playlist.""" - if not media_id.startswith("spotify:playlist:"): + if not media_id.startswith("spotify:"): _LOGGER.error("media id must be spotify playlist uri") return kwargs = {"context_uri": media_id} diff --git a/homeassistant/components/sql/manifest.json b/homeassistant/components/sql/manifest.json index fa641adc839e..39435524c20c 100644 --- a/homeassistant/components/sql/manifest.json +++ b/homeassistant/components/sql/manifest.json @@ -3,7 +3,7 @@ "name": "Sql", "documentation": "https://www.home-assistant.io/integrations/sql", "requirements": [ - "sqlalchemy==1.3.10" + "sqlalchemy==1.3.11" ], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/squeezebox/const.py b/homeassistant/components/squeezebox/const.py new file mode 100644 index 000000000000..1e8fd6f3a2a8 --- /dev/null +++ b/homeassistant/components/squeezebox/const.py @@ -0,0 +1,3 @@ +"""Constants for the Squeezebox component.""" +DOMAIN = "squeezebox" +SERVICE_CALL_METHOD = "call_method" diff --git a/homeassistant/components/squeezebox/media_player.py b/homeassistant/components/squeezebox/media_player.py index d8574223307d..b3fb82591c9f 100644 --- a/homeassistant/components/squeezebox/media_player.py +++ b/homeassistant/components/squeezebox/media_player.py @@ -12,7 +12,6 @@ from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice from homeassistant.components.media_player.const import ( ATTR_MEDIA_ENQUEUE, - DOMAIN, MEDIA_TYPE_MUSIC, SUPPORT_CLEAR_PLAYLIST, SUPPORT_NEXT_TRACK, @@ -44,6 +43,8 @@ from homeassistant.exceptions import PlatformNotReady from homeassistant.util.dt import utcnow +from .const import DOMAIN, SERVICE_CALL_METHOD + _LOGGER = logging.getLogger(__name__) DEFAULT_PORT = 9000 @@ -75,8 +76,6 @@ } ) -SERVICE_CALL_METHOD = "squeezebox_call_method" - DATA_SQUEEZEBOX = "squeezebox" KNOWN_SERVERS = "squeezebox_known_servers" @@ -545,5 +544,5 @@ def async_call_method(self, command, parameters=None): all_params = [command] if parameters: for parameter in parameters: - all_params.append(urllib.parse.quote(parameter, safe=":=/?")) + all_params.append(urllib.parse.quote(parameter, safe="+:=/?")) return self.async_query(*all_params) diff --git a/homeassistant/components/squeezebox/services.yaml b/homeassistant/components/squeezebox/services.yaml index 05c7de07f42b..0c81c369e731 100644 --- a/homeassistant/components/squeezebox/services.yaml +++ b/homeassistant/components/squeezebox/services.yaml @@ -1,4 +1,4 @@ -squeezebox_call_method: +call_method: description: Call a custom Squeezebox JSONRPC API. fields: entity_id: @@ -10,4 +10,3 @@ squeezebox_call_method: parameters: description: Array of additional parameters to pass to Logitech Media Server (p1, ..., pN in the CLI documentation). example: ["loadtracks", "album.titlesearch="] - diff --git a/homeassistant/components/ssdp/__init__.py b/homeassistant/components/ssdp/__init__.py index c4d71e0febd0..b9a9d4b46c9f 100644 --- a/homeassistant/components/ssdp/__init__.py +++ b/homeassistant/components/ssdp/__init__.py @@ -3,9 +3,9 @@ from datetime import timedelta import logging from urllib.parse import urlparse -from xml.etree import ElementTree import aiohttp +from defusedxml import ElementTree from netdisco import ssdp, util from homeassistant.helpers.event import async_track_time_interval diff --git a/homeassistant/components/ssdp/manifest.json b/homeassistant/components/ssdp/manifest.json index 1c3d56fe7fe9..1a6bfa36233a 100644 --- a/homeassistant/components/ssdp/manifest.json +++ b/homeassistant/components/ssdp/manifest.json @@ -3,6 +3,7 @@ "name": "SSDP", "documentation": "https://www.home-assistant.io/integrations/ssdp", "requirements": [ + "defusedxml==0.6.0", "netdisco==2.6.0" ], "dependencies": [ diff --git a/homeassistant/components/starline/.translations/bg.json b/homeassistant/components/starline/.translations/bg.json new file mode 100644 index 000000000000..702c061c6294 --- /dev/null +++ b/homeassistant/components/starline/.translations/bg.json @@ -0,0 +1,42 @@ +{ + "config": { + "error": { + "error_auth_app": "\u041d\u0435\u043f\u0440\u0430\u0432\u0438\u043b\u043d\u043e ID \u043d\u0430 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435 \u0438\u043b\u0438 \u0442\u0430\u0439\u043d\u0430", + "error_auth_mfa": "\u041d\u0435\u043f\u0440\u0430\u0432\u0438\u043b\u0435\u043d \u043a\u043e\u0434", + "error_auth_user": "\u041d\u0435\u043f\u0440\u0430\u0432\u0438\u043b\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": { + "auth_app": { + "data": { + "app_id": "ID \u043d\u0430 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435", + "app_secret": "\u0422\u0430\u0439\u043d\u0430" + }, + "description": "\u0418\u0414 \u043d\u0430 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435 \u0438 \u0442\u0430\u0435\u043d \u043a\u043e\u0434 \u043e\u0442 StarLine \u0430\u043a\u0430\u0443\u043d\u0442 \u043d\u0430 \u0440\u0430\u0437\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a", + "title": "\u0418\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u043e\u043d\u043d\u0438 \u0434\u0430\u043d\u043d\u0438 \u0437\u0430 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435\u0442\u043e" + }, + "auth_captcha": { + "data": { + "captcha_code": "\u041a\u043e\u0434 \u043e\u0442 \u0438\u0437\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u0435\u0442\u043e" + }, + "description": "{captcha_img}", + "title": "Captcha" + }, + "auth_mfa": { + "data": { + "mfa_code": "SMS \u043a\u043e\u0434" + }, + "description": "\u0412\u044a\u0432\u0435\u0434\u0435\u0442\u0435 \u043a\u043e\u0434\u0430, \u0438\u0437\u043f\u0440\u0430\u0442\u0435\u043d \u043d\u0430 \u0442\u0435\u043b\u0435\u0444\u043e\u043d\u0435\u043d \u043d\u043e\u043c\u0435\u0440 {phone_number}", + "title": "\u0414\u0432\u0443\u0444\u0430\u043a\u0442\u043e\u0440\u043d\u0430 \u043e\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u044f" + }, + "auth_user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u0430", + "username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435" + }, + "description": "\u0418\u043c\u0435\u0439\u043b \u0438 \u043f\u0430\u0440\u043e\u043b\u0430 \u0437\u0430 \u0430\u043a\u0430\u0443\u043d\u0442 \u0432 StarLine", + "title": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u0438 \u0438\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u043e\u043d\u043d\u0438 \u0434\u0430\u043d\u043d\u0438" + } + }, + "title": "StarLine" + } +} \ No newline at end of file diff --git a/homeassistant/components/starline/.translations/ca.json b/homeassistant/components/starline/.translations/ca.json new file mode 100644 index 000000000000..04426c2acfa1 --- /dev/null +++ b/homeassistant/components/starline/.translations/ca.json @@ -0,0 +1,41 @@ +{ + "config": { + "error": { + "error_auth_app": "ID d'aplicaci\u00f3 o secret incorrectes", + "error_auth_mfa": "Codi incorrecte", + "error_auth_user": "Nom d'usuari o contrasenya incorrectes" + }, + "step": { + "auth_app": { + "data": { + "app_id": "ID d'aplicaci\u00f3", + "app_secret": "Secret" + }, + "title": "Credencials d'aplicaci\u00f3" + }, + "auth_captcha": { + "data": { + "captcha_code": "Codi des de imatge" + }, + "description": "{captcha_img}", + "title": "Captcha" + }, + "auth_mfa": { + "data": { + "mfa_code": "Codi SMS" + }, + "description": "Introdueix el codi rebut al n\u00famero {phone_number}", + "title": "Verificaci\u00f3 en dos passos" + }, + "auth_user": { + "data": { + "password": "Contrasenya", + "username": "Nom d'usuari" + }, + "description": "Correu electr\u00f2nic i contrasenya del compte StarLine", + "title": "Credencials d\u2019usuari" + } + }, + "title": "StarLine" + } +} \ No newline at end of file diff --git a/homeassistant/components/starline/.translations/de.json b/homeassistant/components/starline/.translations/de.json new file mode 100644 index 000000000000..657e6c08b1a2 --- /dev/null +++ b/homeassistant/components/starline/.translations/de.json @@ -0,0 +1,25 @@ +{ + "config": { + "error": { + "error_auth_mfa": "Ung\u00fcltiger Code" + }, + "step": { + "auth_captcha": { + "title": "Captcha" + }, + "auth_mfa": { + "data": { + "mfa_code": "SMS Code" + }, + "title": "2-Faktor-Authentifizierung" + }, + "auth_user": { + "data": { + "password": "Passwort", + "username": "Benutzername" + }, + "title": "Anmeldeinformationen" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/starline/.translations/en.json b/homeassistant/components/starline/.translations/en.json new file mode 100644 index 000000000000..afe8f8c732b0 --- /dev/null +++ b/homeassistant/components/starline/.translations/en.json @@ -0,0 +1,42 @@ +{ + "config": { + "error": { + "error_auth_app": "Incorrect application id or secret", + "error_auth_mfa": "Incorrect code", + "error_auth_user": "Incorrect username or password" + }, + "step": { + "auth_app": { + "data": { + "app_id": "App ID", + "app_secret": "Secret" + }, + "description": "Application ID and secret code from StarLine developer account", + "title": "Application credentials" + }, + "auth_captcha": { + "data": { + "captcha_code": "Code from image" + }, + "description": "{captcha_img}", + "title": "Captcha" + }, + "auth_mfa": { + "data": { + "mfa_code": "SMS code" + }, + "description": "Enter the code sent to phone {phone_number}", + "title": "Two-factor authorization" + }, + "auth_user": { + "data": { + "password": "Password", + "username": "Username" + }, + "description": "StarLine account email and password", + "title": "User credentials" + } + }, + "title": "StarLine" + } +} \ No newline at end of file diff --git a/homeassistant/components/starline/.translations/es.json b/homeassistant/components/starline/.translations/es.json new file mode 100644 index 000000000000..bc881ced6a2e --- /dev/null +++ b/homeassistant/components/starline/.translations/es.json @@ -0,0 +1,42 @@ +{ + "config": { + "error": { + "error_auth_app": "Id de aplicaci\u00f3n o secreto incorrectos", + "error_auth_mfa": "C\u00f3digo incorrecto", + "error_auth_user": "Nombre de usuario o contrase\u00f1a incorrectos" + }, + "step": { + "auth_app": { + "data": { + "app_id": "ID de la aplicaci\u00f3n", + "app_secret": "Secreto" + }, + "description": "ID de la aplicaci\u00f3n y c\u00f3digo secreto de la cuenta de desarrollador de StarLine", + "title": "Credenciales de la aplicaci\u00f3n" + }, + "auth_captcha": { + "data": { + "captcha_code": "C\u00f3digo de la imagen" + }, + "description": "{captcha_img}", + "title": "Captcha" + }, + "auth_mfa": { + "data": { + "mfa_code": "C\u00f3digo SMS" + }, + "description": "Introduce el c\u00f3digo enviado al tel\u00e9fono {phone_number}", + "title": "Autorizaci\u00f3n de dos factores" + }, + "auth_user": { + "data": { + "password": "Contrase\u00f1a", + "username": "Usuario" + }, + "description": "Correo electr\u00f3nico y contrase\u00f1a de la cuenta StarLine", + "title": "Credenciales de usuario" + } + }, + "title": "StarLine" + } +} \ No newline at end of file diff --git a/homeassistant/components/starline/.translations/fr.json b/homeassistant/components/starline/.translations/fr.json new file mode 100644 index 000000000000..67a7dae4afcd --- /dev/null +++ b/homeassistant/components/starline/.translations/fr.json @@ -0,0 +1,39 @@ +{ + "config": { + "error": { + "error_auth_mfa": "code incorrect", + "error_auth_user": "identifiant ou mot de passe incorrect" + }, + "step": { + "auth_app": { + "data": { + "app_id": "ID de l'application", + "app_secret": "Secret" + }, + "title": "Informations d'identification de l'application" + }, + "auth_captcha": { + "data": { + "captcha_code": "Code de l'image" + }, + "description": "{captcha_img}", + "title": "Captcha" + }, + "auth_mfa": { + "data": { + "mfa_code": "Code SMS" + }, + "title": "Autorisation \u00e0 deux facteurs" + }, + "auth_user": { + "data": { + "password": "Mot de passe", + "username": "Nom d'utilisateur" + }, + "description": "Adresse e-mail et mot de passe du compte StarLine", + "title": "Informations d'identification de l'utilisateur" + } + }, + "title": "StarLine" + } +} \ No newline at end of file diff --git a/homeassistant/components/starline/.translations/it.json b/homeassistant/components/starline/.translations/it.json new file mode 100644 index 000000000000..f68732354c66 --- /dev/null +++ b/homeassistant/components/starline/.translations/it.json @@ -0,0 +1,42 @@ +{ + "config": { + "error": { + "error_auth_app": "ID applicazione o Segreto errati", + "error_auth_mfa": "Codice errato", + "error_auth_user": "Nome utente o password errati" + }, + "step": { + "auth_app": { + "data": { + "app_id": "ID applicazione", + "app_secret": "Segreto" + }, + "description": "ID applicazione e codice segreto da Account sviluppatore StarLine ", + "title": "Credenziali dell'applicazione" + }, + "auth_captcha": { + "data": { + "captcha_code": "Codice dall'immagine" + }, + "description": "{captcha_img}", + "title": "Captcha" + }, + "auth_mfa": { + "data": { + "mfa_code": "Codice SMS" + }, + "description": "Inserisci il codice inviato al telefono {phone_number}.", + "title": "Autenticazione a due fattori" + }, + "auth_user": { + "data": { + "password": "Password", + "username": "Nome utente" + }, + "description": "Email e password dell'account StarLine", + "title": "Credenziali utente" + } + }, + "title": "StarLine" + } +} \ No newline at end of file diff --git a/homeassistant/components/starline/.translations/lb.json b/homeassistant/components/starline/.translations/lb.json new file mode 100644 index 000000000000..527add9920b9 --- /dev/null +++ b/homeassistant/components/starline/.translations/lb.json @@ -0,0 +1,42 @@ +{ + "config": { + "error": { + "error_auth_app": "Ong\u00ebltege Applikatioun's ID oder Schl\u00ebssel", + "error_auth_mfa": "Ong\u00ebltegte Code", + "error_auth_user": "Ong\u00ebltege Benotzernumm oder Passwuert" + }, + "step": { + "auth_app": { + "data": { + "app_id": "App ID", + "app_secret": "Schl\u00ebssel" + }, + "description": "Applikatioun's ID an Schl\u00ebssel vum StarLine Developpeur's Kont", + "title": "Login Informatioune vun der Applikatioun" + }, + "auth_captcha": { + "data": { + "captcha_code": "Cod vum Bild" + }, + "description": "{captcha_img}", + "title": "Captcha" + }, + "auth_mfa": { + "data": { + "mfa_code": "SMS Code" + }, + "description": "Gitt de Code an deen un d'Telefondnummer {phone_number} gesch\u00e9ckt gouf", + "title": "2-Faktor-Authentifikatioun" + }, + "auth_user": { + "data": { + "password": "Passwuert", + "username": "Benotzernumm" + }, + "description": "StarLine Konto Email a Passwuert", + "title": "Login Informatiounen" + } + }, + "title": "StarLine" + } +} \ No newline at end of file diff --git a/homeassistant/components/starline/.translations/nn.json b/homeassistant/components/starline/.translations/nn.json new file mode 100644 index 000000000000..88b146144fbc --- /dev/null +++ b/homeassistant/components/starline/.translations/nn.json @@ -0,0 +1,10 @@ +{ + "config": { + "step": { + "auth_captcha": { + "description": "{captcha_img}" + } + }, + "title": "StarLine" + } +} \ No newline at end of file diff --git a/homeassistant/components/starline/.translations/no.json b/homeassistant/components/starline/.translations/no.json new file mode 100644 index 000000000000..37d55aea194b --- /dev/null +++ b/homeassistant/components/starline/.translations/no.json @@ -0,0 +1,42 @@ +{ + "config": { + "error": { + "error_auth_app": "Feil applikasjons-ID eller hemmelighet", + "error_auth_mfa": "Feil kode", + "error_auth_user": "Ugyldig brukernavn eller passord" + }, + "step": { + "auth_app": { + "data": { + "app_id": "App-ID", + "app_secret": "Hemmelig" + }, + "description": "S\u00f8knads-ID og hemmelig kode fra StarLine utviklerkonto ", + "title": "Bruksanvisning" + }, + "auth_captcha": { + "data": { + "captcha_code": "Kode fra bilde" + }, + "description": "{captcha_img}", + "title": "Captcha" + }, + "auth_mfa": { + "data": { + "mfa_code": "SMS-kode" + }, + "description": "Skriv inn koden som er sendt til telefonen {phone_number}", + "title": "Tofaktorautentisering" + }, + "auth_user": { + "data": { + "password": "Passord", + "username": "Brukernavn" + }, + "description": "E-postadresse og passord for StarLine-kontoen", + "title": "Brukerlegitimasjon" + } + }, + "title": "StarLine" + } +} \ No newline at end of file diff --git a/homeassistant/components/starline/.translations/pl.json b/homeassistant/components/starline/.translations/pl.json new file mode 100644 index 000000000000..71d54bbbcd1b --- /dev/null +++ b/homeassistant/components/starline/.translations/pl.json @@ -0,0 +1,42 @@ +{ + "config": { + "error": { + "error_auth_app": "Niepoprawny identyfikator aplikacji lub tajny kod", + "error_auth_mfa": "Niepoprawny kod", + "error_auth_user": "Niepoprawna nazwa u\u017cytkownika lub has\u0142o" + }, + "step": { + "auth_app": { + "data": { + "app_id": "Identyfikator aplikacji", + "app_secret": "Tajny kod" + }, + "description": "Identyfikator aplikacji i tajny kod z konta programisty StarLine", + "title": "Po\u015bwiadczenia aplikacji" + }, + "auth_captcha": { + "data": { + "captcha_code": "Kod z obrazka" + }, + "description": "{captcha_img}", + "title": "Captcha" + }, + "auth_mfa": { + "data": { + "mfa_code": "Kod SMS" + }, + "description": "Wprowad\u017a kod wys\u0142any na numer telefonu {phone_number}", + "title": "Uwierzytelnianie dwusk\u0142adnikowe" + }, + "auth_user": { + "data": { + "password": "Has\u0142o", + "username": "Nazwa u\u017cytkownika" + }, + "description": "Adres e-mail i has\u0142o do konta StarLine", + "title": "Po\u015bwiadczenia u\u017cytkownika" + } + }, + "title": "StarLine" + } +} \ No newline at end of file diff --git a/homeassistant/components/starline/.translations/ru.json b/homeassistant/components/starline/.translations/ru.json new file mode 100644 index 000000000000..aa84c36772b0 --- /dev/null +++ b/homeassistant/components/starline/.translations/ru.json @@ -0,0 +1,42 @@ +{ + "config": { + "error": { + "error_auth_app": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u0438\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u0438\u043b\u0438 \u0441\u0435\u043a\u0440\u0435\u0442\u043d\u044b\u0439 \u043a\u043e\u0434.", + "error_auth_mfa": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043a\u043e\u0434.", + "error_auth_user": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043b\u043e\u0433\u0438\u043d \u0438\u043b\u0438 \u043f\u0430\u0440\u043e\u043b\u044c." + }, + "step": { + "auth_app": { + "data": { + "app_id": "ID \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f", + "app_secret": "\u0421\u0435\u043a\u0440\u0435\u0442\u043d\u044b\u0439 \u043a\u043e\u0434" + }, + "description": "ID \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u0438 \u0441\u0435\u043a\u0440\u0435\u0442\u043d\u044b\u0439 \u043a\u043e\u0434 \u0438\u0437 \u0443\u0447\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 \u0440\u0430\u0437\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a\u0430 StarLine", + "title": "\u0423\u0447\u0451\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f" + }, + "auth_captcha": { + "data": { + "captcha_code": "\u041a\u043e\u0434 \u0441 \u043a\u0430\u0440\u0442\u0438\u043d\u043a\u0438" + }, + "description": "{captcha_img}", + "title": "CAPTCHA" + }, + "auth_mfa": { + "data": { + "mfa_code": "\u041a\u043e\u0434 \u0438\u0437 SMS" + }, + "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u043a\u043e\u0434, \u043e\u0442\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u043d\u044b\u0439 \u043d\u0430 \u043d\u043e\u043c\u0435\u0440 \u0442\u0435\u043b\u0435\u0444\u043e\u043d\u0430 {phone_number}", + "title": "\u0414\u0432\u0443\u0445\u0444\u0430\u043a\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f" + }, + "auth_user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u041b\u043e\u0433\u0438\u043d" + }, + "description": "\u0410\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 \u0443\u0447\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 StarLine", + "title": "\u0423\u0447\u0451\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" + } + }, + "title": "StarLine" + } +} \ No newline at end of file diff --git a/homeassistant/components/starline/.translations/sl.json b/homeassistant/components/starline/.translations/sl.json new file mode 100644 index 000000000000..3cdc5bc4bac9 --- /dev/null +++ b/homeassistant/components/starline/.translations/sl.json @@ -0,0 +1,42 @@ +{ + "config": { + "error": { + "error_auth_app": "Nepravilen ID ali skrivnost", + "error_auth_mfa": "Napa\u010dna koda", + "error_auth_user": "Nepravilno uporabni\u0161ko ime ali geslo" + }, + "step": { + "auth_app": { + "data": { + "app_id": "ID aplikacije", + "app_secret": "Skrivnost" + }, + "description": "ID aplikacije in tajna koda iz ra\u010duna za razvijalce StarLine ", + "title": "Poverilnice programa" + }, + "auth_captcha": { + "data": { + "captcha_code": "\u0160ifra iz slike" + }, + "description": "{captcha_img}", + "title": "Captcha" + }, + "auth_mfa": { + "data": { + "mfa_code": "SMS koda" + }, + "description": "Vnesite kodo, poslano na telefon {phone_number}", + "title": "2-faktorska avtorizacija" + }, + "auth_user": { + "data": { + "password": "Geslo", + "username": "Uporabni\u0161ko ime" + }, + "description": "StarLine e-po\u0161tni ra\u010dun in geslo", + "title": "Uporabni\u0161ke poverilnice" + } + }, + "title": "StarLine" + } +} \ No newline at end of file diff --git a/homeassistant/components/starline/.translations/zh-Hant.json b/homeassistant/components/starline/.translations/zh-Hant.json new file mode 100644 index 000000000000..0bd69d54ec6a --- /dev/null +++ b/homeassistant/components/starline/.translations/zh-Hant.json @@ -0,0 +1,42 @@ +{ + "config": { + "error": { + "error_auth_app": "\u61c9\u7528\u7a0b\u5f0f ID \u932f\u8aa4\u6216\u4e0d\u6b63\u78ba", + "error_auth_mfa": "\u5bc6\u78bc\u932f\u8aa4", + "error_auth_user": "\u4f7f\u7528\u8005\u540d\u7a31\u6216\u5bc6\u78bc\u932f\u8aa4" + }, + "step": { + "auth_app": { + "data": { + "app_id": "App ID", + "app_secret": "\u5bc6\u78bc" + }, + "description": "Application ID and secret code \u7531 StarLine \u958b\u767c\u8005\u5e33\u865f \u6240\u53d6\u5f97\u7684\u61c9\u7528\u7a0b\u5f0f ID \u8207\u5bc6\u78bc", + "title": "\u61c9\u7528\u6191\u8b49" + }, + "auth_captcha": { + "data": { + "captcha_code": "\u5716\u50cf\u5bc6\u78bc" + }, + "description": "{captcha_img}", + "title": "Captcha" + }, + "auth_mfa": { + "data": { + "mfa_code": "\u7c21\u8a0a\u5bc6\u78bc" + }, + "description": "\u8f38\u5165\u50b3\u9001\u81f3 {phone_number} \u7684\u9a57\u8b49\u78bc", + "title": "\u5169\u968e\u6bb5\u8a8d\u8b49" + }, + "auth_user": { + "data": { + "password": "\u5bc6\u78bc", + "username": "\u4f7f\u7528\u8005\u540d\u7a31" + }, + "description": "StarLine \u5e33\u865f\u90f5\u4ef6\u8207\u5bc6\u78bc", + "title": "\u4f7f\u7528\u8005\u6191\u8b49" + } + }, + "title": "StarLine" + } +} \ No newline at end of file diff --git a/homeassistant/components/starline/__init__.py b/homeassistant/components/starline/__init__.py new file mode 100644 index 000000000000..22772282a7c0 --- /dev/null +++ b/homeassistant/components/starline/__init__.py @@ -0,0 +1,85 @@ +"""The StarLine component.""" +import voluptuous as vol +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import Config, HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady + +from .account import StarlineAccount +from .const import ( + DOMAIN, + PLATFORMS, + SERVICE_UPDATE_STATE, + SERVICE_SET_SCAN_INTERVAL, + CONF_SCAN_INTERVAL, + DEFAULT_SCAN_INTERVAL, +) + + +async def async_setup(hass: HomeAssistant, config: Config) -> bool: + """Set up configured StarLine.""" + return True + + +async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: + """Set up the StarLine device from a config entry.""" + account = StarlineAccount(hass, config_entry) + await account.update() + if not account.api.available: + raise ConfigEntryNotReady + + if DOMAIN not in hass.data: + hass.data[DOMAIN] = {} + hass.data[DOMAIN][config_entry.entry_id] = account + + device_registry = await hass.helpers.device_registry.async_get_registry() + for device in account.api.devices.values(): + device_registry.async_get_or_create( + config_entry_id=config_entry.entry_id, **account.device_info(device) + ) + + for domain in PLATFORMS: + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(config_entry, domain) + ) + + async def async_set_scan_interval(call): + """Service for set scan interval.""" + options = dict(config_entry.options) + options[CONF_SCAN_INTERVAL] = call.data[CONF_SCAN_INTERVAL] + hass.config_entries.async_update_entry(entry=config_entry, options=options) + + hass.services.async_register(DOMAIN, SERVICE_UPDATE_STATE, account.update) + hass.services.async_register( + DOMAIN, + SERVICE_SET_SCAN_INTERVAL, + async_set_scan_interval, + schema=vol.Schema( + { + vol.Required(CONF_SCAN_INTERVAL): vol.All( + vol.Coerce(int), vol.Range(min=10) + ) + } + ), + ) + + config_entry.add_update_listener(async_options_updated) + await async_options_updated(hass, config_entry) + + return True + + +async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: + """Unload a config entry.""" + for domain in PLATFORMS: + await hass.config_entries.async_forward_entry_unload(config_entry, domain) + + account: StarlineAccount = hass.data[DOMAIN][config_entry.entry_id] + account.unload() + return True + + +async def async_options_updated(hass: HomeAssistant, config_entry: ConfigEntry) -> None: + """Triggered by config entry options updates.""" + account: StarlineAccount = hass.data[DOMAIN][config_entry.entry_id] + scan_interval = config_entry.options.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL) + account.set_update_interval(scan_interval) diff --git a/homeassistant/components/starline/account.py b/homeassistant/components/starline/account.py new file mode 100644 index 000000000000..2e7653eb380b --- /dev/null +++ b/homeassistant/components/starline/account.py @@ -0,0 +1,142 @@ +"""StarLine Account.""" +from datetime import timedelta, datetime +from typing import Callable, Optional, Dict, Any +from starline import StarlineApi, StarlineDevice + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.event import async_track_time_interval + +from .const import ( + DOMAIN, + LOGGER, + DEFAULT_SCAN_INTERVAL, + DATA_USER_ID, + DATA_SLNET_TOKEN, + DATA_SLID_TOKEN, + DATA_EXPIRES, +) + + +class StarlineAccount: + """StarLine Account class.""" + + def __init__(self, hass: HomeAssistant, config_entry: ConfigEntry): + """Constructor.""" + self._hass: HomeAssistant = hass + self._config_entry: ConfigEntry = config_entry + self._update_interval: int = DEFAULT_SCAN_INTERVAL + self._unsubscribe_auto_updater: Optional[Callable] = None + self._api: StarlineApi = StarlineApi( + config_entry.data[DATA_USER_ID], config_entry.data[DATA_SLNET_TOKEN] + ) + + def _check_slnet_token(self) -> None: + """Check SLNet token expiration and update if needed.""" + now = datetime.now().timestamp() + slnet_token_expires = self._config_entry.data[DATA_EXPIRES] + + if now + self._update_interval > slnet_token_expires: + self._update_slnet_token() + + def _update_slnet_token(self) -> None: + """Update SLNet token.""" + slid_token = self._config_entry.data[DATA_SLID_TOKEN] + + try: + slnet_token, slnet_token_expires, user_id = self._api.get_user_id( + slid_token + ) + self._api.set_slnet_token(slnet_token) + self._api.set_user_id(user_id) + self._hass.config_entries.async_update_entry( + self._config_entry, + data={ + **self._config_entry.data, + DATA_SLNET_TOKEN: slnet_token, + DATA_EXPIRES: slnet_token_expires, + DATA_USER_ID: user_id, + }, + ) + except Exception as err: # pylint: disable=broad-except + LOGGER.error("Error updating SLNet token: %s", err) + pass + + def _update_data(self): + """Update StarLine data.""" + self._check_slnet_token() + self._api.update() + + @property + def api(self) -> StarlineApi: + """Return the instance of the API.""" + return self._api + + async def update(self, unused=None): + """Update StarLine data.""" + await self._hass.async_add_executor_job(self._update_data) + + def set_update_interval(self, interval: int) -> None: + """Set StarLine API update interval.""" + LOGGER.debug("Setting update interval: %ds", interval) + self._update_interval = interval + if self._unsubscribe_auto_updater is not None: + self._unsubscribe_auto_updater() + + delta = timedelta(seconds=interval) + self._unsubscribe_auto_updater = async_track_time_interval( + self._hass, self.update, delta + ) + + def unload(self): + """Unload StarLine API.""" + LOGGER.debug("Unloading StarLine API.") + if self._unsubscribe_auto_updater is not None: + self._unsubscribe_auto_updater() + self._unsubscribe_auto_updater = None + + @staticmethod + def device_info(device: StarlineDevice) -> Dict[str, Any]: + """Device information for entities.""" + return { + "identifiers": {(DOMAIN, device.device_id)}, + "manufacturer": "StarLine", + "name": device.name, + "sw_version": device.fw_version, + "model": device.typename, + } + + @staticmethod + def gps_attrs(device: StarlineDevice) -> Dict[str, Any]: + """Attributes for device tracker.""" + return { + "updated": datetime.utcfromtimestamp(device.position["ts"]).isoformat(), + "online": device.online, + } + + @staticmethod + def balance_attrs(device: StarlineDevice) -> Dict[str, Any]: + """Attributes for balance sensor.""" + return { + "operator": device.balance.get("operator"), + "state": device.balance.get("state"), + "updated": device.balance.get("ts"), + } + + @staticmethod + def gsm_attrs(device: StarlineDevice) -> Dict[str, Any]: + """Attributes for GSM sensor.""" + return { + "raw": device.gsm_level, + "imei": device.imei, + "phone": device.phone, + "online": device.online, + } + + @staticmethod + def engine_attrs(device: StarlineDevice) -> Dict[str, Any]: + """Attributes for engine switch.""" + return { + "autostart": device.car_state.get("r_start"), + "ignition": device.car_state.get("run"), + } diff --git a/homeassistant/components/starline/binary_sensor.py b/homeassistant/components/starline/binary_sensor.py new file mode 100644 index 000000000000..fd28ff74cf4c --- /dev/null +++ b/homeassistant/components/starline/binary_sensor.py @@ -0,0 +1,58 @@ +"""Reads vehicle status from StarLine API.""" +from homeassistant.components.binary_sensor import ( + BinarySensorDevice, + DEVICE_CLASS_DOOR, + DEVICE_CLASS_LOCK, + DEVICE_CLASS_PROBLEM, + DEVICE_CLASS_POWER, +) +from .account import StarlineAccount, StarlineDevice +from .const import DOMAIN +from .entity import StarlineEntity + +SENSOR_TYPES = { + "hbrake": ["Hand Brake", DEVICE_CLASS_POWER], + "hood": ["Hood", DEVICE_CLASS_DOOR], + "trunk": ["Trunk", DEVICE_CLASS_DOOR], + "alarm": ["Alarm", DEVICE_CLASS_PROBLEM], + "door": ["Doors", DEVICE_CLASS_LOCK], +} + + +async def async_setup_entry(hass, entry, async_add_entities): + """Set up the StarLine sensors.""" + account: StarlineAccount = hass.data[DOMAIN][entry.entry_id] + entities = [] + for device in account.api.devices.values(): + for key, value in SENSOR_TYPES.items(): + if key in device.car_state: + sensor = StarlineSensor(account, device, key, *value) + if sensor.is_on is not None: + entities.append(sensor) + async_add_entities(entities) + + +class StarlineSensor(StarlineEntity, BinarySensorDevice): + """Representation of a StarLine binary sensor.""" + + def __init__( + self, + account: StarlineAccount, + device: StarlineDevice, + key: str, + name: str, + device_class: str, + ): + """Constructor.""" + super().__init__(account, device, key, name) + self._device_class = device_class + + @property + def device_class(self): + """Return the class of the binary sensor.""" + return self._device_class + + @property + def is_on(self): + """Return the state of the binary sensor.""" + return self._device.car_state.get(self._key) diff --git a/homeassistant/components/starline/config_flow.py b/homeassistant/components/starline/config_flow.py new file mode 100644 index 000000000000..2253cc3cd22e --- /dev/null +++ b/homeassistant/components/starline/config_flow.py @@ -0,0 +1,229 @@ +"""Config flow to configure StarLine component.""" +from typing import Optional +from starline import StarlineAuth +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_USERNAME, CONF_PASSWORD + +from .const import ( # pylint: disable=unused-import + DOMAIN, + CONF_APP_ID, + CONF_APP_SECRET, + CONF_MFA_CODE, + CONF_CAPTCHA_CODE, + LOGGER, + ERROR_AUTH_APP, + ERROR_AUTH_USER, + ERROR_AUTH_MFA, + DATA_USER_ID, + DATA_SLNET_TOKEN, + DATA_SLID_TOKEN, + DATA_EXPIRES, +) + + +class StarlineFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a StarLine config flow.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL + + def __init__(self): + """Initialize flow.""" + self._app_id: Optional[str] = None + self._app_secret: Optional[str] = None + self._username: Optional[str] = None + self._password: Optional[str] = None + self._mfa_code: Optional[str] = None + + self._app_code = None + self._app_token = None + self._user_slid = None + self._user_id = None + self._slnet_token = None + self._slnet_token_expires = None + self._captcha_image = None + self._captcha_sid = None + self._captcha_code = None + self._phone_number = None + + self._auth = StarlineAuth() + + async def async_step_user(self, user_input=None): + """Handle a flow initialized by the user.""" + return await self.async_step_auth_app(user_input) + + async def async_step_auth_app(self, user_input=None, error=None): + """Authenticate application step.""" + if user_input is not None: + self._app_id = user_input[CONF_APP_ID] + self._app_secret = user_input[CONF_APP_SECRET] + return await self._async_authenticate_app(error) + return self._async_form_auth_app(error) + + async def async_step_auth_user(self, user_input=None, error=None): + """Authenticate user step.""" + if user_input is not None: + self._username = user_input[CONF_USERNAME] + self._password = user_input[CONF_PASSWORD] + return await self._async_authenticate_user(error) + return self._async_form_auth_user(error) + + async def async_step_auth_mfa(self, user_input=None, error=None): + """Authenticate mfa step.""" + if user_input is not None: + self._mfa_code = user_input[CONF_MFA_CODE] + return await self._async_authenticate_user(error) + return self._async_form_auth_mfa(error) + + async def async_step_auth_captcha(self, user_input=None, error=None): + """Captcha verification step.""" + if user_input is not None: + self._captcha_code = user_input[CONF_CAPTCHA_CODE] + return await self._async_authenticate_user(error) + return self._async_form_auth_captcha(error) + + def _async_form_auth_app(self, error=None): + """Authenticate application form.""" + errors = {} + if error is not None: + errors["base"] = error + + return self.async_show_form( + step_id="auth_app", + data_schema=vol.Schema( + { + vol.Required( + CONF_APP_ID, default=self._app_id or vol.UNDEFINED + ): str, + vol.Required( + CONF_APP_SECRET, default=self._app_secret or vol.UNDEFINED + ): str, + } + ), + errors=errors, + ) + + def _async_form_auth_user(self, error=None): + """Authenticate user form.""" + errors = {} + if error is not None: + errors["base"] = error + + return self.async_show_form( + step_id="auth_user", + data_schema=vol.Schema( + { + vol.Required( + CONF_USERNAME, default=self._username or vol.UNDEFINED + ): str, + vol.Required( + CONF_PASSWORD, default=self._password or vol.UNDEFINED + ): str, + } + ), + errors=errors, + ) + + def _async_form_auth_mfa(self, error=None): + """Authenticate mfa form.""" + errors = {} + if error is not None: + errors["base"] = error + + return self.async_show_form( + step_id="auth_mfa", + data_schema=vol.Schema( + { + vol.Required( + CONF_MFA_CODE, default=self._mfa_code or vol.UNDEFINED + ): str + } + ), + errors=errors, + description_placeholders={"phone_number": self._phone_number}, + ) + + def _async_form_auth_captcha(self, error=None): + """Captcha verification form.""" + errors = {} + if error is not None: + errors["base"] = error + + return self.async_show_form( + step_id="auth_captcha", + data_schema=vol.Schema( + { + vol.Required( + CONF_CAPTCHA_CODE, default=self._captcha_code or vol.UNDEFINED + ): str + } + ), + errors=errors, + description_placeholders={ + "captcha_img": '' + }, + ) + + async def _async_authenticate_app(self, error=None): + """Authenticate application.""" + try: + self._app_code = self._auth.get_app_code(self._app_id, self._app_secret) + self._app_token = self._auth.get_app_token( + self._app_id, self._app_secret, self._app_code + ) + return self._async_form_auth_user(error) + except Exception as err: # pylint: disable=broad-except + LOGGER.error("Error auth StarLine: %s", err) + return self._async_form_auth_app(ERROR_AUTH_APP) + + async def _async_authenticate_user(self, error=None): + """Authenticate user.""" + try: + state, data = self._auth.get_slid_user_token( + self._app_token, + self._username, + self._password, + self._mfa_code, + self._captcha_sid, + self._captcha_code, + ) + + if state == 1: + self._user_slid = data["user_token"] + return await self._async_get_entry() + + if "phone" in data: + self._phone_number = data["phone"] + if state == 0: + error = ERROR_AUTH_MFA + return self._async_form_auth_mfa(error) + + if "captchaSid" in data: + self._captcha_sid = data["captchaSid"] + self._captcha_image = data["captchaImg"] + return self._async_form_auth_captcha(error) + + raise Exception(data) + except Exception as err: # pylint: disable=broad-except + LOGGER.error("Error auth user: %s", err) + return self._async_form_auth_user(ERROR_AUTH_USER) + + async def _async_get_entry(self): + """Create entry.""" + ( + self._slnet_token, + self._slnet_token_expires, + self._user_id, + ) = self._auth.get_user_id(self._user_slid) + + return self.async_create_entry( + title=f"Application {self._app_id}", + data={ + DATA_USER_ID: self._user_id, + DATA_SLNET_TOKEN: self._slnet_token, + DATA_SLID_TOKEN: self._user_slid, + DATA_EXPIRES: self._slnet_token_expires, + }, + ) diff --git a/homeassistant/components/starline/const.py b/homeassistant/components/starline/const.py new file mode 100644 index 000000000000..d76cd47b100d --- /dev/null +++ b/homeassistant/components/starline/const.py @@ -0,0 +1,27 @@ +"""StarLine constants.""" +import logging + +LOGGER = logging.getLogger(__package__) + +DOMAIN = "starline" +PLATFORMS = ["device_tracker", "binary_sensor", "sensor", "lock", "switch"] + +CONF_APP_ID = "app_id" +CONF_APP_SECRET = "app_secret" +CONF_MFA_CODE = "mfa_code" +CONF_CAPTCHA_CODE = "captcha_code" + +CONF_SCAN_INTERVAL = "scan_interval" +DEFAULT_SCAN_INTERVAL = 180 # in seconds + +ERROR_AUTH_APP = "error_auth_app" +ERROR_AUTH_USER = "error_auth_user" +ERROR_AUTH_MFA = "error_auth_mfa" + +DATA_USER_ID = "user_id" +DATA_SLNET_TOKEN = "slnet_token" +DATA_SLID_TOKEN = "slid_token" +DATA_EXPIRES = "expires" + +SERVICE_UPDATE_STATE = "update_state" +SERVICE_SET_SCAN_INTERVAL = "set_scan_interval" diff --git a/homeassistant/components/starline/device_tracker.py b/homeassistant/components/starline/device_tracker.py new file mode 100644 index 000000000000..b5254c761d80 --- /dev/null +++ b/homeassistant/components/starline/device_tracker.py @@ -0,0 +1,60 @@ +"""StarLine device tracker.""" +from homeassistant.components.device_tracker.config_entry import TrackerEntity +from homeassistant.components.device_tracker.const import SOURCE_TYPE_GPS +from homeassistant.helpers.restore_state import RestoreEntity +from .account import StarlineAccount, StarlineDevice +from .const import DOMAIN +from .entity import StarlineEntity + + +async def async_setup_entry(hass, entry, async_add_entities): + """Set up StarLine entry.""" + account: StarlineAccount = hass.data[DOMAIN][entry.entry_id] + entities = [] + for device in account.api.devices.values(): + if device.support_position: + entities.append(StarlineDeviceTracker(account, device)) + async_add_entities(entities) + + +class StarlineDeviceTracker(StarlineEntity, TrackerEntity, RestoreEntity): + """StarLine device tracker.""" + + def __init__(self, account: StarlineAccount, device: StarlineDevice): + """Set up StarLine entity.""" + super().__init__(account, device, "location", "Location") + + @property + def device_state_attributes(self): + """Return device specific attributes.""" + return self._account.gps_attrs(self._device) + + @property + def battery_level(self): + """Return the battery level of the device.""" + return self._device.battery_level + + @property + def location_accuracy(self): + """Return the gps accuracy of the device.""" + return self._device.position["r"] if "r" in self._device.position else 0 + + @property + def latitude(self): + """Return latitude value of the device.""" + return self._device.position["x"] + + @property + def longitude(self): + """Return longitude value of the device.""" + return self._device.position["y"] + + @property + def source_type(self): + """Return the source type, eg gps or router, of the device.""" + return SOURCE_TYPE_GPS + + @property + def icon(self): + """Return the icon to use in the frontend, if any.""" + return "mdi:map-marker-outline" diff --git a/homeassistant/components/starline/entity.py b/homeassistant/components/starline/entity.py new file mode 100644 index 000000000000..b0d948ae2c80 --- /dev/null +++ b/homeassistant/components/starline/entity.py @@ -0,0 +1,59 @@ +"""StarLine base entity.""" +from typing import Callable, Optional +from homeassistant.helpers.entity import Entity +from .account import StarlineAccount, StarlineDevice + + +class StarlineEntity(Entity): + """StarLine base entity class.""" + + def __init__( + self, account: StarlineAccount, device: StarlineDevice, key: str, name: str + ): + """Constructor.""" + self._account = account + self._device = device + self._key = key + self._name = name + self._unsubscribe_api: Optional[Callable] = None + + @property + def should_poll(self): + """No polling needed.""" + return False + + @property + def available(self): + """Return True if entity is available.""" + return self._account.api.available + + @property + def unique_id(self): + """Return the unique ID of the entity.""" + return f"starline-{self._key}-{self._device.device_id}" + + @property + def name(self): + """Return the name of the entity.""" + return f"{self._device.name} {self._name}" + + @property + def device_info(self): + """Return the device info.""" + return self._account.device_info(self._device) + + def update(self): + """Read new state data.""" + self.schedule_update_ha_state() + + async def async_added_to_hass(self): + """Call when entity about to be added to Home Assistant.""" + await super().async_added_to_hass() + self._unsubscribe_api = self._account.api.add_update_listener(self.update) + + async def async_will_remove_from_hass(self): + """Call when entity is being removed from Home Assistant.""" + await super().async_will_remove_from_hass() + if self._unsubscribe_api is not None: + self._unsubscribe_api() + self._unsubscribe_api = None diff --git a/homeassistant/components/starline/lock.py b/homeassistant/components/starline/lock.py new file mode 100644 index 000000000000..0a20a36ae8bd --- /dev/null +++ b/homeassistant/components/starline/lock.py @@ -0,0 +1,72 @@ +"""Support for StarLine lock.""" +from homeassistant.components.lock import LockDevice +from .account import StarlineAccount, StarlineDevice +from .const import DOMAIN +from .entity import StarlineEntity + + +async def async_setup_entry(hass, entry, async_add_entities): + """Set up the StarLine lock.""" + + account: StarlineAccount = hass.data[DOMAIN][entry.entry_id] + entities = [] + for device in account.api.devices.values(): + if device.support_state: + lock = StarlineLock(account, device) + if lock.is_locked is not None: + entities.append(lock) + async_add_entities(entities) + + +class StarlineLock(StarlineEntity, LockDevice): + """Representation of a StarLine lock.""" + + def __init__(self, account: StarlineAccount, device: StarlineDevice): + """Initialize the lock.""" + super().__init__(account, device, "lock", "Security") + + @property + def available(self): + """Return True if entity is available.""" + return super().available and self._device.online + + @property + def device_state_attributes(self): + """Return the state attributes of the lock. + + Possible dictionary keys: + add_h - Additional sensor alarm status (high level) + add_l - Additional channel alarm status (low level) + door - Doors alarm status + hbrake - Hand brake alarm status + hijack - Hijack mode status + hood - Hood alarm status + ign - Ignition alarm status + pbrake - Brake pedal alarm status + shock_h - Shock sensor alarm status (high level) + shock_l - Shock sensor alarm status (low level) + tilt - Tilt sensor alarm status + trunk - Trunk alarm status + Documentation: https://developer.starline.ru/#api-Device-DeviceState + """ + return self._device.alarm_state + + @property + def icon(self): + """Icon to use in the frontend, if any.""" + return ( + "mdi:shield-check-outline" if self.is_locked else "mdi:shield-alert-outline" + ) + + @property + def is_locked(self): + """Return true if lock is locked.""" + return self._device.car_state.get("arm") + + def lock(self, **kwargs): + """Lock the car.""" + self._account.api.set_car_state(self._device.device_id, "arm", True) + + def unlock(self, **kwargs): + """Unlock the car.""" + self._account.api.set_car_state(self._device.device_id, "arm", False) diff --git a/homeassistant/components/starline/manifest.json b/homeassistant/components/starline/manifest.json new file mode 100644 index 000000000000..ef343aae4ce9 --- /dev/null +++ b/homeassistant/components/starline/manifest.json @@ -0,0 +1,13 @@ +{ + "domain": "starline", + "name": "StarLine", + "config_flow": true, + "documentation": "https://www.home-assistant.io/components/starline", + "requirements": [ + "starline==0.1.3" + ], + "dependencies": [], + "codeowners": [ + "@anonym-tsk" + ] +} diff --git a/homeassistant/components/starline/sensor.py b/homeassistant/components/starline/sensor.py new file mode 100644 index 000000000000..ba0cef0255d6 --- /dev/null +++ b/homeassistant/components/starline/sensor.py @@ -0,0 +1,96 @@ +"""Reads vehicle status from StarLine API.""" +from homeassistant.components.sensor import DEVICE_CLASS_TEMPERATURE +from homeassistant.const import TEMP_CELSIUS +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.icon import icon_for_battery_level, icon_for_signal_level +from .account import StarlineAccount, StarlineDevice +from .const import DOMAIN +from .entity import StarlineEntity + +SENSOR_TYPES = { + "battery": ["Battery", None, "V", None], + "balance": ["Balance", None, None, "mdi:cash-multiple"], + "ctemp": ["Interior Temperature", DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, None], + "etemp": ["Engine Temperature", DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, None], + "gsm_lvl": ["GSM Signal", None, "%", None], +} + + +async def async_setup_entry(hass, entry, async_add_entities): + """Set up the StarLine sensors.""" + account: StarlineAccount = hass.data[DOMAIN][entry.entry_id] + entities = [] + for device in account.api.devices.values(): + for key, value in SENSOR_TYPES.items(): + sensor = StarlineSensor(account, device, key, *value) + if sensor.state is not None: + entities.append(sensor) + async_add_entities(entities) + + +class StarlineSensor(StarlineEntity, Entity): + """Representation of a StarLine sensor.""" + + def __init__( + self, + account: StarlineAccount, + device: StarlineDevice, + key: str, + name: str, + device_class: str, + unit: str, + icon: str, + ): + """Constructor.""" + super().__init__(account, device, key, name) + self._device_class = device_class + self._unit = unit + self._icon = icon + + @property + def icon(self): + """Icon to use in the frontend, if any.""" + if self._key == "battery": + return icon_for_battery_level( + battery_level=self._device.battery_level_percent, + charging=self._device.car_state.get("ign", False), + ) + if self._key == "gsm_lvl": + return icon_for_signal_level(signal_level=self._device.gsm_level_percent) + return self._icon + + @property + def state(self): + """Return the state of the sensor.""" + if self._key == "battery": + return self._device.battery_level + if self._key == "balance": + return self._device.balance.get("value") + if self._key == "ctemp": + return self._device.temp_inner + if self._key == "etemp": + return self._device.temp_engine + if self._key == "gsm_lvl": + return self._device.gsm_level_percent + return None + + @property + def unit_of_measurement(self): + """Get the unit of measurement.""" + if self._key == "balance": + return self._device.balance.get("currency") or "₽" + return self._unit + + @property + def device_class(self): + """Return the class of the sensor.""" + return self._device_class + + @property + def device_state_attributes(self): + """Return the state attributes of the sensor.""" + if self._key == "balance": + return self._account.balance_attrs(self._device) + if self._key == "gsm_lvl": + return self._account.gsm_attrs(self._device) + return None diff --git a/homeassistant/components/starline/services.yaml b/homeassistant/components/starline/services.yaml new file mode 100644 index 000000000000..bef3a16803e8 --- /dev/null +++ b/homeassistant/components/starline/services.yaml @@ -0,0 +1,10 @@ +update_state: + description: > + Fetch the last state of the devices from the StarLine server. +set_scan_interval: + description: > + Set update frequency. + fields: + scan_interval: + description: Update frequency (in seconds). + example: 180 diff --git a/homeassistant/components/starline/strings.json b/homeassistant/components/starline/strings.json new file mode 100644 index 000000000000..bf83f652c3c7 --- /dev/null +++ b/homeassistant/components/starline/strings.json @@ -0,0 +1,42 @@ +{ + "config": { + "title": "StarLine", + "step": { + "auth_app": { + "title": "Application credentials", + "description": "Application ID and secret code from StarLine developer account", + "data": { + "app_id": "App ID", + "app_secret": "Secret" + } + }, + "auth_user": { + "title": "User credentials", + "description": "StarLine account email and password", + "data": { + "username": "Username", + "password": "Password" + } + }, + "auth_mfa": { + "title": "Two-factor authorization", + "description": "Enter the code sent to phone {phone_number}", + "data": { + "mfa_code": "SMS code" + } + }, + "auth_captcha": { + "title": "Captcha", + "description": "{captcha_img}", + "data": { + "captcha_code": "Code from image" + } + } + }, + "error": { + "error_auth_app": "Incorrect application id or secret", + "error_auth_user": "Incorrect username or password", + "error_auth_mfa": "Incorrect code" + } + } +} diff --git a/homeassistant/components/starline/switch.py b/homeassistant/components/starline/switch.py new file mode 100644 index 000000000000..92dec10b9d30 --- /dev/null +++ b/homeassistant/components/starline/switch.py @@ -0,0 +1,86 @@ +"""Support for StarLine switch.""" +from homeassistant.components.switch import SwitchDevice +from .account import StarlineAccount, StarlineDevice +from .const import DOMAIN +from .entity import StarlineEntity + +SWITCH_TYPES = { + "ign": ["Engine", "mdi:engine-outline", "mdi:engine-off-outline"], + "webasto": ["Webasto", "mdi:radiator", "mdi:radiator-off"], + "out": [ + "Additional Channel", + "mdi:access-point-network", + "mdi:access-point-network-off", + ], + "poke": ["Horn", "mdi:bullhorn-outline", "mdi:bullhorn-outline"], +} + + +async def async_setup_entry(hass, entry, async_add_entities): + """Set up the StarLine switch.""" + account: StarlineAccount = hass.data[DOMAIN][entry.entry_id] + entities = [] + for device in account.api.devices.values(): + if device.support_state: + for key, value in SWITCH_TYPES.items(): + switch = StarlineSwitch(account, device, key, *value) + if switch.is_on is not None: + entities.append(switch) + async_add_entities(entities) + + +class StarlineSwitch(StarlineEntity, SwitchDevice): + """Representation of a StarLine switch.""" + + def __init__( + self, + account: StarlineAccount, + device: StarlineDevice, + key: str, + name: str, + icon_on: str, + icon_off: str, + ): + """Initialize the switch.""" + super().__init__(account, device, key, name) + self._icon_on = icon_on + self._icon_off = icon_off + + @property + def available(self): + """Return True if entity is available.""" + return super().available and self._device.online + + @property + def device_state_attributes(self): + """Return the state attributes of the switch.""" + if self._key == "ign": + return self._account.engine_attrs(self._device) + return None + + @property + def icon(self): + """Icon to use in the frontend, if any.""" + return self._icon_on if self.is_on else self._icon_off + + @property + def assumed_state(self): + """Return True if unable to access real state of the entity.""" + return True + + @property + def is_on(self): + """Return True if entity is on.""" + if self._key == "poke": + return False + return self._device.car_state.get(self._key) + + def turn_on(self, **kwargs): + """Turn the entity on.""" + self._account.api.set_car_state(self._device.device_id, self._key, True) + + def turn_off(self, **kwargs) -> None: + """Turn the entity off.""" + if self._key == "poke": + return + self._account.api.set_car_state(self._device.device_id, self._key, False) diff --git a/homeassistant/components/starlingbank/sensor.py b/homeassistant/components/starlingbank/sensor.py index 24ca7d4809cc..1e0461923478 100644 --- a/homeassistant/components/starlingbank/sensor.py +++ b/homeassistant/components/starlingbank/sensor.py @@ -2,6 +2,7 @@ import logging import requests +from starlingbank import StarlingAccount import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -40,7 +41,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Sterling Bank sensor platform.""" - from starlingbank import StarlingAccount sensors = [] for account in config[CONF_ACCOUNTS]: diff --git a/homeassistant/components/stiebel_eltron/__init__.py b/homeassistant/components/stiebel_eltron/__init__.py index 20cb6607ea14..956e629dd9db 100644 --- a/homeassistant/components/stiebel_eltron/__init__.py +++ b/homeassistant/components/stiebel_eltron/__init__.py @@ -2,6 +2,7 @@ from datetime import timedelta import logging +from pystiebeleltron import pystiebeleltron import voluptuous as vol from homeassistant.components.modbus import ( @@ -55,7 +56,6 @@ class StiebelEltronData: def __init__(self, name, modbus_client): """Init the STIEBEL ELTRON data object.""" - from pystiebeleltron import pystiebeleltron self.api = pystiebeleltron.StiebelEltronAPI(modbus_client, 1) diff --git a/homeassistant/components/stream/__init__.py b/homeassistant/components/stream/__init__.py index a83f05820e2e..9304257f853b 100644 --- a/homeassistant/components/stream/__init__.py +++ b/homeassistant/components/stream/__init__.py @@ -23,11 +23,6 @@ from .core import PROVIDERS from .hls import async_setup_hls -try: - import uvloop -except ImportError: - uvloop = None - _LOGGER = logging.getLogger(__name__) @@ -42,7 +37,6 @@ vol.Optional(CONF_LOOKBACK, default=0): int, } ) -DATA_UVLOOP_WARN = "stream_uvloop_warn" # Set log level to error for libav logging.getLogger("libav").setLevel(logging.ERROR) @@ -53,21 +47,6 @@ def request_stream(hass, stream_source, *, fmt="hls", keepalive=False, options=N if DOMAIN not in hass.config.components: raise HomeAssistantError("Stream integration is not set up.") - if DATA_UVLOOP_WARN not in hass.data: - hass.data[DATA_UVLOOP_WARN] = True - # Warn about https://github.com/home-assistant/home-assistant/issues/22999 - if ( - uvloop is not None - and isinstance(hass.loop, uvloop.Loop) - and ( - "shell_command" in hass.config.components - or "ffmpeg" in hass.config.components - ) - ): - _LOGGER.warning( - "You are using UVLoop with stream and shell_command. This is known to cause issues. Please uninstall uvloop." - ) - if options is None: options = {} diff --git a/homeassistant/components/streamlabswater/__init__.py b/homeassistant/components/streamlabswater/__init__.py index 68097794d076..836bc9b41830 100644 --- a/homeassistant/components/streamlabswater/__init__.py +++ b/homeassistant/components/streamlabswater/__init__.py @@ -1,6 +1,7 @@ """Support for Streamlabs Water Monitor devices.""" import logging +from streamlabswater import streamlabswater import voluptuous as vol from homeassistant.const import CONF_API_KEY @@ -39,7 +40,6 @@ def setup(hass, config): """Set up the streamlabs water component.""" - from streamlabswater import streamlabswater conf = config[DOMAIN] api_key = conf.get(CONF_API_KEY) diff --git a/homeassistant/components/supla/__init__.py b/homeassistant/components/supla/__init__.py index 4293f187f5bb..fd60254cd0ad 100644 --- a/homeassistant/components/supla/__init__.py +++ b/homeassistant/components/supla/__init__.py @@ -2,6 +2,7 @@ import logging from typing import Optional +from pysupla import SuplaAPI import voluptuous as vol from homeassistant.const import CONF_ACCESS_TOKEN @@ -38,7 +39,6 @@ def setup(hass, base_config): """Set up the Supla component.""" - from pysupla import SuplaAPI server_confs = base_config[DOMAIN][CONF_SERVERS] diff --git a/homeassistant/components/supla/switch.py b/homeassistant/components/supla/switch.py index 5e7a54699505..725771e21e80 100644 --- a/homeassistant/components/supla/switch.py +++ b/homeassistant/components/supla/switch.py @@ -2,8 +2,8 @@ import logging from pprint import pformat -from homeassistant.components.switch import SwitchDevice from homeassistant.components.supla import SuplaChannel +from homeassistant.components.switch import SwitchDevice _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/swiss_hydrological_data/sensor.py b/homeassistant/components/swiss_hydrological_data/sensor.py index 2d5d0e8de3f3..c8e7b9d6fc26 100644 --- a/homeassistant/components/swiss_hydrological_data/sensor.py +++ b/homeassistant/components/swiss_hydrological_data/sensor.py @@ -2,6 +2,7 @@ from datetime import timedelta import logging +from swisshydrodata import SwissHydroData import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -167,7 +168,6 @@ def __init__(self, station): @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): """Get the latest data.""" - from swisshydrodata import SwissHydroData shd = SwissHydroData() self.data = shd.get_station(self.station) diff --git a/homeassistant/components/swiss_public_transport/sensor.py b/homeassistant/components/swiss_public_transport/sensor.py index 3cf8babf5548..be967247dc75 100644 --- a/homeassistant/components/swiss_public_transport/sensor.py +++ b/homeassistant/components/swiss_public_transport/sensor.py @@ -2,6 +2,8 @@ from datetime import timedelta import logging +from opendata_transport import OpendataTransport +from opendata_transport.exceptions import OpendataTransportError import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -45,7 +47,6 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the Swiss public transport sensor.""" - from opendata_transport import OpendataTransport, exceptions name = config.get(CONF_NAME) start = config.get(CONF_START) @@ -56,7 +57,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= try: await opendata.async_get_data() - except exceptions.OpendataTransportError: + except OpendataTransportError: _LOGGER.error( "Check at http://transport.opendata.ch/examples/stationboard.html " "if your station names are valid" @@ -122,7 +123,6 @@ def icon(self): async def async_update(self): """Get the latest data from opendata.ch and update the states.""" - from opendata_transport.exceptions import OpendataTransportError try: if self._remaining_time.total_seconds() < 0: diff --git a/homeassistant/components/switch/__init__.py b/homeassistant/components/switch/__init__.py index aa7459d1d3ce..26d5658d6686 100644 --- a/homeassistant/components/switch/__init__.py +++ b/homeassistant/components/switch/__init__.py @@ -7,7 +7,7 @@ from homeassistant.loader import bind_hass from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.entity import ToggleEntity -from homeassistant.helpers.config_validation import ( # noqa +from homeassistant.helpers.config_validation import ( # noqa: F401 PLATFORM_SCHEMA, PLATFORM_SCHEMA_BASE, ) diff --git a/homeassistant/components/switch/services.yaml b/homeassistant/components/switch/services.yaml index 46b1237f57c2..352ffb6feec6 100644 --- a/homeassistant/components/switch/services.yaml +++ b/homeassistant/components/switch/services.yaml @@ -20,44 +20,3 @@ toggle: entity_id: description: Name(s) of entities to toggle. example: 'switch.living_room' - -mysensors_send_ir_code: - description: Set an IR code as a state attribute for a MySensors IR device switch and turn the switch on. - fields: - entity_id: - description: Name(s) of entities that should have the IR code set and be turned on. Platform dependent. - example: 'switch.living_room_1_1' - V_IR_SEND: - description: IR code to send. - example: '0xC284' - -xiaomi_miio_set_wifi_led_on: - description: Turn the wifi led on. - fields: - entity_id: - description: Name of the xiaomi miio entity. - example: 'switch.xiaomi_miio_device' -xiaomi_miio_set_wifi_led_off: - description: Turn the wifi led off. - fields: - entity_id: - description: Name of the xiaomi miio entity. - example: 'switch.xiaomi_miio_device' -xiaomi_miio_set_power_price: - description: Set the power price. - fields: - entity_id: - description: Name of the xiaomi miio entity. - example: 'switch.xiaomi_miio_device' - mode: - description: Power price, between 0 and 999. - example: 31 -xiaomi_miio_set_power_mode: - description: Set the power mode. - fields: - entity_id: - description: Name of the xiaomi miio entity. - example: 'switch.xiaomi_miio_device' - mode: - description: Power mode, valid values are 'normal' and 'green'. - example: 'green' diff --git a/homeassistant/components/switchbot/switch.py b/homeassistant/components/switchbot/switch.py index bc1fc34a8907..55e2a8b9641f 100644 --- a/homeassistant/components/switchbot/switch.py +++ b/homeassistant/components/switchbot/switch.py @@ -2,11 +2,13 @@ import logging from typing import Any, Dict +# pylint: disable=import-error, no-member +import switchbot import voluptuous as vol +from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchDevice +from homeassistant.const import CONF_MAC, CONF_NAME import homeassistant.helpers.config_validation as cv -from homeassistant.components.switch import SwitchDevice, PLATFORM_SCHEMA -from homeassistant.const import CONF_NAME, CONF_MAC from homeassistant.helpers.restore_state import RestoreEntity _LOGGER = logging.getLogger(__name__) @@ -33,8 +35,6 @@ class SwitchBot(SwitchDevice, RestoreEntity): def __init__(self, mac, name) -> None: """Initialize the Switchbot.""" - # pylint: disable=import-error, no-member - import switchbot self._state = None self._last_run_success = None diff --git a/homeassistant/components/switchmate/switch.py b/homeassistant/components/switchmate/switch.py index 6abbfd5fae5e..ddb0db3feee4 100644 --- a/homeassistant/components/switchmate/switch.py +++ b/homeassistant/components/switchmate/switch.py @@ -2,7 +2,7 @@ from datetime import timedelta import logging -# pylint: disable=import-error, no-member, no-value-for-parameter +# pylint: disable=import-error, no-member import switchmate import voluptuous as vol diff --git a/homeassistant/components/syncthru/sensor.py b/homeassistant/components/syncthru/sensor.py index 1258732223b0..e981154a81ac 100644 --- a/homeassistant/components/syncthru/sensor.py +++ b/homeassistant/components/syncthru/sensor.py @@ -1,13 +1,15 @@ """Support for Samsung Printers with SyncThru web interface.""" import logging + +from pysyncthru import SyncThru import voluptuous as vol -from homeassistant.const import CONF_RESOURCE, CONF_HOST, CONF_NAME +from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import CONF_HOST, CONF_NAME, CONF_RESOURCE from homeassistant.helpers import aiohttp_client -from homeassistant.helpers.entity import Entity import homeassistant.helpers.config_validation as cv -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -33,7 +35,6 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the SyncThru component.""" - from pysyncthru import SyncThru if discovery_info is not None: _LOGGER.info( diff --git a/homeassistant/components/synology/camera.py b/homeassistant/components/synology/camera.py index 91ee5a98fc32..c144a251608d 100644 --- a/homeassistant/components/synology/camera.py +++ b/homeassistant/components/synology/camera.py @@ -2,18 +2,19 @@ import logging import requests +from synology.surveillance_station import SurveillanceStation import voluptuous as vol +from homeassistant.components.camera import PLATFORM_SCHEMA, Camera from homeassistant.const import ( CONF_NAME, - CONF_USERNAME, CONF_PASSWORD, + CONF_TIMEOUT, CONF_URL, - CONF_WHITELIST, + CONF_USERNAME, CONF_VERIFY_SSL, - CONF_TIMEOUT, + CONF_WHITELIST, ) -from homeassistant.components.camera import Camera, PLATFORM_SCHEMA from homeassistant.helpers.aiohttp_client import ( async_aiohttp_proxy_web, async_get_clientsession, @@ -44,8 +45,6 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= timeout = config.get(CONF_TIMEOUT) try: - from synology.surveillance_station import SurveillanceStation - surveillance = SurveillanceStation( config.get(CONF_URL), config.get(CONF_USERNAME), diff --git a/homeassistant/components/synologydsm/sensor.py b/homeassistant/components/synologydsm/sensor.py index e19f6ada8097..d415d009252b 100644 --- a/homeassistant/components/synologydsm/sensor.py +++ b/homeassistant/components/synologydsm/sensor.py @@ -1,24 +1,25 @@ """Support for Synology NAS Sensors.""" -import logging from datetime import timedelta +import logging +from SynologyDSM import SynologyDSM import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( - CONF_NAME, + ATTR_ATTRIBUTION, + CONF_DISKS, CONF_HOST, - CONF_USERNAME, + CONF_MONITORED_CONDITIONS, + CONF_NAME, CONF_PASSWORD, CONF_PORT, CONF_SSL, - ATTR_ATTRIBUTION, - TEMP_CELSIUS, - CONF_MONITORED_CONDITIONS, + CONF_USERNAME, EVENT_HOMEASSISTANT_START, - CONF_DISKS, + TEMP_CELSIUS, ) +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle @@ -119,24 +120,26 @@ def run_setup(event): ] # Handle all volumes - for volume in config.get(CONF_VOLUMES, api.storage.volumes): - sensors += [ - SynoNasStorageSensor( - api, name, variable, _STORAGE_VOL_MON_COND[variable], volume - ) - for variable in monitored_conditions - if variable in _STORAGE_VOL_MON_COND - ] + if api.storage.volumes is not None: + for volume in config.get(CONF_VOLUMES, api.storage.volumes): + sensors += [ + SynoNasStorageSensor( + api, name, variable, _STORAGE_VOL_MON_COND[variable], volume + ) + for variable in monitored_conditions + if variable in _STORAGE_VOL_MON_COND + ] # Handle all disks - for disk in config.get(CONF_DISKS, api.storage.disks): - sensors += [ - SynoNasStorageSensor( - api, name, variable, _STORAGE_DSK_MON_COND[variable], disk - ) - for variable in monitored_conditions - if variable in _STORAGE_DSK_MON_COND - ] + if api.storage.disks is not None: + for disk in config.get(CONF_DISKS, api.storage.disks): + sensors += [ + SynoNasStorageSensor( + api, name, variable, _STORAGE_DSK_MON_COND[variable], disk + ) + for variable in monitored_conditions + if variable in _STORAGE_DSK_MON_COND + ] add_entities(sensors, True) @@ -149,7 +152,6 @@ class SynoApi: def __init__(self, host, port, username, password, temp_unit, use_ssl): """Initialize the API wrapper class.""" - from SynologyDSM import SynologyDSM self.temp_unit = temp_unit diff --git a/homeassistant/components/system_health/__init__.py b/homeassistant/components/system_health/__init__.py index c6b483509593..778ddf601dcf 100644 --- a/homeassistant/components/system_health/__init__.py +++ b/homeassistant/components/system_health/__init__.py @@ -44,7 +44,7 @@ async def _info_wrapper(hass, info_callback): return await info_callback(hass) except asyncio.TimeoutError: return {"error": "Fetching info timed out"} - except Exception as err: # pylint: disable=W0703 + except Exception as err: # pylint: disable=broad-except _LOGGER.exception("Error fetching info") return {"error": str(err)} diff --git a/homeassistant/components/systemmonitor/manifest.json b/homeassistant/components/systemmonitor/manifest.json index 67677bc25726..0614ef4b91ca 100644 --- a/homeassistant/components/systemmonitor/manifest.json +++ b/homeassistant/components/systemmonitor/manifest.json @@ -3,7 +3,7 @@ "name": "Systemmonitor", "documentation": "https://www.home-assistant.io/integrations/systemmonitor", "requirements": [ - "psutil==5.6.5" + "psutil==5.6.7" ], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/tado/__init__.py b/homeassistant/components/tado/__init__.py index ad66a594a86b..1739cbb9254f 100644 --- a/homeassistant/components/tado/__init__.py +++ b/homeassistant/components/tado/__init__.py @@ -1,13 +1,14 @@ """Support for the (unofficial) Tado API.""" +from datetime import timedelta import logging import urllib -from datetime import timedelta +from PyTado.interface import Tado import voluptuous as vol -from homeassistant.helpers.discovery import load_platform +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.helpers import config_validation as cv -from homeassistant.const import CONF_USERNAME, CONF_PASSWORD +from homeassistant.helpers.discovery import load_platform from homeassistant.util import Throttle _LOGGER = logging.getLogger(__name__) @@ -37,8 +38,6 @@ def setup(hass, config): username = config[DOMAIN][CONF_USERNAME] password = config[DOMAIN][CONF_PASSWORD] - from PyTado.interface import Tado - try: tado = Tado(username, password) tado.setDebugging(True) @@ -117,7 +116,7 @@ def get_capabilities(self, tado_id): return self.tado.getCapabilities(tado_id) def get_me(self): - """Wrap for getMet().""" + """Wrap for getMe().""" return self.tado.getMe() def reset_zone_overlay(self, zone_id): diff --git a/homeassistant/components/tado/climate.py b/homeassistant/components/tado/climate.py index 1108b32af4e0..2baf1f380b59 100644 --- a/homeassistant/components/tado/climate.py +++ b/homeassistant/components/tado/climate.py @@ -1,20 +1,20 @@ """Support for Tado to create a climate device for each zone.""" import logging -from typing import Optional, List +from typing import List, Optional from homeassistant.components.climate import ClimateDevice from homeassistant.components.climate.const import ( - CURRENT_HVAC_OFF, CURRENT_HVAC_COOL, CURRENT_HVAC_HEAT, CURRENT_HVAC_IDLE, + CURRENT_HVAC_OFF, FAN_HIGH, FAN_LOW, FAN_MIDDLE, FAN_OFF, HVAC_MODE_AUTO, - HVAC_MODE_HEAT, HVAC_MODE_COOL, + HVAC_MODE_HEAT, HVAC_MODE_HEAT_COOL, HVAC_MODE_OFF, PRESET_AWAY, @@ -48,22 +48,22 @@ HVAC_MAP_TADO_HEAT = { "MANUAL": HVAC_MODE_HEAT, - "TIMER": HVAC_MODE_AUTO, - "TADO_MODE": HVAC_MODE_AUTO, + "TIMER": HVAC_MODE_HEAT, + "TADO_MODE": HVAC_MODE_HEAT, "SMART_SCHEDULE": HVAC_MODE_AUTO, "OFF": HVAC_MODE_OFF, } HVAC_MAP_TADO_COOL = { "MANUAL": HVAC_MODE_COOL, - "TIMER": HVAC_MODE_AUTO, - "TADO_MODE": HVAC_MODE_AUTO, + "TIMER": HVAC_MODE_COOL, + "TADO_MODE": HVAC_MODE_COOL, "SMART_SCHEDULE": HVAC_MODE_AUTO, "OFF": HVAC_MODE_OFF, } HVAC_MAP_TADO_HEAT_COOL = { "MANUAL": HVAC_MODE_HEAT_COOL, - "TIMER": HVAC_MODE_AUTO, - "TADO_MODE": HVAC_MODE_AUTO, + "TIMER": HVAC_MODE_HEAT_COOL, + "TADO_MODE": HVAC_MODE_HEAT_COOL, "SMART_SCHEDULE": HVAC_MODE_AUTO, "OFF": HVAC_MODE_OFF, } @@ -103,6 +103,7 @@ def create_climate_device(tado, hass, zone, name, zone_id): unit = TEMP_CELSIUS ac_device = capabilities["type"] == "AIR_CONDITIONING" + hot_water_device = capabilities["type"] == "HOT_WATER" ac_support_heat = False if ac_device: @@ -134,6 +135,7 @@ def create_climate_device(tado, hass, zone, name, zone_id): hass.config.units.temperature(max_temp, unit), step, ac_device, + hot_water_device, ac_support_heat, ) @@ -157,6 +159,7 @@ def __init__( max_temp, step, ac_device, + hot_water_device, ac_support_heat, tolerance=0.3, ): @@ -168,6 +171,7 @@ def __init__( self.zone_id = zone_id self._ac_device = ac_device + self._hot_water_device = hot_water_device self._ac_support_heat = ac_support_heat self._cooling = False @@ -325,7 +329,7 @@ def set_temperature(self, **kwargs): if temperature is None: return - self._current_operation = CONST_OVERLAY_MANUAL + self._current_operation = CONST_OVERLAY_TADO_MODE self._overlay_mode = None self._target_temp = temperature self._control_heating() @@ -339,11 +343,11 @@ def set_hvac_mode(self, hvac_mode): elif hvac_mode == HVAC_MODE_AUTO: mode = CONST_MODE_SMART_SCHEDULE elif hvac_mode == HVAC_MODE_HEAT: - mode = CONST_OVERLAY_MANUAL + mode = CONST_OVERLAY_TADO_MODE elif hvac_mode == HVAC_MODE_COOL: - mode = CONST_OVERLAY_MANUAL + mode = CONST_OVERLAY_TADO_MODE elif hvac_mode == HVAC_MODE_HEAT_COOL: - mode = CONST_OVERLAY_MANUAL + mode = CONST_OVERLAY_TADO_MODE self._current_operation = mode self._overlay_mode = None @@ -493,6 +497,15 @@ def _control_heating(self): self._store.set_zone_off( self.zone_id, CONST_OVERLAY_MANUAL, "AIR_CONDITIONING" ) + elif self._hot_water_device: + _LOGGER.info( + "Switching mytado.com to OFF for zone %s (%d) - HOT_WATER", + self.zone_name, + self.zone_id, + ) + self._store.set_zone_off( + self.zone_id, CONST_OVERLAY_MANUAL, "HOT_WATER" + ) else: _LOGGER.info( "Switching mytado.com to OFF for zone %s (%d) - HEATING", @@ -519,6 +532,21 @@ def _control_heating(self): "AIR_CONDITIONING", "COOL", ) + elif self._hot_water_device: + _LOGGER.info( + "Switching mytado.com to %s mode for zone %s (%d). Temp (%s) - HOT_WATER", + self._current_operation, + self.zone_name, + self.zone_id, + self._target_temp, + ) + self._store.set_zone_overlay( + self.zone_id, + self._current_operation, + self._target_temp, + None, + "HOT_WATER", + ) else: _LOGGER.info( "Switching mytado.com to %s mode for zone %s (%d). Temp (%s) - HEATING", diff --git a/homeassistant/components/tado/device_tracker.py b/homeassistant/components/tado/device_tracker.py index 797692bf32c1..c63f5061dfa9 100644 --- a/homeassistant/components/tado/device_tracker.py +++ b/homeassistant/components/tado/device_tracker.py @@ -1,22 +1,22 @@ """Support for Tado Smart device trackers.""" -import logging -from datetime import timedelta +import asyncio from collections import namedtuple +from datetime import timedelta +import logging -import asyncio import aiohttp import async_timeout import voluptuous as vol -import homeassistant.helpers.config_validation as cv -from homeassistant.const import CONF_USERNAME, CONF_PASSWORD -from homeassistant.util import Throttle from homeassistant.components.device_tracker import ( DOMAIN, PLATFORM_SCHEMA, DeviceScanner, ) +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.helpers.aiohttp_client import async_create_clientsession +import homeassistant.helpers.config_validation as cv +from homeassistant.util import Throttle _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/tado/manifest.json b/homeassistant/components/tado/manifest.json index 9a884fa010c4..4728f1622eda 100644 --- a/homeassistant/components/tado/manifest.json +++ b/homeassistant/components/tado/manifest.json @@ -6,5 +6,7 @@ "python-tado==0.2.9" ], "dependencies": [], - "codeowners": [] + "codeowners": [ + "@michaelarnauts" + ] } diff --git a/homeassistant/components/tado/sensor.py b/homeassistant/components/tado/sensor.py index 7b4bd643f3d1..346b27bec26b 100644 --- a/homeassistant/components/tado/sensor.py +++ b/homeassistant/components/tado/sensor.py @@ -20,6 +20,7 @@ "heating", "tado mode", "overlay", + "early start", ] CLIMATE_COOL_SENSOR_TYPES = [ @@ -252,3 +253,9 @@ def update(self): else: self._state = False self._state_attributes = {} + + elif self.zone_variable == "early start": + if "preparation" in data and data["preparation"] is not None: + self._state = True + else: + self._state = False diff --git a/homeassistant/components/tahoma/__init__.py b/homeassistant/components/tahoma/__init__.py index 9fc8ca3cf2e0..02cdba5c46ac 100644 --- a/homeassistant/components/tahoma/__init__.py +++ b/homeassistant/components/tahoma/__init__.py @@ -1,12 +1,13 @@ """Support for Tahoma devices.""" from collections import defaultdict import logging -import voluptuous as vol + from requests.exceptions import RequestException +from tahoma_api import Action, TahomaApi +import voluptuous as vol -from homeassistant.const import CONF_USERNAME, CONF_PASSWORD, CONF_EXCLUDE -from homeassistant.helpers import discovery -from homeassistant.helpers import config_validation as cv +from homeassistant.const import CONF_EXCLUDE, CONF_PASSWORD, CONF_USERNAME +from homeassistant.helpers import config_validation as cv, discovery from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -63,7 +64,6 @@ def setup(hass, config): """Activate Tahoma component.""" - from tahoma_api import TahomaApi conf = config[DOMAIN] username = conf.get(CONF_USERNAME) @@ -133,7 +133,6 @@ def device_state_attributes(self): def apply_action(self, cmd_name, *args): """Apply Action to Device.""" - from tahoma_api import Action action = Action(self.tahoma_device.url) action.add_command(cmd_name, *args) diff --git a/homeassistant/components/tank_utility/sensor.py b/homeassistant/components/tank_utility/sensor.py index 78fcd47099de..3ab4a027b04f 100644 --- a/homeassistant/components/tank_utility/sensor.py +++ b/homeassistant/components/tank_utility/sensor.py @@ -4,14 +4,14 @@ import logging import requests +import tank_utility import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import CONF_DEVICES, CONF_EMAIL, CONF_PASSWORD +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity - _LOGGER = logging.getLogger(__name__) SCAN_INTERVAL = datetime.timedelta(hours=1) @@ -41,14 +41,13 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Tank Utility sensor.""" - from tank_utility import auth email = config.get(CONF_EMAIL) password = config.get(CONF_PASSWORD) devices = config.get(CONF_DEVICES) try: - token = auth.get_token(email, password) + token = tank_utility.auth.get_token(email, password) except requests.exceptions.HTTPError as http_error: if ( http_error.response.status_code @@ -109,19 +108,20 @@ def get_data(self): Flatten dictionary to map device to map of device data. """ - from tank_utility import auth, device data = {} try: - data = device.get_device_data(self._token, self.device) + data = tank_utility.device.get_device_data(self._token, self.device) except requests.exceptions.HTTPError as http_error: if ( http_error.response.status_code == requests.codes.unauthorized # pylint: disable=no-member ): _LOGGER.info("Getting new token") - self._token = auth.get_token(self._email, self._password, force=True) - data = device.get_device_data(self._token, self.device) + self._token = tank_utility.auth.get_token( + self._email, self._password, force=True + ) + data = tank_utility.device.get_device_data(self._token, self.device) else: raise http_error data.update(data.pop("device", {})) diff --git a/homeassistant/components/tapsaff/binary_sensor.py b/homeassistant/components/tapsaff/binary_sensor.py index fe6b01ced4e1..e54bc7298b0a 100644 --- a/homeassistant/components/tapsaff/binary_sensor.py +++ b/homeassistant/components/tapsaff/binary_sensor.py @@ -2,6 +2,7 @@ from datetime import timedelta import logging +from tapsaff import TapsAff import voluptuous as vol from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorDevice @@ -62,7 +63,6 @@ class TapsAffData: def __init__(self, location): """Initialize the data object.""" - from tapsaff import TapsAff self._is_taps_aff = None self.taps_aff = TapsAff(location) diff --git a/homeassistant/components/tautulli/sensor.py b/homeassistant/components/tautulli/sensor.py index 14b678389062..b800bf6af1e0 100644 --- a/homeassistant/components/tautulli/sensor.py +++ b/homeassistant/components/tautulli/sensor.py @@ -2,6 +2,7 @@ from datetime import timedelta import logging +from pytautulli import Tautulli import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -10,10 +11,10 @@ CONF_HOST, CONF_MONITORED_CONDITIONS, CONF_NAME, + CONF_PATH, CONF_PORT, CONF_SSL, CONF_VERIFY_SSL, - CONF_PATH, ) from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -50,7 +51,6 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Create the Tautulli sensor.""" - from pytautulli import Tautulli name = config.get(CONF_NAME) host = config[CONF_HOST] diff --git a/homeassistant/components/telegram_bot/__init__.py b/homeassistant/components/telegram_bot/__init__.py index 7acf4985def3..d365060e2047 100644 --- a/homeassistant/components/telegram_bot/__init__.py +++ b/homeassistant/components/telegram_bot/__init__.py @@ -731,6 +731,8 @@ def _get_message_data(self, msg_data): ATTR_USER_ID: msg_data["from"]["id"], ATTR_FROM_FIRST: msg_data["from"]["first_name"], } + if "message_id" in msg_data: + data[ATTR_MSGID] = msg_data["message_id"] if "last_name" in msg_data["from"]: data[ATTR_FROM_LAST] = msg_data["from"]["last_name"] if "chat" in msg_data: @@ -752,6 +754,9 @@ def process_message(self, data): if event_data is None: return message_ok + if ATTR_MSGID in data: + event_data[ATTR_MSGID] = data[ATTR_MSGID] + if "text" in data: if data["text"][0] == "/": pieces = data["text"].split(" ") diff --git a/homeassistant/components/telegram_bot/polling.py b/homeassistant/components/telegram_bot/polling.py index 314cb31a3730..cf3d13d5edc8 100644 --- a/homeassistant/components/telegram_bot/polling.py +++ b/homeassistant/components/telegram_bot/polling.py @@ -55,7 +55,7 @@ def __init__(self): """Initialize the messages handler instance.""" super().__init__(handler) - def check_update(self, update): # pylint: disable=no-self-use + def check_update(self, update): """Check is update valid.""" return isinstance(update, Update) diff --git a/homeassistant/components/tellduslive/.translations/es.json b/homeassistant/components/tellduslive/.translations/es.json index 677e0389d45b..0cee7ade0d76 100644 --- a/homeassistant/components/tellduslive/.translations/es.json +++ b/homeassistant/components/tellduslive/.translations/es.json @@ -11,7 +11,7 @@ }, "step": { "auth": { - "description": "Para vincular tu cuenta de TelldusLivet:\n 1. Pulsa el siguiente enlace\n 2. Inicia sesi\u00f3n en Telldus Live\n 3. Autoriza **{app_name}** (pulsa en **Yes**).\n 4. Vuelve aqu\u00ed y pulsa **ENVIAR**.\n\n [Link TelldusLive account]({auth_url})", + "description": "Para vincular tu cuenta de Telldus Live:\n 1. Pulsa el siguiente enlace\n 2. Inicia sesi\u00f3n en Telldus Live\n 3. Autoriza **{app_name}** (pulsa en **Yes**).\n 4. Vuelve atr\u00e1s y pulsa **ENVIAR**.\n\n [Link TelldusLive account]({auth_url})", "title": "Autenticaci\u00f3n contra TelldusLive" }, "user": { diff --git a/homeassistant/components/tellduslive/__init__.py b/homeassistant/components/tellduslive/__init__.py index 7234127a1523..313699e6f1c7 100644 --- a/homeassistant/components/tellduslive/__init__.py +++ b/homeassistant/components/tellduslive/__init__.py @@ -10,7 +10,7 @@ from homeassistant.const import CONF_SCAN_INTERVAL from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.event import async_call_later -from . import config_flow # noqa pylint_disable=unused-import +from . import config_flow # noqa: F401 from .const import ( CONF_HOST, DOMAIN, diff --git a/homeassistant/components/tellduslive/const.py b/homeassistant/components/tellduslive/const.py index b352f339d22b..8d9f28cc5cf9 100644 --- a/homeassistant/components/tellduslive/const.py +++ b/homeassistant/components/tellduslive/const.py @@ -1,7 +1,7 @@ """Consts used by TelldusLive.""" from datetime import timedelta -from homeassistant.const import ( # noqa pylint: disable=unused-import +from homeassistant.const import ( # noqa: F401 pylint: disable=unused-import ATTR_BATTERY_LEVEL, CONF_HOST, CONF_TOKEN, diff --git a/homeassistant/components/temper/sensor.py b/homeassistant/components/temper/sensor.py index a32de3da10fb..8b782ae4d797 100644 --- a/homeassistant/components/temper/sensor.py +++ b/homeassistant/components/temper/sensor.py @@ -1,5 +1,7 @@ """Support for getting temperature from TEMPer devices.""" import logging + +from temperusb.temper import TemperHandler import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -24,7 +26,6 @@ def get_temper_devices(): """Scan the Temper devices from temperusb.""" - from temperusb.temper import TemperHandler return TemperHandler().get_devices() diff --git a/homeassistant/components/template/__init__.py b/homeassistant/components/template/__init__.py index 0c205a0196c5..80421b8e3f88 100644 --- a/homeassistant/components/template/__init__.py +++ b/homeassistant/components/template/__init__.py @@ -1 +1,60 @@ """The template component.""" + +import logging + +from itertools import chain +from homeassistant.const import MATCH_ALL + + +_LOGGER = logging.getLogger(__name__) + + +def initialise_templates(hass, templates, attribute_templates=None): + """Initialise templates and attribute templates.""" + if attribute_templates is None: + attribute_templates = dict() + for template in chain(templates.values(), attribute_templates.values()): + if template is None: + continue + template.hass = hass + + +def extract_entities( + device_name, device_type, manual_entity_ids, templates, attribute_templates=None +): + """Extract entity ids from templates and attribute templates.""" + if attribute_templates is None: + attribute_templates = dict() + entity_ids = set() + if manual_entity_ids is None: + invalid_templates = [] + for template_name, template in chain( + templates.items(), attribute_templates.items() + ): + if template is None: + continue + + template_entity_ids = template.extract_entities() + + if template_entity_ids != MATCH_ALL: + entity_ids |= set(template_entity_ids) + else: + invalid_templates.append(template_name.replace("_template", "")) + + if invalid_templates: + entity_ids = MATCH_ALL + _LOGGER.warning( + "Template %s '%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_type, + device_name, + ", ".join(invalid_templates), + ) + else: + entity_ids = list(entity_ids) + else: + entity_ids = manual_entity_ids + + return entity_ids diff --git a/homeassistant/components/template/binary_sensor.py b/homeassistant/components/template/binary_sensor.py index d5ade703c974..116862abc79d 100644 --- a/homeassistant/components/template/binary_sensor.py +++ b/homeassistant/components/template/binary_sensor.py @@ -1,6 +1,5 @@ """Support for exposing a templated binary sensor.""" import logging -from itertools import chain import voluptuous as vol @@ -26,6 +25,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, async_track_same_state +from . import extract_entities, initialise_templates from .const import CONF_AVAILABILITY_TEMPLATE _LOGGER = logging.getLogger(__name__) @@ -63,11 +63,12 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= 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, {}) - invalid_templates = [] + friendly_name = device_config.get(ATTR_FRIENDLY_NAME, device) + device_class = device_config.get(CONF_DEVICE_CLASS) + delay_on = device_config.get(CONF_DELAY_ON) + delay_off = device_config.get(CONF_DELAY_OFF) templates = { CONF_VALUE_TEMPLATE: value_template, @@ -76,41 +77,14 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= CONF_AVAILABILITY_TEMPLATE: availability_template, } - for tpl_name, template in chain(templates.items(), attribute_templates.items()): - if template is None: - continue - template.hass = hass - - if manual_entity_ids is not None: - continue - - template_entity_ids = template.extract_entities() - if template_entity_ids == MATCH_ALL: - entity_ids = MATCH_ALL - # Cut off _template from name - invalid_templates.append(tpl_name.replace("_template", "")) - elif entity_ids != MATCH_ALL: - entity_ids |= set(template_entity_ids) - - if manual_entity_ids is not None: - entity_ids = manual_entity_ids - elif entity_ids != MATCH_ALL: - entity_ids = list(entity_ids) - - if invalid_templates: - _LOGGER.warning( - "Template binary 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), - ) - - friendly_name = device_config.get(ATTR_FRIENDLY_NAME, device) - device_class = device_config.get(CONF_DEVICE_CLASS) - delay_on = device_config.get(CONF_DELAY_ON) - delay_off = device_config.get(CONF_DELAY_OFF) + initialise_templates(hass, templates, attribute_templates) + entity_ids = extract_entities( + device, + "binary sensor", + device_config.get(ATTR_ENTITY_ID), + templates, + attribute_templates, + ) sensors.append( BinarySensorTemplate( diff --git a/homeassistant/components/template/cover.py b/homeassistant/components/template/cover.py index 483ee1ae8723..22035af24ec4 100644 --- a/homeassistant/components/template/cover.py +++ b/homeassistant/components/template/cover.py @@ -24,7 +24,6 @@ CONF_FRIENDLY_NAME, CONF_ENTITY_ID, EVENT_HOMEASSISTANT_START, - MATCH_ALL, CONF_VALUE_TEMPLATE, CONF_ICON_TEMPLATE, CONF_DEVICE_CLASS, @@ -38,6 +37,7 @@ 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 . import extract_entities, initialise_templates from .const import CONF_AVAILABILITY_TEMPLATE _LOGGER = logging.getLogger(__name__) @@ -100,13 +100,14 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= covers = [] for device, device_config in config[CONF_COVERS].items(): - friendly_name = device_config.get(CONF_FRIENDLY_NAME, device) state_template = device_config.get(CONF_VALUE_TEMPLATE) 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) + + friendly_name = device_config.get(CONF_FRIENDLY_NAME, device) device_class = device_config.get(CONF_DEVICE_CLASS) open_action = device_config.get(OPEN_ACTION) close_action = device_config.get(CLOSE_ACTION) @@ -121,41 +122,18 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= "Must specify at least one of %s" or "%s", OPEN_ACTION, POSITION_ACTION ) continue - template_entity_ids = set() - if state_template is not None: - temp_ids = state_template.extract_entities() - if str(temp_ids) != MATCH_ALL: - template_entity_ids |= set(temp_ids) - - if position_template is not None: - temp_ids = position_template.extract_entities() - if str(temp_ids) != MATCH_ALL: - template_entity_ids |= set(temp_ids) - - if tilt_template is not None: - temp_ids = tilt_template.extract_entities() - if str(temp_ids) != MATCH_ALL: - template_entity_ids |= set(temp_ids) - - if icon_template is not None: - temp_ids = icon_template.extract_entities() - if str(temp_ids) != MATCH_ALL: - template_entity_ids |= set(temp_ids) - - if entity_picture_template is not None: - temp_ids = entity_picture_template.extract_entities() - 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 - - entity_ids = device_config.get(CONF_ENTITY_ID, template_entity_ids) + + templates = { + CONF_VALUE_TEMPLATE: state_template, + CONF_POSITION_TEMPLATE: position_template, + CONF_TILT_TEMPLATE: tilt_template, + CONF_ICON_TEMPLATE: icon_template, + CONF_AVAILABILITY_TEMPLATE: availability_template, + CONF_ENTITY_PICTURE_TEMPLATE: entity_picture_template, + } + + initialise_templates(hass, templates) + entity_ids = extract_entities(device, "cover", None, templates) covers.append( CoverTemplate( diff --git a/homeassistant/components/template/fan.py b/homeassistant/components/template/fan.py index 606f18e5fe19..ebb9bcc8b142 100644 --- a/homeassistant/components/template/fan.py +++ b/homeassistant/components/template/fan.py @@ -25,7 +25,6 @@ CONF_ENTITY_ID, STATE_ON, STATE_OFF, - MATCH_ALL, EVENT_HOMEASSISTANT_START, STATE_UNKNOWN, ) @@ -33,6 +32,7 @@ from homeassistant.exceptions import TemplateError from homeassistant.helpers.entity import async_generate_entity_id from homeassistant.helpers.script import Script +from . import extract_entities, initialise_templates from .const import CONF_AVAILABILITY_TEMPLATE _LOGGER = logging.getLogger(__name__) @@ -98,33 +98,16 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= speed_list = device_config[CONF_SPEED_LIST] - entity_ids = set() - manual_entity_ids = device_config.get(CONF_ENTITY_ID) - - for template in ( - state_template, - speed_template, - oscillating_template, - direction_template, - availability_template, - ): - if template is None: - continue - template.hass = hass - - if entity_ids == MATCH_ALL or manual_entity_ids is not None: - continue - - template_entity_ids = template.extract_entities() - if template_entity_ids == MATCH_ALL: - entity_ids = MATCH_ALL - else: - entity_ids |= set(template_entity_ids) + templates = { + CONF_VALUE_TEMPLATE: state_template, + CONF_SPEED_TEMPLATE: speed_template, + CONF_OSCILLATING_TEMPLATE: oscillating_template, + CONF_DIRECTION_TEMPLATE: direction_template, + CONF_AVAILABILITY_TEMPLATE: availability_template, + } - if manual_entity_ids is not None: - entity_ids = manual_entity_ids - elif entity_ids != MATCH_ALL: - entity_ids = list(entity_ids) + initialise_templates(hass, templates) + entity_ids = extract_entities(device, "fan", None, templates) fans.append( TemplateFan( diff --git a/homeassistant/components/template/light.py b/homeassistant/components/template/light.py index 552c21f170db..b71aadd0155b 100644 --- a/homeassistant/components/template/light.py +++ b/homeassistant/components/template/light.py @@ -19,7 +19,6 @@ STATE_ON, STATE_OFF, EVENT_HOMEASSISTANT_START, - MATCH_ALL, CONF_LIGHTS, ) from homeassistant.helpers.config_validation import PLATFORM_SCHEMA @@ -28,6 +27,7 @@ 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 . import extract_entities, initialise_templates from .const import CONF_AVAILABILITY_TEMPLATE _LOGGER = logging.getLogger(__name__) @@ -64,46 +64,27 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= for device, device_config in config[CONF_LIGHTS].items(): friendly_name = device_config.get(CONF_FRIENDLY_NAME, device) + 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) + level_template = device_config.get(CONF_LEVEL_TEMPLATE) + on_action = device_config[CONF_ON_ACTION] off_action = device_config[CONF_OFF_ACTION] level_action = device_config.get(CONF_LEVEL_ACTION) - level_template = device_config.get(CONF_LEVEL_TEMPLATE) - - template_entity_ids = set() - - if state_template is not None: - temp_ids = state_template.extract_entities() - if str(temp_ids) != MATCH_ALL: - template_entity_ids |= set(temp_ids) - - if level_template is not None: - temp_ids = level_template.extract_entities() - if str(temp_ids) != MATCH_ALL: - template_entity_ids |= set(temp_ids) - - if icon_template is not None: - temp_ids = icon_template.extract_entities() - if str(temp_ids) != MATCH_ALL: - template_entity_ids |= set(temp_ids) - - if entity_picture_template is not None: - temp_ids = entity_picture_template.extract_entities() - 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 + templates = { + CONF_VALUE_TEMPLATE: state_template, + CONF_ICON_TEMPLATE: icon_template, + CONF_ENTITY_PICTURE_TEMPLATE: entity_picture_template, + CONF_AVAILABILITY_TEMPLATE: availability_template, + CONF_LEVEL_TEMPLATE: level_template, + } - entity_ids = device_config.get(CONF_ENTITY_ID, template_entity_ids) + initialise_templates(hass, templates) + entity_ids = extract_entities(device, "light", None, templates) lights.append( LightTemplate( diff --git a/homeassistant/components/template/lock.py b/homeassistant/components/template/lock.py index aa8cc8b1224e..71e9cc6642d5 100644 --- a/homeassistant/components/template/lock.py +++ b/homeassistant/components/template/lock.py @@ -19,6 +19,7 @@ from homeassistant.exceptions import TemplateError from homeassistant.helpers.event import async_track_state_change from homeassistant.helpers.script import Script +from . import extract_entities, initialise_templates from .const import CONF_AVAILABILITY_TEMPLATE _LOGGER = logging.getLogger(__name__) @@ -43,39 +44,26 @@ async def async_setup_platform(hass, config, async_add_devices, discovery_info=None): """Set up the Template lock.""" - name = config.get(CONF_NAME) + device = config.get(CONF_NAME) value_template = config.get(CONF_VALUE_TEMPLATE) - value_template.hass = hass - value_template_entity_ids = value_template.extract_entities() - - 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. This entity will only be able to be updated " - "manually.", - name, - CONF_VALUE_TEMPLATE, - ) + availability_template = config.get(CONF_AVAILABILITY_TEMPLATE) - template_entity_ids = set() - template_entity_ids |= set(value_template_entity_ids) + templates = { + CONF_VALUE_TEMPLATE: value_template, + CONF_AVAILABILITY_TEMPLATE: availability_template, + } - 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) + initialise_templates(hass, templates) + entity_ids = extract_entities(device, "lock", None, templates) async_add_devices( [ TemplateLock( hass, - name, + device, value_template, availability_template, - template_entity_ids, + entity_ids, config.get(CONF_LOCK), config.get(CONF_UNLOCK), config.get(CONF_OPTIMISTIC), diff --git a/homeassistant/components/template/sensor.py b/homeassistant/components/template/sensor.py index a87681937365..4ea7daa54f6b 100644 --- a/homeassistant/components/template/sensor.py +++ b/homeassistant/components/template/sensor.py @@ -1,7 +1,6 @@ """Allows the creation of a sensor that breaks out state_attributes.""" import logging from typing import Optional -from itertools import chain import voluptuous as vol @@ -29,6 +28,7 @@ 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 . import extract_entities, initialise_templates from .const import CONF_AVAILABILITY_TEMPLATE CONF_ATTRIBUTE_TEMPLATES = "attribute_templates" @@ -72,10 +72,6 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= device_class = device_config.get(CONF_DEVICE_CLASS) attribute_templates = device_config[CONF_ATTRIBUTE_TEMPLATES] - entity_ids = set() - manual_entity_ids = device_config.get(ATTR_ENTITY_ID) - invalid_templates = [] - templates = { CONF_VALUE_TEMPLATE: state_template, CONF_ICON_TEMPLATE: icon_template, @@ -84,36 +80,14 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= CONF_AVAILABILITY_TEMPLATE: availability_template, } - for tpl_name, template in chain(templates.items(), attribute_templates.items()): - if template is None: - continue - template.hass = hass - - if manual_entity_ids is not None: - continue - - template_entity_ids = template.extract_entities() - if template_entity_ids == MATCH_ALL: - entity_ids = MATCH_ALL - # Cut off _template from name - invalid_templates.append(tpl_name.replace("_template", "")) - 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), - ) - - if manual_entity_ids is not None: - entity_ids = manual_entity_ids - elif entity_ids != MATCH_ALL: - entity_ids = list(entity_ids) + initialise_templates(hass, templates, attribute_templates) + entity_ids = extract_entities( + device, + "sensor", + device_config.get(ATTR_ENTITY_ID), + templates, + attribute_templates, + ) sensors.append( SensorTemplate( @@ -131,7 +105,9 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= attribute_templates, ) ) + async_add_entities(sensors) + return True diff --git a/homeassistant/components/template/switch.py b/homeassistant/components/template/switch.py index 2d4dda032ca8..e06ca0c8d546 100644 --- a/homeassistant/components/template/switch.py +++ b/homeassistant/components/template/switch.py @@ -19,13 +19,13 @@ 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 . import extract_entities, initialise_templates from .const import CONF_AVAILABILITY_TEMPLATE _LOGGER = logging.getLogger(__name__) @@ -64,8 +64,6 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= availability_template = device_config.get(CONF_AVAILABILITY_TEMPLATE) on_action = device_config[ON_ACTION] off_action = device_config[OFF_ACTION] - manual_entity_ids = device_config.get(ATTR_ENTITY_ID) - entity_ids = set() templates = { CONF_VALUE_TEMPLATE: state_template, @@ -73,35 +71,11 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= CONF_ENTITY_PICTURE_TEMPLATE: entity_picture_template, CONF_AVAILABILITY_TEMPLATE: availability_template, } - invalid_templates = [] - - for template_name, template in templates.items(): - if template is not None: - 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 + + initialise_templates(hass, templates) + entity_ids = extract_entities( + device, "switch", device_config.get(ATTR_ENTITY_ID), templates + ) switches.append( SwitchTemplate( @@ -117,6 +91,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= entity_ids, ) ) + if not switches: _LOGGER.error("No switches added") return False diff --git a/homeassistant/components/template/vacuum.py b/homeassistant/components/template/vacuum.py index 6a6523514c48..8201842e1312 100644 --- a/homeassistant/components/template/vacuum.py +++ b/homeassistant/components/template/vacuum.py @@ -43,7 +43,7 @@ from homeassistant.exceptions import TemplateError from homeassistant.helpers.entity import async_generate_entity_id from homeassistant.helpers.script import Script - +from . import extract_entities, initialise_templates from .const import CONF_AVAILABILITY_TEMPLATE _LOGGER = logging.getLogger(__name__) @@ -109,45 +109,15 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= fan_speed_list = device_config[CONF_FAN_SPEED_LIST] - entity_ids = set() - manual_entity_ids = device_config.get(CONF_ENTITY_ID) - invalid_templates = [] - - for tpl_name, template in ( - (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 - template.hass = hass - - if manual_entity_ids is not None: - continue - - template_entity_ids = template.extract_entities() - if template_entity_ids == MATCH_ALL: - entity_ids = MATCH_ALL - # Cut off _template from name - invalid_templates.append(tpl_name[:-9]) - elif entity_ids != MATCH_ALL: - entity_ids |= set(template_entity_ids) - - if invalid_templates: - _LOGGER.warning( - "Template vacuum %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), - ) + templates = { + 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 manual_entity_ids is not None: - entity_ids = manual_entity_ids - elif entity_ids != MATCH_ALL: - entity_ids = list(entity_ids) + initialise_templates(hass, templates) + entity_ids = extract_entities(device, "vacuum", None, templates) vacuums.append( TemplateVacuum( diff --git a/homeassistant/components/tensorflow/image_processing.py b/homeassistant/components/tensorflow/image_processing.py index ea73d52fe4a6..5f576f176e82 100644 --- a/homeassistant/components/tensorflow/image_processing.py +++ b/homeassistant/components/tensorflow/image_processing.py @@ -89,25 +89,22 @@ def setup_platform(hass, config, add_entities, discovery_info=None): try: # Verify that the TensorFlow Object Detection API is pre-installed - # pylint: disable=unused-import,unused-variable os.environ["TF_CPP_MIN_LOG_LEVEL"] = "2" # These imports shouldn't be moved to the top, because they depend on code from the model_dir. # (The model_dir is created during the manual setup process. See integration docs.) - import tensorflow as tf # noqa - from object_detection.utils import label_map_util # noqa + import tensorflow as tf + from object_detection.utils import label_map_util except ImportError: - # pylint: disable=line-too-long _LOGGER.error( "No TensorFlow Object Detection library found! Install or compile " "for your system following instructions here: " "https://github.com/tensorflow/models/blob/master/research/object_detection/g3doc/installation.md" - ) # noqa + ) return try: # Display warning that PIL will be used if no OpenCV is found. - # pylint: disable=unused-import,unused-variable - import cv2 # noqa + import cv2 # noqa: F401 pylint: disable=unused-import except ImportError: _LOGGER.warning( "No OpenCV library found. TensorFlow will process image with " diff --git a/homeassistant/components/tensorflow/manifest.json b/homeassistant/components/tensorflow/manifest.json index e0a8728b2957..278a92a481f9 100644 --- a/homeassistant/components/tensorflow/manifest.json +++ b/homeassistant/components/tensorflow/manifest.json @@ -2,11 +2,7 @@ "domain": "tensorflow", "name": "Tensorflow", "documentation": "https://www.home-assistant.io/integrations/tensorflow", - "requirements": [ - "tensorflow==1.13.2", - "numpy==1.17.3", - "protobuf==3.6.1" - ], + "requirements": ["tensorflow==1.13.2", "numpy==1.17.4", "protobuf==3.6.1"], "dependencies": [], "codeowners": [] -} \ No newline at end of file +} diff --git a/homeassistant/components/tesla/__init__.py b/homeassistant/components/tesla/__init__.py index a08112d66b31..a3d45eed01c6 100644 --- a/homeassistant/components/tesla/__init__.py +++ b/homeassistant/components/tesla/__init__.py @@ -2,9 +2,8 @@ from collections import defaultdict import logging -import voluptuous as vol from teslajsonpy import Controller as teslaAPI, TeslaException - +import voluptuous as vol from homeassistant.const import ( ATTR_BATTERY_LEVEL, @@ -12,18 +11,14 @@ CONF_SCAN_INTERVAL, CONF_USERNAME, ) -from homeassistant.helpers import config_validation as cv -from homeassistant.helpers import discovery +from homeassistant.helpers import aiohttp_client, config_validation as cv, discovery from homeassistant.helpers.entity import Entity from homeassistant.util import slugify -DOMAIN = "tesla" +from .const import DOMAIN, TESLA_COMPONENTS _LOGGER = logging.getLogger(__name__) -TESLA_ID_FORMAT = "{}_{}" -TESLA_ID_LIST_SCHEMA = vol.Schema([int]) - CONFIG_SCHEMA = vol.Schema( { DOMAIN: vol.Schema( @@ -42,17 +37,8 @@ NOTIFICATION_ID = "tesla_integration_notification" NOTIFICATION_TITLE = "Tesla integration setup" -TESLA_COMPONENTS = [ - "sensor", - "lock", - "climate", - "binary_sensor", - "device_tracker", - "switch", -] - -def setup(hass, base_config): +async def async_setup(hass, base_config): """Set up of Tesla component.""" config = base_config.get(DOMAIN) @@ -61,10 +47,15 @@ def setup(hass, base_config): update_interval = config.get(CONF_SCAN_INTERVAL) if hass.data.get(DOMAIN) is None: try: - hass.data[DOMAIN] = { - "controller": teslaAPI(email, password, update_interval), - "devices": defaultdict(list), - } + websession = aiohttp_client.async_get_clientsession(hass) + controller = teslaAPI( + websession, + email=email, + password=password, + update_interval=update_interval, + ) + await controller.connect(test_login=False) + hass.data[DOMAIN] = {"controller": controller, "devices": defaultdict(list)} _LOGGER.debug("Connected to the Tesla API.") except TeslaException as ex: if ex.code == 401: @@ -85,9 +76,7 @@ def setup(hass, base_config): ) _LOGGER.error("Unable to communicate with Tesla API: %s", ex.message) return False - - all_devices = hass.data[DOMAIN]["controller"].list_vehicles() - + all_devices = controller.get_homeassistant_components() if not all_devices: return False @@ -95,8 +84,9 @@ def setup(hass, base_config): hass.data[DOMAIN]["devices"][device.hass_type].append(device) for component in TESLA_COMPONENTS: - discovery.load_platform(hass, component, DOMAIN, {}, base_config) - + hass.async_create_task( + discovery.async_load_platform(hass, component, DOMAIN, {}, base_config) + ) return True @@ -104,11 +94,12 @@ class TeslaDevice(Entity): """Representation of a Tesla device.""" def __init__(self, tesla_device, controller): - """Initialise of the Tesla device.""" + """Initialise the Tesla device.""" self.tesla_device = tesla_device self.controller = controller self._name = self.tesla_device.name self.tesla_id = slugify(self.tesla_device.uniq_name) + self._attributes = {} @property def name(self): @@ -128,8 +119,19 @@ def should_poll(self): @property def device_state_attributes(self): """Return the state attributes of the device.""" - attr = {} - + attr = self._attributes if self.tesla_device.has_battery(): attr[ATTR_BATTERY_LEVEL] = self.tesla_device.battery_level() return attr + + async def async_added_to_hass(self): + """Register state update callback.""" + pass + + async def async_will_remove_from_hass(self): + """Prepare for unload.""" + pass + + async def async_update(self): + """Update the state of the device.""" + await self.tesla_device.async_update() diff --git a/homeassistant/components/tesla/binary_sensor.py b/homeassistant/components/tesla/binary_sensor.py index 2a452dcc8324..738533a9b568 100644 --- a/homeassistant/components/tesla/binary_sensor.py +++ b/homeassistant/components/tesla/binary_sensor.py @@ -8,7 +8,7 @@ _LOGGER = logging.getLogger(__name__) -def setup_platform(hass, config, add_entities, discovery_info=None): +async def async_setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Tesla binary sensor.""" devices = [ TeslaBinarySensor(device, hass.data[TESLA_DOMAIN]["controller"], "connectivity") @@ -41,8 +41,8 @@ def is_on(self): """Return the state of the binary sensor.""" return self._state - def update(self): + async def async_update(self): """Update the state of the device.""" _LOGGER.debug("Updating sensor: %s", self._name) - self.tesla_device.update() + await super().async_update() self._state = self.tesla_device.get_value() diff --git a/homeassistant/components/tesla/climate.py b/homeassistant/components/tesla/climate.py index 45858dcf985f..85fd8a8e258d 100644 --- a/homeassistant/components/tesla/climate.py +++ b/homeassistant/components/tesla/climate.py @@ -16,7 +16,7 @@ SUPPORT_HVAC = [HVAC_MODE_HEAT, HVAC_MODE_OFF] -def setup_platform(hass, config, add_entities, discovery_info=None): +async def async_setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Tesla climate platform.""" devices = [ TeslaThermostat(device, hass.data[TESLA_DOMAIN]["controller"]) @@ -57,10 +57,10 @@ def hvac_modes(self): """ return SUPPORT_HVAC - def update(self): + async def async_update(self): """Call by the Tesla device callback to update state.""" _LOGGER.debug("Updating: %s", self._name) - self.tesla_device.update() + await super().async_update() self._target_temperature = self.tesla_device.get_goal_temp() self._temperature = self.tesla_device.get_current_temp() @@ -83,17 +83,17 @@ def target_temperature(self): """Return the temperature we try to reach.""" return self._target_temperature - def set_temperature(self, **kwargs): + async def async_set_temperature(self, **kwargs): """Set new target temperatures.""" _LOGGER.debug("Setting temperature for: %s", self._name) temperature = kwargs.get(ATTR_TEMPERATURE) if temperature: - self.tesla_device.set_temperature(temperature) + await self.tesla_device.set_temperature(temperature) - def set_hvac_mode(self, hvac_mode): + async def async_set_hvac_mode(self, hvac_mode): """Set new target hvac mode.""" _LOGGER.debug("Setting mode for: %s", self._name) if hvac_mode == HVAC_MODE_OFF: - self.tesla_device.set_status(False) + await self.tesla_device.set_status(False) elif hvac_mode == HVAC_MODE_HEAT: - self.tesla_device.set_status(True) + await self.tesla_device.set_status(True) diff --git a/homeassistant/components/tesla/const.py b/homeassistant/components/tesla/const.py new file mode 100644 index 000000000000..30a58b733edc --- /dev/null +++ b/homeassistant/components/tesla/const.py @@ -0,0 +1,24 @@ +"""Const file for Tesla cars.""" +DOMAIN = "tesla" +DATA_LISTENER = "listener" +TESLA_COMPONENTS = [ + "sensor", + "lock", + "climate", + "binary_sensor", + "device_tracker", + "switch", +] +SENSOR_ICONS = { + "battery sensor": "mdi:battery", + "range sensor": "mdi:gauge", + "mileage sensor": "mdi:counter", + "parking brake sensor": "mdi:car-brake-parking", + "charger sensor": "mdi:ev-station", + "charger switch": "mdi:battery-charging", + "update switch": "mdi:update", + "maxrange switch": "mdi:gauge-full", + "temperature sensor": "mdi:thermometer", + "location tracker": "mdi:crosshairs-gps", + "charging rate sensor": "mdi:speedometer", +} diff --git a/homeassistant/components/tesla/device_tracker.py b/homeassistant/components/tesla/device_tracker.py index 9db7bb3eb489..c205cc587eba 100644 --- a/homeassistant/components/tesla/device_tracker.py +++ b/homeassistant/components/tesla/device_tracker.py @@ -1,7 +1,7 @@ """Support for tracking Tesla cars.""" import logging -from homeassistant.helpers.event import track_utc_time_change +from homeassistant.helpers.event import async_track_utc_time_change from homeassistant.util import slugify from . import DOMAIN as TESLA_DOMAIN @@ -9,11 +9,13 @@ _LOGGER = logging.getLogger(__name__) -def setup_scanner(hass, config, see, discovery_info=None): +async def async_setup_scanner(hass, config, async_see, discovery_info=None): """Set up the Tesla tracker.""" - TeslaDeviceTracker( - hass, config, see, hass.data[TESLA_DOMAIN]["devices"]["devices_tracker"] + tracker = TeslaDeviceTracker( + hass, config, async_see, hass.data[TESLA_DOMAIN]["devices"]["devices_tracker"] ) + await tracker.update_info() + async_track_utc_time_change(hass, tracker.update_info, second=range(0, 60, 30)) return True @@ -25,14 +27,11 @@ def __init__(self, hass, config, see, tesla_devices): self.hass = hass self.see = see self.devices = tesla_devices - self._update_info() - track_utc_time_change(self.hass, self._update_info, second=range(0, 60, 30)) - - def _update_info(self, now=None): + async def update_info(self, now=None): """Update the device info.""" for device in self.devices: - device.update() + await device.async_update() name = device.name _LOGGER.debug("Updating device position: %s", name) dev_id = slugify(device.uniq_name) @@ -41,6 +40,6 @@ def _update_info(self, now=None): lat = location["latitude"] lon = location["longitude"] attrs = {"trackr_id": dev_id, "id": dev_id, "name": name} - self.see( + await self.see( dev_id=dev_id, host_name=name, gps=(lat, lon), attributes=attrs ) diff --git a/homeassistant/components/tesla/lock.py b/homeassistant/components/tesla/lock.py index 389d6ee76e3c..5e97602357df 100644 --- a/homeassistant/components/tesla/lock.py +++ b/homeassistant/components/tesla/lock.py @@ -9,7 +9,7 @@ _LOGGER = logging.getLogger(__name__) -def setup_platform(hass, config, add_entities, discovery_info=None): +async def async_setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Tesla lock platform.""" devices = [ TeslaLock(device, hass.data[TESLA_DOMAIN]["controller"]) @@ -26,23 +26,23 @@ def __init__(self, tesla_device, controller): self._state = None super().__init__(tesla_device, controller) - def lock(self, **kwargs): + async def async_lock(self, **kwargs): """Send the lock command.""" _LOGGER.debug("Locking doors for: %s", self._name) - self.tesla_device.lock() + await self.tesla_device.lock() - def unlock(self, **kwargs): + async def async_unlock(self, **kwargs): """Send the unlock command.""" _LOGGER.debug("Unlocking doors for: %s", self._name) - self.tesla_device.unlock() + await self.tesla_device.unlock() @property def is_locked(self): """Get whether the lock is in locked state.""" return self._state == STATE_LOCKED - def update(self): + async def async_update(self): """Update state of the lock.""" _LOGGER.debug("Updating state for: %s", self._name) - self.tesla_device.update() + await super().async_update() self._state = STATE_LOCKED if self.tesla_device.is_locked() else STATE_UNLOCKED diff --git a/homeassistant/components/tesla/manifest.json b/homeassistant/components/tesla/manifest.json index 87d76c16f05f..a20210924130 100644 --- a/homeassistant/components/tesla/manifest.json +++ b/homeassistant/components/tesla/manifest.json @@ -2,7 +2,7 @@ "domain": "tesla", "name": "Tesla", "documentation": "https://www.home-assistant.io/integrations/tesla", - "requirements": ["teslajsonpy==0.0.26"], + "requirements": ["teslajsonpy==0.2.0"], "dependencies": [], "codeowners": ["@zabuldon"] } diff --git a/homeassistant/components/tesla/sensor.py b/homeassistant/components/tesla/sensor.py index c737b2f0bba5..1cce37f232a5 100644 --- a/homeassistant/components/tesla/sensor.py +++ b/homeassistant/components/tesla/sensor.py @@ -1,5 +1,4 @@ """Support for the Tesla sensors.""" -from datetime import timedelta import logging from homeassistant.const import ( @@ -14,10 +13,8 @@ _LOGGER = logging.getLogger(__name__) -SCAN_INTERVAL = timedelta(minutes=5) - -def setup_platform(hass, config, add_entities, discovery_info=None): +async def async_setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Tesla sensor platform.""" controller = hass.data[TESLA_DOMAIN]["devices"]["controller"] devices = [] @@ -26,7 +23,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): if device.bin_type == 0x4: devices.append(TeslaSensor(device, controller, "inside")) devices.append(TeslaSensor(device, controller, "outside")) - else: + elif device.bin_type in [0xA, 0xB, 0x5]: devices.append(TeslaSensor(device, controller)) add_entities(devices, True) @@ -62,10 +59,10 @@ def unit_of_measurement(self): """Return the unit_of_measurement of the device.""" return self._unit - def update(self): + async def async_update(self): """Update the state from the sensor.""" _LOGGER.debug("Updating sensor: %s", self._name) - self.tesla_device.update() + await super().async_update() units = self.tesla_device.measurement if self.tesla_device.bin_type == 0x4: diff --git a/homeassistant/components/tesla/switch.py b/homeassistant/components/tesla/switch.py index 985194f87b2a..5f432875aebc 100644 --- a/homeassistant/components/tesla/switch.py +++ b/homeassistant/components/tesla/switch.py @@ -9,7 +9,7 @@ _LOGGER = logging.getLogger(__name__) -def setup_platform(hass, config, add_entities, discovery_info=None): +async def async_setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Tesla switch platform.""" controller = hass.data[TESLA_DOMAIN]["controller"] devices = [] @@ -30,25 +30,25 @@ def __init__(self, tesla_device, controller): self._state = None super().__init__(tesla_device, controller) - def turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs): """Send the on command.""" _LOGGER.debug("Enable charging: %s", self._name) - self.tesla_device.start_charge() + await self.tesla_device.start_charge() - def turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs): """Send the off command.""" _LOGGER.debug("Disable charging for: %s", self._name) - self.tesla_device.stop_charge() + await self.tesla_device.stop_charge() @property def is_on(self): """Get whether the switch is in on state.""" return self._state == STATE_ON - def update(self): + async def async_update(self): """Update the state of the switch.""" _LOGGER.debug("Updating state for: %s", self._name) - self.tesla_device.update() + await super().async_update() self._state = STATE_ON if self.tesla_device.is_charging() else STATE_OFF @@ -56,29 +56,29 @@ class RangeSwitch(TeslaDevice, SwitchDevice): """Representation of a Tesla max range charging switch.""" def __init__(self, tesla_device, controller): - """Initialise of the switch.""" + """Initialise the switch.""" self._state = None super().__init__(tesla_device, controller) - def turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs): """Send the on command.""" _LOGGER.debug("Enable max range charging: %s", self._name) - self.tesla_device.set_max() + await self.tesla_device.set_max() - def turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs): """Send the off command.""" _LOGGER.debug("Disable max range charging: %s", self._name) - self.tesla_device.set_standard() + await self.tesla_device.set_standard() @property def is_on(self): """Get whether the switch is in on state.""" return self._state - def update(self): + async def async_update(self): """Update the state of the switch.""" _LOGGER.debug("Updating state for: %s", self._name) - self.tesla_device.update() + await super().async_update() self._state = bool(self.tesla_device.is_maxrange()) @@ -86,18 +86,19 @@ class UpdateSwitch(TeslaDevice, SwitchDevice): """Representation of a Tesla update switch.""" def __init__(self, tesla_device, controller): - """Initialise of the switch.""" + """Initialise the switch.""" self._state = None + tesla_device.type = "update switch" super().__init__(tesla_device, controller) self._name = self._name.replace("charger", "update") self.tesla_id = self.tesla_id.replace("charger", "update") - def turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs): """Send the on command.""" _LOGGER.debug("Enable updates: %s %s", self._name, self.tesla_device.id()) self.controller.set_updates(self.tesla_device.id(), True) - def turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs): """Send the off command.""" _LOGGER.debug("Disable updates: %s %s", self._name, self.tesla_device.id()) self.controller.set_updates(self.tesla_device.id(), False) @@ -107,8 +108,9 @@ def is_on(self): """Get whether the switch is in on state.""" return self._state - def update(self): + async def async_update(self): """Update the state of the switch.""" car_id = self.tesla_device.id() _LOGGER.debug("Updating state for: %s %s", self._name, car_id) + await super().async_update() self._state = bool(self.controller.get_updates(car_id)) diff --git a/homeassistant/components/thinkingcleaner/sensor.py b/homeassistant/components/thinkingcleaner/sensor.py index 2c2194f6ace8..7a45be7eb61d 100644 --- a/homeassistant/components/thinkingcleaner/sensor.py +++ b/homeassistant/components/thinkingcleaner/sensor.py @@ -1,6 +1,8 @@ """Support for ThinkingCleaner sensors.""" -import logging from datetime import timedelta +import logging + +from pythinkingcleaner import Discovery from homeassistant import util from homeassistant.helpers.entity import Entity @@ -46,7 +48,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the ThinkingCleaner platform.""" - from pythinkingcleaner import Discovery discovery = Discovery() devices = discovery.discover() diff --git a/homeassistant/components/thinkingcleaner/switch.py b/homeassistant/components/thinkingcleaner/switch.py index aa57077734a0..88d87e4e5fe2 100644 --- a/homeassistant/components/thinkingcleaner/switch.py +++ b/homeassistant/components/thinkingcleaner/switch.py @@ -1,10 +1,12 @@ """Support for ThinkingCleaner switches.""" -import time -import logging from datetime import timedelta +import logging +import time + +from pythinkingcleaner import Discovery from homeassistant import util -from homeassistant.const import STATE_ON, STATE_OFF +from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.helpers.entity import ToggleEntity _LOGGER = logging.getLogger(__name__) @@ -24,7 +26,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the ThinkingCleaner platform.""" - from pythinkingcleaner import Discovery discovery = Discovery() devices = discovery.discover() diff --git a/homeassistant/components/tibber/manifest.json b/homeassistant/components/tibber/manifest.json index 11c5a676bf6b..99c1335517e4 100644 --- a/homeassistant/components/tibber/manifest.json +++ b/homeassistant/components/tibber/manifest.json @@ -3,7 +3,7 @@ "name": "Tibber", "documentation": "https://www.home-assistant.io/integrations/tibber", "requirements": [ - "pyTibber==0.11.7" + "pyTibber==0.12.0" ], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/timer/__init__.py b/homeassistant/components/timer/__init__.py index 3ac41b84d82a..de60752eada9 100644 --- a/homeassistant/components/timer/__init__.py +++ b/homeassistant/components/timer/__init__.py @@ -6,7 +6,6 @@ from homeassistant.const import CONF_ICON, CONF_NAME import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.config_validation import ENTITY_SERVICE_SCHEMA from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.event import async_track_point_in_utc_time from homeassistant.helpers.restore_state import RestoreEntity @@ -37,10 +36,6 @@ SERVICE_CANCEL = "cancel" SERVICE_FINISH = "finish" -SERVICE_SCHEMA_DURATION = ENTITY_SERVICE_SCHEMA.extend( - {vol.Optional(ATTR_DURATION, default=timedelta(DEFAULT_DURATION)): cv.time_period} -) - CONFIG_SCHEMA = vol.Schema( { DOMAIN: cv.schema_with_slug_keys( @@ -80,17 +75,17 @@ async def async_setup(hass, config): return False component.async_register_entity_service( - SERVICE_START, SERVICE_SCHEMA_DURATION, "async_start" - ) - component.async_register_entity_service( - SERVICE_PAUSE, ENTITY_SERVICE_SCHEMA, "async_pause" - ) - component.async_register_entity_service( - SERVICE_CANCEL, ENTITY_SERVICE_SCHEMA, "async_cancel" - ) - component.async_register_entity_service( - SERVICE_FINISH, ENTITY_SERVICE_SCHEMA, "async_finish" + SERVICE_START, + { + vol.Optional( + ATTR_DURATION, default=timedelta(DEFAULT_DURATION) + ): cv.time_period + }, + "async_start", ) + component.async_register_entity_service(SERVICE_PAUSE, {}, "async_pause") + component.async_register_entity_service(SERVICE_CANCEL, {}, "async_cancel") + component.async_register_entity_service(SERVICE_FINISH, {}, "async_finish") await component.async_add_entities(entities) return True @@ -162,7 +157,6 @@ async def async_start(self, duration): event = EVENT_TIMER_RESTARTED self._state = STATUS_ACTIVE - # pylint: disable=redefined-outer-name start = dt_util.utcnow() if self._remaining and newduration is None: self._end = start + self._remaining diff --git a/homeassistant/components/todoist/calendar.py b/homeassistant/components/todoist/calendar.py index 1179fd908683..eabec37a0539 100644 --- a/homeassistant/components/todoist/calendar.py +++ b/homeassistant/components/todoist/calendar.py @@ -2,98 +2,50 @@ from datetime import datetime, timedelta import logging +from todoist.api import TodoistAPI import voluptuous as vol -from homeassistant.components.calendar import ( - DOMAIN, - PLATFORM_SCHEMA, - CalendarEventDevice, -) +from homeassistant.components.calendar import PLATFORM_SCHEMA, CalendarEventDevice from homeassistant.const import CONF_ID, CONF_NAME, CONF_TOKEN import homeassistant.helpers.config_validation as cv from homeassistant.helpers.template import DATE_STR_FORMAT from homeassistant.util import Throttle, dt -_LOGGER = logging.getLogger(__name__) +from .const import ( + ALL_DAY, + ALL_TASKS, + CHECKED, + COMPLETED, + CONF_PROJECT_DUE_DATE, + CONF_EXTRA_PROJECTS, + CONF_PROJECT_LABEL_WHITELIST, + CONF_PROJECT_WHITELIST, + CONTENT, + DATETIME, + DESCRIPTION, + DOMAIN, + DUE, + DUE_DATE, + DUE_DATE_LANG, + DUE_DATE_STRING, + DUE_DATE_VALID_LANGS, + DUE_TODAY, + END, + ID, + LABELS, + NAME, + OVERDUE, + PRIORITY, + PROJECT_ID, + PROJECT_NAME, + PROJECTS, + SERVICE_NEW_TASK, + START, + SUMMARY, + TASKS, +) -CONF_EXTRA_PROJECTS = "custom_projects" -CONF_PROJECT_DUE_DATE = "due_date_days" -CONF_PROJECT_LABEL_WHITELIST = "labels" -CONF_PROJECT_WHITELIST = "include_projects" - -# Calendar Platform: Does this calendar event last all day? -ALL_DAY = "all_day" -# Attribute: All tasks in this project -ALL_TASKS = "all_tasks" -# Todoist API: "Completed" flag -- 1 if complete, else 0 -CHECKED = "checked" -# Attribute: Is this task complete? -COMPLETED = "completed" -# Todoist API: What is this task about? -# Service Call: What is this task about? -CONTENT = "content" -# Calendar Platform: Get a calendar event's description -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 -DUE_DATE_LANG = "due_date_lang" -# Service Call: The available options of DUE_DATE_LANG -DUE_DATE_VALID_LANGS = [ - "en", - "da", - "pl", - "zh", - "ko", - "de", - "pt", - "ja", - "it", - "fr", - "sv", - "ru", - "es", - "nl", -] -# Attribute: When is this task due? -# Service Call: When is this task due? -DUE_DATE = "due_date" -# Todoist API: Look up a task's due date -DUE_DATE_UTC = "due_date_utc" -# Attribute: Is this task due today? -DUE_TODAY = "due_today" -# Calendar Platform: When a calendar event ends -END = "end" -# Todoist API: Look up a Project/Label/Task ID -ID = "id" -# Todoist API: Fetch all labels -# Service Call: What are the labels attached to this task? -LABELS = "labels" -# Todoist API: "Name" value -NAME = "name" -# Attribute: Is this task overdue? -OVERDUE = "overdue" -# Attribute: What is this task's priority? -# Todoist API: Get a task's priority -# Service Call: What is this task's priority? -PRIORITY = "priority" -# Todoist API: Look up the Project ID a Task belongs to -PROJECT_ID = "project_id" -# Service Call: What Project do you want a Task added to? -PROJECT_NAME = "project" -# Todoist API: Fetch all Projects -PROJECTS = "projects" -# Calendar Platform: When does a calendar event start? -START = "start" -# Calendar Platform: What is the next calendar event about? -SUMMARY = "summary" -# Todoist API: Fetch all Tasks -TASKS = "items" - -SERVICE_NEW_TASK = "todoist_new_task" +_LOGGER = logging.getLogger(__name__) NEW_TASK_SERVICE_SCHEMA = vol.Schema( { @@ -143,8 +95,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): project_id_lookup = {} label_id_lookup = {} - from todoist.api import TodoistAPI - api = TodoistAPI(token) api.sync() diff --git a/homeassistant/components/todoist/const.py b/homeassistant/components/todoist/const.py new file mode 100644 index 000000000000..a1e37bf0292d --- /dev/null +++ b/homeassistant/components/todoist/const.py @@ -0,0 +1,79 @@ +"""Constants for the Todoist component.""" +CONF_EXTRA_PROJECTS = "custom_projects" +CONF_PROJECT_DUE_DATE = "due_date_days" +CONF_PROJECT_LABEL_WHITELIST = "labels" +CONF_PROJECT_WHITELIST = "include_projects" + +# Calendar Platform: Does this calendar event last all day? +ALL_DAY = "all_day" +# Attribute: All tasks in this project +ALL_TASKS = "all_tasks" +# Todoist API: "Completed" flag -- 1 if complete, else 0 +CHECKED = "checked" +# Attribute: Is this task complete? +COMPLETED = "completed" +# Todoist API: What is this task about? +# Service Call: What is this task about? +CONTENT = "content" +# Calendar Platform: Get a calendar event's description +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 +DUE_DATE_LANG = "due_date_lang" +# Service Call: The available options of DUE_DATE_LANG +DUE_DATE_VALID_LANGS = [ + "en", + "da", + "pl", + "zh", + "ko", + "de", + "pt", + "ja", + "it", + "fr", + "sv", + "ru", + "es", + "nl", +] +# Attribute: When is this task due? +# Service Call: When is this task due? +DUE_DATE = "due_date" +# Attribute: Is this task due today? +DUE_TODAY = "due_today" +# Calendar Platform: When a calendar event ends +END = "end" +# Todoist API: Look up a Project/Label/Task ID +ID = "id" +# Todoist API: Fetch all labels +# Service Call: What are the labels attached to this task? +LABELS = "labels" +# Todoist API: "Name" value +NAME = "name" +# Attribute: Is this task overdue? +OVERDUE = "overdue" +# Attribute: What is this task's priority? +# Todoist API: Get a task's priority +# Service Call: What is this task's priority? +PRIORITY = "priority" +# Todoist API: Look up the Project ID a Task belongs to +PROJECT_ID = "project_id" +# Service Call: What Project do you want a Task added to? +PROJECT_NAME = "project" +# Todoist API: Fetch all Projects +PROJECTS = "projects" +# Calendar Platform: When does a calendar event start? +START = "start" +# Calendar Platform: What is the next calendar event about? +SUMMARY = "summary" +# Todoist API: Fetch all Tasks +TASKS = "items" + +DOMAIN = "todoist" + +SERVICE_NEW_TASK = "new_task" diff --git a/homeassistant/components/tof/sensor.py b/homeassistant/components/tof/sensor.py index d9e85b1e22b1..58f50f4899ec 100644 --- a/homeassistant/components/tof/sensor.py +++ b/homeassistant/components/tof/sensor.py @@ -1,15 +1,16 @@ """Platform for Time of Flight sensor VL53L1X from STMicroelectronics.""" import asyncio -import logging from functools import partial +import logging +from VL53L1X2 import VL53L1X # pylint: disable=import-error import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA -import homeassistant.helpers.config_validation as cv from homeassistant.components import rpi_gpio +from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import CONF_NAME +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -51,7 +52,6 @@ def init_tof_1(xshut): async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Reset and initialize the VL53L1X ToF Sensor from STMicroelectronics.""" - from VL53L1X2 import VL53L1X # pylint: disable=import-error name = config.get(CONF_NAME) bus_number = config.get(CONF_I2C_BUS) diff --git a/homeassistant/components/toon/.translations/pl.json b/homeassistant/components/toon/.translations/pl.json index 403be9bc067a..52da6579a030 100644 --- a/homeassistant/components/toon/.translations/pl.json +++ b/homeassistant/components/toon/.translations/pl.json @@ -4,7 +4,7 @@ "client_id": "Identyfikator klienta z konfiguracji jest nieprawid\u0142owy.", "client_secret": "Tajny klucz klienta z konfiguracji jest nieprawid\u0142owy.", "no_agreements": "To konto nie posiada wy\u015bwietlaczy Toon.", - "no_app": "Musisz skonfigurowa\u0107 Toon zanim b\u0119dziesz m\u00f3g\u0142 si\u0119 z nim uwierzytelni\u0107. Prosz\u0119 przeczyta\u0107 instrukcj\u0119] (https://www.home-assistant.io/components/toon/).", + "no_app": "Musisz skonfigurowa\u0107 Toon, aby m\u00f3c si\u0119 z nim uwierzytelni\u0107. Zapoznaj si\u0119 z [instrukcj\u0105](https://www.home-assistant.io/components/toon/).", "unknown_auth_fail": "Wyst\u0105pi\u0142 nieoczekiwany b\u0142\u0105d podczas uwierzytelniania." }, "error": { diff --git a/homeassistant/components/toon/.translations/ru.json b/homeassistant/components/toon/.translations/ru.json index 58e6f53986c1..427f717e3adc 100644 --- a/homeassistant/components/toon/.translations/ru.json +++ b/homeassistant/components/toon/.translations/ru.json @@ -3,12 +3,12 @@ "abort": { "client_id": "Client ID \u0438\u0437 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 \u043d\u0435\u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0442\u0435\u043b\u0435\u043d.", "client_secret": "Client secret \u0438\u0437 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 \u043d\u0435\u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0442\u0435\u043b\u0435\u043d.", - "no_agreements": "\u0423 \u044d\u0442\u043e\u0439 \u0443\u0447\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 \u043d\u0435\u0442 \u0434\u0438\u0441\u043f\u043b\u0435\u0435\u0432 Toon.", + "no_agreements": "\u0423 \u044d\u0442\u043e\u0439 \u0443\u0447\u0451\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 \u043d\u0435\u0442 \u0434\u0438\u0441\u043f\u043b\u0435\u0435\u0432 Toon.", "no_app": "\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 Toon \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/toon/).", "unknown_auth_fail": "\u0412\u043e \u0432\u0440\u0435\u043c\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438 \u043f\u0440\u043e\u0438\u0437\u043e\u0448\u043b\u0430 \u043d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, "error": { - "credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435.", + "credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0443\u0447\u0451\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435.", "display_exists": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0432\u044b\u0431\u0440\u0430\u043d\u043d\u043e\u0433\u043e \u0434\u0438\u0441\u043f\u043b\u0435\u044f \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430." }, "step": { @@ -18,8 +18,8 @@ "tenant": "\u0412\u043b\u0430\u0434\u0435\u043b\u0435\u0446", "username": "\u041b\u043e\u0433\u0438\u043d" }, - "description": "\u0412\u044b\u043f\u043e\u043b\u043d\u0438\u0442\u0435 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044e \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e \u0441\u0432\u043e\u0435\u0439 \u0443\u0447\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 Eneco Toon (\u043d\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435 \u0443\u0447\u0435\u0442\u043d\u0443\u044e \u0437\u0430\u043f\u0438\u0441\u044c \u0440\u0430\u0437\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a\u0430).", - "title": "\u041f\u0440\u0438\u0432\u044f\u0437\u0430\u0442\u044c \u0443\u0447\u0435\u0442\u043d\u0443\u044e \u0437\u0430\u043f\u0438\u0441\u044c Toon" + "description": "\u0412\u044b\u043f\u043e\u043b\u043d\u0438\u0442\u0435 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044e \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e \u0441\u0432\u043e\u0435\u0439 \u0443\u0447\u0451\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 Eneco Toon (\u043d\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435 \u0443\u0447\u0451\u0442\u043d\u0443\u044e \u0437\u0430\u043f\u0438\u0441\u044c \u0440\u0430\u0437\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a\u0430).", + "title": "\u041f\u0440\u0438\u0432\u044f\u0437\u0430\u0442\u044c \u0443\u0447\u0451\u0442\u043d\u0443\u044e \u0437\u0430\u043f\u0438\u0441\u044c Toon" }, "display": { "data": { diff --git a/homeassistant/components/toon/__init__.py b/homeassistant/components/toon/__init__.py index 4a3afb9b87b4..19466ba49c4a 100644 --- a/homeassistant/components/toon/__init__.py +++ b/homeassistant/components/toon/__init__.py @@ -13,7 +13,7 @@ from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.dispatcher import dispatcher_send, async_dispatcher_connect -from . import config_flow # noqa pylint_disable=unused-import +from . import config_flow # noqa: F401 from .const import ( CONF_CLIENT_ID, CONF_CLIENT_SECRET, @@ -139,7 +139,7 @@ def update(self, now=None): """Update all Toon data and notify entities.""" # Ignore the TTL meganism from client library # It causes a lots of issues, hence we take control over caching - self._toon._clear_cache() # noqa pylint: disable=W0212 + self._toon._clear_cache() # pylint: disable=protected-access # Gather data from client library (single API call) self.gas = self._toon.gas diff --git a/homeassistant/components/totalconnect/alarm_control_panel.py b/homeassistant/components/totalconnect/alarm_control_panel.py index e5f0b0c82793..b8b4236806f1 100644 --- a/homeassistant/components/totalconnect/alarm_control_panel.py +++ b/homeassistant/components/totalconnect/alarm_control_panel.py @@ -2,15 +2,20 @@ import logging import homeassistant.components.alarm_control_panel as alarm +from homeassistant.components.alarm_control_panel.const import ( + SUPPORT_ALARM_ARM_AWAY, + SUPPORT_ALARM_ARM_HOME, + SUPPORT_ALARM_ARM_NIGHT, +) from homeassistant.const import ( STATE_ALARM_ARMED_AWAY, + STATE_ALARM_ARMED_CUSTOM_BYPASS, STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT, - STATE_ALARM_DISARMED, STATE_ALARM_ARMING, + STATE_ALARM_DISARMED, STATE_ALARM_DISARMING, STATE_ALARM_TRIGGERED, - STATE_ALARM_ARMED_CUSTOM_BYPASS, ) from . import DOMAIN as TOTALCONNECT_DOMAIN @@ -55,6 +60,11 @@ def state(self): """Return the state of the device.""" return self._state + @property + def supported_features(self) -> int: + """Return the list of supported features.""" + return SUPPORT_ALARM_ARM_HOME | SUPPORT_ALARM_ARM_AWAY | SUPPORT_ALARM_ARM_NIGHT + @property def device_state_attributes(self): """Return the state attributes of the device.""" diff --git a/homeassistant/components/touchline/climate.py b/homeassistant/components/touchline/climate.py index 46ac30d0d97a..984e454ae02b 100644 --- a/homeassistant/components/touchline/climate.py +++ b/homeassistant/components/touchline/climate.py @@ -2,14 +2,15 @@ import logging from typing import List +from pytouchline import PyTouchline import voluptuous as vol -from homeassistant.components.climate import ClimateDevice, PLATFORM_SCHEMA +from homeassistant.components.climate import PLATFORM_SCHEMA, ClimateDevice from homeassistant.components.climate.const import ( - SUPPORT_TARGET_TEMPERATURE, HVAC_MODE_HEAT, + SUPPORT_TARGET_TEMPERATURE, ) -from homeassistant.const import CONF_HOST, TEMP_CELSIUS, ATTR_TEMPERATURE +from homeassistant.const import ATTR_TEMPERATURE, CONF_HOST, TEMP_CELSIUS import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -21,7 +22,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Touchline devices.""" - from pytouchline import PyTouchline host = config[CONF_HOST] py_touchline = PyTouchline() diff --git a/homeassistant/components/tplink/switch.py b/homeassistant/components/tplink/switch.py index 791d358c5095..b6ca69f4ccd2 100644 --- a/homeassistant/components/tplink/switch.py +++ b/homeassistant/components/tplink/switch.py @@ -115,6 +115,12 @@ def device_state_attributes(self): """Return the state attributes of the device.""" return self._emeter_params + @property + def _plug_from_context(self): + """Return the plug from the context.""" + children = self.smartplug.sys_info["children"] + return next(c for c in children if c["id"] == self.smartplug.context) + def update(self): """Update the TP-Link switch's state.""" try: @@ -126,21 +132,13 @@ def update(self): self._alias = self.smartplug.alias self._device_id = self._mac else: - self._alias = [ - child - for child in self.smartplug.sys_info["children"] - if child["id"] == self.smartplug.context - ][0]["alias"] + self._alias = self._plug_from_context["alias"] self._device_id = self.smartplug.context if self.smartplug.context is None: self._state = self.smartplug.state == self.smartplug.SWITCH_STATE_ON else: - self._state = [ - child - for child in self.smartplug.sys_info["children"] - if child["id"] == self.smartplug.context - ][0]["state"] == 1 + self._state = self._plug_from_context["state"] == 1 if self.smartplug.has_emeter: emeter_readings = self.smartplug.get_emeter_realtime() diff --git a/homeassistant/components/traccar/.translations/pl.json b/homeassistant/components/traccar/.translations/pl.json index 74ff0c089d87..95b7eb1af00b 100644 --- a/homeassistant/components/traccar/.translations/pl.json +++ b/homeassistant/components/traccar/.translations/pl.json @@ -5,7 +5,7 @@ "one_instance_allowed": "Niezb\u0119dna jest tylko jedna instancja." }, "create_entry": { - "default": "Aby wys\u0142a\u0107 wydarzenia do Home Assistant, musisz skonfigurowa\u0107 funkcj\u0119 webhook w Traccar. \n\n U\u017cyj nast\u0119puj\u0105cego URL: ` {webhook_url} ` \n\n Zobacz [dokumentacj\u0119] ( {docs_url} ) w celu uzyskania dalszych szczeg\u00f3\u0142\u00f3w." + "default": "Aby wysy\u0142a\u0107 zdarzenia do Home Assistant'a, musisz skonfigurowa\u0107 webhook w Traccar. \n\n U\u017cyj nast\u0119puj\u0105cego URL: `{webhook_url}` \n\nZapoznaj si\u0119 z [dokumentacj\u0105]({docs_url}), by pozna\u0107 szczeg\u00f3\u0142y." }, "step": { "user": { diff --git a/homeassistant/components/traccar/__init__.py b/homeassistant/components/traccar/__init__.py index 5eb87de0db28..7e94ab0a3510 100644 --- a/homeassistant/components/traccar/__init__.py +++ b/homeassistant/components/traccar/__init__.py @@ -1,14 +1,15 @@ """Support for Traccar.""" import logging -import voluptuous as vol from aiohttp import web +import voluptuous as vol -import homeassistant.helpers.config_validation as cv -from homeassistant.const import HTTP_UNPROCESSABLE_ENTITY, HTTP_OK, CONF_WEBHOOK_ID +from homeassistant.components.device_tracker import DOMAIN as DEVICE_TRACKER +from homeassistant.const import CONF_WEBHOOK_ID, HTTP_OK, HTTP_UNPROCESSABLE_ENTITY from homeassistant.helpers import config_entry_flow +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_send -from homeassistant.components.device_tracker import DOMAIN as DEVICE_TRACKER + from .const import ( ATTR_ACCURACY, ATTR_ALTITUDE, diff --git a/homeassistant/components/traccar/config_flow.py b/homeassistant/components/traccar/config_flow.py index 4bd75910163f..3702316ffb90 100644 --- a/homeassistant/components/traccar/config_flow.py +++ b/homeassistant/components/traccar/config_flow.py @@ -1,7 +1,7 @@ """Config flow for Traccar.""" from homeassistant.helpers import config_entry_flow -from .const import DOMAIN +from .const import DOMAIN config_entry_flow.register_webhook_flow( DOMAIN, diff --git a/homeassistant/components/traccar/device_tracker.py b/homeassistant/components/traccar/device_tracker.py index c7fdda013b0a..7f23d6cf31e3 100644 --- a/homeassistant/components/traccar/device_tracker.py +++ b/homeassistant/components/traccar/device_tracker.py @@ -2,29 +2,31 @@ from datetime import datetime, timedelta import logging +from pytraccar.api import API +from stringcase import camelcase import voluptuous as vol -from homeassistant.core import callback +from homeassistant.components.device_tracker import PLATFORM_SCHEMA, SOURCE_TYPE_GPS +from homeassistant.components.device_tracker.config_entry import TrackerEntity from homeassistant.const import ( + CONF_EVENT, CONF_HOST, + CONF_MONITORED_CONDITIONS, + CONF_PASSWORD, CONF_PORT, + CONF_SCAN_INTERVAL, CONF_SSL, - CONF_VERIFY_SSL, - CONF_PASSWORD, CONF_USERNAME, - CONF_SCAN_INTERVAL, - CONF_MONITORED_CONDITIONS, - CONF_EVENT, + CONF_VERIFY_SSL, ) -from homeassistant.components.device_tracker import PLATFORM_SCHEMA, SOURCE_TYPE_GPS -from homeassistant.components.device_tracker.config_entry import TrackerEntity +from homeassistant.core import callback from homeassistant.helpers import device_registry +from homeassistant.helpers.aiohttp_client import async_get_clientsession +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.typing import HomeAssistantType -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.helpers.event import async_track_time_interval from homeassistant.util import slugify from . import DOMAIN, TRACKER_UPDATE @@ -41,30 +43,29 @@ ATTR_MOTION, ATTR_SPEED, ATTR_STATUS, - ATTR_TRACKER, ATTR_TRACCAR_ID, - EVENT_DEVICE_MOVING, + ATTR_TRACKER, + CONF_MAX_ACCURACY, + CONF_SKIP_ACCURACY_ON, + EVENT_ALARM, + EVENT_ALL_EVENTS, EVENT_COMMAND_RESULT, EVENT_DEVICE_FUEL_DROP, - EVENT_GEOFENCE_ENTER, + EVENT_DEVICE_MOVING, EVENT_DEVICE_OFFLINE, - EVENT_DRIVER_CHANGED, - EVENT_GEOFENCE_EXIT, - EVENT_DEVICE_OVERSPEED, EVENT_DEVICE_ONLINE, + EVENT_DEVICE_OVERSPEED, EVENT_DEVICE_STOPPED, - EVENT_MAINTENANCE, - EVENT_ALARM, - EVENT_TEXT_MESSAGE, EVENT_DEVICE_UNKNOWN, + EVENT_DRIVER_CHANGED, + EVENT_GEOFENCE_ENTER, + EVENT_GEOFENCE_EXIT, EVENT_IGNITION_OFF, EVENT_IGNITION_ON, - EVENT_ALL_EVENTS, - CONF_MAX_ACCURACY, - CONF_SKIP_ACCURACY_ON, + EVENT_MAINTENANCE, + EVENT_TEXT_MESSAGE, ) - _LOGGER = logging.getLogger(__name__) DEFAULT_SCAN_INTERVAL = timedelta(seconds=30) @@ -156,7 +157,6 @@ def _receive_data(device, latitude, longitude, battery, accuracy, attrs): async def async_setup_scanner(hass, config, async_see, discovery_info=None): """Validate the configuration and return a Traccar scanner.""" - from pytraccar.api import API session = async_get_clientsession(hass, config[CONF_VERIFY_SSL]) @@ -199,7 +199,6 @@ def __init__( event_types, ): """Initialize.""" - from stringcase import camelcase self._event_types = {camelcase(evt): evt for evt in event_types} self._custom_attributes = custom_attributes diff --git a/homeassistant/components/trackr/device_tracker.py b/homeassistant/components/trackr/device_tracker.py index 580f49b908fa..07d3c60e2568 100644 --- a/homeassistant/components/trackr/device_tracker.py +++ b/homeassistant/components/trackr/device_tracker.py @@ -1,10 +1,11 @@ """Support for the TrackR platform.""" import logging +from pytrackr.api import trackrApiInterface import voluptuous as vol from homeassistant.components.device_tracker import PLATFORM_SCHEMA -from homeassistant.const import CONF_USERNAME, CONF_PASSWORD +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME import homeassistant.helpers.config_validation as cv from homeassistant.helpers.event import track_utc_time_change from homeassistant.util import slugify @@ -27,7 +28,6 @@ class TrackRDeviceScanner: def __init__(self, hass, config: dict, see) -> None: """Initialize the TrackR device scanner.""" - from pytrackr.api import trackrApiInterface self.hass = hass self.api = trackrApiInterface( diff --git a/homeassistant/components/tradfri/__init__.py b/homeassistant/components/tradfri/__init__.py index 9d1a43b240f7..a797607e2433 100644 --- a/homeassistant/components/tradfri/__init__.py +++ b/homeassistant/components/tradfri/__init__.py @@ -1,32 +1,33 @@ """Support for IKEA Tradfri.""" import logging -import voluptuous as vol from pytradfri import Gateway, RequestError from pytradfri.api.aiocoap_api import APIFactory +import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant import config_entries from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.exceptions import ConfigEntryNotReady +import homeassistant.helpers.config_validation as cv from homeassistant.util.json import load_json -from . import config_flow # noqa pylint_disable=unused-import + +from . import config_flow # noqa: F401 from .const import ( - DOMAIN, - CONFIG_FILE, - KEY_GATEWAY, - KEY_API, - CONF_ALLOW_TRADFRI_GROUPS, - DEFAULT_ALLOW_TRADFRI_GROUPS, - TRADFRI_DEVICE_TYPES, - ATTR_TRADFRI_MANUFACTURER, ATTR_TRADFRI_GATEWAY, ATTR_TRADFRI_GATEWAY_MODEL, - CONF_IMPORT_GROUPS, - CONF_IDENTITY, + ATTR_TRADFRI_MANUFACTURER, + CONF_ALLOW_TRADFRI_GROUPS, + CONF_GATEWAY_ID, CONF_HOST, + CONF_IDENTITY, + CONF_IMPORT_GROUPS, CONF_KEY, - CONF_GATEWAY_ID, + CONFIG_FILE, + DEFAULT_ALLOW_TRADFRI_GROUPS, + DOMAIN, + KEY_API, + KEY_GATEWAY, + TRADFRI_DEVICE_TYPES, ) _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/tradfri/base_class.py b/homeassistant/components/tradfri/base_class.py index ba90fe05d1e3..358056d7ef60 100644 --- a/homeassistant/components/tradfri/base_class.py +++ b/homeassistant/components/tradfri/base_class.py @@ -5,6 +5,7 @@ from homeassistant.core import callback from homeassistant.helpers.entity import Entity + from .const import DOMAIN _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/tradfri/config_flow.py b/homeassistant/components/tradfri/config_flow.py index 24c3fbc18768..048541b54025 100644 --- a/homeassistant/components/tradfri/config_flow.py +++ b/homeassistant/components/tradfri/config_flow.py @@ -4,15 +4,18 @@ from uuid import uuid4 import async_timeout +from pytradfri import Gateway, RequestError +from pytradfri.api.aiocoap_api import APIFactory import voluptuous as vol from homeassistant import config_entries + from .const import ( - CONF_IMPORT_GROUPS, - CONF_IDENTITY, + CONF_GATEWAY_ID, CONF_HOST, + CONF_IDENTITY, + CONF_IMPORT_GROUPS, CONF_KEY, - CONF_GATEWAY_ID, KEY_SECURITY_CODE, ) @@ -153,8 +156,6 @@ async def _entry_from_data(self, data): async def authenticate(hass, host, security_code): """Authenticate with a Tradfri hub.""" - from pytradfri.api.aiocoap_api import APIFactory - from pytradfri import RequestError identity = uuid4().hex @@ -173,8 +174,6 @@ async def authenticate(hass, host, security_code): async def get_gateway_info(hass, host, identity, key): """Return info for the gateway.""" - from pytradfri.api.aiocoap_api import APIFactory - from pytradfri import Gateway, RequestError try: factory = APIFactory(host, psk_id=identity, psk=key, loop=hass.loop) diff --git a/homeassistant/components/tradfri/const.py b/homeassistant/components/tradfri/const.py index 038f0e91c76c..88225d3282a6 100644 --- a/homeassistant/components/tradfri/const.py +++ b/homeassistant/components/tradfri/const.py @@ -1,6 +1,6 @@ """Consts used by Tradfri.""" -from homeassistant.components.light import SUPPORT_TRANSITION, SUPPORT_BRIGHTNESS -from homeassistant.const import CONF_HOST # noqa pylint: disable=unused-import +from homeassistant.components.light import SUPPORT_BRIGHTNESS, SUPPORT_TRANSITION +from homeassistant.const import CONF_HOST # noqa: F401 pylint: disable=unused-import ATTR_DIMMER = "dimmer" ATTR_HUE = "hue" diff --git a/homeassistant/components/tradfri/cover.py b/homeassistant/components/tradfri/cover.py index ae7d6a09ce3e..d978e5129204 100644 --- a/homeassistant/components/tradfri/cover.py +++ b/homeassistant/components/tradfri/cover.py @@ -1,8 +1,9 @@ """Support for IKEA Tradfri covers.""" -from homeassistant.components.cover import CoverDevice, ATTR_POSITION +from homeassistant.components.cover import ATTR_POSITION, CoverDevice + from .base_class import TradfriBaseDevice -from .const import KEY_GATEWAY, KEY_API, CONF_GATEWAY_ID +from .const import CONF_GATEWAY_ID, KEY_API, KEY_GATEWAY async def async_setup_entry(hass, config_entry, async_add_entities): diff --git a/homeassistant/components/tradfri/light.py b/homeassistant/components/tradfri/light.py index 9ee3c5d6a8cc..0fe826be9af4 100644 --- a/homeassistant/components/tradfri/light.py +++ b/homeassistant/components/tradfri/light.py @@ -1,29 +1,30 @@ """Support for IKEA Tradfri lights.""" import logging -import homeassistant.util.color as color_util from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_HS_COLOR, ATTR_TRANSITION, - Light, SUPPORT_BRIGHTNESS, SUPPORT_COLOR, SUPPORT_COLOR_TEMP, + Light, ) -from .base_class import TradfriBaseDevice, TradfriBaseClass +import homeassistant.util.color as color_util + +from .base_class import TradfriBaseClass, TradfriBaseDevice from .const import ( ATTR_DIMMER, ATTR_HUE, ATTR_SAT, ATTR_TRANSITION_TIME, - SUPPORTED_LIGHT_FEATURES, - SUPPORTED_GROUP_FEATURES, CONF_GATEWAY_ID, CONF_IMPORT_GROUPS, - KEY_GATEWAY, KEY_API, + KEY_GATEWAY, + SUPPORTED_GROUP_FEATURES, + SUPPORTED_LIGHT_FEATURES, ) _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/tradfri/sensor.py b/homeassistant/components/tradfri/sensor.py index cf797f34e3b4..c3a08ab16753 100644 --- a/homeassistant/components/tradfri/sensor.py +++ b/homeassistant/components/tradfri/sensor.py @@ -1,8 +1,9 @@ """Support for IKEA Tradfri sensors.""" from homeassistant.const import DEVICE_CLASS_BATTERY + from .base_class import TradfriBaseDevice -from .const import KEY_GATEWAY, KEY_API, CONF_GATEWAY_ID +from .const import CONF_GATEWAY_ID, KEY_API, KEY_GATEWAY async def async_setup_entry(hass, config_entry, async_add_entities): diff --git a/homeassistant/components/tradfri/switch.py b/homeassistant/components/tradfri/switch.py index e1c549a1805f..fffbf320c7e9 100644 --- a/homeassistant/components/tradfri/switch.py +++ b/homeassistant/components/tradfri/switch.py @@ -1,7 +1,8 @@ """Support for IKEA Tradfri switches.""" from homeassistant.components.switch import SwitchDevice + from .base_class import TradfriBaseDevice -from .const import KEY_GATEWAY, KEY_API, CONF_GATEWAY_ID +from .const import CONF_GATEWAY_ID, KEY_API, KEY_GATEWAY async def async_setup_entry(hass, config_entry, async_add_entities): diff --git a/homeassistant/components/trafikverket_weatherstation/sensor.py b/homeassistant/components/trafikverket_weatherstation/sensor.py index cb80e8d441bf..802bb897b961 100644 --- a/homeassistant/components/trafikverket_weatherstation/sensor.py +++ b/homeassistant/components/trafikverket_weatherstation/sensor.py @@ -5,6 +5,7 @@ import logging import aiohttp +from pytrafikverket.trafikverket_weather import TrafikverketWeather import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -106,7 +107,6 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the Trafikverket sensor platform.""" - from pytrafikverket.trafikverket_weather import TrafikverketWeather sensor_name = config[CONF_NAME] sensor_api = config[CONF_API_KEY] diff --git a/homeassistant/components/transmission/.translations/pt.json b/homeassistant/components/transmission/.translations/pt.json index f681da4210f8..0421228d0f0d 100644 --- a/homeassistant/components/transmission/.translations/pt.json +++ b/homeassistant/components/transmission/.translations/pt.json @@ -1,10 +1,15 @@ { "config": { + "error": { + "wrong_credentials": "Nome de utilizador ou palavra passe incorretos" + }, "step": { "user": { "data": { "host": "Servidor", - "port": "Porta" + "password": "Palavra-passe", + "port": "Porta", + "username": "Utilizador" } } } diff --git a/homeassistant/components/transmission/__init__.py b/homeassistant/components/transmission/__init__.py index be41ca85998f..7bbc61a192ff 100644 --- a/homeassistant/components/transmission/__init__.py +++ b/homeassistant/components/transmission/__init__.py @@ -19,7 +19,6 @@ from homeassistant.helpers import config_validation as cv from homeassistant.helpers.dispatcher import dispatcher_send from homeassistant.helpers.event import async_track_time_interval -from homeassistant.util import slugify from .const import ( ATTR_TORRENT, @@ -28,13 +27,16 @@ DEFAULT_SCAN_INTERVAL, DOMAIN, SERVICE_ADD_TORRENT, + DATA_UPDATED, ) from .errors import AuthenticationError, CannotConnect, UnknownError _LOGGER = logging.getLogger(__name__) -SERVICE_ADD_TORRENT_SCHEMA = vol.Schema({vol.Required(ATTR_TORRENT): cv.string}) +SERVICE_ADD_TORRENT_SCHEMA = vol.Schema( + {vol.Required(ATTR_TORRENT): cv.string, vol.Required(CONF_NAME): cv.string} +) TRANS_SCHEMA = vol.All( vol.Schema( @@ -55,6 +57,8 @@ {DOMAIN: vol.All(cv.ensure_list, [TRANS_SCHEMA])}, extra=vol.ALLOW_EXTRA ) +PLATFORMS = ["sensor", "switch"] + async def async_setup(hass, config): """Import the Transmission Component from config.""" @@ -82,15 +86,15 @@ async def async_setup_entry(hass, config_entry): async def async_unload_entry(hass, config_entry): """Unload Transmission Entry from config_entry.""" - client = hass.data[DOMAIN][config_entry.entry_id] - hass.services.async_remove(DOMAIN, client.service_name) + client = hass.data[DOMAIN].pop(config_entry.entry_id) if client.unsub_timer: client.unsub_timer() - for component in "sensor", "switch": - await hass.config_entries.async_forward_entry_unload(config_entry, component) + for platform in PLATFORMS: + await hass.config_entries.async_forward_entry_unload(config_entry, platform) - hass.data[DOMAIN].pop(config_entry.entry_id) + if not hass.data[DOMAIN]: + hass.services.async_remove(DOMAIN, SERVICE_ADD_TORRENT) return True @@ -128,14 +132,10 @@ def __init__(self, hass, config_entry): """Initialize the Transmission RPC API.""" self.hass = hass self.config_entry = config_entry + self.tm_api = None self._tm_data = None self.unsub_timer = None - @property - def service_name(self): - """Return the service name.""" - return slugify(f"{SERVICE_ADD_TORRENT}_{self.config_entry.data[CONF_NAME]}") - @property def api(self): """Return the tm_data object.""" @@ -145,20 +145,20 @@ async def async_setup(self): """Set up the Transmission client.""" try: - api = await get_api(self.hass, self.config_entry.data) + self.tm_api = await get_api(self.hass, self.config_entry.data) except CannotConnect: raise ConfigEntryNotReady except (AuthenticationError, UnknownError): return False - self._tm_data = TransmissionData(self.hass, self.config_entry, api) + self._tm_data = TransmissionData(self.hass, self.config_entry, self.tm_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.add_options() self.set_scan_interval(self.config_entry.options[CONF_SCAN_INTERVAL]) - for platform in ["sensor", "switch"]: + for platform in PLATFORMS: self.hass.async_create_task( self.hass.config_entries.async_forward_entry_setup( self.config_entry, platform @@ -167,18 +167,26 @@ async def async_setup(self): def add_torrent(service): """Add new torrent to download.""" + tm_client = None + for entry in self.hass.config_entries.async_entries(DOMAIN): + if entry.data[CONF_NAME] == service.data[CONF_NAME]: + tm_client = self.hass.data[DOMAIN][entry.entry_id] + break + if tm_client is None: + _LOGGER.error("Transmission instance is not found") + return torrent = service.data[ATTR_TORRENT] if torrent.startswith( ("http", "ftp:", "magnet:") ) or self.hass.config.is_allowed_path(torrent): - api.add_torrent(torrent) + tm_client.tm_api.add_torrent(torrent) else: _LOGGER.warning( "Could not add torrent: unsupported type or no permission" ) self.hass.services.async_register( - DOMAIN, self.service_name, add_torrent, schema=SERVICE_ADD_TORRENT_SCHEMA + DOMAIN, SERVICE_ADD_TORRENT, add_torrent, schema=SERVICE_ADD_TORRENT_SCHEMA ) self.config_entry.add_update_listener(self.async_options_updated) @@ -200,7 +208,7 @@ def add_options(self): def set_scan_interval(self, scan_interval): """Update scan interval.""" - async def refresh(event_time): + def refresh(event_time): """Get the latest data from Transmission.""" self._tm_data.update() @@ -240,9 +248,9 @@ def host(self): return self.config.data[CONF_HOST] @property - def signal_options_update(self): - """Option update signal per transmission entry.""" - return f"tm-options-{self.host}" + def signal_update(self): + """Update signal per transmission entry.""" + return f"{DATA_UPDATED}-{self.host}" def update(self): """Get the latest data from Transmission instance.""" @@ -260,7 +268,7 @@ def update(self): except TransmissionError: self.available = False _LOGGER.error("Unable to connect to Transmission client %s", self.host) - dispatcher_send(self.hass, self.signal_options_update) + dispatcher_send(self.hass, self.signal_update) def init_torrent_list(self): """Initialize torrent lists.""" diff --git a/homeassistant/components/transmission/config_flow.py b/homeassistant/components/transmission/config_flow.py index d7b9efb15d87..193c152d7c10 100644 --- a/homeassistant/components/transmission/config_flow.py +++ b/homeassistant/components/transmission/config_flow.py @@ -16,9 +16,19 @@ from .const import DEFAULT_NAME, DEFAULT_PORT, DEFAULT_SCAN_INTERVAL, DOMAIN from .errors import AuthenticationError, CannotConnect, UnknownError +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, + } +) + class TransmissionFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): - """Handle a UniFi config flow.""" + """Handle Tansmission config flow.""" VERSION = 1 CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL @@ -57,17 +67,7 @@ async def async_step_user(self, user_input=None): ) 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=errors, + step_id="user", data_schema=DATA_SCHEMA, errors=errors, ) async def async_step_import(self, import_config): diff --git a/homeassistant/components/transmission/sensor.py b/homeassistant/components/transmission/sensor.py index 489582de1572..c51d48eb5325 100644 --- a/homeassistant/components/transmission/sensor.py +++ b/homeassistant/components/transmission/sensor.py @@ -52,6 +52,7 @@ def __init__( self._data = None self.client_name = client_name self.type = sensor_type + self.unsub_update = None @property def name(self): @@ -92,9 +93,9 @@ def device_state_attributes(self): async def async_added_to_hass(self): """Handle entity which will be added.""" - async_dispatcher_connect( + self.unsub_update = async_dispatcher_connect( self.hass, - self._tm_client.api.signal_options_update, + self._tm_client.api.signal_update, self._schedule_immediate_update, ) @@ -102,6 +103,12 @@ async def async_added_to_hass(self): def _schedule_immediate_update(self): self.async_schedule_update_ha_state(True) + async def will_remove_from_hass(self): + """Unsubscribe from update dispatcher.""" + if self.unsub_update: + self.unsub_update() + self.unsub_update = None + def update(self): """Get the latest data from Transmission and updates the state.""" self._data = self._tm_client.api.data diff --git a/homeassistant/components/transmission/services.yaml b/homeassistant/components/transmission/services.yaml index ab383584e83f..de3314e20f64 100644 --- a/homeassistant/components/transmission/services.yaml +++ b/homeassistant/components/transmission/services.yaml @@ -1,6 +1,9 @@ add_torrent: description: Add a new torrent to download (URL, magnet link or Base64 encoded). fields: + name: + description: Instance name as entered during entry config + example: Transmission torrent: description: URL, magnet link or Base64 encoded file. example: http://releases.ubuntu.com/19.04/ubuntu-19.04-desktop-amd64.iso.torrent diff --git a/homeassistant/components/transmission/switch.py b/homeassistant/components/transmission/switch.py index 4b93b3f06e20..adf94c64fd6a 100644 --- a/homeassistant/components/transmission/switch.py +++ b/homeassistant/components/transmission/switch.py @@ -40,6 +40,7 @@ def __init__(self, switch_type, switch_name, tm_client, name): self._tm_client = tm_client self._state = STATE_OFF self._data = None + self.unsub_update = None @property def name(self): @@ -93,9 +94,9 @@ def turn_off(self, **kwargs): async def async_added_to_hass(self): """Handle entity which will be added.""" - async_dispatcher_connect( + self.unsub_update = async_dispatcher_connect( self.hass, - self._tm_client.api.signal_options_update, + self._tm_client.api.signal_update, self._schedule_immediate_update, ) @@ -103,6 +104,12 @@ async def async_added_to_hass(self): def _schedule_immediate_update(self): self.async_schedule_update_ha_state(True) + async def will_remove_from_hass(self): + """Unsubscribe from update dispatcher.""" + if self.unsub_update: + self.unsub_update() + self.unsub_update = None + def update(self): """Get the latest data from Transmission and updates the state.""" active = None diff --git a/homeassistant/components/travisci/sensor.py b/homeassistant/components/travisci/sensor.py index b86b62fc1e95..ba698c2b64d9 100644 --- a/homeassistant/components/travisci/sensor.py +++ b/homeassistant/components/travisci/sensor.py @@ -1,17 +1,19 @@ """This component provides HA sensor support for Travis CI framework.""" -import logging from datetime import timedelta +import logging +from travispy import TravisPy +from travispy.errors import TravisError 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_API_KEY, - CONF_SCAN_INTERVAL, CONF_MONITORED_CONDITIONS, + CONF_SCAN_INTERVAL, ) +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -53,8 +55,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Travis CI sensor.""" - from travispy import TravisPy - from travispy.errors import TravisError token = config.get(CONF_API_KEY) repositories = config.get(CONF_REPOSITORY) diff --git a/homeassistant/components/trend/manifest.json b/homeassistant/components/trend/manifest.json index cf9333be7c38..8842b03b594f 100644 --- a/homeassistant/components/trend/manifest.json +++ b/homeassistant/components/trend/manifest.json @@ -2,9 +2,7 @@ "domain": "trend", "name": "Trend", "documentation": "https://www.home-assistant.io/integrations/trend", - "requirements": [ - "numpy==1.17.3" - ], + "requirements": ["numpy==1.17.4"], "dependencies": [], "codeowners": [] -} \ No newline at end of file +} diff --git a/homeassistant/components/tts/__init__.py b/homeassistant/components/tts/__init__.py index d17f64a3a3a4..8ae06771618e 100644 --- a/homeassistant/components/tts/__init__.py +++ b/homeassistant/components/tts/__init__.py @@ -1,4 +1,4 @@ -"""Provide functionality to TTS.""" +"""Provide functionality for TTS.""" import asyncio import ctypes import functools as ft @@ -353,7 +353,7 @@ async def async_get_tts_audio(self, engine, key, message, cache, language, optio raise HomeAssistantError(f"No TTS from {engine} for '{message}'") # Create file infos - filename = (f"{key}.{extension}").lower() + filename = f"{key}.{extension}".lower() data = self.write_tags(filename, data, provider, message, language, options) @@ -438,7 +438,7 @@ async def async_read_tts(self, filename): await self.async_file_to_mem(key) content, _ = mimetypes.guess_type(filename) - return (content, self.mem_cache[key][MEM_CACHE_VOICE]) + return content, self.mem_cache[key][MEM_CACHE_VOICE] @staticmethod def write_tags(filename, data, provider, message, language, options): diff --git a/homeassistant/components/tts/manifest.json b/homeassistant/components/tts/manifest.json index ca2059a4d19e..cb7805239777 100644 --- a/homeassistant/components/tts/manifest.json +++ b/homeassistant/components/tts/manifest.json @@ -3,7 +3,7 @@ "name": "Tts", "documentation": "https://www.home-assistant.io/integrations/tts", "requirements": [ - "mutagen==1.42.0" + "mutagen==1.43.0" ], "dependencies": [ "http" diff --git a/homeassistant/components/tuya/__init__.py b/homeassistant/components/tuya/__init__.py index 4d6506d950d8..dffd66265a68 100644 --- a/homeassistant/components/tuya/__init__.py +++ b/homeassistant/components/tuya/__init__.py @@ -1,13 +1,15 @@ """Support for Tuya Smart devices.""" from datetime import timedelta import logging + +from tuyaha import TuyaApi import voluptuous as vol +from homeassistant.const import CONF_PASSWORD, CONF_PLATFORM, CONF_USERNAME from homeassistant.core import callback -import homeassistant.helpers.config_validation as cv -from homeassistant.const import CONF_USERNAME, CONF_PASSWORD, CONF_PLATFORM from homeassistant.helpers import discovery -from homeassistant.helpers.dispatcher import dispatcher_send, async_dispatcher_connect +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.dispatcher import async_dispatcher_connect, dispatcher_send from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import track_time_interval @@ -50,7 +52,6 @@ def setup(hass, config): """Set up Tuya Component.""" - from tuyaha import TuyaApi tuya = TuyaApi() username = config[DOMAIN][CONF_USERNAME] diff --git a/homeassistant/components/tuya/climate.py b/homeassistant/components/tuya/climate.py index 02e8a1bf8218..6450920b8064 100644 --- a/homeassistant/components/tuya/climate.py +++ b/homeassistant/components/tuya/climate.py @@ -5,9 +5,9 @@ HVAC_MODE_COOL, HVAC_MODE_FAN_ONLY, HVAC_MODE_HEAT, + HVAC_MODE_OFF, SUPPORT_FAN_MODE, SUPPORT_TARGET_TEMPERATURE, - HVAC_MODE_OFF, ) from homeassistant.components.fan import SPEED_HIGH, SPEED_LOW, SPEED_MEDIUM from homeassistant.const import ( diff --git a/homeassistant/components/twilio_call/notify.py b/homeassistant/components/twilio_call/notify.py index 827050918144..83ca081b26e4 100644 --- a/homeassistant/components/twilio_call/notify.py +++ b/homeassistant/components/twilio_call/notify.py @@ -2,6 +2,7 @@ import logging import urllib +from twilio.base.exceptions import TwilioRestException import voluptuous as vol from homeassistant.components.notify import ( @@ -42,7 +43,6 @@ def __init__(self, twilio_client, from_number): def send_message(self, message="", **kwargs): """Call to specified target users.""" - from twilio.base.exceptions import TwilioRestException targets = kwargs.get(ATTR_TARGET) diff --git a/homeassistant/components/ubee/device_tracker.py b/homeassistant/components/ubee/device_tracker.py index 86b2e3c09af0..6fe7e90f4c7f 100644 --- a/homeassistant/components/ubee/device_tracker.py +++ b/homeassistant/components/ubee/device_tracker.py @@ -1,6 +1,8 @@ """Support for Ubee router.""" import logging + +from pyubee import Ubee import voluptuous as vol from homeassistant.components.device_tracker import ( @@ -36,8 +38,6 @@ def get_scanner(hass, config): password = info[CONF_PASSWORD] model = info[CONF_MODEL] - from pyubee import Ubee - ubee = Ubee(host, username, password, model) if not ubee.login(): _LOGGER.error("Login failed") diff --git a/homeassistant/components/unifi/.translations/es.json b/homeassistant/components/unifi/.translations/es.json index 1db6712142d5..677899c0958f 100644 --- a/homeassistant/components/unifi/.translations/es.json +++ b/homeassistant/components/unifi/.translations/es.json @@ -35,8 +35,8 @@ }, "init": { "data": { - "one": "uno", - "other": "otro" + "one": "vac\u00edo", + "other": "vac\u00edo" } }, "statistics_sensors": { diff --git a/homeassistant/components/unifi/.translations/ru.json b/homeassistant/components/unifi/.translations/ru.json index dbb6efd83432..b01cdb84fbf9 100644 --- a/homeassistant/components/unifi/.translations/ru.json +++ b/homeassistant/components/unifi/.translations/ru.json @@ -5,7 +5,7 @@ "user_privilege": "\u041f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c \u0434\u043e\u043b\u0436\u0435\u043d \u0431\u044b\u0442\u044c \u0430\u0434\u043c\u0438\u043d\u0438\u0441\u0442\u0440\u0430\u0442\u043e\u0440\u043e\u043c." }, "error": { - "faulty_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435.", + "faulty_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0443\u0447\u0451\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435.", "service_unavailable": "\u0421\u043b\u0443\u0436\u0431\u0430 \u043d\u0435 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0430." }, "step": { diff --git a/homeassistant/components/unifi/device_tracker.py b/homeassistant/components/unifi/device_tracker.py index b92211c4eae5..086393b85d2d 100644 --- a/homeassistant/components/unifi/device_tracker.py +++ b/homeassistant/components/unifi/device_tracker.py @@ -1,5 +1,6 @@ """Track devices using UniFi controllers.""" import logging +from pprint import pformat from homeassistant.components.unifi.config_flow import get_controller_from_config_entry from homeassistant.components.device_tracker import DOMAIN as DEVICE_TRACKER_DOMAIN @@ -76,6 +77,9 @@ def update_disable_on_entities(): """Update the values of the controller.""" for entity in tracked.values(): + if entity.entity_registry_enabled_default == entity.enabled: + continue + disabled_by = None if not entity.entity_registry_enabled_default and entity.enabled: disabled_by = DISABLED_CONFIG_ENTRY @@ -125,6 +129,7 @@ def __init__(self, client, controller): self.client = client self.controller = controller self.is_wired = self.client.mac not in controller.wireless_clients + self.wired_bug = None @property def entity_registry_enabled_default(self): @@ -153,27 +158,35 @@ async def async_update(self): 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 + LOGGER.debug( + "Updating UniFi tracked client %s\n%s", + self.entity_id, + pformat(self.client.raw), + ) + @property def is_connected(self): """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)) + if self.is_wired != self.client.is_wired: + if not self.wired_bug: + self.wired_bug = dt_util.utcnow() + since_last_seen = dt_util.utcnow() - self.wired_bug + + else: + self.wired_bug = None + since_last_seen = dt_util.utcnow() - dt_util.utc_from_timestamp( + float(self.client.last_seen) ) - < self.controller.option_detection_time - ): + + if since_last_seen < self.controller.option_detection_time: return True return False @@ -228,10 +241,9 @@ def __init__(self, device, controller): @property def entity_registry_enabled_default(self): """Return if the entity should be enabled when first added to the entity registry.""" - if not self.controller.option_track_devices: - return False - - return True + if self.controller.option_track_devices: + return True + return False async def async_added_to_hass(self): """Subscribe to device events.""" @@ -239,10 +251,13 @@ async def async_added_to_hass(self): async def async_update(self): """Synchronize state with controller.""" + await self.controller.request_update() + LOGGER.debug( - "Updating UniFi tracked device %s (%s)", self.entity_id, self.device.mac + "Updating UniFi tracked device %s\n%s", + self.entity_id, + pformat(self.device.raw), ) - await self.controller.request_update() @property def is_connected(self): diff --git a/homeassistant/components/unifi/sensor.py b/homeassistant/components/unifi/sensor.py index e4f9b0df6c9d..9145fd8e00f4 100644 --- a/homeassistant/components/unifi/sensor.py +++ b/homeassistant/components/unifi/sensor.py @@ -40,6 +40,9 @@ def update_disable_on_entities(): """Update the values of the controller.""" for entity in sensors.values(): + if entity.entity_registry_enabled_default == entity.enabled: + continue + disabled_by = None if not entity.entity_registry_enabled_default and entity.enabled: disabled_by = DISABLED_CONFIG_ENTRY diff --git a/homeassistant/components/unifi/switch.py b/homeassistant/components/unifi/switch.py index 82aa6f0384de..45f74f8882f4 100644 --- a/homeassistant/components/unifi/switch.py +++ b/homeassistant/components/unifi/switch.py @@ -1,5 +1,6 @@ """Support for devices connected to UniFi POE.""" import logging +from pprint import pformat from homeassistant.components.unifi.config_flow import get_controller_from_config_entry from homeassistant.components.switch import SwitchDevice @@ -73,12 +74,13 @@ def update_items(controller, async_add_entities, switches, switches_off): block_client_id = f"block-{client_id}" if block_client_id in switches: - LOGGER.debug( - "Updating UniFi block switch %s (%s)", - switches[block_client_id].entity_id, - switches[block_client_id].client.mac, - ) - switches[block_client_id].async_schedule_update_ha_state() + if switches[block_client_id].enabled: + LOGGER.debug( + "Updating UniFi block switch %s (%s)", + switches[block_client_id].entity_id, + switches[block_client_id].client.mac, + ) + switches[block_client_id].async_schedule_update_ha_state() continue if client_id not in controller.api.clients_all: @@ -87,7 +89,6 @@ def update_items(controller, async_add_entities, switches, switches_off): client = controller.api.clients_all[client_id] switches[block_client_id] = UniFiBlockClientSwitch(client, controller) new_switches.append(switches[block_client_id]) - LOGGER.debug("New UniFi Block switch %s (%s)", client.hostname, client.mac) # control POE for client_id in controller.api.clients: @@ -95,12 +96,13 @@ def update_items(controller, async_add_entities, switches, switches_off): poe_client_id = f"poe-{client_id}" if poe_client_id in switches: - LOGGER.debug( - "Updating UniFi POE switch %s (%s)", - switches[poe_client_id].entity_id, - switches[poe_client_id].client.mac, - ) - switches[poe_client_id].async_schedule_update_ha_state() + if switches[poe_client_id].enabled: + LOGGER.debug( + "Updating UniFi POE switch %s (%s)", + switches[poe_client_id].entity_id, + switches[poe_client_id].client.mac, + ) + switches[poe_client_id].async_schedule_update_ha_state() continue client = controller.api.clients[client_id] @@ -138,7 +140,6 @@ def update_items(controller, async_add_entities, switches, switches_off): switches[poe_client_id] = UniFiPOEClientSwitch(client, controller) new_switches.append(switches[poe_client_id]) - LOGGER.debug("New UniFi POE switch %s (%s)", client.hostname, client.mac) if new_switches: async_add_entities(new_switches) @@ -179,6 +180,7 @@ def __init__(self, client, controller): async def async_added_to_hass(self): """Call when entity about to be added to Home Assistant.""" + LOGGER.debug("New UniFi POE switch %s (%s)", self.name, self.client.mac) state = await self.async_get_last_state() if state is None: @@ -193,6 +195,16 @@ async def async_added_to_hass(self): if not self.client.sw_port: self.client.raw["sw_port"] = state.attributes["port"] + async def async_update(self): + """Log client information after update.""" + await super().async_update() + + LOGGER.debug( + "Updating UniFi POE controlled client %s\n%s", + self.entity_id, + pformat(self.client.raw), + ) + @property def unique_id(self): """Return a unique identifier for this switch.""" @@ -252,6 +264,10 @@ def port(self): class UniFiBlockClientSwitch(UniFiClient, SwitchDevice): """Representation of a blockable client.""" + async def async_added_to_hass(self): + """Call when entity about to be added to Home Assistant.""" + LOGGER.debug("New UniFi Block switch %s (%s)", self.name, self.client.mac) + @property def unique_id(self): """Return a unique identifier for this switch.""" diff --git a/homeassistant/components/unifi_direct/device_tracker.py b/homeassistant/components/unifi_direct/device_tracker.py index a526cc926d31..558a99811717 100644 --- a/homeassistant/components/unifi_direct/device_tracker.py +++ b/homeassistant/components/unifi_direct/device_tracker.py @@ -1,16 +1,17 @@ """Support for Unifi AP direct access.""" -import logging import json +import logging +from pexpect import exceptions, pxssh 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_PORT +from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_USERNAME +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -74,7 +75,6 @@ def get_device_name(self, device): def _connect(self): """Connect to the Unifi AP SSH server.""" - from pexpect import pxssh, exceptions self.ssh = pxssh.pxssh() try: @@ -98,7 +98,6 @@ def _disconnect(self): self.connected = False def _get_update(self): - from pexpect import pxssh, exceptions try: if not self.connected: diff --git a/homeassistant/components/universal/media_player.py b/homeassistant/components/universal/media_player.py index 09771b551acd..63e3ff7448d8 100644 --- a/homeassistant/components/universal/media_player.py +++ b/homeassistant/components/universal/media_player.py @@ -255,7 +255,10 @@ def state(self): @property def volume_level(self): """Volume level of entity specified in attributes or active child.""" - return self._override_or_child_attr(ATTR_MEDIA_VOLUME_LEVEL) + try: + return float(self._override_or_child_attr(ATTR_MEDIA_VOLUME_LEVEL)) + except (TypeError, ValueError): + return None @property def is_volume_muted(self): diff --git a/homeassistant/components/upcloud/__init__.py b/homeassistant/components/upcloud/__init__.py index c77b0fe3cddf..cd2cf5d02c01 100644 --- a/homeassistant/components/upcloud/__init__.py +++ b/homeassistant/components/upcloud/__init__.py @@ -18,6 +18,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect, dispatcher_send from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import track_time_interval +from homeassistant.util import dt _LOGGER = logging.getLogger(__name__) @@ -82,6 +83,7 @@ def upcloud_update(event_time): dispatcher_send(hass, SIGNAL_UPDATE_UPCLOUD) # Call the UpCloud API to refresh data + upcloud_update(dt.utcnow()) track_time_interval(hass, upcloud_update, scan_interval) return True @@ -108,6 +110,7 @@ def __init__(self, upcloud, uuid): self._upcloud = upcloud self.uuid = uuid self.data = None + self._unsub_handlers = [] @property def unique_id(self) -> str: @@ -124,10 +127,18 @@ def name(self): async def async_added_to_hass(self): """Register callbacks.""" - async_dispatcher_connect( - self.hass, SIGNAL_UPDATE_UPCLOUD, self._update_callback + self._unsub_handlers.append( + async_dispatcher_connect( + self.hass, SIGNAL_UPDATE_UPCLOUD, self._update_callback + ) ) + async def async_will_remove_from_hass(self) -> None: + """Invoke unsubscription handlers.""" + for unsub in self._unsub_handlers: + unsub() + self._unsub_handlers.clear() + @callback def _update_callback(self): """Call update method.""" diff --git a/homeassistant/components/upcloud/manifest.json b/homeassistant/components/upcloud/manifest.json index 62ce608a911f..0499ce1e9ad8 100644 --- a/homeassistant/components/upcloud/manifest.json +++ b/homeassistant/components/upcloud/manifest.json @@ -1,9 +1,9 @@ { "domain": "upcloud", - "name": "Upcloud", + "name": "UpCloud", "documentation": "https://www.home-assistant.io/integrations/upcloud", "requirements": [ - "upcloud-api==0.4.3" + "upcloud-api==0.4.5" ], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/upcloud/switch.py b/homeassistant/components/upcloud/switch.py index 66f3d9f42b17..5cb1d86671e4 100644 --- a/homeassistant/components/upcloud/switch.py +++ b/homeassistant/components/upcloud/switch.py @@ -6,8 +6,9 @@ from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchDevice from homeassistant.const import STATE_OFF import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.dispatcher import dispatcher_send -from . import CONF_SERVERS, DATA_UPCLOUD, UpCloudServerEntity +from . import CONF_SERVERS, DATA_UPCLOUD, SIGNAL_UPDATE_UPCLOUD, UpCloudServerEntity _LOGGER = logging.getLogger(__name__) @@ -34,6 +35,7 @@ def turn_on(self, **kwargs): """Start the server.""" if self.state == STATE_OFF: self.data.start() + dispatcher_send(self.hass, SIGNAL_UPDATE_UPCLOUD) def turn_off(self, **kwargs): """Stop the server.""" diff --git a/homeassistant/components/updater/__init__.py b/homeassistant/components/updater/__init__.py index 22c11d0c38ef..08f08e1bb64f 100644 --- a/homeassistant/components/updater/__init__.py +++ b/homeassistant/components/updater/__init__.py @@ -2,7 +2,7 @@ import asyncio from datetime import timedelta -# pylint: disable=import-error,no-name-in-module +# pylint: disable=import-error from distutils.version import StrictVersion import json import logging diff --git a/homeassistant/components/upnp/__init__.py b/homeassistant/components/upnp/__init__.py index bbb49ebd1d44..9a7e06738dba 100644 --- a/homeassistant/components/upnp/__init__.py +++ b/homeassistant/components/upnp/__init__.py @@ -7,11 +7,12 @@ from homeassistant import config_entries from homeassistant.config_entries import ConfigEntry from homeassistant.const import EVENT_HOMEASSISTANT_STOP -from homeassistant.helpers import config_validation as cv -from homeassistant.helpers import device_registry as dr -from homeassistant.helpers import dispatcher -from homeassistant.helpers.typing import ConfigType -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.helpers import ( + config_validation as cv, + device_registry as dr, + dispatcher, +) +from homeassistant.helpers.typing import ConfigType, HomeAssistantType from homeassistant.util import get_local_ip from .const import ( @@ -20,10 +21,10 @@ CONF_HASS, CONF_LOCAL_IP, CONF_PORTS, + DOMAIN, + LOGGER as _LOGGER, SIGNAL_REMOVE_SENSOR, ) -from .const import DOMAIN -from .const import LOGGER as _LOGGER from .device import Device NOTIFICATION_ID = "upnp_notification" diff --git a/homeassistant/components/upnp/config_flow.py b/homeassistant/components/upnp/config_flow.py index cdcce76dbc38..1601595b6a9f 100644 --- a/homeassistant/components/upnp/config_flow.py +++ b/homeassistant/components/upnp/config_flow.py @@ -1,11 +1,10 @@ """Config flow for UPNP.""" -from homeassistant.helpers import config_entry_flow from homeassistant import config_entries +from homeassistant.helpers import config_entry_flow from .const import DOMAIN from .device import Device - config_entry_flow.register_discovery_flow( DOMAIN, "UPnP/IGD", Device.async_discover, config_entries.CONN_CLASS_LOCAL_POLL ) diff --git a/homeassistant/components/upnp/device.py b/homeassistant/components/upnp/device.py index de3c93a82ed3..fffee57b4118 100644 --- a/homeassistant/components/upnp/device.py +++ b/homeassistant/components/upnp/device.py @@ -3,13 +3,14 @@ from ipaddress import IPv4Address import aiohttp +from async_upnp_client import UpnpError, UpnpFactory +from async_upnp_client.aiohttp import AiohttpSessionRequester from async_upnp_client.profiles.igd import IgdDevice from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.typing import HomeAssistantType -from .const import LOGGER as _LOGGER -from .const import DOMAIN, CONF_LOCAL_IP +from .const import CONF_LOCAL_IP, DOMAIN, LOGGER as _LOGGER class Device: @@ -48,14 +49,10 @@ async def async_discover(cls, hass: HomeAssistantType): async def async_create_device(cls, hass: HomeAssistantType, ssdp_description: str): """Create UPnP/IGD device.""" # build async_upnp_client requester - from async_upnp_client.aiohttp import AiohttpSessionRequester - session = async_get_clientsession(hass) requester = AiohttpSessionRequester(session, True) # create async_upnp_client device - from async_upnp_client import UpnpFactory - factory = UpnpFactory(requester, disable_state_variable_validation=True) upnp_device = await factory.async_create_device(ssdp_description) @@ -99,8 +96,6 @@ async def async_add_port_mappings(self, ports, local_ip): async def _async_add_port_mapping(self, external_port, local_ip, internal_port): """Add a port mapping.""" # create port mapping - from async_upnp_client import UpnpError - _LOGGER.info( "Creating port mapping %s:%s:%s (TCP)", external_port, @@ -135,8 +130,6 @@ async def async_delete_port_mappings(self): async def _async_delete_port_mapping(self, external_port): """Remove a port mapping.""" - from async_upnp_client import UpnpError - _LOGGER.info("Deleting port mapping %s (TCP)", external_port) try: await self._igd_device.async_delete_port_mapping( @@ -157,7 +150,6 @@ async def async_get_total_bytes_sent(self): async def async_get_total_packets_received(self): """Get total packets received.""" - # pylint: disable=invalid-name return await self._igd_device.async_get_total_packets_received() async def async_get_total_packets_sent(self): diff --git a/homeassistant/components/uptimerobot/binary_sensor.py b/homeassistant/components/uptimerobot/binary_sensor.py index 2075d9304946..401da496d2f8 100644 --- a/homeassistant/components/uptimerobot/binary_sensor.py +++ b/homeassistant/components/uptimerobot/binary_sensor.py @@ -1,6 +1,7 @@ """A platform that to monitor Uptime Robot monitors.""" import logging +from pyuptimerobot import UptimeRobot import voluptuous as vol from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorDevice @@ -18,7 +19,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Uptime Robot binary_sensors.""" - from pyuptimerobot import UptimeRobot up_robot = UptimeRobot() api_key = config.get(CONF_API_KEY) diff --git a/homeassistant/components/usgs_earthquakes_feed/geo_location.py b/homeassistant/components/usgs_earthquakes_feed/geo_location.py index 7890243c1e0a..37934db30525 100644 --- a/homeassistant/components/usgs_earthquakes_feed/geo_location.py +++ b/homeassistant/components/usgs_earthquakes_feed/geo_location.py @@ -3,6 +3,9 @@ import logging from typing import Optional +from geojson_client.usgs_earthquake_hazards_program_feed import ( + UsgsEarthquakeHazardsProgramFeedManager, +) import voluptuous as vol from homeassistant.components.geo_location import PLATFORM_SCHEMA, GeolocationEvent @@ -122,9 +125,6 @@ def __init__( minimum_magnitude, ): """Initialize the Feed Entity Manager.""" - from geojson_client.usgs_earthquake_hazards_program_feed import ( - UsgsEarthquakeHazardsProgramFeedManager, - ) self._hass = hass self._feed_manager = UsgsEarthquakeHazardsProgramFeedManager( diff --git a/homeassistant/components/utility_meter/__init__.py b/homeassistant/components/utility_meter/__init__.py index 17eacc326d3b..04e472a78286 100644 --- a/homeassistant/components/utility_meter/__init__.py +++ b/homeassistant/components/utility_meter/__init__.py @@ -7,7 +7,6 @@ from homeassistant.const import CONF_NAME import homeassistant.helpers.config_validation as cv from homeassistant.helpers import discovery -from homeassistant.helpers.config_validation import ENTITY_SERVICE_SCHEMA from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.restore_state import RestoreEntity @@ -39,11 +38,8 @@ DEFAULT_OFFSET = timedelta(hours=0) -SERVICE_SELECT_TARIFF_SCHEMA = ENTITY_SERVICE_SCHEMA.extend( - {vol.Required(ATTR_TARIFF): cv.string} -) -METER_CONFIG_SCHEMA = ENTITY_SERVICE_SCHEMA.extend( +METER_CONFIG_SCHEMA = vol.Schema( { vol.Required(CONF_SOURCE_SENSOR): cv.entity_id, vol.Optional(CONF_NAME): cv.string, @@ -110,16 +106,16 @@ async def async_setup(hass, config): register_services = True if register_services: - component.async_register_entity_service( - SERVICE_RESET, ENTITY_SERVICE_SCHEMA, "async_reset_meters" - ) + component.async_register_entity_service(SERVICE_RESET, {}, "async_reset_meters") component.async_register_entity_service( - SERVICE_SELECT_TARIFF, SERVICE_SELECT_TARIFF_SCHEMA, "async_select_tariff" + SERVICE_SELECT_TARIFF, + {vol.Required(ATTR_TARIFF): cv.string}, + "async_select_tariff", ) component.async_register_entity_service( - SERVICE_SELECT_NEXT_TARIFF, ENTITY_SERVICE_SCHEMA, "async_next_tariff" + SERVICE_SELECT_NEXT_TARIFF, {}, "async_next_tariff" ) return True diff --git a/homeassistant/components/uvc/camera.py b/homeassistant/components/uvc/camera.py index 20aae3849ab3..b9a6262cd4f3 100644 --- a/homeassistant/components/uvc/camera.py +++ b/homeassistant/components/uvc/camera.py @@ -3,12 +3,13 @@ import socket import requests +from uvcclient import camera as uvc_camera, nvr import voluptuous as vol +from homeassistant.components.camera import PLATFORM_SCHEMA, Camera from homeassistant.const import CONF_PORT, CONF_SSL -from homeassistant.components.camera import Camera, PLATFORM_SCHEMA -import homeassistant.helpers.config_validation as cv from homeassistant.exceptions import PlatformNotReady +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -39,8 +40,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): port = config[CONF_PORT] ssl = config[CONF_SSL] - from uvcclient import nvr - try: # Exceptions may be raised in all method calls to the nvr library. nvrconn = nvr.UVCRemote(addr, port, key, ssl=ssl) @@ -76,10 +75,10 @@ def setup_platform(hass, config, add_entities, discovery_info=None): class UnifiVideoCamera(Camera): """A Ubiquiti Unifi Video Camera.""" - def __init__(self, nvr, uuid, name, password): + def __init__(self, camera, uuid, name, password): """Initialize an Unifi camera.""" super().__init__() - self._nvr = nvr + self._nvr = camera self._uuid = uuid self._name = name self._password = password @@ -118,7 +117,6 @@ def model(self): def _login(self): """Login to the camera.""" - from uvcclient import camera as uvc_camera caminfo = self._nvr.get_camera(self._uuid) if self._connect_addr: @@ -160,7 +158,6 @@ def _login(self): def camera_image(self): """Return the image of this camera.""" - from uvcclient import camera as uvc_camera if not self._camera: if not self._login(): @@ -182,7 +179,6 @@ def _get_image(retry=True): def set_motion_detection(self, mode): """Set motion detection on or off.""" - from uvcclient.nvr import NvrError if mode is True: set_mode = "motion" @@ -192,7 +188,7 @@ def set_motion_detection(self, mode): try: self._nvr.set_recordmode(self._uuid, set_mode) self._motion_status = mode - except NvrError as err: + except nvr.NvrError as err: _LOGGER.error("Unable to set recordmode to %s", set_mode) _LOGGER.debug(err) diff --git a/homeassistant/components/vacuum/.translations/bg.json b/homeassistant/components/vacuum/.translations/bg.json new file mode 100644 index 000000000000..2d422284a389 --- /dev/null +++ b/homeassistant/components/vacuum/.translations/bg.json @@ -0,0 +1,16 @@ +{ + "device_automation": { + "action_type": { + "clean": "\u041d\u0435\u043a\u0430 {entity_name} \u043f\u043e\u0447\u0438\u0441\u0442\u0438", + "dock": "\u041d\u0435\u043a\u0430 {entity_name} \u0434\u0430 \u0441\u0435 \u0432\u044a\u0440\u043d\u0435 \u0432 \u0431\u0430\u0437\u043e\u0432\u0430\u0442\u0430 \u0441\u0442\u0430\u043d\u0446\u0438\u044f" + }, + "condtion_type": { + "is_cleaning": "{entity_name} \u043f\u043e\u0447\u0438\u0441\u0442\u0432\u0430", + "is_docked": "{entity_name} \u0435 \u0432 \u0431\u0430\u0437\u043e\u0432\u0430\u0442\u0430 \u0441\u0442\u0430\u043d\u0446\u0438\u044f" + }, + "trigger_type": { + "cleaning": "{entity_name} \u0437\u0430\u043f\u043e\u0447\u043d\u0430 \u043f\u043e\u0447\u0438\u0441\u0442\u0432\u0430\u043d\u0435", + "docked": "{entity_name} \u0432 \u0431\u0430\u0437\u043e\u0432\u0430 \u0441\u0442\u0430\u043d\u0446\u0438\u044f" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vacuum/.translations/ca.json b/homeassistant/components/vacuum/.translations/ca.json index df2ea439e0e1..ee69152ed5c5 100644 --- a/homeassistant/components/vacuum/.translations/ca.json +++ b/homeassistant/components/vacuum/.translations/ca.json @@ -1,10 +1,16 @@ { "device_automation": { + "action_type": { + "clean": "Fes que {entity_name} netegi", + "dock": "Fes que {entity_name} torni a la base" + }, "condtion_type": { - "is_cleaning": "{entity_name} est\u00e0 netejant" + "is_cleaning": "{entity_name} est\u00e0 netejant", + "is_docked": "{entity_name} est\u00e0 acoblada" }, "trigger_type": { - "cleaning": "{entity_name} ha comen\u00e7at a netejar" + "cleaning": "{entity_name} ha comen\u00e7at a netejar", + "docked": "{entity_name} acoblada" } } } \ No newline at end of file diff --git a/homeassistant/components/vacuum/.translations/de.json b/homeassistant/components/vacuum/.translations/de.json index 060358a0a7a4..7aed7da23e3f 100644 --- a/homeassistant/components/vacuum/.translations/de.json +++ b/homeassistant/components/vacuum/.translations/de.json @@ -1,7 +1,16 @@ { "device_automation": { + "action_type": { + "clean": "Lass {entity_name} reinigen", + "dock": "Lass {entity_name} zum Dock zur\u00fcckkehren" + }, "condtion_type": { - "is_cleaning": "{entity_name} reinigt" + "is_cleaning": "{entity_name} reinigt", + "is_docked": "{entity_name} ist angedockt" + }, + "trigger_type": { + "cleaning": "{entity_name} hat mit der Reinigung begonnen", + "docked": "{entity_name} angedockt" } } } \ No newline at end of file diff --git a/homeassistant/components/vacuum/.translations/fr.json b/homeassistant/components/vacuum/.translations/fr.json index 44e7b2887e27..4a0ab7f8de72 100644 --- a/homeassistant/components/vacuum/.translations/fr.json +++ b/homeassistant/components/vacuum/.translations/fr.json @@ -6,11 +6,11 @@ }, "condtion_type": { "is_cleaning": "{entity_name} nettoie", - "is_docked": "{entity_name} est sur la base" + "is_docked": "{entity_name} est connect\u00e9" }, "trigger_type": { "cleaning": "{entity_name} commence \u00e0 nettoyer", - "docked": "{entity_name} est sur la base" + "docked": "{entity_name} connect\u00e9" } } } \ No newline at end of file diff --git a/homeassistant/components/vacuum/.translations/nl.json b/homeassistant/components/vacuum/.translations/nl.json new file mode 100644 index 000000000000..3032fc22508f --- /dev/null +++ b/homeassistant/components/vacuum/.translations/nl.json @@ -0,0 +1,10 @@ +{ + "device_automation": { + "condtion_type": { + "is_cleaning": "{entity_name} is aan het schoonmaken" + }, + "trigger_type": { + "cleaning": "{entity_name} begon met schoonmaken" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vacuum/.translations/pl.json b/homeassistant/components/vacuum/.translations/pl.json new file mode 100644 index 000000000000..e637c26b3ede --- /dev/null +++ b/homeassistant/components/vacuum/.translations/pl.json @@ -0,0 +1,16 @@ +{ + "device_automation": { + "action_type": { + "clean": "niech {entity_name} sprz\u0105ta", + "dock": "niech {entity_name} wr\u00f3ci do bazy" + }, + "condtion_type": { + "is_cleaning": "{entity_name} sprz\u0105ta", + "is_docked": "{entity_name} jest w bazie" + }, + "trigger_type": { + "cleaning": "{entity_name} zacznie sprz\u0105ta\u0107", + "docked": "{entity_name} wr\u00f3ci do bazy" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vacuum/.translations/pt.json b/homeassistant/components/vacuum/.translations/pt.json new file mode 100644 index 000000000000..42b8bdabc0f3 --- /dev/null +++ b/homeassistant/components/vacuum/.translations/pt.json @@ -0,0 +1,10 @@ +{ + "device_automation": { + "action_type": { + "clean": "Deixar {entity_name} limpar" + }, + "condtion_type": { + "is_cleaning": "{entity_name} est\u00e1 a limpar" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vacuum/__init__.py b/homeassistant/components/vacuum/__init__.py index ace3f6106060..85b3d665e170 100644 --- a/homeassistant/components/vacuum/__init__.py +++ b/homeassistant/components/vacuum/__init__.py @@ -18,8 +18,8 @@ ) from homeassistant.loader import bind_hass import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.config_validation import ( # noqa - ENTITY_SERVICE_SCHEMA, +from homeassistant.helpers.config_validation import ( # noqa: F401 + make_entity_service_schema, PLATFORM_SCHEMA, PLATFORM_SCHEMA_BASE, ) @@ -55,16 +55,6 @@ SERVICE_PAUSE = "pause" SERVICE_STOP = "stop" -VACUUM_SET_FAN_SPEED_SERVICE_SCHEMA = ENTITY_SERVICE_SCHEMA.extend( - {vol.Required(ATTR_FAN_SPEED): cv.string} -) - -VACUUM_SEND_COMMAND_SERVICE_SCHEMA = ENTITY_SERVICE_SCHEMA.extend( - { - vol.Required(ATTR_COMMAND): cv.string, - vol.Optional(ATTR_PARAMS): vol.Any(dict, cv.ensure_list), - } -) STATE_CLEANING = "cleaning" STATE_DOCKED = "docked" @@ -106,43 +96,32 @@ async def async_setup(hass, config): await component.async_setup(config) + component.async_register_entity_service(SERVICE_TURN_ON, {}, "async_turn_on") + component.async_register_entity_service(SERVICE_TURN_OFF, {}, "async_turn_off") + component.async_register_entity_service(SERVICE_TOGGLE, {}, "async_toggle") component.async_register_entity_service( - SERVICE_TURN_ON, ENTITY_SERVICE_SCHEMA, "async_turn_on" - ) - component.async_register_entity_service( - SERVICE_TURN_OFF, ENTITY_SERVICE_SCHEMA, "async_turn_off" - ) - component.async_register_entity_service( - SERVICE_TOGGLE, ENTITY_SERVICE_SCHEMA, "async_toggle" - ) - component.async_register_entity_service( - SERVICE_START_PAUSE, ENTITY_SERVICE_SCHEMA, "async_start_pause" - ) - component.async_register_entity_service( - SERVICE_START, ENTITY_SERVICE_SCHEMA, "async_start" - ) - component.async_register_entity_service( - SERVICE_PAUSE, ENTITY_SERVICE_SCHEMA, "async_pause" - ) - component.async_register_entity_service( - SERVICE_RETURN_TO_BASE, ENTITY_SERVICE_SCHEMA, "async_return_to_base" - ) - component.async_register_entity_service( - SERVICE_CLEAN_SPOT, ENTITY_SERVICE_SCHEMA, "async_clean_spot" - ) - component.async_register_entity_service( - SERVICE_LOCATE, ENTITY_SERVICE_SCHEMA, "async_locate" + SERVICE_START_PAUSE, {}, "async_start_pause" ) + component.async_register_entity_service(SERVICE_START, {}, "async_start") + component.async_register_entity_service(SERVICE_PAUSE, {}, "async_pause") component.async_register_entity_service( - SERVICE_STOP, ENTITY_SERVICE_SCHEMA, "async_stop" + SERVICE_RETURN_TO_BASE, {}, "async_return_to_base" ) + component.async_register_entity_service(SERVICE_CLEAN_SPOT, {}, "async_clean_spot") + component.async_register_entity_service(SERVICE_LOCATE, {}, "async_locate") + component.async_register_entity_service(SERVICE_STOP, {}, "async_stop") component.async_register_entity_service( SERVICE_SET_FAN_SPEED, - VACUUM_SET_FAN_SPEED_SERVICE_SCHEMA, + {vol.Required(ATTR_FAN_SPEED): cv.string}, "async_set_fan_speed", ) component.async_register_entity_service( - SERVICE_SEND_COMMAND, VACUUM_SEND_COMMAND_SERVICE_SCHEMA, "async_send_command" + SERVICE_SEND_COMMAND, + { + vol.Required(ATTR_COMMAND): cv.string, + vol.Optional(ATTR_PARAMS): vol.Any(dict, cv.ensure_list), + }, + "async_send_command", ) return True diff --git a/homeassistant/components/vacuum/services.yaml b/homeassistant/components/vacuum/services.yaml index fe5bb77cefea..7db70c5cd516 100644 --- a/homeassistant/components/vacuum/services.yaml +++ b/homeassistant/components/vacuum/services.yaml @@ -85,81 +85,3 @@ set_fan_speed: fan_speed: description: Platform dependent vacuum cleaner fan speed, with speed steps, like 'medium' or by percentage, between 0 and 100. example: 'low' - -xiaomi_remote_control_start: - description: Start remote control of the vacuum cleaner. You can then move it with `remote_control_move`, when done call `remote_control_stop`. - fields: - entity_id: - description: Name of the vacuum entity. - example: 'vacuum.xiaomi_vacuum_cleaner' - -xiaomi_remote_control_stop: - description: Stop remote control mode of the vacuum cleaner. - fields: - entity_id: - description: Name of the vacuum entity. - example: 'vacuum.xiaomi_vacuum_cleaner' - -xiaomi_remote_control_move: - description: Remote control the vacuum cleaner, make sure you first set it in remote control mode with `remote_control_start`. - fields: - entity_id: - description: Name of the vacuum entity. - example: 'vacuum.xiaomi_vacuum_cleaner' - velocity: - description: Speed, between -0.29 and 0.29. - example: '0.2' - rotation: - description: Rotation, between -179 degrees and 179 degrees. - example: '90' - duration: - description: Duration of the movement. - example: '1500' - -xiaomi_remote_control_move_step: - description: Remote control the vacuum cleaner, only makes one move and then stops. - fields: - entity_id: - description: Name of the vacuum entity. - example: 'vacuum.xiaomi_vacuum_cleaner' - velocity: - description: Speed, between -0.29 and 0.29. - example: '0.2' - rotation: - description: Rotation, between -179 degrees and 179 degrees. - example: '90' - duration: - description: Duration of the movement. - example: '1500' - -xiaomi_clean_zone: - description: Start the cleaning operation in the selected areas for the number of repeats indicated. - fields: - entity_id: - description: Name of the vacuum entity. - example: 'vacuum.xiaomi_vacuum_cleaner' - zone: - description: Array of zones. Each zone is an array of 4 integer values. - example: '[[23510,25311,25110,26362]]' - repeats: - description: Number of cleaning repeats for each zone between 1 and 3. - example: '1' - -neato_custom_cleaning: - description: Zone Cleaning service call specific to Neato Botvacs. - fields: - entity_id: - description: Name of the vacuum entity. [Required] - example: 'vacuum.neato' - mode: - description: "Set the cleaning mode: 1 for eco and 2 for turbo. Defaults to turbo if not set." - example: 2 - navigation: - description: "Set the navigation mode: 1 for normal, 2 for extra care, 3 for deep. Defaults to normal if not set." - example: 1 - category: - description: "Whether to use a persistent map or not for cleaning (i.e. No go lines): 2 for no map, 4 for map. Default to using map if not set (and fallback to no map if no map is found)." - example: 2 - zone: - description: Only supported on the Botvac D7. Name of the zone to clean. Defaults to no zone i.e. complete house cleanup. - example: "Kitchen" diff --git a/homeassistant/components/velux/__init__.py b/homeassistant/components/velux/__init__.py index 51f615e68aaf..bac65c969cff 100644 --- a/homeassistant/components/velux/__init__.py +++ b/homeassistant/components/velux/__init__.py @@ -1,12 +1,12 @@ """Support for VELUX KLF 200 devices.""" import logging + +from pyvlx import PyVLX, PyVLXException import voluptuous as vol -from pyvlx import PyVLX -from pyvlx import PyVLXException +from homeassistant.const import CONF_HOST, CONF_PASSWORD, EVENT_HOMEASSISTANT_STOP from homeassistant.helpers import discovery import homeassistant.helpers.config_validation as cv -from homeassistant.const import CONF_HOST, CONF_PASSWORD, EVENT_HOMEASSISTANT_STOP DOMAIN = "velux" DATA_VELUX = "data_velux" diff --git a/homeassistant/components/velux/cover.py b/homeassistant/components/velux/cover.py index a471960b048d..7d4adc7350c8 100644 --- a/homeassistant/components/velux/cover.py +++ b/homeassistant/components/velux/cover.py @@ -1,4 +1,7 @@ """Support for Velux covers.""" +from pyvlx import OpeningDevice, Position +from pyvlx.opening_device import Awning, Blind, RollerShutter, Window + from homeassistant.components.cover import ( ATTR_POSITION, SUPPORT_CLOSE, @@ -16,7 +19,6 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= """Set up cover(s) for Velux platform.""" entities = [] for node in hass.data[DATA_VELUX].pyvlx.nodes: - from pyvlx import OpeningDevice if isinstance(node, OpeningDevice): entities.append(VeluxCover(node)) @@ -67,8 +69,6 @@ def current_cover_position(self): @property def device_class(self): """Define this cover as either window/blind/awning/shutter.""" - from pyvlx.opening_device import Blind, RollerShutter, Window, Awning - if isinstance(self.node, Window): return "window" if isinstance(self.node, Blind): @@ -96,7 +96,6 @@ async def async_set_cover_position(self, **kwargs): """Move the cover to a specific position.""" if ATTR_POSITION in kwargs: position_percent = 100 - kwargs[ATTR_POSITION] - from pyvlx import Position await self.node.set_position( Position(position_percent=position_percent), wait_for_completion=False diff --git a/homeassistant/components/verisure/__init__.py b/homeassistant/components/verisure/__init__.py index d5cc7f31efb6..3ab98a6560e5 100644 --- a/homeassistant/components/verisure/__init__.py +++ b/homeassistant/components/verisure/__init__.py @@ -2,6 +2,7 @@ import logging import threading from datetime import timedelta + from jsonpath import jsonpath import verisure diff --git a/homeassistant/components/verisure/alarm_control_panel.py b/homeassistant/components/verisure/alarm_control_panel.py index 02f64b6fa9cd..78a09e439d7d 100644 --- a/homeassistant/components/verisure/alarm_control_panel.py +++ b/homeassistant/components/verisure/alarm_control_panel.py @@ -3,6 +3,10 @@ from time import sleep import homeassistant.components.alarm_control_panel as alarm +from homeassistant.components.alarm_control_panel.const import ( + SUPPORT_ALARM_ARM_AWAY, + SUPPORT_ALARM_ARM_HOME, +) from homeassistant.const import ( STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, @@ -33,7 +37,6 @@ def set_arm_state(state, code=None): while "result" not in transaction: sleep(0.5) transaction = hub.session.get_arm_state_transaction(transaction_id) - # pylint: disable=unexpected-keyword-arg hub.update_overview(no_throttle=True) @@ -64,6 +67,11 @@ def state(self): """Return the state of the device.""" return self._state + @property + def supported_features(self) -> int: + """Return the list of supported features.""" + return SUPPORT_ALARM_ARM_HOME | SUPPORT_ALARM_ARM_AWAY + @property def code_format(self): """Return one or more digits/characters.""" diff --git a/homeassistant/components/verisure/binary_sensor.py b/homeassistant/components/verisure/binary_sensor.py index b7c8cbb49a3e..47ec3c536b33 100644 --- a/homeassistant/components/verisure/binary_sensor.py +++ b/homeassistant/components/verisure/binary_sensor.py @@ -1,7 +1,10 @@ """Support for Verisure binary sensors.""" import logging -from homeassistant.components.binary_sensor import BinarySensorDevice +from homeassistant.components.binary_sensor import ( + BinarySensorDevice, + DEVICE_CLASS_CONNECTIVITY, +) from . import CONF_DOOR_WINDOW, HUB as hub @@ -22,6 +25,8 @@ def setup_platform(hass, config, add_entities, discovery_info=None): ) ] ) + + sensors.extend([VerisureEthernetStatus()]) add_entities(sensors) @@ -66,3 +71,32 @@ def available(self): def update(self): """Update the state of the sensor.""" hub.update_overview() + + +class VerisureEthernetStatus(BinarySensorDevice): + """Representation of a Verisure VBOX internet status.""" + + @property + def name(self): + """Return the name of the binary sensor.""" + return "Verisure Ethernet status" + + @property + def is_on(self): + """Return the state of the sensor.""" + return hub.get_first("$.ethernetConnectedNow") + + @property + def available(self): + """Return True if entity is available.""" + return hub.get_first("$.ethernetConnectedNow") is not None + + # pylint: disable=no-self-use + def update(self): + """Update the state of the sensor.""" + hub.update_overview() + + @property + def device_class(self): + """Return the class of this device, from component DEVICE_CLASSES.""" + return DEVICE_CLASS_CONNECTIVITY diff --git a/homeassistant/components/verisure/manifest.json b/homeassistant/components/verisure/manifest.json index 38ea8c731476..13962e81b7bb 100644 --- a/homeassistant/components/verisure/manifest.json +++ b/homeassistant/components/verisure/manifest.json @@ -3,8 +3,8 @@ "name": "Verisure", "documentation": "https://www.home-assistant.io/integrations/verisure", "requirements": [ - "jsonpath==0.75", - "vsure==1.5.2" + "jsonpath==0.82", + "vsure==1.5.4" ], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/versasense/__init__.py b/homeassistant/components/versasense/__init__.py new file mode 100644 index 000000000000..4f378f4ab00c --- /dev/null +++ b/homeassistant/components/versasense/__init__.py @@ -0,0 +1,97 @@ +"""Support for VersaSense MicroPnP devices.""" +import logging + +import pyversasense as pyv +import voluptuous as vol + +from homeassistant.const import CONF_HOST +from homeassistant.helpers import aiohttp_client +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.discovery import async_load_platform + +from .const import ( + PERIPHERAL_CLASS_SENSOR, + PERIPHERAL_CLASS_SENSOR_ACTUATOR, + KEY_IDENTIFIER, + KEY_PARENT_NAME, + KEY_PARENT_MAC, + KEY_UNIT, + KEY_MEASUREMENT, + KEY_CONSUMER, +) + +_LOGGER = logging.getLogger(__name__) + +DOMAIN = "versasense" + +# Validation of the user's configuration +CONFIG_SCHEMA = vol.Schema( + {DOMAIN: vol.Schema({vol.Required(CONF_HOST): cv.string})}, extra=vol.ALLOW_EXTRA +) + + +async def async_setup(hass, config): + """Set up the versasense component.""" + session = aiohttp_client.async_get_clientsession(hass) + consumer = pyv.Consumer(config[DOMAIN]["host"], session) + + hass.data[DOMAIN] = {KEY_CONSUMER: consumer} + + await _configure_entities(hass, config, consumer) + + # Return boolean to indicate that initialization was successful. + return True + + +async def _configure_entities(hass, config, consumer): + """Fetch all devices with their peripherals for representation.""" + devices = await consumer.fetchDevices() + _LOGGER.debug(devices) + + sensor_info_list = [] + switch_info_list = [] + + for mac, device in devices.items(): + _LOGGER.info("Device connected: %s %s", device.name, mac) + hass.data[DOMAIN][mac] = {} + + for peripheral_id, peripheral in device.peripherals.items(): + hass.data[DOMAIN][mac][peripheral_id] = peripheral + + if peripheral.classification == PERIPHERAL_CLASS_SENSOR: + sensor_info_list = _add_entity_info_to_list( + peripheral, device, sensor_info_list + ) + elif peripheral.classification == PERIPHERAL_CLASS_SENSOR_ACTUATOR: + switch_info_list = _add_entity_info_to_list( + peripheral, device, switch_info_list + ) + + if sensor_info_list: + _load_platform(hass, config, "sensor", sensor_info_list) + + if switch_info_list: + _load_platform(hass, config, "switch", switch_info_list) + + +def _add_entity_info_to_list(peripheral, device, entity_info_list): + """Add info from a peripheral to specified list.""" + for measurement in peripheral.measurements: + entity_info = { + KEY_IDENTIFIER: peripheral.identifier, + KEY_UNIT: measurement.unit, + KEY_MEASUREMENT: measurement.name, + KEY_PARENT_NAME: device.name, + KEY_PARENT_MAC: device.mac, + } + + entity_info_list.append(entity_info) + + return entity_info_list + + +def _load_platform(hass, config, entity_type, entity_info_list): + """Load platform with list of entity info.""" + hass.async_create_task( + async_load_platform(hass, entity_type, DOMAIN, entity_info_list, config) + ) diff --git a/homeassistant/components/versasense/const.py b/homeassistant/components/versasense/const.py new file mode 100644 index 000000000000..5283f61ac261 --- /dev/null +++ b/homeassistant/components/versasense/const.py @@ -0,0 +1,11 @@ +"""Constants for versasense.""" +KEY_CONSUMER = "consumer" +KEY_IDENTIFIER = "identifier" +KEY_MEASUREMENT = "measurement" +KEY_PARENT_MAC = "parent_mac" +KEY_PARENT_NAME = "parent_name" +KEY_UNIT = "unit" +PERIPHERAL_CLASS_SENSOR = "sensor" +PERIPHERAL_CLASS_SENSOR_ACTUATOR = "sensor-actuator" +PERIPHERAL_STATE_OFF = "OFF" +PERIPHERAL_STATE_ON = "ON" diff --git a/homeassistant/components/versasense/manifest.json b/homeassistant/components/versasense/manifest.json new file mode 100644 index 000000000000..3e2be6131d11 --- /dev/null +++ b/homeassistant/components/versasense/manifest.json @@ -0,0 +1,8 @@ +{ + "domain": "versasense", + "name": "VersaSense", + "documentation": "https://www.home-assistant.io/components/versasense", + "dependencies": [], + "codeowners": ["@flamm3blemuff1n"], + "requirements": ["pyversasense==0.0.6"] +} diff --git a/homeassistant/components/versasense/sensor.py b/homeassistant/components/versasense/sensor.py new file mode 100644 index 000000000000..4253bfcbba4c --- /dev/null +++ b/homeassistant/components/versasense/sensor.py @@ -0,0 +1,97 @@ +"""Support for VersaSense sensor peripheral.""" +import logging + +from homeassistant.helpers.entity import Entity + +from . import DOMAIN +from .const import ( + KEY_IDENTIFIER, + KEY_PARENT_NAME, + KEY_PARENT_MAC, + KEY_UNIT, + KEY_MEASUREMENT, + KEY_CONSUMER, +) + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Set up the sensor platform.""" + if discovery_info is None: + return None + + consumer = hass.data[DOMAIN][KEY_CONSUMER] + + sensor_list = [] + + for entity_info in discovery_info: + peripheral = hass.data[DOMAIN][entity_info[KEY_PARENT_MAC]][ + entity_info[KEY_IDENTIFIER] + ] + parent_name = entity_info[KEY_PARENT_NAME] + unit = entity_info[KEY_UNIT] + measurement = entity_info[KEY_MEASUREMENT] + + sensor_list.append( + VSensor(peripheral, parent_name, unit, measurement, consumer) + ) + + async_add_entities(sensor_list) + + +class VSensor(Entity): + """Representation of a Sensor.""" + + def __init__(self, peripheral, parent_name, unit, measurement, consumer): + """Initialize the sensor.""" + self._state = None + self._available = True + self._name = f"{parent_name} {measurement}" + self._parent_mac = peripheral.parentMac + self._identifier = peripheral.identifier + self._unit = unit + self._measurement = measurement + self.consumer = consumer + + @property + def unique_id(self): + """Return the unique id of the sensor.""" + return f"{self._parent_mac}/{self._identifier}/{self._measurement}" + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @property + def state(self): + """Return the state of the sensor.""" + return self._state + + @property + def unit_of_measurement(self): + """Return the unit of measurement.""" + return self._unit + + @property + def available(self): + """Return if the sensor is available.""" + return self._available + + async def async_update(self): + """Fetch new state data for the sensor.""" + samples = await self.consumer.fetchPeripheralSample( + None, self._identifier, self._parent_mac + ) + + if samples is not None: + for sample in samples: + if sample.measurement == self._measurement: + self._available = True + self._state = sample.value + break + else: + _LOGGER.error("Sample unavailable") + self._available = False + self._state = None diff --git a/homeassistant/components/versasense/switch.py b/homeassistant/components/versasense/switch.py new file mode 100644 index 000000000000..4ea118a6c97c --- /dev/null +++ b/homeassistant/components/versasense/switch.py @@ -0,0 +1,113 @@ +"""Support for VersaSense actuator peripheral.""" +import logging + +from homeassistant.components.switch import SwitchDevice + +from . import DOMAIN +from .const import ( + PERIPHERAL_STATE_ON, + PERIPHERAL_STATE_OFF, + KEY_IDENTIFIER, + KEY_PARENT_NAME, + KEY_PARENT_MAC, + KEY_UNIT, + KEY_MEASUREMENT, + KEY_CONSUMER, +) + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Set up actuator platform.""" + if discovery_info is None: + return None + + consumer = hass.data[DOMAIN][KEY_CONSUMER] + + actuator_list = [] + + for entity_info in discovery_info: + peripheral = hass.data[DOMAIN][entity_info[KEY_PARENT_MAC]][ + entity_info[KEY_IDENTIFIER] + ] + parent_name = entity_info[KEY_PARENT_NAME] + unit = entity_info[KEY_UNIT] + measurement = entity_info[KEY_MEASUREMENT] + + actuator_list.append( + VActuator(peripheral, parent_name, unit, measurement, consumer) + ) + + async_add_entities(actuator_list) + + +class VActuator(SwitchDevice): + """Representation of an Actuator.""" + + def __init__(self, peripheral, parent_name, unit, measurement, consumer): + """Initialize the sensor.""" + self._is_on = False + self._available = True + self._name = f"{parent_name} {measurement}" + self._parent_mac = peripheral.parentMac + self._identifier = peripheral.identifier + self._unit = unit + self._measurement = measurement + self.consumer = consumer + + @property + def unique_id(self): + """Return the unique id of the actuator.""" + return f"{self._parent_mac}/{self._identifier}/{self._measurement}" + + @property + def name(self): + """Return the name of the actuator.""" + return self._name + + @property + def is_on(self): + """Return the state of the actuator.""" + return self._is_on + + @property + def available(self): + """Return if the actuator is available.""" + return self._available + + async def async_turn_off(self, **kwargs): + """Turn off the actuator.""" + await self.update_state(0) + + async def async_turn_on(self, **kwargs): + """Turn on the actuator.""" + await self.update_state(1) + + async def update_state(self, state): + """Update the state of the actuator.""" + payload = {"id": "state-num", "value": state} + + await self.consumer.actuatePeripheral( + None, self._identifier, self._parent_mac, payload + ) + + async def async_update(self): + """Fetch state data from the actuator.""" + samples = await self.consumer.fetchPeripheralSample( + None, self._identifier, self._parent_mac + ) + + if samples is not None: + for sample in samples: + if sample.measurement == self._measurement: + self._available = True + if sample.value == PERIPHERAL_STATE_OFF: + self._is_on = False + elif sample.value == PERIPHERAL_STATE_ON: + self._is_on = True + break + else: + _LOGGER.error("Sample unavailable") + self._available = False + self._is_on = None diff --git a/homeassistant/components/version/sensor.py b/homeassistant/components/version/sensor.py index 3e00b87e9840..a6e43251b54a 100644 --- a/homeassistant/components/version/sensor.py +++ b/homeassistant/components/version/sensor.py @@ -2,6 +2,13 @@ import logging from datetime import timedelta +from pyhaversion import ( + LocalVersion, + DockerVersion, + HassioVersion, + PyPiVersion, + HaIoVersion, +) import voluptuous as vol import homeassistant.helpers.config_validation as cv @@ -24,6 +31,8 @@ "raspberrypi2", "raspberrypi3", "raspberrypi3-64", + "raspberrypi4", + "raspberrypi4-64", "tinker", "odroid-c2", "odroid-xu", @@ -54,13 +63,6 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the Version sensor platform.""" - from pyhaversion import ( - LocalVersion, - DockerVersion, - HassioVersion, - PyPiVersion, - HaIoVersion, - ) beta = config.get(CONF_BETA) image = config.get(CONF_IMAGE) diff --git a/homeassistant/components/vesync/.translations/lb.json b/homeassistant/components/vesync/.translations/lb.json index cfccd8b1dbb4..0825bd0805d6 100644 --- a/homeassistant/components/vesync/.translations/lb.json +++ b/homeassistant/components/vesync/.translations/lb.json @@ -12,7 +12,7 @@ "password": "Passwuert", "username": "E-Mail Adresse" }, - "title": "Benotznumm a Passwuert aginn" + "title": "Benotzernumm a Passwuert aginn" } }, "title": "VeSync" diff --git a/homeassistant/components/vesync/.translations/pt.json b/homeassistant/components/vesync/.translations/pt.json new file mode 100644 index 000000000000..395907056e9c --- /dev/null +++ b/homeassistant/components/vesync/.translations/pt.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "invalid_login": "Nome de utilizador ou palavra passe incorretos" + }, + "step": { + "user": { + "data": { + "password": "Palavra-passe", + "username": "Endere\u00e7o de e-mail" + }, + "title": "Introduza o nome de utilizador e a palavra-passe" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vicare/climate.py b/homeassistant/components/vicare/climate.py index 7e330383b30d..4f6f0cedcd98 100644 --- a/homeassistant/components/vicare/climate.py +++ b/homeassistant/components/vicare/climate.py @@ -1,7 +1,6 @@ """Viessmann ViCare climate device.""" import logging import requests -import simplejson from homeassistant.components.climate import ClimateDevice from homeassistant.components.climate.const import ( @@ -169,7 +168,7 @@ def update(self): ] = self._api.getReturnTemperature() except requests.exceptions.ConnectionError: _LOGGER.error("Unable to retrieve data from ViCare server") - except simplejson.errors.JSONDecodeError: + except ValueError: _LOGGER.error("Unable to decode data from ViCare server") @property diff --git a/homeassistant/components/vicare/water_heater.py b/homeassistant/components/vicare/water_heater.py index 1f56c46dc1ce..eefacf99c396 100644 --- a/homeassistant/components/vicare/water_heater.py +++ b/homeassistant/components/vicare/water_heater.py @@ -1,7 +1,6 @@ """Viessmann ViCare water_heater device.""" import logging import requests -import simplejson from homeassistant.components.water_heater import ( SUPPORT_TARGET_TEMPERATURE, @@ -93,7 +92,7 @@ def update(self): self._current_mode = self._api.getActiveMode() except requests.exceptions.ConnectionError: _LOGGER.error("Unable to retrieve data from ViCare server") - except simplejson.errors.JSONDecodeError: + except ValueError: _LOGGER.error("Unable to decode data from ViCare server") @property diff --git a/homeassistant/components/vivotek/camera.py b/homeassistant/components/vivotek/camera.py index c39a9b495bda..2e604199dd81 100644 --- a/homeassistant/components/vivotek/camera.py +++ b/homeassistant/components/vivotek/camera.py @@ -19,12 +19,13 @@ _LOGGER = logging.getLogger(__name__) CONF_FRAMERATE = "framerate" - +CONF_SECURITY_LEVEL = "security_level" CONF_STREAM_PATH = "stream_path" DEFAULT_CAMERA_BRAND = "Vivotek" DEFAULT_NAME = "Vivotek Camera" DEFAULT_EVENT_0_KEY = "event_i0_enable" +DEFAULT_SECURITY_LEVEL = "admin" DEFAULT_STREAM_SOURCE = "live.sdp" PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( @@ -36,6 +37,7 @@ vol.Optional(CONF_SSL, default=False): cv.boolean, vol.Optional(CONF_VERIFY_SSL, default=True): cv.boolean, vol.Optional(CONF_FRAMERATE, default=2): cv.positive_int, + vol.Optional(CONF_SECURITY_LEVEL, default=DEFAULT_SECURITY_LEVEL): cv.string, vol.Optional(CONF_STREAM_PATH, default=DEFAULT_STREAM_SOURCE): cv.string, } ) @@ -52,6 +54,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): verify_ssl=config[CONF_VERIFY_SSL], usr=config[CONF_USERNAME], pwd=config[CONF_PASSWORD], + sec_lvl=config[CONF_SECURITY_LEVEL], ), stream_source=f"rtsp://{creds}@{config[CONF_IP_ADDRESS]}:554/{config[CONF_STREAM_PATH]}", ) diff --git a/homeassistant/components/vivotek/manifest.json b/homeassistant/components/vivotek/manifest.json index ff4989911274..c97a8461da92 100644 --- a/homeassistant/components/vivotek/manifest.json +++ b/homeassistant/components/vivotek/manifest.json @@ -3,7 +3,7 @@ "name": "Vivotek", "documentation": "https://www.home-assistant.io/integrations/vivotek", "requirements": [ - "libpyvivotek==0.2.2" + "libpyvivotek==0.3.1" ], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/vizio/media_player.py b/homeassistant/components/vizio/media_player.py index f64fd2ca531c..94601216f23c 100644 --- a/homeassistant/components/vizio/media_player.py +++ b/homeassistant/components/vizio/media_player.py @@ -2,11 +2,12 @@ from datetime import timedelta import logging -import voluptuous as vol from pyvizio import Vizio +from requests.packages import urllib3 +import voluptuous as vol from homeassistant import util -from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA +from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice from homeassistant.components.media_player.const import ( SUPPORT_NEXT_TRACK, SUPPORT_PREVIOUS_TRACK, @@ -110,8 +111,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): return if config[CONF_SUPPRESS_WARNING]: - from requests.packages import urllib3 - _LOGGER.warning( "InsecureRequestWarning is disabled " "because of Vizio platform configuration" diff --git a/homeassistant/components/volkszaehler/sensor.py b/homeassistant/components/volkszaehler/sensor.py index 6a0815a9b140..a2da620c1a56 100644 --- a/homeassistant/components/volkszaehler/sensor.py +++ b/homeassistant/components/volkszaehler/sensor.py @@ -2,6 +2,8 @@ from datetime import timedelta import logging +from volkszaehler import Volkszaehler +from volkszaehler.exceptions import VolkszaehlerApiConnectionError import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -51,7 +53,6 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the Volkszaehler sensors.""" - from volkszaehler import Volkszaehler host = config[CONF_HOST] name = config[CONF_NAME] @@ -130,7 +131,6 @@ def __init__(self, api): @Throttle(MIN_TIME_BETWEEN_UPDATES) async def async_update(self): """Get the latest data from the Volkszaehler REST API.""" - from volkszaehler.exceptions import VolkszaehlerApiConnectionError try: await self.api.get_data() diff --git a/homeassistant/components/volvooncall/__init__.py b/homeassistant/components/volvooncall/__init__.py index c41c72020c42..c621a12943b8 100644 --- a/homeassistant/components/volvooncall/__init__.py +++ b/homeassistant/components/volvooncall/__init__.py @@ -1,10 +1,10 @@ """Support for Volvo On Call.""" -import logging from datetime import timedelta +import logging import voluptuous as vol +from volvooncall import Connection -import homeassistant.helpers.config_validation as cv from homeassistant.const import ( CONF_NAME, CONF_PASSWORD, @@ -14,6 +14,7 @@ ) from homeassistant.helpers import discovery from homeassistant.helpers.aiohttp_client import async_get_clientsession +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send, @@ -115,8 +116,6 @@ async def async_setup(hass, config): """Set up the Volvo On Call component.""" session = async_get_clientsession(hass) - from volvooncall import Connection - connection = Connection( session=session, username=config[DOMAIN].get(CONF_USERNAME), diff --git a/homeassistant/components/vultr/__init__.py b/homeassistant/components/vultr/__init__.py index 50e77c01c436..9b26c4a75b3d 100644 --- a/homeassistant/components/vultr/__init__.py +++ b/homeassistant/components/vultr/__init__.py @@ -1,12 +1,13 @@ """Support for Vultr.""" -import logging from datetime import timedelta +import logging import voluptuous as vol +from vultr import Vultr as VultrAPI from homeassistant.const import CONF_API_KEY -from homeassistant.util import Throttle import homeassistant.helpers.config_validation as cv +from homeassistant.util import Throttle _LOGGER = logging.getLogger(__name__) @@ -69,7 +70,6 @@ class Vultr: def __init__(self, api_key): """Initialize the Vultr connection.""" - from vultr import Vultr as VultrAPI self._api_key = api_key self.data = None diff --git a/homeassistant/components/wake_on_lan/__init__.py b/homeassistant/components/wake_on_lan/__init__.py index b4aad4925b99..d5b8f92a9bc1 100644 --- a/homeassistant/components/wake_on_lan/__init__.py +++ b/homeassistant/components/wake_on_lan/__init__.py @@ -5,15 +5,13 @@ import voluptuous as vol import wakeonlan -from homeassistant.const import CONF_MAC +from homeassistant.const import CONF_BROADCAST_ADDRESS, CONF_MAC import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) DOMAIN = "wake_on_lan" -CONF_BROADCAST_ADDRESS = "broadcast_address" - SERVICE_SEND_MAGIC_PACKET = "send_magic_packet" WAKE_ON_LAN_SEND_MAGIC_PACKET_SCHEMA = vol.Schema( diff --git a/homeassistant/components/wake_on_lan/switch.py b/homeassistant/components/wake_on_lan/switch.py index 01f696798296..8200a0309fa7 100644 --- a/homeassistant/components/wake_on_lan/switch.py +++ b/homeassistant/components/wake_on_lan/switch.py @@ -7,14 +7,12 @@ import wakeonlan from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchDevice -from homeassistant.const import CONF_HOST, CONF_NAME +from homeassistant.const import CONF_BROADCAST_ADDRESS, CONF_HOST, CONF_MAC, CONF_NAME import homeassistant.helpers.config_validation as cv from homeassistant.helpers.script import Script _LOGGER = logging.getLogger(__name__) -CONF_BROADCAST_ADDRESS = "broadcast_address" -CONF_MAC_ADDRESS = "mac_address" CONF_OFF_ACTION = "turn_off" DEFAULT_NAME = "Wake on LAN" @@ -22,7 +20,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { - vol.Required(CONF_MAC_ADDRESS): cv.string, + vol.Required(CONF_MAC): cv.string, vol.Optional(CONF_BROADCAST_ADDRESS): cv.string, vol.Optional(CONF_HOST): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, @@ -35,16 +33,16 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up a wake on lan switch.""" broadcast_address = config.get(CONF_BROADCAST_ADDRESS) host = config.get(CONF_HOST) - mac_address = config.get(CONF_MAC_ADDRESS) - name = config.get(CONF_NAME) + mac_address = config[CONF_MAC] + name = config[CONF_NAME] off_action = config.get(CONF_OFF_ACTION) add_entities( - [WOLSwitch(hass, name, host, mac_address, off_action, broadcast_address)], True + [WolSwitch(hass, name, host, mac_address, off_action, broadcast_address)], True ) -class WOLSwitch(SwitchDevice): +class WolSwitch(SwitchDevice): """Representation of a wake on lan switch.""" def __init__(self, hass, name, host, mac_address, off_action, broadcast_address): diff --git a/homeassistant/components/water_heater/__init__.py b/homeassistant/components/water_heater/__init__.py index c41381fe5fa5..6e7b918c289e 100644 --- a/homeassistant/components/water_heater/__init__.py +++ b/homeassistant/components/water_heater/__init__.py @@ -9,7 +9,7 @@ from homeassistant.util.temperature import convert as convert_temperature from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.entity import Entity -from homeassistant.helpers.config_validation import ( # noqa +from homeassistant.helpers.config_validation import ( # noqa: F401 PLATFORM_SCHEMA, PLATFORM_SCHEMA_BASE, ) diff --git a/homeassistant/components/water_heater/services.yaml b/homeassistant/components/water_heater/services.yaml index 72a3f909fbbd..7a26e5bc0d4b 100644 --- a/homeassistant/components/water_heater/services.yaml +++ b/homeassistant/components/water_heater/services.yaml @@ -29,23 +29,3 @@ set_operation_mode: operation_mode: description: New value of operation mode. example: eco - -econet_add_vacation: - description: Add a vacation to your water heater. - fields: - entity_id: - description: Name(s) of entities to change. - example: 'water_heater.econet' - start_date: - description: The timestamp of when the vacation should start. (Optional, defaults to now) - example: 1513186320 - end_date: - description: The timestamp of when the vacation should end. - example: 1513445520 - -econet_delete_vacation: - description: Delete your existing vacation from your water heater. - fields: - entity_id: - description: Name(s) of entities to change. - example: 'water_heater.econet' \ No newline at end of file diff --git a/homeassistant/components/watson_tts/tts.py b/homeassistant/components/watson_tts/tts.py index 40ebd768a310..520151cf20eb 100644 --- a/homeassistant/components/watson_tts/tts.py +++ b/homeassistant/components/watson_tts/tts.py @@ -1,6 +1,8 @@ """Support for IBM Watson TTS integration.""" import logging +from ibm_watson import TextToSpeechV1 +from ibm_cloud_sdk_core.authenticators import IAMAuthenticator import voluptuous as vol from homeassistant.components.tts import PLATFORM_SCHEMA, Provider @@ -92,8 +94,6 @@ def get_engine(hass, config, discovery_info=None): """Set up IBM Watson TTS component.""" - from ibm_watson import TextToSpeechV1 - from ibm_cloud_sdk_core.authenticators import IAMAuthenticator authenticator = IAMAuthenticator(config[CONF_APIKEY]) service = TextToSpeechV1(authenticator) diff --git a/homeassistant/components/waze_travel_time/manifest.json b/homeassistant/components/waze_travel_time/manifest.json index 85bcc19032e9..32083ca8ca83 100644 --- a/homeassistant/components/waze_travel_time/manifest.json +++ b/homeassistant/components/waze_travel_time/manifest.json @@ -3,7 +3,7 @@ "name": "Waze travel time", "documentation": "https://www.home-assistant.io/integrations/waze_travel_time", "requirements": [ - "WazeRouteCalculator==0.10" + "WazeRouteCalculator==0.12" ], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/waze_travel_time/sensor.py b/homeassistant/components/waze_travel_time/sensor.py index 4392a20d801d..b9ca64c09703 100644 --- a/homeassistant/components/waze_travel_time/sensor.py +++ b/homeassistant/components/waze_travel_time/sensor.py @@ -38,10 +38,16 @@ CONF_REALTIME = "realtime" CONF_UNITS = "units" CONF_VEHICLE_TYPE = "vehicle_type" +CONF_AVOID_TOLL_ROADS = "avoid_toll_roads" +CONF_AVOID_SUBSCRIPTION_ROADS = "avoid_subscription_roads" +CONF_AVOID_FERRIES = "avoid_ferries" DEFAULT_NAME = "Waze Travel Time" DEFAULT_REALTIME = True DEFAULT_VEHICLE_TYPE = "car" +DEFAULT_AVOID_TOLL_ROADS = False +DEFAULT_AVOID_SUBSCRIPTION_ROADS = False +DEFAULT_AVOID_FERRIES = False ICON = "mdi:car" @@ -65,6 +71,13 @@ VEHICLE_TYPES ), vol.Optional(CONF_UNITS): vol.In(UNITS), + vol.Optional( + CONF_AVOID_TOLL_ROADS, default=DEFAULT_AVOID_TOLL_ROADS + ): cv.boolean, + vol.Optional( + CONF_AVOID_SUBSCRIPTION_ROADS, default=DEFAULT_AVOID_SUBSCRIPTION_ROADS + ): cv.boolean, + vol.Optional(CONF_AVOID_FERRIES, default=DEFAULT_AVOID_FERRIES): cv.boolean, } ) @@ -79,10 +92,23 @@ def setup_platform(hass, config, add_entities, discovery_info=None): excl_filter = config.get(CONF_EXCL_FILTER) realtime = config.get(CONF_REALTIME) vehicle_type = config.get(CONF_VEHICLE_TYPE) + avoid_toll_roads = config.get(CONF_AVOID_TOLL_ROADS) + avoid_subscription_roads = config.get(CONF_AVOID_SUBSCRIPTION_ROADS) + avoid_ferries = config.get(CONF_AVOID_FERRIES) units = config.get(CONF_UNITS, hass.config.units.name) data = WazeTravelTimeData( - None, None, region, incl_filter, excl_filter, realtime, units, vehicle_type + None, + None, + region, + incl_filter, + excl_filter, + realtime, + units, + vehicle_type, + avoid_toll_roads, + avoid_subscription_roads, + avoid_ferries, ) sensor = WazeTravelTime(name, origin, destination, data) @@ -236,6 +262,9 @@ def __init__( realtime, units, vehicle_type, + avoid_toll_roads, + avoid_subscription_roads, + avoid_ferries, ): """Set up WazeRouteCalculator.""" @@ -251,6 +280,9 @@ def __init__( self.duration = None self.distance = None self.route = None + self.avoid_toll_roads = avoid_toll_roads + self.avoid_subscription_roads = avoid_subscription_roads + self.avoid_ferries = avoid_ferries # Currently WazeRouteCalc only supports PRIVATE, TAXI, MOTORCYCLE. if vehicle_type.upper() == "CAR": @@ -268,7 +300,9 @@ def update(self): self.destination, self.region, self.vehicle_type, - log_lvl=logging.DEBUG, + self.avoid_toll_roads, + self.avoid_subscription_roads, + self.avoid_ferries, ) routes = params.calc_all_routes_info(real_time=self.realtime) @@ -286,7 +320,7 @@ def update(self): if self.exclude.lower() not in k.lower() } - route = sorted(routes, key=(lambda key: routes[key][0]))[0] + route = list(routes)[0] self.duration, distance = routes[route] diff --git a/homeassistant/components/weather/__init__.py b/homeassistant/components/weather/__init__.py index fd122f66ac2b..bdeedd4cd6be 100644 --- a/homeassistant/components/weather/__init__.py +++ b/homeassistant/components/weather/__init__.py @@ -3,7 +3,7 @@ import logging from homeassistant.const import PRECISION_TENTHS, PRECISION_WHOLE, TEMP_CELSIUS -from homeassistant.helpers.config_validation import ( # noqa +from homeassistant.helpers.config_validation import ( # noqa: F401 PLATFORM_SCHEMA, PLATFORM_SCHEMA_BASE, ) diff --git a/homeassistant/components/webostv/media_player.py b/homeassistant/components/webostv/media_player.py index 913d193845fb..3bf0011907d0 100644 --- a/homeassistant/components/webostv/media_player.py +++ b/homeassistant/components/webostv/media_player.py @@ -2,13 +2,15 @@ import asyncio from datetime import timedelta import logging -from urllib.parse import urlparse from typing import Dict +from urllib.parse import urlparse +from pylgtv import PyLGTVPairException, WebOsClient import voluptuous as vol +from websockets.exceptions import ConnectionClosed from homeassistant import util -from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA +from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice from homeassistant.components.media_player.const import ( MEDIA_TYPE_CHANNEL, SUPPORT_NEXT_TRACK, @@ -108,9 +110,6 @@ def setup_tv( host, name, customize, config, timeout, hass, add_entities, turn_on_action ): """Set up a LG WebOS TV based on host parameter.""" - from pylgtv import WebOsClient - from pylgtv import PyLGTVPairException - from websockets.exceptions import ConnectionClosed client = WebOsClient(host, config, timeout) @@ -185,7 +184,6 @@ class LgWebOSDevice(MediaPlayerDevice): def __init__(self, host, name, customize, config, timeout, hass, on_action): """Initialize the webos device.""" - from pylgtv import WebOsClient self._client = WebOsClient(host, config, timeout) self._on_script = Script(hass, on_action) if on_action else None @@ -208,7 +206,6 @@ def __init__(self, host, name, customize, config, timeout, hass, on_action): @util.Throttle(MIN_TIME_BETWEEN_SCANS, MIN_TIME_BETWEEN_FORCED_SCANS) def update(self): """Retrieve the latest data.""" - from websockets.exceptions import ConnectionClosed try: current_input = self._client.get_input() @@ -331,7 +328,6 @@ def supported_features(self): def turn_off(self): """Turn off media player.""" - from websockets.exceptions import ConnectionClosed self._state = STATE_OFF try: diff --git a/homeassistant/components/webostv/notify.py b/homeassistant/components/webostv/notify.py index f96c20d49aa7..f62c41e9a956 100644 --- a/homeassistant/components/webostv/notify.py +++ b/homeassistant/components/webostv/notify.py @@ -1,15 +1,16 @@ """Support for LG WebOS TV notification service.""" import logging +from pylgtv import PyLGTVPairException, WebOsClient import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.notify import ( ATTR_DATA, - BaseNotificationService, PLATFORM_SCHEMA, + BaseNotificationService, ) from homeassistant.const import CONF_FILENAME, CONF_HOST, CONF_ICON +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -26,8 +27,6 @@ def get_service(hass, config, discovery_info=None): """Return the notify service.""" - from pylgtv import WebOsClient - from pylgtv import PyLGTVPairException path = hass.config.path(config.get(CONF_FILENAME)) client = WebOsClient(config.get(CONF_HOST), key_file_path=path, timeout_connect=8) @@ -55,7 +54,6 @@ def __init__(self, client, icon_path): def send_message(self, message="", **kwargs): """Send a message to the tv.""" - from pylgtv import PyLGTVPairException try: data = kwargs.get(ATTR_DATA) diff --git a/homeassistant/components/wemo/__init__.py b/homeassistant/components/wemo/__init__.py index df2d8ed1f314..fe63f10aebae 100644 --- a/homeassistant/components/wemo/__init__.py +++ b/homeassistant/components/wemo/__init__.py @@ -11,8 +11,7 @@ from homeassistant.helpers import discovery from homeassistant.const import EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP - -DOMAIN = "wemo" +from .const import DOMAIN # Mapping from Wemo model_name to component. WEMO_MODEL_DISPATCH = { diff --git a/homeassistant/components/wemo/const.py b/homeassistant/components/wemo/const.py new file mode 100644 index 000000000000..e9272d39bddb --- /dev/null +++ b/homeassistant/components/wemo/const.py @@ -0,0 +1,5 @@ +"""Constants for the Belkin Wemo component.""" +DOMAIN = "wemo" + +SERVICE_SET_HUMIDITY = "set_humidity" +SERVICE_RESET_FILTER_LIFE = "reset_filter_life" diff --git a/homeassistant/components/wemo/fan.py b/homeassistant/components/wemo/fan.py index 91273fa033fc..4a8be4fba81f 100644 --- a/homeassistant/components/wemo/fan.py +++ b/homeassistant/components/wemo/fan.py @@ -10,7 +10,6 @@ import homeassistant.helpers.config_validation as cv from homeassistant.components.fan import ( - DOMAIN, SUPPORT_SET_SPEED, FanEntity, SPEED_OFF, @@ -22,6 +21,7 @@ from homeassistant.const import ATTR_ENTITY_ID from . import SUBSCRIPTION_REGISTRY +from .const import DOMAIN, SERVICE_RESET_FILTER_LIFE, SERVICE_SET_HUMIDITY SCAN_INTERVAL = timedelta(seconds=10) DATA_KEY = "fan.wemo" @@ -79,8 +79,6 @@ if k not in [WEMO_FAN_LOW, WEMO_FAN_HIGH] } -SERVICE_SET_HUMIDITY = "wemo_set_humidity" - SET_HUMIDITY_SCHEMA = vol.Schema( { vol.Required(ATTR_ENTITY_ID): cv.entity_ids, @@ -90,8 +88,6 @@ } ) -SERVICE_RESET_FILTER_LIFE = "wemo_reset_filter_life" - RESET_FILTER_LIFE_SCHEMA = vol.Schema({vol.Required(ATTR_ENTITY_ID): cv.entity_ids}) diff --git a/homeassistant/components/wemo/services.yaml b/homeassistant/components/wemo/services.yaml index e69de29bb2d1..c2415265c62c 100644 --- a/homeassistant/components/wemo/services.yaml +++ b/homeassistant/components/wemo/services.yaml @@ -0,0 +1,16 @@ +set_humidity: + description: Set the target humidity of WeMo humidifier devices. + fields: + entity_id: + description: Names of the WeMo humidifier entities (1 or more entity_ids are required). + example: 'fan.wemo_humidifier' + target_humidity: + description: Target humidity. This is a float value between 0 and 100, but will be mapped to the humidity levels that WeMo humidifiers support (45, 50, 55, 60, and 100/Max) by rounding the value down to the nearest supported value. + example: 56.5 + +reset_filter_life: + description: Reset the WeMo Humidifier's filter life to 100%. + fields: + entity_id: + description: Names of the WeMo humidifier entities (1 or more entity_ids are required). + example: 'fan.wemo_humidifier' diff --git a/homeassistant/components/wemo/switch.py b/homeassistant/components/wemo/switch.py index c1d07a069021..1c0606b489db 100644 --- a/homeassistant/components/wemo/switch.py +++ b/homeassistant/components/wemo/switch.py @@ -12,7 +12,8 @@ from homeassistant.util import convert from homeassistant.const import STATE_OFF, STATE_ON, STATE_STANDBY, STATE_UNKNOWN -from . import SUBSCRIPTION_REGISTRY, DOMAIN as WEMO_DOMAIN +from . import SUBSCRIPTION_REGISTRY +from .const import DOMAIN SCAN_INTERVAL = timedelta(seconds=10) @@ -96,7 +97,7 @@ def name(self): @property def device_info(self): """Return the device info.""" - return {"name": self._name, "identifiers": {(WEMO_DOMAIN, self._serialnumber)}} + return {"name": self._name, "identifiers": {(DOMAIN, self._serialnumber)}} @property def device_state_attributes(self): diff --git a/homeassistant/components/wink/__init__.py b/homeassistant/components/wink/__init__.py index e2eb98938bb7..3bda8b314f2f 100644 --- a/homeassistant/components/wink/__init__.py +++ b/homeassistant/components/wink/__init__.py @@ -25,7 +25,7 @@ from homeassistant.core import callback from homeassistant.helpers import discovery import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.config_validation import ENTITY_SERVICE_SCHEMA +from homeassistant.helpers.config_validation import make_entity_service_schema from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.event import track_time_interval @@ -131,11 +131,11 @@ extra=vol.ALLOW_EXTRA, ) -RENAME_DEVICE_SCHEMA = ENTITY_SERVICE_SCHEMA.extend( +RENAME_DEVICE_SCHEMA = make_entity_service_schema( {vol.Required(ATTR_NAME): cv.string}, extra=vol.ALLOW_EXTRA ) -DELETE_DEVICE_SCHEMA = ENTITY_SERVICE_SCHEMA.extend({}, extra=vol.ALLOW_EXTRA) +DELETE_DEVICE_SCHEMA = make_entity_service_schema({}, extra=vol.ALLOW_EXTRA) SET_PAIRING_MODE_SCHEMA = vol.Schema( { @@ -146,31 +146,31 @@ extra=vol.ALLOW_EXTRA, ) -SET_VOLUME_SCHEMA = ENTITY_SERVICE_SCHEMA.extend( +SET_VOLUME_SCHEMA = make_entity_service_schema( {vol.Required(ATTR_VOLUME): vol.In(VOLUMES)} ) -SET_SIREN_TONE_SCHEMA = ENTITY_SERVICE_SCHEMA.extend( +SET_SIREN_TONE_SCHEMA = make_entity_service_schema( {vol.Required(ATTR_TONE): vol.In(TONES)} ) -SET_CHIME_MODE_SCHEMA = ENTITY_SERVICE_SCHEMA.extend( +SET_CHIME_MODE_SCHEMA = make_entity_service_schema( {vol.Required(ATTR_TONE): vol.In(CHIME_TONES)} ) -SET_AUTO_SHUTOFF_SCHEMA = ENTITY_SERVICE_SCHEMA.extend( +SET_AUTO_SHUTOFF_SCHEMA = make_entity_service_schema( {vol.Required(ATTR_AUTO_SHUTOFF): vol.In(AUTO_SHUTOFF_TIMES)} ) -SET_STROBE_ENABLED_SCHEMA = ENTITY_SERVICE_SCHEMA.extend( +SET_STROBE_ENABLED_SCHEMA = make_entity_service_schema( {vol.Required(ATTR_ENABLED): cv.boolean} ) -ENABLED_SIREN_SCHEMA = ENTITY_SERVICE_SCHEMA.extend( +ENABLED_SIREN_SCHEMA = make_entity_service_schema( {vol.Required(ATTR_ENABLED): cv.boolean} ) -DIAL_CONFIG_SCHEMA = ENTITY_SERVICE_SCHEMA.extend( +DIAL_CONFIG_SCHEMA = make_entity_service_schema( { vol.Optional(ATTR_MIN_VALUE): vol.Coerce(int), vol.Optional(ATTR_MAX_VALUE): vol.Coerce(int), @@ -182,7 +182,7 @@ } ) -DIAL_STATE_SCHEMA = ENTITY_SERVICE_SCHEMA.extend( +DIAL_STATE_SCHEMA = make_entity_service_schema( { vol.Required(ATTR_VALUE): vol.Coerce(int), vol.Optional(ATTR_LABELS): cv.ensure_list(cv.string), diff --git a/homeassistant/components/wink/alarm_control_panel.py b/homeassistant/components/wink/alarm_control_panel.py index 654252f5ffea..733022e91b15 100644 --- a/homeassistant/components/wink/alarm_control_panel.py +++ b/homeassistant/components/wink/alarm_control_panel.py @@ -4,6 +4,10 @@ import pywink import homeassistant.components.alarm_control_panel as alarm +from homeassistant.components.alarm_control_panel.const import ( + SUPPORT_ALARM_ARM_AWAY, + SUPPORT_ALARM_ARM_HOME, +) from homeassistant.const import ( STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, @@ -52,6 +56,11 @@ def state(self): state = None return state + @property + def supported_features(self) -> int: + """Return the list of supported features.""" + return SUPPORT_ALARM_ARM_HOME | SUPPORT_ALARM_ARM_AWAY + def alarm_disarm(self, code=None): """Send disarm command.""" self.wink.set_mode("home") diff --git a/homeassistant/components/wink/lock.py b/homeassistant/components/wink/lock.py index 37b27c0d500b..57cf9d304ec9 100644 --- a/homeassistant/components/wink/lock.py +++ b/homeassistant/components/wink/lock.py @@ -18,12 +18,12 @@ _LOGGER = logging.getLogger(__name__) -SERVICE_SET_VACATION_MODE = "wink_set_lock_vacation_mode" -SERVICE_SET_ALARM_MODE = "wink_set_lock_alarm_mode" -SERVICE_SET_ALARM_SENSITIVITY = "wink_set_lock_alarm_sensitivity" -SERVICE_SET_ALARM_STATE = "wink_set_lock_alarm_state" -SERVICE_SET_BEEPER_STATE = "wink_set_lock_beeper_state" -SERVICE_ADD_KEY = "wink_add_new_lock_key_code" +SERVICE_SET_VACATION_MODE = "set_lock_vacation_mode" +SERVICE_SET_ALARM_MODE = "set_lock_alarm_mode" +SERVICE_SET_ALARM_SENSITIVITY = "set_lock_alarm_sensitivity" +SERVICE_SET_ALARM_STATE = "set_lock_alarm_state" +SERVICE_SET_BEEPER_STATE = "set_lock_beeper_state" +SERVICE_ADD_KEY = "add_new_lock_key_code" ATTR_ENABLED = "enabled" ATTR_SENSITIVITY = "sensitivity" diff --git a/homeassistant/components/wink/services.yaml b/homeassistant/components/wink/services.yaml index a3b489f9cf54..93d53159702a 100644 --- a/homeassistant/components/wink/services.yaml +++ b/homeassistant/components/wink/services.yaml @@ -151,4 +151,67 @@ set_nimbus_dial_state: example: 250 labels: description: The values shown on the dial labels ["Dial 1", "test"] the first value is what is shown by default the second value is shown when the nimbus is pressed - example: ["example", "test"] \ No newline at end of file + example: ["example", "test"] + +set_lock_vacation_mode: + description: Set vacation mode for all or specified locks. Disables all user codes. + fields: + entity_id: + description: Name of lock to unlock. + example: 'lock.front_door' + enabled: + description: enable or disable. true or false. + example: true + +set_lock_alarm_mode: + description: Set alarm mode for all or specified locks. + fields: + entity_id: + description: Name of lock to unlock. + example: 'lock.front_door' + mode: + description: One of tamper, activity, or forced_entry. + example: tamper + +set_lock_alarm_sensitivity: + description: Set alarm sensitivity for all or specified locks. + fields: + entity_id: + description: Name of lock to unlock. + example: 'lock.front_door' + sensitivity: + description: One of low, medium_low, medium, medium_high, high. + example: medium + +set_lock_alarm_state: + description: Set alarm state. + fields: + entity_id: + description: Name of lock to unlock. + example: 'lock.front_door' + enabled: + description: enable or disable. true or false. + example: true + +set_lock_beeper_state: + description: Set beeper state. + fields: + entity_id: + description: Name of lock to unlock. + example: 'lock.front_door' + enabled: + description: enable or disable. true or false. + example: true + +add_new_lock_key_code: + description: Add a new user key code. + fields: + entity_id: + description: Name of lock to unlock. + example: 'lock.front_door' + name: + description: name of the new key code. + example: Bob + code: + description: new key code, length must match length of other codes. Default length is 4. + example: 1234 \ No newline at end of file diff --git a/homeassistant/components/wirelesstag/__init__.py b/homeassistant/components/wirelesstag/__init__.py index 5e0da881076f..1bc971f13727 100644 --- a/homeassistant/components/wirelesstag/__init__.py +++ b/homeassistant/components/wirelesstag/__init__.py @@ -1,18 +1,20 @@ """Support for Wireless Sensor Tags.""" import logging -from requests.exceptions import HTTPError, ConnectTimeout +from requests.exceptions import ConnectTimeout, HTTPError import voluptuous as vol +from wirelesstagpy import NotificationConfig as NC + +from homeassistant import util from homeassistant.const import ( ATTR_BATTERY_LEVEL, ATTR_VOLTAGE, - CONF_USERNAME, CONF_PASSWORD, + CONF_USERNAME, ) import homeassistant.helpers.config_validation as cv -from homeassistant import util -from homeassistant.helpers.entity import Entity from homeassistant.helpers.dispatcher import dispatcher_send +from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -96,7 +98,6 @@ def make_notifications(self, binary_sensors, mac): configs.extend(bi_sensor.event.build_notifications(bi_url, mac)) update_url = self.update_callback_url - from wirelesstagpy import NotificationConfig as NC update_config = NC.make_config_for_update_event(update_url, mac) diff --git a/homeassistant/components/withings/.translations/pl.json b/homeassistant/components/withings/.translations/pl.json index 4f1ee47ab0e4..90fe281c29fe 100644 --- a/homeassistant/components/withings/.translations/pl.json +++ b/homeassistant/components/withings/.translations/pl.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "no_flows": "Musisz skonfigurowa\u0107 Withings, aby m\u00f3c si\u0119 z nim uwierzytelni\u0107. Przeczytaj prosz\u0119 dokumentacj\u0119." + "no_flows": "Musisz skonfigurowa\u0107 Withings, aby m\u00f3c si\u0119 z nim uwierzytelni\u0107. Zapoznaj si\u0119 z dokumentacj\u0105." }, "create_entry": { "default": "Pomy\u015blnie uwierzytelniono z Withings dla wybranego profilu" @@ -18,7 +18,7 @@ "data": { "profile": "Profil" }, - "description": "Wybierz profil u\u017cytkownika Withings, na kt\u00f3ry chcesz po\u0142\u0105czy\u0107 z Home Assistant'em. Na stronie Withings wybierz ten sam profil u\u017cytkownika by dane by\u0142y poprawnie oznaczone.", + "description": "Wybierz profil u\u017cytkownika Withings, na kt\u00f3ry chcesz po\u0142\u0105czy\u0107 z Home Assistant'em. Na stronie Withings wybierz ten sam profil u\u017cytkownika, by dane by\u0142y poprawnie oznaczone.", "title": "Profil u\u017cytkownika" } }, diff --git a/homeassistant/components/withings/.translations/pt.json b/homeassistant/components/withings/.translations/pt.json new file mode 100644 index 000000000000..0a1f02335ccb --- /dev/null +++ b/homeassistant/components/withings/.translations/pt.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "profile": { + "data": { + "profile": "Perfil" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/withings/common.py b/homeassistant/components/withings/common.py index 911bb08906b1..655776ae0043 100644 --- a/homeassistant/components/withings/common.py +++ b/homeassistant/components/withings/common.py @@ -226,7 +226,7 @@ async def call(self, function, throttle_domain=None) -> Any: WithingsDataManager.print_service_available() return result - except Exception as ex: # pylint: disable=broad-except + except Exception as ex: # Withings api encountered error. if isinstance(ex, (UnauthorizedException, AuthFailedException)): raise NotAuthenticatedError(ex) diff --git a/homeassistant/components/wled/.translations/bg.json b/homeassistant/components/wled/.translations/bg.json new file mode 100644 index 000000000000..d99df20187f7 --- /dev/null +++ b/homeassistant/components/wled/.translations/bg.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "\u0422\u043e\u0432\u0430 WLED \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e.", + "connection_error": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435 \u0441 WLED \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e." + }, + "error": { + "connection_error": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435 \u0441 WLED \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e." + }, + "flow_title": "WLED: {name}", + "step": { + "user": { + "data": { + "host": "\u0410\u0434\u0440\u0435\u0441" + }, + "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 \u0432\u0430\u0448\u0438\u044f WLED \u0434\u0430 \u0441\u0435 \u0438\u043d\u0442\u0435\u0433\u0440\u0438\u0440\u0430 \u0441 Home Assistant.", + "title": "\u0421\u0432\u044a\u0440\u0436\u0435\u0442\u0435 \u0412\u0430\u0448\u0438\u044f WLED" + }, + "zeroconf_confirm": { + "description": "\u0418\u0441\u043a\u0430\u0442\u0435 \u043b\u0438 \u0434\u0430 \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u0435 WLED \u0441 \u0438\u043c\u0435 {name} `\u043a\u044a\u043c Home Assistant?", + "title": "\u041e\u0442\u043a\u0440\u0438\u0442\u043e \u0435 WLED \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e" + } + }, + "title": "WLED" + } +} \ No newline at end of file diff --git a/homeassistant/components/wled/.translations/ca.json b/homeassistant/components/wled/.translations/ca.json index b86eefd62c83..347dc576d91a 100644 --- a/homeassistant/components/wled/.translations/ca.json +++ b/homeassistant/components/wled/.translations/ca.json @@ -1,14 +1,23 @@ { "config": { + "abort": { + "already_configured": "Aquest dispositiu WLED ja est\u00e0 configurat.", + "connection_error": "No s'ha pogut connectar amb el dispositiu WLED." + }, + "error": { + "connection_error": "No s'ha pogut connectar amb el dispositiu WLED." + }, "flow_title": "WLED: {name}", "step": { "user": { "data": { "host": "Amfitri\u00f3 o adre\u00e7a IP" }, + "description": "Configura el teu WLED per integrar-lo amb Home Assistant.", "title": "Enlla\u00e7a el teu WLED" }, "zeroconf_confirm": { + "description": "Vols afegir el WLED `{name}` a Home Assistant?", "title": "Dispositiu WLED descobert" } }, diff --git a/homeassistant/components/wled/.translations/de.json b/homeassistant/components/wled/.translations/de.json index f50a24eeac06..2d1cc5ef97dc 100644 --- a/homeassistant/components/wled/.translations/de.json +++ b/homeassistant/components/wled/.translations/de.json @@ -1,6 +1,23 @@ { "config": { + "abort": { + "already_configured": "Dieses WLED-Ger\u00e4t ist bereits konfiguriert.", + "connection_error": "Verbindung zum WLED-Ger\u00e4t fehlgeschlagen." + }, + "error": { + "connection_error": "Verbindung zum WLED-Ger\u00e4t fehlgeschlagen." + }, "flow_title": "WLED: {name}", + "step": { + "user": { + "data": { + "host": "Hostname oder IP-Adresse" + } + }, + "zeroconf_confirm": { + "title": "Gefundenes WLED-Ger\u00e4t" + } + }, "title": "WLED" } } \ No newline at end of file diff --git a/homeassistant/components/wled/.translations/fr.json b/homeassistant/components/wled/.translations/fr.json index 5da30ab62885..6f275ad81998 100644 --- a/homeassistant/components/wled/.translations/fr.json +++ b/homeassistant/components/wled/.translations/fr.json @@ -17,7 +17,7 @@ "title": "Liez votre WLED" }, "zeroconf_confirm": { - "description": "Voulez-vous ajouter le dispositif WLED nomm\u00e9 '{name}' \u00e0 Home Assistant?", + "description": "Voulez-vous ajouter le dispositif WLED nomm\u00e9 `{name}` \u00e0 Home Assistant?", "title": "Dispositif WLED d\u00e9couvert" } }, diff --git a/homeassistant/components/wled/.translations/ko.json b/homeassistant/components/wled/.translations/ko.json new file mode 100644 index 000000000000..bee9c2a6204b --- /dev/null +++ b/homeassistant/components/wled/.translations/ko.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "WLED \uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", + "connection_error": "WLED \uae30\uae30\uc5d0 \uc5f0\uacb0\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4." + }, + "error": { + "connection_error": "WLED \uae30\uae30\uc5d0 \uc5f0\uacb0\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4." + }, + "flow_title": "WLED: {name}", + "step": { + "user": { + "data": { + "host": "\ud638\uc2a4\ud2b8 \ub610\ub294 IP \uc8fc\uc18c" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wled/.translations/nl.json b/homeassistant/components/wled/.translations/nl.json new file mode 100644 index 000000000000..1bf70b7a0952 --- /dev/null +++ b/homeassistant/components/wled/.translations/nl.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "Dit WLED-apparaat is al geconfigureerd.", + "connection_error": "Kan geen verbinding maken met WLED-apparaat." + }, + "error": { + "connection_error": "Kan geen verbinding maken met WLED-apparaat." + }, + "flow_title": "WLED: {name}", + "step": { + "user": { + "data": { + "host": "Hostnaam of IP-adres" + }, + "title": "Koppel je WLED" + }, + "zeroconf_confirm": { + "description": "Wil je de WLED genaamd `{name}` toevoegen aan Home Assistant?", + "title": "Ontdekt WLED-apparaat" + } + }, + "title": "WLED" + } +} \ No newline at end of file diff --git a/homeassistant/components/wled/.translations/nn.json b/homeassistant/components/wled/.translations/nn.json new file mode 100644 index 000000000000..f50a24eeac06 --- /dev/null +++ b/homeassistant/components/wled/.translations/nn.json @@ -0,0 +1,6 @@ +{ + "config": { + "flow_title": "WLED: {name}", + "title": "WLED" + } +} \ No newline at end of file diff --git a/homeassistant/components/wled/.translations/pl.json b/homeassistant/components/wled/.translations/pl.json new file mode 100644 index 000000000000..c10c8ab34d6a --- /dev/null +++ b/homeassistant/components/wled/.translations/pl.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "To urz\u0105dzenie WLED jest ju\u017c skonfigurowane", + "connection_error": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia z urz\u0105dzeniem WLED." + }, + "error": { + "connection_error": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia z urz\u0105dzeniem WLED." + }, + "flow_title": "WLED: {name}", + "step": { + "user": { + "data": { + "host": "Nazwa hosta lub adres IP" + }, + "description": "Konfiguracja WLED w celu integracji z Home Assistant'em.", + "title": "Po\u0142\u0105cz sw\u00f3j WLED" + }, + "zeroconf_confirm": { + "description": "Czy chcesz doda\u0107 WLED o nazwie `{name}` do Home Assistant'a?", + "title": "Wykryto urz\u0105dzenie WLED" + } + }, + "title": "WLED" + } +} \ No newline at end of file diff --git a/homeassistant/components/wled/.translations/pt.json b/homeassistant/components/wled/.translations/pt.json new file mode 100644 index 000000000000..521434d11a8b --- /dev/null +++ b/homeassistant/components/wled/.translations/pt.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "connection_error": "Falha ao ligar ao dispositivo WLED" + }, + "error": { + "connection_error": "Falha ao ligar ao dispositivo WLED" + }, + "step": { + "user": { + "data": { + "host": "Nome servidor ou endere\u00e7o IP" + }, + "title": "Associar WLED" + }, + "zeroconf_confirm": { + "title": "Dispositivo WLED descoberto" + } + }, + "title": "" + } +} \ No newline at end of file diff --git a/homeassistant/components/wled/config_flow.py b/homeassistant/components/wled/config_flow.py index 7be283874e0f..c6b11fa1eb64 100644 --- a/homeassistant/components/wled/config_flow.py +++ b/homeassistant/components/wled/config_flow.py @@ -14,7 +14,7 @@ from homeassistant.helpers import ConfigType from homeassistant.helpers.aiohttp_client import async_get_clientsession -from .const import DOMAIN # pylint: disable=W0611 +from .const import DOMAIN # pylint: disable=unused-import _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/wled/light.py b/homeassistant/components/wled/light.py index 3d2c9d6ef2c9..8bc1a56b2051 100644 --- a/homeassistant/components/wled/light.py +++ b/homeassistant/components/wled/light.py @@ -10,11 +10,13 @@ ATTR_EFFECT, ATTR_HS_COLOR, ATTR_TRANSITION, + ATTR_WHITE_VALUE, SUPPORT_BRIGHTNESS, SUPPORT_COLOR, SUPPORT_COLOR_TEMP, SUPPORT_EFFECT, SUPPORT_TRANSITION, + SUPPORT_WHITE_VALUE, Light, ) from homeassistant.config_entries import ConfigEntry @@ -79,6 +81,7 @@ def __init__( self._color: Optional[Tuple[float, float]] = None self._effect: Optional[str] = None self._state: Optional[bool] = None + self._white_value: Optional[int] = None # Only apply the segment ID if it is not the first segment name = wled.device.info.name @@ -107,10 +110,15 @@ def brightness(self) -> Optional[int]: """Return the brightness of this light between 1..255.""" return self._brightness + @property + def white_value(self) -> Optional[int]: + """Return the white value of this light between 0..255.""" + return self._white_value + @property def supported_features(self) -> int: """Flag supported features.""" - return ( + flags = ( SUPPORT_BRIGHTNESS | SUPPORT_COLOR | SUPPORT_COLOR_TEMP @@ -118,6 +126,11 @@ def supported_features(self) -> int: | SUPPORT_TRANSITION ) + if self._rgbw: + flags |= SUPPORT_WHITE_VALUE + + return flags + @property def effect_list(self) -> List[str]: """Return the list of supported effects.""" @@ -163,11 +176,21 @@ async def async_turn_on(self, **kwargs: Any) -> None: if ATTR_EFFECT in kwargs: data[ATTR_EFFECT] = kwargs[ATTR_EFFECT] - # Support for RGBW strips - if self._rgbw and any(x in (ATTR_COLOR_TEMP, ATTR_HS_COLOR) for x in kwargs): - data[ATTR_COLOR_PRIMARY] = color_util.color_rgb_to_rgbw( - *data[ATTR_COLOR_PRIMARY] - ) + # Support for RGBW strips, adds white value + if self._rgbw and any( + x in (ATTR_COLOR_TEMP, ATTR_HS_COLOR, ATTR_WHITE_VALUE) for x in kwargs + ): + # WLED cannot just accept a white value, it needs the color. + # We use the last know color in case just the white value changes. + if not any(x in (ATTR_COLOR_TEMP, ATTR_HS_COLOR) for x in kwargs): + hue, sat = self._color + data[ATTR_COLOR_PRIMARY] = color_util.color_hsv_to_RGB(hue, sat, 100) + + # Add requested or last known white value + if ATTR_WHITE_VALUE in kwargs: + data[ATTR_COLOR_PRIMARY] += (kwargs[ATTR_WHITE_VALUE],) + else: + data[ATTR_COLOR_PRIMARY] += (self._white_value,) try: await self.wled.light(**data) @@ -186,6 +209,9 @@ async def async_turn_on(self, **kwargs: Any) -> None: if ATTR_COLOR_TEMP in kwargs: self._color = color_util.color_temperature_to_hs(mireds) + if ATTR_WHITE_VALUE in kwargs: + self._white_value = kwargs[ATTR_WHITE_VALUE] + except WLEDError: _LOGGER.error("An error occurred while turning on WLED light.") self._available = False @@ -198,9 +224,9 @@ async def _wled_update(self) -> None: self._state = self.wled.device.state.on color = self.wled.device.state.segments[self._segment].color_primary + self._color = color_util.color_RGB_to_hs(*color[:3]) if self._rgbw: - color = color_util.color_rgbw_to_rgb(*color) - self._color = color_util.color_RGB_to_hs(*color) + self._white_value = color[-1] playlist = self.wled.device.state.playlist if playlist == -1: diff --git a/homeassistant/components/workday/binary_sensor.py b/homeassistant/components/workday/binary_sensor.py index 3ca2afcc7490..f95447c1e72f 100644 --- a/homeassistant/components/workday/binary_sensor.py +++ b/homeassistant/components/workday/binary_sensor.py @@ -13,6 +13,7 @@ _LOGGER = logging.getLogger(__name__) # List of all countries currently supported by holidays +# Source: https://github.com/dr-prodigy/python-holidays#available-countries # There seems to be no way to get the list out at runtime ALL_COUNTRIES = [ "Argentina", @@ -42,6 +43,8 @@ "Denmark", "DK", "England", + "Estonia", + "EE", "EuropeanCentralBank", "ECB", "TAR", @@ -54,7 +57,9 @@ "Hungary", "HU", "Honduras", - "HUD", + "HND", + "Iceland", + "IS", "India", "IND", "Ireland", @@ -64,6 +69,8 @@ "IT", "Japan", "JP", + "Kenya", + "KE", "Lithuania", "LT", "Luxembourg", @@ -77,6 +84,9 @@ "Northern Ireland", "Norway", "NO", + "Peru", + "PE", + "Poland", "Polish", "PL", "Portugal", diff --git a/homeassistant/components/xeoma/camera.py b/homeassistant/components/xeoma/camera.py index 2ca4aab7aff4..bb5febe6bd78 100644 --- a/homeassistant/components/xeoma/camera.py +++ b/homeassistant/components/xeoma/camera.py @@ -2,6 +2,7 @@ import logging import voluptuous as vol +from pyxeoma.xeoma import Xeoma, XeomaError from homeassistant.components.camera import PLATFORM_SCHEMA, Camera from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_USERNAME @@ -40,7 +41,6 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Discover and setup Xeoma Cameras.""" - from pyxeoma.xeoma import Xeoma, XeomaError host = config[CONF_HOST] login = config.get(CONF_USERNAME) @@ -111,7 +111,6 @@ def __init__(self, xeoma, image, name, username, password): async def async_camera_image(self): """Return a still image response from the camera.""" - from pyxeoma.xeoma import XeomaError try: image = await self._xeoma.async_get_camera_image( diff --git a/homeassistant/components/xfinity/device_tracker.py b/homeassistant/components/xfinity/device_tracker.py index 93603ae5797e..712d31d46db5 100644 --- a/homeassistant/components/xfinity/device_tracker.py +++ b/homeassistant/components/xfinity/device_tracker.py @@ -3,6 +3,7 @@ from requests.exceptions import RequestException import voluptuous as vol +from xfinity_gateway import XfinityGateway import homeassistant.helpers.config_validation as cv from homeassistant.components.device_tracker import ( @@ -23,7 +24,6 @@ def get_scanner(hass, config): """Validate the configuration and return an Xfinity Gateway scanner.""" - from xfinity_gateway import XfinityGateway gateway = XfinityGateway(config[DOMAIN][CONF_HOST]) scanner = None diff --git a/homeassistant/components/xiaomi/camera.py b/homeassistant/components/xiaomi/camera.py index 363c17fe4a9c..cc85f17146b2 100644 --- a/homeassistant/components/xiaomi/camera.py +++ b/homeassistant/components/xiaomi/camera.py @@ -1,20 +1,23 @@ """This component provides support for Xiaomi Cameras.""" import asyncio +from ftplib import FTP, error_perm import logging +from haffmpeg.camera import CameraMjpeg +from haffmpeg.tools import IMAGE_JPEG, ImageFrame import voluptuous as vol -from homeassistant.components.camera import Camera, PLATFORM_SCHEMA -from homeassistant.exceptions import TemplateError +from homeassistant.components.camera import PLATFORM_SCHEMA, Camera from homeassistant.components.ffmpeg import DATA_FFMPEG from homeassistant.const import ( CONF_HOST, CONF_NAME, - CONF_PATH, CONF_PASSWORD, + CONF_PATH, CONF_PORT, CONF_USERNAME, ) +from homeassistant.exceptions import TemplateError from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_aiohttp_proxy_stream @@ -88,7 +91,6 @@ def model(self): def get_latest_video_url(self, host): """Retrieve the latest video file from the Xiaomi Camera FTP server.""" - from ftplib import FTP, error_perm ftp = FTP(host) try: @@ -140,7 +142,6 @@ def get_latest_video_url(self, host): async def async_camera_image(self): """Return a still image response from the camera.""" - from haffmpeg.tools import ImageFrame, IMAGE_JPEG try: host = self.host.async_render() @@ -162,7 +163,6 @@ async def async_camera_image(self): async def handle_async_mjpeg_stream(self, request): """Generate an HTTP MJPEG stream from the camera.""" - from haffmpeg.camera import CameraMjpeg stream = CameraMjpeg(self._manager.binary, loop=self.hass.loop) await stream.open_camera(self._last_url, extra_cmd=self._extra_arguments) diff --git a/homeassistant/components/xiaomi/device_tracker.py b/homeassistant/components/xiaomi/device_tracker.py index dbc647f49827..df16b13b9311 100644 --- a/homeassistant/components/xiaomi/device_tracker.py +++ b/homeassistant/components/xiaomi/device_tracker.py @@ -4,13 +4,13 @@ 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 +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/xiaomi_aqara/__init__.py b/homeassistant/components/xiaomi_aqara/__init__.py index 7a337dcc497a..ae032a8b35f2 100644 --- a/homeassistant/components/xiaomi_aqara/__init__.py +++ b/homeassistant/components/xiaomi_aqara/__init__.py @@ -1,9 +1,9 @@ """Support for Xiaomi Gateways.""" -import logging - from datetime import timedelta +import logging import voluptuous as vol +from xiaomi_gateway import XiaomiGatewayDiscovery from homeassistant.components.discovery import SERVICE_XIAOMI_GW from homeassistant.const import ( @@ -135,8 +135,6 @@ async def xiaomi_gw_discovered(service, discovery_info): discovery.listen(hass, SERVICE_XIAOMI_GW, xiaomi_gw_discovered) - from xiaomi_gateway import XiaomiGatewayDiscovery - xiaomi = hass.data[PY_XIAOMI_GATEWAY] = XiaomiGatewayDiscovery( hass.add_job, gateways, interface ) diff --git a/homeassistant/components/xiaomi_miio/const.py b/homeassistant/components/xiaomi_miio/const.py new file mode 100644 index 000000000000..f8be37b313c8 --- /dev/null +++ b/homeassistant/components/xiaomi_miio/const.py @@ -0,0 +1,48 @@ +"""Constants for the Xiaomi Miio component.""" +DOMAIN = "xiaomi_miio" + +# Fan Services +SERVICE_SET_BUZZER_ON = "fan_set_buzzer_on" +SERVICE_SET_BUZZER_OFF = "fan_set_buzzer_off" +SERVICE_SET_LED_ON = "fan_set_led_on" +SERVICE_SET_LED_OFF = "fan_set_led_off" +SERVICE_SET_CHILD_LOCK_ON = "fan_set_child_lock_on" +SERVICE_SET_CHILD_LOCK_OFF = "fan_set_child_lock_off" +SERVICE_SET_LED_BRIGHTNESS = "fan_set_led_brightness" +SERVICE_SET_FAVORITE_LEVEL = "fan_set_favorite_level" +SERVICE_SET_AUTO_DETECT_ON = "fan_set_auto_detect_on" +SERVICE_SET_AUTO_DETECT_OFF = "fan_set_auto_detect_off" +SERVICE_SET_LEARN_MODE_ON = "fan_set_learn_mode_on" +SERVICE_SET_LEARN_MODE_OFF = "fan_set_learn_mode_off" +SERVICE_SET_VOLUME = "fan_set_volume" +SERVICE_RESET_FILTER = "fan_reset_filter" +SERVICE_SET_EXTRA_FEATURES = "fan_set_extra_features" +SERVICE_SET_TARGET_HUMIDITY = "fan_set_target_humidity" +SERVICE_SET_DRY_ON = "fan_set_dry_on" +SERVICE_SET_DRY_OFF = "fan_set_dry_off" + +# Light Services +SERVICE_SET_SCENE = "light_set_scene" +SERVICE_SET_DELAYED_TURN_OFF = "light_set_delayed_turn_off" +SERVICE_REMINDER_ON = "light_reminder_on" +SERVICE_REMINDER_OFF = "light_reminder_off" +SERVICE_NIGHT_LIGHT_MODE_ON = "light_night_light_mode_on" +SERVICE_NIGHT_LIGHT_MODE_OFF = "light_night_light_mode_off" +SERVICE_EYECARE_MODE_ON = "light_eyecare_mode_on" +SERVICE_EYECARE_MODE_OFF = "light_eyecare_mode_off" + +# Remote Services +SERVICE_LEARN = "remote_learn_command" + +# Switch Services +SERVICE_SET_WIFI_LED_ON = "switch_set_wifi_led_on" +SERVICE_SET_WIFI_LED_OFF = "switch_set_wifi_led_off" +SERVICE_SET_POWER_MODE = "switch_set_power_mode" +SERVICE_SET_POWER_PRICE = "switch_set_power_price" + +# Vacuum Services +SERVICE_MOVE_REMOTE_CONTROL = "vacuum_remote_control_move" +SERVICE_MOVE_REMOTE_CONTROL_STEP = "vacuum_remote_control_move_step" +SERVICE_START_REMOTE_CONTROL = "vacuum_remote_control_start" +SERVICE_STOP_REMOTE_CONTROL = "vacuum_remote_control_stop" +SERVICE_CLEAN_ZONE = "vacuum_clean_zone" diff --git a/homeassistant/components/xiaomi_miio/fan.py b/homeassistant/components/xiaomi_miio/fan.py index 9e496893d569..91b18aaf3644 100644 --- a/homeassistant/components/xiaomi_miio/fan.py +++ b/homeassistant/components/xiaomi_miio/fan.py @@ -26,12 +26,7 @@ OperationMode as AirpurifierOperationMode, ) -from homeassistant.components.fan import ( - DOMAIN, - PLATFORM_SCHEMA, - SUPPORT_SET_SPEED, - FanEntity, -) +from homeassistant.components.fan import PLATFORM_SCHEMA, SUPPORT_SET_SPEED, FanEntity from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_MODE, @@ -42,6 +37,28 @@ from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv +from .const import ( + DOMAIN, + SERVICE_SET_BUZZER_ON, + SERVICE_SET_BUZZER_OFF, + SERVICE_SET_LED_ON, + SERVICE_SET_LED_OFF, + SERVICE_SET_CHILD_LOCK_ON, + SERVICE_SET_CHILD_LOCK_OFF, + SERVICE_SET_LED_BRIGHTNESS, + SERVICE_SET_FAVORITE_LEVEL, + SERVICE_SET_AUTO_DETECT_ON, + SERVICE_SET_AUTO_DETECT_OFF, + SERVICE_SET_LEARN_MODE_ON, + SERVICE_SET_LEARN_MODE_OFF, + SERVICE_SET_VOLUME, + SERVICE_RESET_FILTER, + SERVICE_SET_EXTRA_FEATURES, + SERVICE_SET_TARGET_HUMIDITY, + SERVICE_SET_DRY_ON, + SERVICE_SET_DRY_OFF, +) + _LOGGER = logging.getLogger(__name__) DEFAULT_NAME = "Xiaomi Miio Device" @@ -368,25 +385,6 @@ | FEATURE_SET_EXTRA_FEATURES ) -SERVICE_SET_BUZZER_ON = "xiaomi_miio_set_buzzer_on" -SERVICE_SET_BUZZER_OFF = "xiaomi_miio_set_buzzer_off" -SERVICE_SET_LED_ON = "xiaomi_miio_set_led_on" -SERVICE_SET_LED_OFF = "xiaomi_miio_set_led_off" -SERVICE_SET_CHILD_LOCK_ON = "xiaomi_miio_set_child_lock_on" -SERVICE_SET_CHILD_LOCK_OFF = "xiaomi_miio_set_child_lock_off" -SERVICE_SET_LED_BRIGHTNESS = "xiaomi_miio_set_led_brightness" -SERVICE_SET_FAVORITE_LEVEL = "xiaomi_miio_set_favorite_level" -SERVICE_SET_AUTO_DETECT_ON = "xiaomi_miio_set_auto_detect_on" -SERVICE_SET_AUTO_DETECT_OFF = "xiaomi_miio_set_auto_detect_off" -SERVICE_SET_LEARN_MODE_ON = "xiaomi_miio_set_learn_mode_on" -SERVICE_SET_LEARN_MODE_OFF = "xiaomi_miio_set_learn_mode_off" -SERVICE_SET_VOLUME = "xiaomi_miio_set_volume" -SERVICE_RESET_FILTER = "xiaomi_miio_reset_filter" -SERVICE_SET_EXTRA_FEATURES = "xiaomi_miio_set_extra_features" -SERVICE_SET_TARGET_HUMIDITY = "xiaomi_miio_set_target_humidity" -SERVICE_SET_DRY_ON = "xiaomi_miio_set_dry_on" -SERVICE_SET_DRY_OFF = "xiaomi_miio_set_dry_off" - AIRPURIFIER_SERVICE_SCHEMA = vol.Schema({vol.Optional(ATTR_ENTITY_ID): cv.entity_ids}) SERVICE_SCHEMA_LED_BRIGHTNESS = AIRPURIFIER_SERVICE_SCHEMA.extend( diff --git a/homeassistant/components/xiaomi_miio/light.py b/homeassistant/components/xiaomi_miio/light.py index 5b454512f335..2343a6787c29 100644 --- a/homeassistant/components/xiaomi_miio/light.py +++ b/homeassistant/components/xiaomi_miio/light.py @@ -21,7 +21,6 @@ ATTR_COLOR_TEMP, ATTR_ENTITY_ID, ATTR_HS_COLOR, - DOMAIN, PLATFORM_SCHEMA, SUPPORT_BRIGHTNESS, SUPPORT_COLOR, @@ -33,6 +32,18 @@ import homeassistant.helpers.config_validation as cv from homeassistant.util import color, dt +from .const import ( + DOMAIN, + SERVICE_SET_SCENE, + SERVICE_SET_DELAYED_TURN_OFF, + SERVICE_REMINDER_ON, + SERVICE_REMINDER_OFF, + SERVICE_NIGHT_LIGHT_MODE_ON, + SERVICE_NIGHT_LIGHT_MODE_OFF, + SERVICE_EYECARE_MODE_ON, + SERVICE_EYECARE_MODE_OFF, +) + _LOGGER = logging.getLogger(__name__) DEFAULT_NAME = "Xiaomi Philips Light" @@ -85,15 +96,6 @@ ATTR_BRAND_SLEEP = "brand_sleep" ATTR_BRAND = "brand" -SERVICE_SET_SCENE = "xiaomi_miio_set_scene" -SERVICE_SET_DELAYED_TURN_OFF = "xiaomi_miio_set_delayed_turn_off" -SERVICE_REMINDER_ON = "xiaomi_miio_reminder_on" -SERVICE_REMINDER_OFF = "xiaomi_miio_reminder_off" -SERVICE_NIGHT_LIGHT_MODE_ON = "xiaomi_miio_night_light_mode_on" -SERVICE_NIGHT_LIGHT_MODE_OFF = "xiaomi_miio_night_light_mode_off" -SERVICE_EYECARE_MODE_ON = "xiaomi_miio_eyecare_mode_on" -SERVICE_EYECARE_MODE_OFF = "xiaomi_miio_eyecare_mode_off" - XIAOMI_MIIO_SERVICE_SCHEMA = vol.Schema({vol.Optional(ATTR_ENTITY_ID): cv.entity_ids}) SERVICE_SCHEMA_SET_SCENE = XIAOMI_MIIO_SERVICE_SCHEMA.extend( diff --git a/homeassistant/components/xiaomi_miio/remote.py b/homeassistant/components/xiaomi_miio/remote.py index 0e2ac476e057..1e7cada1a7b3 100644 --- a/homeassistant/components/xiaomi_miio/remote.py +++ b/homeassistant/components/xiaomi_miio/remote.py @@ -11,7 +11,6 @@ ATTR_DELAY_SECS, ATTR_NUM_REPEATS, DEFAULT_DELAY_SECS, - DOMAIN, PLATFORM_SCHEMA, RemoteDevice, ) @@ -28,9 +27,10 @@ import homeassistant.helpers.config_validation as cv from homeassistant.util.dt import utcnow +from .const import DOMAIN, SERVICE_LEARN + _LOGGER = logging.getLogger(__name__) -SERVICE_LEARN = "xiaomi_miio_learn_command" DATA_KEY = "remote.xiaomi_miio" CONF_SLOT = "slot" diff --git a/homeassistant/components/xiaomi_miio/services.yaml b/homeassistant/components/xiaomi_miio/services.yaml index e69de29bb2d1..36dcbc950be3 100644 --- a/homeassistant/components/xiaomi_miio/services.yaml +++ b/homeassistant/components/xiaomi_miio/services.yaml @@ -0,0 +1,308 @@ +fan_set_buzzer_on: + description: Turn the buzzer on. + fields: + entity_id: + description: Name of the xiaomi miio entity. + example: 'fan.xiaomi_miio_device' + +fan_set_buzzer_off: + description: Turn the buzzer off. + fields: + entity_id: + description: Name of the xiaomi miio entity. + example: 'fan.xiaomi_miio_device' + +fan_set_led_on: + description: Turn the led on. + fields: + entity_id: + description: Name of the xiaomi miio entity. + example: 'fan.xiaomi_miio_device' + +fan_set_led_off: + description: Turn the led off. + fields: + entity_id: + description: Name of the xiaomi miio entity. + example: 'fan.xiaomi_miio_device' + +fan_set_child_lock_on: + description: Turn the child lock on. + fields: + entity_id: + description: Name of the xiaomi miio entity. + example: 'fan.xiaomi_miio_device' + +fan_set_child_lock_off: + description: Turn the child lock off. + fields: + entity_id: + description: Name of the xiaomi miio entity. + example: 'fan.xiaomi_miio_device' + +fan_set_favorite_level: + description: Set the favorite level. + fields: + entity_id: + description: Name of the xiaomi miio entity. + example: 'fan.xiaomi_miio_device' + level: + description: Level, between 0 and 16. + example: 1 + +fan_set_led_brightness: + description: Set the led brightness. + fields: + entity_id: + description: Name of the xiaomi miio entity. + example: 'fan.xiaomi_miio_device' + brightness: + description: Brightness (0 = Bright, 1 = Dim, 2 = Off) + example: 1 + +fan_set_auto_detect_on: + description: Turn the auto detect on. + fields: + entity_id: + description: Name of the xiaomi miio entity. + example: 'fan.xiaomi_miio_device' + +fan_set_auto_detect_off: + description: Turn the auto detect off. + fields: + entity_id: + description: Name of the xiaomi miio entity. + example: 'fan.xiaomi_miio_device' + +fan_set_learn_mode_on: + description: Turn the learn mode on. + fields: + entity_id: + description: Name of the xiaomi miio entity. + example: 'fan.xiaomi_miio_device' + +fan_set_learn_mode_off: + description: Turn the learn mode off. + fields: + entity_id: + description: Name of the xiaomi miio entity. + example: 'fan.xiaomi_miio_device' + +fan_set_volume: + description: Set the sound volume. + fields: + entity_id: + description: Name of the xiaomi miio entity. + example: 'fan.xiaomi_miio_device' + volume: + description: Volume, between 0 and 100. + example: 50 + +fan_reset_filter: + description: Reset the filter lifetime and usage. + fields: + entity_id: + description: Name of the xiaomi miio entity. + example: 'fan.xiaomi_miio_device' + +fan_set_extra_features: + description: Manipulates a storage register which advertises extra features. The Mi Home app evaluates the value. A feature called "turbo mode" is unlocked in the app on value 1. + fields: + entity_id: + description: Name of the xiaomi miio entity. + example: 'fan.xiaomi_miio_device' + features: + description: Integer, known values are 0 (default) and 1 (turbo mode). + example: 1 + +fan_set_target_humidity: + description: Set the target humidity. + fields: + entity_id: + description: Name of the xiaomi miio entity. + example: 'fan.xiaomi_miio_device' + humidity: + description: Target humidity. Allowed values are 30, 40, 50, 60, 70 and 80. + example: 50 + +fan_set_dry_on: + description: Turn the dry mode on. + fields: + entity_id: + description: Name of the xiaomi miio entity. + example: 'fan.xiaomi_miio_device' + +fan_set_dry_off: + description: Turn the dry mode off. + fields: + entity_id: + description: Name of the xiaomi miio entity. + example: 'fan.xiaomi_miio_device' + +light_set_scene: + description: Set a fixed scene. + fields: + entity_id: + description: Name of the light entity. + example: "light.xiaomi_miio" + scene: + description: Number of the fixed scene, between 1 and 4. + example: 1 + +light_set_delayed_turn_off: + description: Delayed turn off. + fields: + entity_id: + description: Name of the light entity. + example: "light.xiaomi_miio" + time_period: + description: Time period for the delayed turn off. + example: "5, '0:05', {'minutes': 5}" + +light_reminder_on: + description: Enable the eye fatigue reminder/notification (EYECARE SMART LAMP 2 ONLY). + fields: + entity_id: + description: 'Name of the entity to act on.' + example: 'light.xiaomi_miio' + +light_reminder_off: + description: Disable the eye fatigue reminder/notification (EYECARE SMART LAMP 2 ONLY). + fields: + entity_id: + description: 'Name of the entity to act on.' + example: 'light.xiaomi_miio' + +light_night_light_mode_on: + description: Turn the eyecare mode on (EYECARE SMART LAMP 2 ONLY). + fields: + entity_id: + description: 'Name of the entity to act on.' + example: 'light.xiaomi_miio' + +light_night_light_mode_off: + description: Turn the eyecare mode fan_set_dry_off (EYECARE SMART LAMP 2 ONLY). + fields: + entity_id: + description: 'Name of the entity to act on.' + example: 'light.xiaomi_miio' + +light_eyecare_mode_on: + description: Enable the eye fatigue reminder/notification (EYECARE SMART LAMP 2 ONLY). + fields: + entity_id: + description: 'Name of the entity to act on.' + example: 'light.xiaomi_miio' + +light_eyecare_mode_off: + description: Disable the eye fatigue reminder/notification (EYECARE SMART LAMP 2 ONLY). + fields: + entity_id: + description: 'Name of the entity to act on.' + example: 'light.xiaomi_miio' + +remote_learn_command: + description: 'Learn an IR command, press "Call Service", point the remote at the IR device, and the learned command will be shown as a notification in Overview.' + fields: + entity_id: + description: 'Name of the entity to learn command from.' + example: 'remote.xiaomi_miio' + slot: + description: 'Define the slot used to save the IR command (Value from 1 to 1000000)' + example: '1' + timeout: + description: 'Define the timeout in seconds, before which the command must be learned.' + example: '30' + +switch_set_wifi_led_on: + description: Turn the wifi led on. + fields: + entity_id: + description: Name of the xiaomi miio entity. + example: 'switch.xiaomi_miio_device' + +switch_set_wifi_led_off: + description: Turn the wifi led off. + fields: + entity_id: + description: Name of the xiaomi miio entity. + example: 'switch.xiaomi_miio_device' + +switch_set_power_price: + description: Set the power price. + fields: + entity_id: + description: Name of the xiaomi miio entity. + example: 'switch.xiaomi_miio_device' + mode: + description: Power price, between 0 and 999. + example: 31 + +switch_set_power_mode: + description: Set the power mode. + fields: + entity_id: + description: Name of the xiaomi miio entity. + example: 'switch.xiaomi_miio_device' + mode: + description: Power mode, valid values are 'normal' and 'green'. + example: 'green' + +vacuum_remote_control_start: + description: Start remote control of the vacuum cleaner. You can then move it with `remote_control_move`, when done call `remote_control_stop`. + fields: + entity_id: + description: Name of the vacuum entity. + example: 'vacuum.xiaomi_vacuum_cleaner' + +vacuum_remote_control_stop: + description: Stop remote control mode of the vacuum cleaner. + fields: + entity_id: + description: Name of the vacuum entity. + example: 'vacuum.xiaomi_vacuum_cleaner' + +vacuum_remote_control_move: + description: Remote control the vacuum cleaner, make sure you first set it in remote control mode with `remote_control_start`. + fields: + entity_id: + description: Name of the vacuum entity. + example: 'vacuum.xiaomi_vacuum_cleaner' + velocity: + description: Speed, between -0.29 and 0.29. + example: '0.2' + rotation: + description: Rotation, between -179 degrees and 179 degrees. + example: '90' + duration: + description: Duration of the movement. + example: '1500' + +vacuum_remote_control_move_step: + description: Remote control the vacuum cleaner, only makes one move and then stops. + fields: + entity_id: + description: Name of the vacuum entity. + example: 'vacuum.xiaomi_vacuum_cleaner' + velocity: + description: Speed, between -0.29 and 0.29. + example: '0.2' + rotation: + description: Rotation, between -179 degrees and 179 degrees. + example: '90' + duration: + description: Duration of the movement. + example: '1500' + +vacuum_clean_zone: + description: Start the cleaning operation in the selected areas for the number of repeats indicated. + fields: + entity_id: + description: Name of the vacuum entity. + example: 'vacuum.xiaomi_vacuum_cleaner' + zone: + description: Array of zones. Each zone is an array of 4 integer values. + example: '[[23510,25311,25110,26362]]' + repeats: + description: Number of cleaning repeats for each zone between 1 and 3. + example: '1' diff --git a/homeassistant/components/xiaomi_miio/switch.py b/homeassistant/components/xiaomi_miio/switch.py index 023243a1995c..f9a06924b5c3 100644 --- a/homeassistant/components/xiaomi_miio/switch.py +++ b/homeassistant/components/xiaomi_miio/switch.py @@ -13,7 +13,7 @@ from miio.powerstrip import PowerMode # pylint: disable=import-error import voluptuous as vol -from homeassistant.components.switch import DOMAIN, PLATFORM_SCHEMA, SwitchDevice +from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchDevice from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_MODE, @@ -24,6 +24,14 @@ from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv +from .const import ( + DOMAIN, + SERVICE_SET_WIFI_LED_ON, + SERVICE_SET_WIFI_LED_OFF, + SERVICE_SET_POWER_MODE, + SERVICE_SET_POWER_PRICE, +) + _LOGGER = logging.getLogger(__name__) DEFAULT_NAME = "Xiaomi Miio Switch" @@ -80,11 +88,6 @@ FEATURE_FLAGS_PLUG_V3 = FEATURE_SET_WIFI_LED -SERVICE_SET_WIFI_LED_ON = "xiaomi_miio_set_wifi_led_on" -SERVICE_SET_WIFI_LED_OFF = "xiaomi_miio_set_wifi_led_off" -SERVICE_SET_POWER_MODE = "xiaomi_miio_set_power_mode" -SERVICE_SET_POWER_PRICE = "xiaomi_miio_set_power_price" - SERVICE_SCHEMA = vol.Schema({vol.Optional(ATTR_ENTITY_ID): cv.entity_ids}) SERVICE_SCHEMA_POWER_MODE = SERVICE_SCHEMA.extend( diff --git a/homeassistant/components/xiaomi_miio/vacuum.py b/homeassistant/components/xiaomi_miio/vacuum.py index b18a54ce97a9..f1845f534bb1 100644 --- a/homeassistant/components/xiaomi_miio/vacuum.py +++ b/homeassistant/components/xiaomi_miio/vacuum.py @@ -8,7 +8,6 @@ from homeassistant.components.vacuum import ( ATTR_CLEANED_AREA, - DOMAIN, PLATFORM_SCHEMA, STATE_CLEANING, STATE_DOCKED, @@ -38,6 +37,15 @@ ) import homeassistant.helpers.config_validation as cv +from .const import ( + DOMAIN, + SERVICE_MOVE_REMOTE_CONTROL, + SERVICE_MOVE_REMOTE_CONTROL_STEP, + SERVICE_START_REMOTE_CONTROL, + SERVICE_STOP_REMOTE_CONTROL, + SERVICE_CLEAN_ZONE, +) + _LOGGER = logging.getLogger(__name__) DEFAULT_NAME = "Xiaomi Vacuum cleaner" @@ -52,13 +60,7 @@ extra=vol.ALLOW_EXTRA, ) -SERVICE_MOVE_REMOTE_CONTROL = "xiaomi_remote_control_move" -SERVICE_MOVE_REMOTE_CONTROL_STEP = "xiaomi_remote_control_move_step" -SERVICE_START_REMOTE_CONTROL = "xiaomi_remote_control_start" -SERVICE_STOP_REMOTE_CONTROL = "xiaomi_remote_control_stop" -SERVICE_CLEAN_ZONE = "xiaomi_clean_zone" - -FAN_SPEEDS = {"Quiet": 38, "Balanced": 60, "Turbo": 77, "Max": 90} +FAN_SPEEDS = {"Quiet": 38, "Balanced": 60, "Turbo": 77, "Max": 90, "Gentle": 105} ATTR_CLEAN_START = "clean_start" ATTR_CLEAN_STOP = "clean_stop" diff --git a/homeassistant/components/xiaomi_tv/media_player.py b/homeassistant/components/xiaomi_tv/media_player.py index 352ce0c48355..c34448ba63b4 100644 --- a/homeassistant/components/xiaomi_tv/media_player.py +++ b/homeassistant/components/xiaomi_tv/media_player.py @@ -2,6 +2,7 @@ import logging import voluptuous as vol +import pymitv from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA from homeassistant.components.media_player.const import ( @@ -29,7 +30,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Xiaomi TV platform.""" - from pymitv import Discover # If a hostname is set. Discovery is skipped. host = config.get(CONF_HOST) @@ -37,14 +37,14 @@ def setup_platform(hass, config, add_entities, discovery_info=None): if host is not None: # Check if there's a valid TV at the IP address. - if not Discover().check_ip(host): + if not pymitv.Discover().check_ip(host): _LOGGER.error("Could not find Xiaomi TV with specified IP: %s", host) else: # Register TV with Home Assistant. add_entities([XiaomiTV(host, name)]) else: # Otherwise, discover TVs on network. - add_entities(XiaomiTV(tv, DEFAULT_NAME) for tv in Discover().scan()) + add_entities(XiaomiTV(tv, DEFAULT_NAME) for tv in pymitv.Discover().scan()) class XiaomiTV(MediaPlayerDevice): @@ -52,11 +52,9 @@ class XiaomiTV(MediaPlayerDevice): def __init__(self, ip, name): """Receive IP address and name to construct class.""" - # Import pymitv library. - from pymitv import TV # Initialize the Xiaomi TV. - self._tv = TV(ip) + self._tv = pymitv.TV(ip) # Default name value, only to be overridden by user. self._name = name self._state = STATE_OFF diff --git a/homeassistant/components/xmpp/notify.py b/homeassistant/components/xmpp/notify.py index 5aa9dbfffd11..338b0b85c038 100644 --- a/homeassistant/components/xmpp/notify.py +++ b/homeassistant/components/xmpp/notify.py @@ -190,7 +190,6 @@ async def send_file(self, timeout=None): message = self.Message(sto=recipient, stype="chat") message["body"] = url - # pylint: disable=invalid-sequence-index message["oob"]["url"] = url try: message.send() diff --git a/homeassistant/components/yale_smart_alarm/alarm_control_panel.py b/homeassistant/components/yale_smart_alarm/alarm_control_panel.py index 094263a658b5..7f2cbc2a33dd 100644 --- a/homeassistant/components/yale_smart_alarm/alarm_control_panel.py +++ b/homeassistant/components/yale_smart_alarm/alarm_control_panel.py @@ -2,15 +2,26 @@ import logging import voluptuous as vol +from yalesmartalarmclient.client import ( + YaleSmartAlarmClient, + AuthenticationError, + YALE_STATE_DISARM, + YALE_STATE_ARM_PARTIAL, + YALE_STATE_ARM_FULL, +) from homeassistant.components.alarm_control_panel import ( - AlarmControlPanel, PLATFORM_SCHEMA, + AlarmControlPanel, +) +from homeassistant.components.alarm_control_panel.const import ( + SUPPORT_ALARM_ARM_AWAY, + SUPPORT_ALARM_ARM_HOME, ) from homeassistant.const import ( + CONF_NAME, CONF_PASSWORD, CONF_USERNAME, - CONF_NAME, STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED, @@ -42,8 +53,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): password = config[CONF_PASSWORD] area_id = config[CONF_AREA_ID] - from yalesmartalarmclient.client import YaleSmartAlarmClient, AuthenticationError - try: client = YaleSmartAlarmClient(username, password, area_id) except AuthenticationError: @@ -62,12 +71,6 @@ def __init__(self, name, client): self._client = client self._state = None - from yalesmartalarmclient.client import ( - YALE_STATE_DISARM, - YALE_STATE_ARM_PARTIAL, - YALE_STATE_ARM_FULL, - ) - self._state_map = { YALE_STATE_DISARM: STATE_ALARM_DISARMED, YALE_STATE_ARM_PARTIAL: STATE_ALARM_ARMED_HOME, @@ -84,6 +87,11 @@ def state(self): """Return the state of the device.""" return self._state + @property + def supported_features(self) -> int: + """Return the list of supported features.""" + return SUPPORT_ALARM_ARM_HOME | SUPPORT_ALARM_ARM_AWAY + def update(self): """Return the state of the device.""" armed_status = self._client.get_armed_status() diff --git a/homeassistant/components/yamaha/const.py b/homeassistant/components/yamaha/const.py new file mode 100644 index 000000000000..e2a0c5eceeaf --- /dev/null +++ b/homeassistant/components/yamaha/const.py @@ -0,0 +1,3 @@ +"""Constants for the Yamaha component.""" +DOMAIN = "yamaha" +SERVICE_ENABLE_OUTPUT = "enable_output" diff --git a/homeassistant/components/yamaha/media_player.py b/homeassistant/components/yamaha/media_player.py index eabb1ef34f13..fa2c68dce88b 100644 --- a/homeassistant/components/yamaha/media_player.py +++ b/homeassistant/components/yamaha/media_player.py @@ -7,7 +7,6 @@ from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA from homeassistant.components.media_player.const import ( - DOMAIN, MEDIA_TYPE_MUSIC, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, @@ -34,6 +33,8 @@ ) import homeassistant.helpers.config_validation as cv +from .const import DOMAIN, SERVICE_ENABLE_OUTPUT + _LOGGER = logging.getLogger(__name__) ATTR_ENABLED = "enabled" @@ -53,8 +54,6 @@ {vol.Required(ATTR_ENABLED): cv.boolean, vol.Required(ATTR_PORT): cv.string} ) -SERVICE_ENABLE_OUTPUT = "yamaha_enable_output" - SUPPORT_YAMAHA = ( SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE diff --git a/homeassistant/components/yamaha/services.yaml b/homeassistant/components/yamaha/services.yaml index e69de29bb2d1..592a1d1342e3 100644 --- a/homeassistant/components/yamaha/services.yaml +++ b/homeassistant/components/yamaha/services.yaml @@ -0,0 +1,12 @@ +enable_output: + description: Enable or disable an output port + fields: + entity_id: + description: Name(s) of entites to enable/disable port on. + example: 'media_player.yamaha' + port: + description: Name of port to enable/disable. + example: 'hdmi1' + enabled: + description: Boolean indicating if port should be enabled or not. + example: true \ No newline at end of file diff --git a/homeassistant/components/yi/camera.py b/homeassistant/components/yi/camera.py index fb1b46344ca7..c8417748fd9e 100644 --- a/homeassistant/components/yi/camera.py +++ b/homeassistant/components/yi/camera.py @@ -2,21 +2,24 @@ import asyncio import logging +from aioftp import Client, StatusCodeError +from haffmpeg.camera import CameraMjpeg +from haffmpeg.tools import IMAGE_JPEG, ImageFrame import voluptuous as vol -from homeassistant.components.camera import Camera, PLATFORM_SCHEMA +from homeassistant.components.camera import PLATFORM_SCHEMA, Camera from homeassistant.components.ffmpeg import DATA_FFMPEG from homeassistant.const import ( CONF_HOST, CONF_NAME, - CONF_PATH, CONF_PASSWORD, + CONF_PATH, CONF_PORT, CONF_USERNAME, ) +from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_aiohttp_proxy_stream -from homeassistant.exceptions import PlatformNotReady _LOGGER = logging.getLogger(__name__) @@ -82,8 +85,6 @@ def name(self): async def _get_latest_video_url(self): """Retrieve the latest video file from the customized Yi FTP server.""" - from aioftp import Client, StatusCodeError - ftp = Client() try: await ftp.connect(self.host) @@ -125,8 +126,6 @@ async def _get_latest_video_url(self): async def async_camera_image(self): """Return a still image response from the camera.""" - from haffmpeg.tools import ImageFrame, IMAGE_JPEG - url = await self._get_latest_video_url() if url and url != self._last_url: ffmpeg = ImageFrame(self._manager.binary, loop=self.hass.loop) @@ -142,8 +141,6 @@ async def async_camera_image(self): async def handle_async_mjpeg_stream(self, request): """Generate an HTTP MJPEG stream from the camera.""" - from haffmpeg.camera import CameraMjpeg - if not self._is_on: return diff --git a/homeassistant/components/zabbix/__init__.py b/homeassistant/components/zabbix/__init__.py index f1c5dbffead3..0926f35af383 100644 --- a/homeassistant/components/zabbix/__init__.py +++ b/homeassistant/components/zabbix/__init__.py @@ -2,13 +2,14 @@ import logging from urllib.parse import urljoin +from pyzabbix import ZabbixAPI, ZabbixAPIException import voluptuous as vol from homeassistant.const import ( - CONF_PATH, CONF_HOST, - CONF_SSL, CONF_PASSWORD, + CONF_PATH, + CONF_SSL, CONF_USERNAME, ) import homeassistant.helpers.config_validation as cv @@ -37,7 +38,6 @@ def setup(hass, config): """Set up the Zabbix component.""" - from pyzabbix import ZabbixAPI, ZabbixAPIException conf = config[DOMAIN] if conf[CONF_SSL]: diff --git a/homeassistant/components/zabbix/sensor.py b/homeassistant/components/zabbix/sensor.py index 6b2c06eab2f4..3fa29a078968 100644 --- a/homeassistant/components/zabbix/sensor.py +++ b/homeassistant/components/zabbix/sensor.py @@ -4,9 +4,9 @@ import voluptuous as vol from homeassistant.components import zabbix -import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import CONF_NAME +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/zeroconf/__init__.py b/homeassistant/components/zeroconf/__init__.py index 2f9fb7b4580f..9f27a5fafc9a 100644 --- a/homeassistant/components/zeroconf/__init__.py +++ b/homeassistant/components/zeroconf/__init__.py @@ -1,14 +1,18 @@ """Support for exposing Home Assistant via Zeroconf.""" -# PyLint bug confuses absolute/relative imports -# https://github.com/PyCQA/pylint/issues/1931 -# pylint: disable=no-name-in-module + import logging import socket import ipaddress import voluptuous as vol -from zeroconf import ServiceBrowser, ServiceInfo, ServiceStateChange, Zeroconf +from zeroconf import ( + ServiceBrowser, + ServiceInfo, + ServiceStateChange, + Zeroconf, + NonUniqueNameException, +) from homeassistant import util from homeassistant.const import ( @@ -43,7 +47,7 @@ def setup(hass, config): params = { "version": __version__, "base_url": hass.config.api.base_url, - # always needs authentication + # Always needs authentication "requires_api_password": True, } @@ -69,7 +73,12 @@ def zeroconf_hass_start(_event): Wait till started or otherwise HTTP is not up and running. """ _LOGGER.info("Starting Zeroconf broadcast") - zeroconf.register_service(info) + try: + zeroconf.register_service(info) + except NonUniqueNameException: + _LOGGER.error( + "Home Assistant instance with identical name present in the local network" + ) hass.bus.listen_once(EVENT_HOMEASSISTANT_START, zeroconf_hass_start) diff --git a/homeassistant/components/zeroconf/manifest.json b/homeassistant/components/zeroconf/manifest.json index 39f016e9d0e8..ba764300daee 100644 --- a/homeassistant/components/zeroconf/manifest.json +++ b/homeassistant/components/zeroconf/manifest.json @@ -3,7 +3,7 @@ "name": "Zeroconf", "documentation": "https://www.home-assistant.io/integrations/zeroconf", "requirements": [ - "zeroconf==0.23.0" + "zeroconf==0.24.0" ], "dependencies": [ "api" diff --git a/homeassistant/components/zha/.translations/nn.json b/homeassistant/components/zha/.translations/nn.json index ad2c240baf18..392018bb1f1c 100644 --- a/homeassistant/components/zha/.translations/nn.json +++ b/homeassistant/components/zha/.translations/nn.json @@ -6,5 +6,10 @@ } }, "title": "ZHA" + }, + "device_automation": { + "action_type": { + "squawk": "Squawk" + } } } \ No newline at end of file diff --git a/homeassistant/components/zha/__init__.py b/homeassistant/components/zha/__init__.py index ab686a97989b..ecd27c48839f 100644 --- a/homeassistant/components/zha/__init__.py +++ b/homeassistant/components/zha/__init__.py @@ -8,7 +8,7 @@ from homeassistant.helpers.device_registry import CONNECTION_ZIGBEE # Loading the config flow file will register the flow -from . import config_flow # noqa # pylint: disable=unused-import +from . import config_flow # noqa: F401 pylint: disable=unused-import from . import api from .core import ZHAGateway from .core.const import ( @@ -100,8 +100,7 @@ async def async_setup_entry(hass, config_entry): if config.get(CONF_ENABLE_QUIRKS, True): # needs to be done here so that the ZHA module is finished loading # before zhaquirks is imported - # pylint: disable=W0611, W0612 - import zhaquirks # noqa + import zhaquirks # noqa: F401 pylint: disable=unused-import zha_gateway = ZHAGateway(hass, config, config_entry) await zha_gateway.async_initialize() diff --git a/homeassistant/components/zha/api.py b/homeassistant/components/zha/api.py index 6f24db442dd2..438b93244cf8 100644 --- a/homeassistant/components/zha/api.py +++ b/homeassistant/components/zha/api.py @@ -57,6 +57,7 @@ DEVICE_INFO = "device_info" ATTR_DURATION = "duration" +ATTR_GROUP = "group" ATTR_IEEE_ADDRESS = "ieee_address" ATTR_IEEE = "ieee" ATTR_SOURCE_IEEE = "source_ieee" @@ -68,6 +69,7 @@ SERVICE_REMOVE = "remove" SERVICE_SET_ZIGBEE_CLUSTER_ATTRIBUTE = "set_zigbee_cluster_attribute" SERVICE_ISSUE_ZIGBEE_CLUSTER_COMMAND = "issue_zigbee_cluster_command" +SERVICE_ISSUE_ZIGBEE_GROUP_COMMAND = "issue_zigbee_group_command" SERVICE_DIRECT_ZIGBEE_BIND = "issue_direct_zigbee_bind" SERVICE_DIRECT_ZIGBEE_UNBIND = "issue_direct_zigbee_unbind" SERVICE_WARNING_DEVICE_SQUAWK = "warning_device_squawk" @@ -139,7 +141,17 @@ vol.Optional(ATTR_CLUSTER_TYPE, default=CLUSTER_TYPE_IN): cv.string, vol.Required(ATTR_COMMAND): cv.positive_int, vol.Required(ATTR_COMMAND_TYPE): cv.string, - vol.Optional(ATTR_ARGS, default=""): cv.string, + vol.Optional(ATTR_ARGS, default=[]): cv.ensure_list, + vol.Optional(ATTR_MANUFACTURER): cv.positive_int, + } + ), + SERVICE_ISSUE_ZIGBEE_GROUP_COMMAND: vol.Schema( + { + vol.Required(ATTR_GROUP): cv.positive_int, + vol.Required(ATTR_CLUSTER_ID): cv.positive_int, + vol.Optional(ATTR_CLUSTER_TYPE, default=CLUSTER_TYPE_IN): cv.string, + vol.Required(ATTR_COMMAND): cv.positive_int, + vol.Optional(ATTR_ARGS, default=[]): cv.ensure_list, vol.Optional(ATTR_MANUFACTURER): cv.positive_int, } ), @@ -637,7 +649,7 @@ async def issue_zigbee_cluster_command(service): cluster_id, command, command_type, - args, + *args, cluster_type=cluster_type, manufacturer=manufacturer, ) @@ -660,6 +672,38 @@ async def issue_zigbee_cluster_command(service): schema=SERVICE_SCHEMAS[SERVICE_ISSUE_ZIGBEE_CLUSTER_COMMAND], ) + async def issue_zigbee_group_command(service): + """Issue command on zigbee cluster on a zigbee group.""" + group_id = service.data.get(ATTR_GROUP) + cluster_id = service.data.get(ATTR_CLUSTER_ID) + command = service.data.get(ATTR_COMMAND) + args = service.data.get(ATTR_ARGS) + manufacturer = service.data.get(ATTR_MANUFACTURER) or None + group = zha_gateway.get_group(group_id) + if cluster_id >= MFG_CLUSTER_ID_START and manufacturer is None: + _LOGGER.error("Missing manufacturer attribute for cluster: %d", cluster_id) + response = None + if group is not None: + cluster = group.endpoint[cluster_id] + response = await cluster.command( + command, *args, manufacturer=manufacturer, expect_reply=True + ) + _LOGGER.debug( + "Issue group command for: %s %s %s %s %s", + f"{ATTR_CLUSTER_ID}: [{cluster_id}]", + f"{ATTR_COMMAND}: [{command}]", + f"{ATTR_ARGS}: [{args}]", + f"{ATTR_MANUFACTURER}: [{manufacturer}]", + f"{RESPONSE}: [{response}]", + ) + + hass.helpers.service.async_register_admin_service( + DOMAIN, + SERVICE_ISSUE_ZIGBEE_GROUP_COMMAND, + issue_zigbee_group_command, + schema=SERVICE_SCHEMAS[SERVICE_ISSUE_ZIGBEE_GROUP_COMMAND], + ) + async def warning_device_squawk(service): """Issue the squawk command for an IAS warning device.""" ieee = service.data[ATTR_IEEE] @@ -758,5 +802,6 @@ 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_ISSUE_ZIGBEE_GROUP_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/channels/__init__.py b/homeassistant/components/zha/core/channels/__init__.py index 66a31ff8f21f..29cecb7784e9 100644 --- a/homeassistant/components/zha/core/channels/__init__.py +++ b/homeassistant/components/zha/core/channels/__init__.py @@ -395,14 +395,14 @@ def cluster_command(self, tsn, command_id, args): # pylint: disable=wrong-import-position -from . import closures # noqa -from . import general # noqa -from . import homeautomation # noqa -from . import hvac # noqa -from . import lighting # noqa -from . import lightlink # noqa -from . import manufacturerspecific # noqa -from . import measurement # noqa -from . import protocol # noqa -from . import security # noqa -from . import smartenergy # noqa +from . import closures # noqa: F401 +from . import general # noqa: F401 +from . import homeautomation # noqa: F401 +from . import hvac # noqa: F401 +from . import lighting # noqa: F401 +from . import lightlink # noqa: F401 +from . import manufacturerspecific # noqa: F401 +from . import measurement # noqa: F401 +from . import protocol # noqa: F401 +from . import security # noqa: F401 +from . import smartenergy # noqa: F401 diff --git a/homeassistant/components/zha/core/decorators.py b/homeassistant/components/zha/core/decorators.py index 4148cff6ca9f..c416548dbe97 100644 --- a/homeassistant/components/zha/core/decorators.py +++ b/homeassistant/components/zha/core/decorators.py @@ -1,7 +1,7 @@ """Decorators for ZHA core registries.""" from typing import Callable, TypeVar, Union -CALLABLE_T = TypeVar("CALLABLE_T", bound=Callable) # noqa pylint: disable=invalid-name +CALLABLE_T = TypeVar("CALLABLE_T", bound=Callable) # pylint: disable=invalid-name class DictRegistry(dict): diff --git a/homeassistant/components/zha/core/device.py b/homeassistant/components/zha/core/device.py index b3be8037ff61..e5d1678ad6fa 100644 --- a/homeassistant/components/zha/core/device.py +++ b/homeassistant/components/zha/core/device.py @@ -479,7 +479,7 @@ async def issue_cluster_command( cluster_id, command, command_type, - args, + *args, cluster_type=CLUSTER_TYPE_IN, manufacturer=None, ): @@ -487,7 +487,6 @@ async def issue_cluster_command( cluster = self.async_get_cluster(endpoint_id, cluster_id, cluster_type) if cluster is None: return None - response = None if command_type == CLUSTER_COMMAND_SERVER: response = await cluster.command( command, *args, manufacturer=manufacturer, expect_reply=True diff --git a/homeassistant/components/zha/core/gateway.py b/homeassistant/components/zha/core/gateway.py index 77702c8f3de8..ef81705ce471 100644 --- a/homeassistant/components/zha/core/gateway.py +++ b/homeassistant/components/zha/core/gateway.py @@ -222,6 +222,10 @@ def get_device(self, ieee): """Return ZHADevice for given ieee.""" return self._devices.get(ieee) + def get_group(self, group_id): + """Return Group for given group id.""" + return self.application_controller.groups[group_id] + def get_entity_reference(self, entity_id): """Return entity reference for given entity_id if found.""" for entity_reference in itertools.chain.from_iterable( diff --git a/homeassistant/components/zha/core/registries.py b/homeassistant/components/zha/core/registries.py index 571e77d4fae3..13688a6c4204 100644 --- a/homeassistant/components/zha/core/registries.py +++ b/homeassistant/components/zha/core/registries.py @@ -27,7 +27,7 @@ from homeassistant.components.switch import DOMAIN as SWITCH # importing channels updates registries -from . import channels # noqa pylint: disable=wrong-import-position,unused-import +from . import channels # noqa: F401 pylint: disable=unused-import from .const import ( CONTROLLER, SENSOR_ACCELERATION, diff --git a/homeassistant/components/zha/core/store.py b/homeassistant/components/zha/core/store.py index cea38517767c..bcc9b2a42d4e 100644 --- a/homeassistant/components/zha/core/store.py +++ b/homeassistant/components/zha/core/store.py @@ -1,5 +1,5 @@ """Data storage helper for ZHA.""" -# pylint: disable=W0611 +# pylint: disable=unused-import from collections import OrderedDict import logging from typing import MutableMapping diff --git a/homeassistant/components/zha/services.yaml b/homeassistant/components/zha/services.yaml index d279af46335f..ab8e9c195801 100644 --- a/homeassistant/components/zha/services.yaml +++ b/homeassistant/components/zha/services.yaml @@ -78,7 +78,27 @@ issue_zigbee_cluster_command: example: "server" args: description: args to pass to the command - example: {} + example: '[arg1, arg2, argN]' + manufacturer: + description: manufacturer code + example: 0x00FC + +issue_zigbee_group_command: + description: >- + Issue command on the specified cluster on the specified group. + fields: + group: + description: Hexadecimal address of the group + example: 0x0222 + cluster_id: + description: ZCL cluster to send command to + example: 6 + command: + description: id of the command to execute + example: 0 + args: + description: args to pass to the command + example: '[arg1, arg2, argN]' manufacturer: description: manufacturer code example: 0x00FC diff --git a/homeassistant/components/zhong_hong/climate.py b/homeassistant/components/zhong_hong/climate.py index f1a363cfedec..b94e19d6dbdb 100644 --- a/homeassistant/components/zhong_hong/climate.py +++ b/homeassistant/components/zhong_hong/climate.py @@ -2,6 +2,8 @@ import logging import voluptuous as vol +from zhong_hong_hvac.hub import ZhongHongGateway +from zhong_hong_hvac.hvac import HVAC as ZhongHongHVAC from homeassistant.components.climate import PLATFORM_SCHEMA, ClimateDevice from homeassistant.components.climate.const import ( @@ -71,7 +73,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the ZhongHong HVAC platform.""" - from zhong_hong_hvac.hub import ZhongHongGateway host = config.get(CONF_HOST) port = config.get(CONF_PORT) @@ -117,9 +118,8 @@ class ZhongHongClimate(ClimateDevice): def __init__(self, hub, addr_out, addr_in): """Set up the ZhongHong climate devices.""" - from zhong_hong_hvac.hvac import HVAC - self._device = HVAC(hub, addr_out, addr_in) + self._device = ZhongHongHVAC(hub, addr_out, addr_in) self._hub = hub self._current_operation = None self._current_temperature = None diff --git a/homeassistant/components/ziggo_mediabox_xl/media_player.py b/homeassistant/components/ziggo_mediabox_xl/media_player.py index a5f8b38ac377..83a7dbbaba92 100644 --- a/homeassistant/components/ziggo_mediabox_xl/media_player.py +++ b/homeassistant/components/ziggo_mediabox_xl/media_player.py @@ -3,8 +3,9 @@ import socket import voluptuous as vol +from ziggo_mediabox_xl import ZiggoMediaboxXL -from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA +from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice from homeassistant.components.media_player.const import ( SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, @@ -44,7 +45,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Ziggo Mediabox XL platform.""" - from ziggo_mediabox_xl import ZiggoMediaboxXL hass.data[DATA_KNOWN_DEVICES] = known_devices = set() diff --git a/homeassistant/components/zoneminder/__init__.py b/homeassistant/components/zoneminder/__init__.py index a116cc31891e..3007c981480f 100644 --- a/homeassistant/components/zoneminder/__init__.py +++ b/homeassistant/components/zoneminder/__init__.py @@ -2,18 +2,19 @@ import logging import voluptuous as vol +from zoneminder.zm import ZoneMinder -import homeassistant.helpers.config_validation as cv from homeassistant.const import ( + ATTR_ID, + ATTR_NAME, CONF_HOST, CONF_PASSWORD, CONF_PATH, CONF_SSL, CONF_USERNAME, CONF_VERIFY_SSL, - ATTR_NAME, - ATTR_ID, ) +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.discovery import async_load_platform _LOGGER = logging.getLogger(__name__) @@ -51,7 +52,6 @@ def setup(hass, config): """Set up the ZoneMinder component.""" - from zoneminder.zm import ZoneMinder hass.data[DOMAIN] = {} diff --git a/homeassistant/components/zoneminder/sensor.py b/homeassistant/components/zoneminder/sensor.py index bfcfcb8f907a..75531e79e13b 100644 --- a/homeassistant/components/zoneminder/sensor.py +++ b/homeassistant/components/zoneminder/sensor.py @@ -2,6 +2,7 @@ import logging import voluptuous as vol +from zoneminder.monitor import TimePeriod from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import CONF_MONITORED_CONDITIONS @@ -95,7 +96,6 @@ class ZMSensorEvents(Entity): def __init__(self, monitor, include_archived, sensor_type): """Initialize event sensor.""" - from zoneminder.monitor import TimePeriod self._monitor = monitor self._include_archived = include_archived diff --git a/homeassistant/components/zoneminder/switch.py b/homeassistant/components/zoneminder/switch.py index d2d761aab1e6..5eaf2ed4901d 100644 --- a/homeassistant/components/zoneminder/switch.py +++ b/homeassistant/components/zoneminder/switch.py @@ -2,6 +2,7 @@ import logging import voluptuous as vol +from zoneminder.monitor import MonitorState from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchDevice from homeassistant.const import CONF_COMMAND_OFF, CONF_COMMAND_ON @@ -21,7 +22,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the ZoneMinder switch platform.""" - from zoneminder.monitor import MonitorState on_state = MonitorState(config.get(CONF_COMMAND_ON)) off_state = MonitorState(config.get(CONF_COMMAND_OFF)) diff --git a/homeassistant/components/zwave/__init__.py b/homeassistant/components/zwave/__init__.py index 97a904c5d994..293cc45273f3 100644 --- a/homeassistant/components/zwave/__init__.py +++ b/homeassistant/components/zwave/__init__.py @@ -35,7 +35,7 @@ ) from . import const -from . import config_flow # noqa pylint: disable=unused-import +from . import config_flow # noqa: F401 pylint: disable=unused-import from . import websocket_api as wsapi from .const import ( CONF_AUTOHEAL, diff --git a/homeassistant/components/zwave/climate.py b/homeassistant/components/zwave/climate.py index b40fff669589..e50908783283 100644 --- a/homeassistant/components/zwave/climate.py +++ b/homeassistant/components/zwave/climate.py @@ -2,6 +2,8 @@ # Because we do not compile openzwave on CI import logging +from typing import Optional + from homeassistant.components.climate import ClimateDevice from homeassistant.components.climate.const import ( CURRENT_HVAC_COOL, @@ -17,18 +19,23 @@ HVAC_MODE_DRY, HVAC_MODE_FAN_ONLY, HVAC_MODE_OFF, + PRESET_AWAY, PRESET_BOOST, PRESET_NONE, SUPPORT_AUX_HEAT, SUPPORT_FAN_MODE, SUPPORT_SWING_MODE, SUPPORT_TARGET_TEMPERATURE, + SUPPORT_TARGET_TEMPERATURE_RANGE, SUPPORT_PRESET_MODE, + ATTR_TARGET_TEMP_LOW, + ATTR_TARGET_TEMP_HIGH, ) from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect + from . import ZWaveDeviceEntity _LOGGER = logging.getLogger(__name__) @@ -66,6 +73,33 @@ "auto changeover": HVAC_MODE_HEAT_COOL, } +MODE_SETPOINT_MAPPINGS = { + "off": (), + "heat": ("setpoint_heating",), + "cool": ("setpoint_cooling",), + "auto": ("setpoint_heating", "setpoint_cooling"), + "aux heat": ("setpoint_heating",), + "furnace": ("setpoint_furnace",), + "dry air": ("setpoint_dry_air",), + "moist air": ("setpoint_moist_air",), + "auto changeover": ("setpoint_auto_changeover",), + "heat econ": ("setpoint_eco_heating",), + "cool econ": ("setpoint_eco_cooling",), + "away": ("setpoint_away_heating", "setpoint_away_cooling"), + "full power": ("setpoint_full_power",), + # aliases found in xml configs + "comfort": ("setpoint_heating",), + "heat mode": ("setpoint_heating",), + "heat (default)": ("setpoint_heating",), + "dry floor": ("setpoint_dry_air",), + "heat eco": ("setpoint_eco_heating",), + "energy saving": ("setpoint_eco_heating",), + "energy heat": ("setpoint_eco_heating",), + "vacation": ("setpoint_away_heating", "setpoint_away_cooling"), + # for tests + "heat_cool": ("setpoint_heating", "setpoint_cooling"), +} + HVAC_CURRENT_MAPPINGS = { "idle": CURRENT_HVAC_IDLE, "heat": CURRENT_HVAC_HEAT, @@ -80,6 +114,7 @@ } PRESET_MAPPINGS = { + "away": PRESET_AWAY, "full power": PRESET_BOOST, "manufacturer specific": PRESET_MANUFACTURER_SPECIFIC, } @@ -124,6 +159,7 @@ def __init__(self, values, temp_unit): """Initialize the Z-Wave climate device.""" ZWaveDeviceEntity.__init__(self, values, DOMAIN) self._target_temperature = None + self._target_temperature_range = (None, None) self._current_temperature = None self._hvac_action = None self._hvac_list = None # [zwave_mode] @@ -154,10 +190,20 @@ def __init__(self, values, temp_unit): self._zxt_120 = 1 self.update_properties() + def _current_mode_setpoints(self): + current_mode = str(self.values.primary.data).lower() + setpoints_names = MODE_SETPOINT_MAPPINGS.get(current_mode, ()) + return tuple(getattr(self.values, name, None) for name in setpoints_names) + @property def supported_features(self): """Return the list of supported features.""" support = SUPPORT_TARGET_TEMPERATURE + if HVAC_MODE_HEAT_COOL in self._hvac_list: + support |= SUPPORT_TARGET_TEMPERATURE_RANGE + if PRESET_AWAY in self._preset_list: + support |= SUPPORT_TARGET_TEMPERATURE_RANGE + if self.values.fan_mode: support |= SUPPORT_FAN_MODE if self._zxt_120 == 1 and self.values.zxt_120_swing_mode: @@ -193,13 +239,13 @@ def update_properties(self): def _update_operation_mode(self): """Update hvac and preset modes.""" - if self.values.mode: + if self.values.primary: self._hvac_list = [] self._hvac_mapping = {} self._preset_list = [] self._preset_mapping = {} - mode_list = self.values.mode.data_items + mode_list = self.values.primary.data_items if mode_list: for mode in mode_list: ha_mode = HVAC_STATE_MAPPINGS.get(str(mode).lower()) @@ -227,7 +273,7 @@ def _update_operation_mode(self): # Presets are supported self._preset_list.append(PRESET_NONE) - current_mode = self.values.mode.data + current_mode = self.values.primary.data _LOGGER.debug("current_mode=%s", current_mode) _hvac_temp = next( ( @@ -313,15 +359,21 @@ def _update_swing_mode(self): def _update_target_temp(self): """Update target temperature.""" - if self.values.primary.data == 0: - _LOGGER.debug( - "Setpoint is 0, setting default to " "current_temperature=%s", - self._current_temperature, - ) - if self._current_temperature is not None: - self._target_temperature = round((float(self._current_temperature)), 1) - else: - self._target_temperature = round((float(self.values.primary.data)), 1) + current_setpoints = self._current_mode_setpoints() + self._target_temperature = None + self._target_temperature_range = (None, None) + if len(current_setpoints) == 1: + (setpoint,) = current_setpoints + if setpoint is not None: + self._target_temperature = round((float(setpoint.data)), 1) + elif len(current_setpoints) == 2: + (setpoint_low, setpoint_high) = current_setpoints + target_low, target_high = None, None + if setpoint_low is not None: + target_low = round((float(setpoint_low.data)), 1) + if setpoint_high is not None: + target_high = round((float(setpoint_high.data)), 1) + self._target_temperature_range = (target_low, target_high) def _update_operating_state(self): """Update operating state.""" @@ -374,7 +426,7 @@ def hvac_mode(self): Need to be one of HVAC_MODE_*. """ - if self.values.mode: + if self.values.primary: return self._hvac_mode return self._default_hvac_mode @@ -384,7 +436,7 @@ def hvac_modes(self): Need to be a subset of HVAC_MODES. """ - if self.values.mode: + if self.values.primary: return self._hvac_list return [] @@ -401,7 +453,7 @@ def is_aux_heat(self): """Return true if aux heater.""" if not self._aux_heat: return None - if self.values.mode.data == AUX_HEAT_ZWAVE_MODE: + if self.values.primary.data == AUX_HEAT_ZWAVE_MODE: return True return False @@ -411,7 +463,7 @@ def preset_mode(self): Need to be one of PRESET_*. """ - if self.values.mode: + if self.values.primary: return self._preset_mode return PRESET_NONE @@ -421,7 +473,7 @@ def preset_modes(self): Need to be a subset of PRESET_MODES. """ - if self.values.mode: + if self.values.primary: return self._preset_list return [] @@ -430,12 +482,35 @@ def target_temperature(self): """Return the temperature we try to reach.""" return self._target_temperature + @property + def target_temperature_low(self) -> Optional[float]: + """Return the lowbound target temperature we try to reach.""" + return self._target_temperature_range[0] + + @property + def target_temperature_high(self) -> Optional[float]: + """Return the highbound target temperature we try to reach.""" + return self._target_temperature_range[1] + def set_temperature(self, **kwargs): """Set new target temperature.""" - _LOGGER.debug("Set temperature to %s", kwargs.get(ATTR_TEMPERATURE)) - if kwargs.get(ATTR_TEMPERATURE) is None: - return - self.values.primary.data = kwargs.get(ATTR_TEMPERATURE) + current_setpoints = self._current_mode_setpoints() + if len(current_setpoints) == 1: + (setpoint,) = current_setpoints + target_temp = kwargs.get(ATTR_TEMPERATURE) + if setpoint is not None and target_temp is not None: + _LOGGER.debug("Set temperature to %s", target_temp) + setpoint.data = target_temp + elif len(current_setpoints) == 2: + (setpoint_low, setpoint_high) = current_setpoints + target_temp_low = kwargs.get(ATTR_TARGET_TEMP_LOW) + target_temp_high = kwargs.get(ATTR_TARGET_TEMP_HIGH) + if setpoint_low is not None and target_temp_low is not None: + _LOGGER.debug("Set low temperature to %s", target_temp_low) + setpoint_low.data = target_temp_low + if setpoint_high is not None and target_temp_high is not None: + _LOGGER.debug("Set high temperature to %s", target_temp_high) + setpoint_high.data = target_temp_high def set_fan_mode(self, fan_mode): """Set new target fan mode.""" @@ -447,11 +522,11 @@ def set_fan_mode(self, fan_mode): def set_hvac_mode(self, hvac_mode): """Set new target hvac mode.""" _LOGGER.debug("Set hvac_mode to %s", hvac_mode) - if not self.values.mode: + if not self.values.primary: return operation_mode = self._hvac_mapping.get(hvac_mode) _LOGGER.debug("Set operation_mode to %s", operation_mode) - self.values.mode.data = operation_mode + self.values.primary.data = operation_mode def turn_aux_heat_on(self): """Turn auxillary heater on.""" @@ -459,7 +534,7 @@ def turn_aux_heat_on(self): return operation_mode = AUX_HEAT_ZWAVE_MODE _LOGGER.debug("Aux heat on. Set operation mode to %s", operation_mode) - self.values.mode.data = operation_mode + self.values.primary.data = operation_mode def turn_aux_heat_off(self): """Turn auxillary heater off.""" @@ -470,23 +545,23 @@ def turn_aux_heat_off(self): else: operation_mode = self._hvac_mapping.get(HVAC_MODE_OFF) _LOGGER.debug("Aux heat off. Set operation mode to %s", operation_mode) - self.values.mode.data = operation_mode + self.values.primary.data = operation_mode def set_preset_mode(self, preset_mode): """Set new target preset mode.""" _LOGGER.debug("Set preset_mode to %s", preset_mode) - if not self.values.mode: + if not self.values.primary: return if preset_mode == PRESET_NONE: # Activate the current hvac mode self._update_operation_mode() operation_mode = self._hvac_mapping.get(self.hvac_mode) _LOGGER.debug("Set operation_mode to %s", operation_mode) - self.values.mode.data = operation_mode + self.values.primary.data = operation_mode else: operation_mode = self._preset_mapping.get(preset_mode, preset_mode) _LOGGER.debug("Set operation_mode to %s", operation_mode) - self.values.mode.data = operation_mode + self.values.primary.data = operation_mode def set_swing_mode(self, swing_mode): """Set new target swing mode.""" diff --git a/homeassistant/components/zwave/config_flow.py b/homeassistant/components/zwave/config_flow.py index f28502db57fc..a264cdea7dab 100644 --- a/homeassistant/components/zwave/config_flow.py +++ b/homeassistant/components/zwave/config_flow.py @@ -48,8 +48,7 @@ async def async_step_user(self, user_input=None): try: from functools import partial - # pylint: disable=unused-variable - option = await self.hass.async_add_executor_job( # noqa: F841 + option = await self.hass.async_add_executor_job( # noqa: F841 pylint: disable=unused-variable partial( ZWaveOption, user_input[CONF_USB_STICK_PATH], diff --git a/homeassistant/components/zwave/discovery_schemas.py b/homeassistant/components/zwave/discovery_schemas.py index e2254073290e..2d6f08169eaf 100644 --- a/homeassistant/components/zwave/discovery_schemas.py +++ b/homeassistant/components/zwave/discovery_schemas.py @@ -57,17 +57,68 @@ DEFAULT_VALUES_SCHEMA, **{ const.DISC_PRIMARY: { - const.DISC_COMMAND_CLASS: [const.COMMAND_CLASS_THERMOSTAT_SETPOINT] + const.DISC_COMMAND_CLASS: [const.COMMAND_CLASS_THERMOSTAT_MODE] + }, + "setpoint_heating": { + const.DISC_COMMAND_CLASS: [const.COMMAND_CLASS_THERMOSTAT_SETPOINT], + const.DISC_INDEX: [1], + const.DISC_OPTIONAL: True, + }, + "setpoint_cooling": { + const.DISC_COMMAND_CLASS: [const.COMMAND_CLASS_THERMOSTAT_SETPOINT], + const.DISC_INDEX: [2], + const.DISC_OPTIONAL: True, + }, + "setpoint_furnace": { + const.DISC_COMMAND_CLASS: [const.COMMAND_CLASS_THERMOSTAT_SETPOINT], + const.DISC_INDEX: [7], + const.DISC_OPTIONAL: True, + }, + "setpoint_dry_air": { + const.DISC_COMMAND_CLASS: [const.COMMAND_CLASS_THERMOSTAT_SETPOINT], + const.DISC_INDEX: [8], + const.DISC_OPTIONAL: True, + }, + "setpoint_moist_air": { + const.DISC_COMMAND_CLASS: [const.COMMAND_CLASS_THERMOSTAT_SETPOINT], + const.DISC_INDEX: [9], + const.DISC_OPTIONAL: True, + }, + "setpoint_auto_changeover": { + const.DISC_COMMAND_CLASS: [const.COMMAND_CLASS_THERMOSTAT_SETPOINT], + const.DISC_INDEX: [10], + const.DISC_OPTIONAL: True, + }, + "setpoint_eco_heating": { + const.DISC_COMMAND_CLASS: [const.COMMAND_CLASS_THERMOSTAT_SETPOINT], + const.DISC_INDEX: [11], + const.DISC_OPTIONAL: True, + }, + "setpoint_eco_cooling": { + const.DISC_COMMAND_CLASS: [const.COMMAND_CLASS_THERMOSTAT_SETPOINT], + const.DISC_INDEX: [12], + const.DISC_OPTIONAL: True, + }, + "setpoint_away_heating": { + const.DISC_COMMAND_CLASS: [const.COMMAND_CLASS_THERMOSTAT_SETPOINT], + const.DISC_INDEX: [13], + const.DISC_OPTIONAL: True, + }, + "setpoint_away_cooling": { + const.DISC_COMMAND_CLASS: [const.COMMAND_CLASS_THERMOSTAT_SETPOINT], + const.DISC_INDEX: [14], + const.DISC_OPTIONAL: True, + }, + "setpoint_full_power": { + const.DISC_COMMAND_CLASS: [const.COMMAND_CLASS_THERMOSTAT_SETPOINT], + const.DISC_INDEX: [15], + const.DISC_OPTIONAL: True, }, "temperature": { const.DISC_COMMAND_CLASS: [const.COMMAND_CLASS_SENSOR_MULTILEVEL], const.DISC_INDEX: [const.INDEX_SENSOR_MULTILEVEL_TEMPERATURE], const.DISC_OPTIONAL: True, }, - "mode": { - const.DISC_COMMAND_CLASS: [const.COMMAND_CLASS_THERMOSTAT_MODE], - const.DISC_OPTIONAL: True, - }, "fan_mode": { const.DISC_COMMAND_CLASS: [const.COMMAND_CLASS_THERMOSTAT_FAN_MODE], const.DISC_OPTIONAL: True, diff --git a/homeassistant/components/zwave/manifest.json b/homeassistant/components/zwave/manifest.json index 9268a50a14d7..c781a493b550 100644 --- a/homeassistant/components/zwave/manifest.json +++ b/homeassistant/components/zwave/manifest.json @@ -3,12 +3,7 @@ "name": "Z-Wave", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/zwave", - "requirements": [ - "homeassistant-pyozw==0.1.4", - "pydispatcher==2.0.5" - ], + "requirements": ["homeassistant-pyozw==0.1.7", "pydispatcher==2.0.5"], "dependencies": [], - "codeowners": [ - "@home-assistant/z-wave" - ] + "codeowners": ["@home-assistant/z-wave"] } diff --git a/homeassistant/config.py b/homeassistant/config.py index e6be2b9c7a55..71628be8006e 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -627,7 +627,6 @@ async def merge_packages_config( _log_pkg_error: Callable = _log_pkg_error, ) -> Dict: """Merge packages into the top-level configuration. Mutate config.""" - # pylint: disable=too-many-nested-blocks PACKAGES_CONFIG_SCHEMA(packages) for pack_name, pack_conf in packages.items(): for comp_name, comp_conf in pack_conf.items(): @@ -766,7 +765,6 @@ async def async_process_component_config( # Validate platform specific schema if hasattr(platform, "PLATFORM_SCHEMA"): - # pylint: disable=no-member try: p_validated = platform.PLATFORM_SCHEMA( # type: ignore p_config diff --git a/homeassistant/const.py b/homeassistant/const.py index 927739486d69..e312bd4c2574 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 -MINOR_VERSION = 102 -PATCH_VERSION = "3" +MINOR_VERSION = 103 +PATCH_VERSION = "0" __short_version__ = "{}.{}".format(MAJOR_VERSION, MINOR_VERSION) __version__ = "{}.{}".format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 6, 1) @@ -40,6 +40,7 @@ CONF_BINARY_SENSORS = "binary_sensors" CONF_BLACKLIST = "blacklist" CONF_BRIGHTNESS = "brightness" +CONF_BROADCAST_ADDRESS = "broadcast_address" CONF_CLIENT_ID = "client_id" CONF_CLIENT_SECRET = "client_secret" CONF_CODE = "code" diff --git a/homeassistant/core.py b/homeassistant/core.py index 01c5561d939f..2859e0fe1572 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -68,14 +68,13 @@ from homeassistant import util import homeassistant.util.dt as dt_util from homeassistant.util import location, slugify -from homeassistant.util.unit_system import ( # NOQA +from homeassistant.util.unit_system import ( UnitSystem, IMPERIAL_SYSTEM, METRIC_SYSTEM, ) # Typing imports that create a circular dependency -# pylint: disable=using-constant-test if TYPE_CHECKING: from homeassistant.config_entries import ConfigEntries from homeassistant.components.http import HomeAssistantHTTP diff --git a/homeassistant/exceptions.py b/homeassistant/exceptions.py index 89caf730ad7f..6147e26c8090 100644 --- a/homeassistant/exceptions.py +++ b/homeassistant/exceptions.py @@ -2,10 +2,8 @@ from typing import Optional, Tuple, TYPE_CHECKING import jinja2 -# pylint: disable=using-constant-test if TYPE_CHECKING: - # pylint: disable=unused-import - from .core import Context # noqa + from .core import Context # noqa: F401 pylint: disable=unused-import class HomeAssistantError(Exception): diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 519df86f5e9e..8d4be47f5f86 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -24,10 +24,12 @@ "esphome", "geofency", "geonetnz_quakes", + "geonetnz_volcano", "glances", "gpslogger", "hangouts", "heos", + "hisense_aehw4a1", "homekit_controller", "homematicip_cloud", "huawei_lte", @@ -67,6 +69,7 @@ "soma", "somfy", "sonos", + "starline", "tellduslive", "toon", "tplink", diff --git a/homeassistant/helpers/__init__.py b/homeassistant/helpers/__init__.py index 4c1a9803d754..fe60ffc4b33f 100644 --- a/homeassistant/helpers/__init__.py +++ b/homeassistant/helpers/__init__.py @@ -4,7 +4,6 @@ from homeassistant.const import CONF_PLATFORM -# pylint: disable=invalid-name ConfigType = Dict[str, Any] diff --git a/homeassistant/helpers/config_entry_flow.py b/homeassistant/helpers/config_entry_flow.py index 7a1512957a29..41f90effb89f 100644 --- a/homeassistant/helpers/config_entry_flow.py +++ b/homeassistant/helpers/config_entry_flow.py @@ -24,7 +24,7 @@ def __init__( self._domain = domain self._title = title self._discovery_function = discovery_function - self.CONNECTION_CLASS = connection_class # pylint: disable=C0103 + self.CONNECTION_CLASS = connection_class # pylint: disable=invalid-name async def async_step_user(self, user_input=None): """Handle a flow initialized by the user.""" diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index 7ca5a7e86f92..948fb017d9d1 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -489,8 +489,9 @@ def template_complex(value): for key, element in return_value.items(): return_value[key] = template_complex(element) return return_value - - return template(value) + if isinstance(value, str): + return template(value) + return value def datetime(value): @@ -715,12 +716,23 @@ def custom_serializer(schema): PLATFORM_SCHEMA_BASE = PLATFORM_SCHEMA.extend({}, extra=vol.ALLOW_EXTRA) -ENTITY_SERVICE_SCHEMA = vol.Schema( - { - vol.Optional(ATTR_ENTITY_ID): comp_entity_ids, - vol.Optional(ATTR_AREA_ID): vol.All(ensure_list, [str]), - } -) + +def make_entity_service_schema( + schema: dict, *, extra: int = vol.PREVENT_EXTRA +) -> vol.All: + """Create an entity service schema.""" + return vol.All( + vol.Schema( + { + **schema, + vol.Optional(ATTR_ENTITY_ID): comp_entity_ids, + vol.Optional(ATTR_AREA_ID): vol.All(ensure_list, [str]), + }, + extra=extra, + ), + has_at_least_one_key(ATTR_ENTITY_ID, ATTR_AREA_ID), + ) + EVENT_SCHEMA = vol.Schema( { diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index 0d2182f88e1f..ed6560614016 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -1,5 +1,5 @@ """An abstract class for entities.""" - +from abc import ABC import asyncio from datetime import datetime, timedelta import logging @@ -85,7 +85,7 @@ def async_generate_entity_id( return ensure_unique_string(entity_id_format.format(slugify(name)), current_ids) -class Entity: +class Entity(ABC): """An abstract class for Home Assistant entities.""" # SAFE TO OVERWRITE @@ -144,6 +144,17 @@ def state(self) -> Union[None, str, int, float]: """Return the state of the entity.""" return STATE_UNKNOWN + @property + def capability_attributes(self) -> Optional[Dict[str, Any]]: + """Return the capability attributes. + + Attributes that explain the capabilities of an entity. + + Implemented by component base class. Convention for attribute names + is lowercase snake_case. + """ + return None + @property def state_attributes(self) -> Optional[Dict[str, Any]]: """Return the state attributes. @@ -302,7 +313,7 @@ def _async_write_ha_state(self): start = timer() - attr = {} + attr = self.capability_attributes or {} if not self.available: state = STATE_UNAVAILABLE else: diff --git a/homeassistant/helpers/entity_component.py b/homeassistant/helpers/entity_component.py index 42b19da889ec..63d1b21fc9aa 100644 --- a/homeassistant/helpers/entity_component.py +++ b/homeassistant/helpers/entity_component.py @@ -15,7 +15,7 @@ from homeassistant.core import callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_per_platform, discovery -from homeassistant.helpers.config_validation import ENTITY_SERVICE_SCHEMA +from homeassistant.helpers.config_validation import make_entity_service_schema from homeassistant.helpers.service import async_extract_entity_ids from homeassistant.loader import bind_hass, async_get_integration from homeassistant.util import slugify @@ -173,24 +173,16 @@ async def async_unload_entry(self, config_entry): async def async_extract_from_service(self, service, expand_group=True): """Extract all known and available entities from a service call. - Will return all entities if no entities specified in call. Will return an empty list if entities specified but unknown. This method must be run in the event loop. """ data_ent_id = service.data.get(ATTR_ENTITY_ID) - if data_ent_id in (None, ENTITY_MATCH_ALL): - if data_ent_id is None: - self.logger.warning( - "Not passing an entity ID to a service to target all " - "entities is deprecated. Update your call to %s.%s to be " - "instead: entity_id: %s", - service.domain, - service.service, - ENTITY_MATCH_ALL, - ) + if data_ent_id is None: + return [] + if data_ent_id == ENTITY_MATCH_ALL: return [entity for entity in self.entities if entity.available] entity_ids = await async_extract_entity_ids(self.hass, service, expand_group) @@ -204,7 +196,7 @@ async def async_extract_from_service(self, service, expand_group=True): def async_register_entity_service(self, name, schema, func, required_features=None): """Register an entity service.""" if isinstance(schema, dict): - schema = ENTITY_SERVICE_SCHEMA.extend(schema) + schema = make_entity_service_schema(schema) async def handle_service(call): """Handle the service.""" diff --git a/homeassistant/helpers/icon.py b/homeassistant/helpers/icon.py index 96c3b7e08c16..b2a1d58717bc 100644 --- a/homeassistant/helpers/icon.py +++ b/homeassistant/helpers/icon.py @@ -18,3 +18,14 @@ def icon_for_battery_level( elif 5 < battery_level < 95: icon += "-{}".format(int(round(battery_level / 10 - 0.01)) * 10) return icon + + +def icon_for_signal_level(signal_level: Optional[int] = None) -> str: + """Return a signal icon valid identifier.""" + if signal_level is None or signal_level == 0: + return "mdi:signal-cellular-outline" + if signal_level > 70: + return "mdi:signal-cellular-3" + if signal_level > 30: + return "mdi:signal-cellular-2" + return "mdi:signal-cellular-1" diff --git a/homeassistant/helpers/intent.py b/homeassistant/helpers/intent.py index dc48d825348f..12b346603f05 100644 --- a/homeassistant/helpers/intent.py +++ b/homeassistant/helpers/intent.py @@ -6,7 +6,7 @@ import voluptuous as vol from homeassistant.const import ATTR_SUPPORTED_FEATURES -from homeassistant.core import callback, State, T +from homeassistant.core import callback, State, T, Context from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_validation as cv from homeassistant.helpers.typing import HomeAssistantType @@ -53,6 +53,7 @@ async def async_handle( intent_type: str, slots: Optional[_SlotsType] = None, text_input: Optional[str] = None, + context: Optional[Context] = None, ) -> "IntentResponse": """Handle an intent.""" handler: IntentHandler = hass.data.get(DATA_KEY, {}).get(intent_type) @@ -60,7 +61,10 @@ async def async_handle( if handler is None: raise UnknownIntent(f"Unknown intent {intent_type}") - intent = Intent(hass, platform, intent_type, slots or {}, text_input) + if context is None: + context = Context() + + intent = Intent(hass, platform, intent_type, slots or {}, text_input, context) try: _LOGGER.info("Triggering intent handler %s", handler) @@ -196,7 +200,10 @@ async def async_handle(self, intent_obj: "Intent") -> "IntentResponse": state = async_match_state(hass, slots["name"]["value"]) await hass.services.async_call( - self.domain, self.service, {ATTR_ENTITY_ID: state.entity_id} + self.domain, + self.service, + {ATTR_ENTITY_ID: state.entity_id}, + context=intent_obj.context, ) response = intent_obj.create_response() @@ -207,7 +214,7 @@ async def async_handle(self, intent_obj: "Intent") -> "IntentResponse": class Intent: """Hold the intent.""" - __slots__ = ["hass", "platform", "intent_type", "slots", "text_input"] + __slots__ = ["hass", "platform", "intent_type", "slots", "text_input", "context"] def __init__( self, @@ -216,6 +223,7 @@ def __init__( intent_type: str, slots: _SlotsType, text_input: Optional[str], + context: Context, ) -> None: """Initialize an intent.""" self.hass = hass @@ -223,6 +231,7 @@ def __init__( self.intent_type = intent_type self.slots = slots self.text_input = text_input + self.context = context @callback def create_response(self) -> "IntentResponse": diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py index 1e65c24eaaf4..21dd0b71487f 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -231,7 +231,6 @@ def async_log_exception(self, logger, message_base, exception): Should only be called on exceptions raised by this scripts async_run. """ - # pylint: disable=protected-access step = self._exception_step action = self.sequence[step] action_type = _determine_action(action) @@ -281,7 +280,6 @@ async def _async_delay(self, action, variables, context): @callback def async_script_delay(now): """Handle delay.""" - # pylint: disable=cell-var-from-loop with suppress(ValueError): self._async_listener.remove(unsub) diff --git a/homeassistant/helpers/service.py b/homeassistant/helpers/service.py index e177c86c65c9..45393dc04860 100644 --- a/homeassistant/helpers/service.py +++ b/homeassistant/helpers/service.py @@ -260,19 +260,7 @@ async def entity_service_call( else: entity_perms = None - # Are we trying to target all entities - if ATTR_ENTITY_ID in call.data: - target_all_entities = call.data[ATTR_ENTITY_ID] == ENTITY_MATCH_ALL - else: - # Remove the service_name parameter along with this warning - _LOGGER.warning( - "Not passing an entity ID to a service to target all " - "entities is deprecated. Update your call to %s to be " - "instead: entity_id: %s", - service_name, - ENTITY_MATCH_ALL, - ) - target_all_entities = True + target_all_entities = call.data.get(ATTR_ENTITY_ID) == ENTITY_MATCH_ALL if not target_all_entities: # A set of entities we're trying to target. diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index aa17b2a1fba9..7dcf08ebf921 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -69,7 +69,9 @@ def render_complex(value, variables=None): return [render_complex(item, variables) for item in value] if isinstance(value, dict): return {key: render_complex(item, variables) for key, item in value.items()} - return value.async_render(variables) + if isinstance(value, Template): + return value.async_render(variables) + return value def extract_entities( @@ -142,7 +144,7 @@ def _filter_lifecycle(self, entity_id: str) -> bool: def result(self) -> str: """Results of the template computation.""" if self._exception is not None: - raise self._exception # pylint: disable=raising-bad-type + raise self._exception return self._result def _freeze(self) -> None: @@ -669,6 +671,8 @@ def forgiving_round(value, precision=0, method="common"): value = math.ceil(float(value) * multiplier) / multiplier elif method == "floor": value = math.floor(float(value) * multiplier) / multiplier + elif method == "half": + value = round(float(value) * 2) / 2 else: # if method is common or something else, use common rounding value = round(float(value), precision) diff --git a/homeassistant/loader.py b/homeassistant/loader.py index e32303ecfabb..af76b073cd39 100644 --- a/homeassistant/loader.py +++ b/homeassistant/loader.py @@ -26,11 +26,11 @@ ) # Typing imports that create a circular dependency -# pylint: disable=using-constant-test,unused-import +# pylint: disable=unused-import if TYPE_CHECKING: - from homeassistant.core import HomeAssistant # noqa + from homeassistant.core import HomeAssistant -CALLABLE_T = TypeVar("CALLABLE_T", bound=Callable) # noqa pylint: disable=invalid-name +CALLABLE_T = TypeVar("CALLABLE_T", bound=Callable) # pylint: disable=invalid-name DEPENDENCY_BLACKLIST = {"config"} diff --git a/homeassistant/monkey_patch.py b/homeassistant/monkey_patch.py index 2c5227988d31..c6e2e66ab131 100644 --- a/homeassistant/monkey_patch.py +++ b/homeassistant/monkey_patch.py @@ -68,6 +68,6 @@ def find_module(self, fullname: str, path: Any = None) -> None: sys.path.insert(0, AsyncioImportFinder.PATH_TRIGGER) try: - import _asyncio # noqa + import _asyncio # noqa: F401 except ImportError: pass diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 07ebfdec4f9a..90a6ac8c8a74 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -9,9 +9,10 @@ bcrypt==3.1.7 certifi>=2019.9.11 contextvars==2.4;python_version<"3.7" cryptography==2.8 +defusedxml==0.6.0 distro==1.4.0 -hass-nabucasa==0.29 -home-assistant-frontend==20191119.6 +hass-nabucasa==0.30 +home-assistant-frontend==20191204.1 importlib-metadata==0.23 jinja2>=2.10.3 netdisco==2.6.0 @@ -21,10 +22,10 @@ pytz>=2019.03 pyyaml==5.1.2 requests==2.22.0 ruamel.yaml==0.15.100 -sqlalchemy==1.3.10 +sqlalchemy==1.3.11 voluptuous-serialize==2.3.0 voluptuous==0.11.7 -zeroconf==0.23.0 +zeroconf==0.24.0 pycryptodome>=3.6.6 diff --git a/homeassistant/util/__init__.py b/homeassistant/util/__init__.py index 0f4f3c867ad8..408d1e370d47 100644 --- a/homeassistant/util/__init__.py +++ b/homeassistant/util/__init__.py @@ -15,7 +15,7 @@ TypeVar, Callable, KeysView, - Union, # noqa + Union, Iterable, Coroutine, ) diff --git a/homeassistant/util/color.py b/homeassistant/util/color.py index 837a0e77cd73..7361b711dd2c 100644 --- a/homeassistant/util/color.py +++ b/homeassistant/util/color.py @@ -107,7 +107,7 @@ "mediumslateblue": (123, 104, 238), "mediumspringgreen": (0, 250, 154), "mediumturquoise": (72, 209, 204), - "mediumvioletredred": (199, 21, 133), + "mediumvioletred": (199, 21, 133), "midnightblue": (25, 25, 112), "mintcream": (245, 255, 250), "mistyrose": (255, 228, 225), diff --git a/homeassistant/util/decorator.py b/homeassistant/util/decorator.py index ecf1284b1b1c..83c63711c7dd 100644 --- a/homeassistant/util/decorator.py +++ b/homeassistant/util/decorator.py @@ -1,7 +1,7 @@ """Decorator utility functions.""" from typing import Callable, TypeVar -CALLABLE_T = TypeVar("CALLABLE_T", bound=Callable) # noqa pylint: disable=invalid-name +CALLABLE_T = TypeVar("CALLABLE_T", bound=Callable) # pylint: disable=invalid-name class Registry(dict): diff --git a/homeassistant/util/yaml/loader.py b/homeassistant/util/yaml/loader.py index bbbe22c2ba8b..fec09a1d6901 100644 --- a/homeassistant/util/yaml/loader.py +++ b/homeassistant/util/yaml/loader.py @@ -68,7 +68,6 @@ def load_yaml(fname: str) -> JSON_TYPE: raise HomeAssistantError(exc) -# pylint: disable=pointless-statement @overload def _add_reference( obj: Union[list, NodeListClass], loader: yaml.SafeLoader, node: yaml.nodes.Node @@ -76,14 +75,14 @@ def _add_reference( ... -@overload # noqa: F811 +@overload def _add_reference( obj: Union[str, NodeStrClass], loader: yaml.SafeLoader, node: yaml.nodes.Node ) -> NodeStrClass: ... -@overload # noqa: F811 +@overload def _add_reference( obj: DICT_T, loader: yaml.SafeLoader, node: yaml.nodes.Node ) -> DICT_T: @@ -93,7 +92,7 @@ def _add_reference( # pylint: enable=pointless-statement -def _add_reference( # type: ignore # noqa: F811 +def _add_reference( # type: ignore obj, loader: SafeLineLoader, node: yaml.nodes.Node ): """Add file reference information to an object.""" @@ -211,7 +210,7 @@ def _ordered_dict(loader: SafeLineLoader, node: yaml.nodes.MappingNode) -> Order if key in seen: fname = getattr(loader.stream, "name", "") - _LOGGER.error( + _LOGGER.warning( 'YAML file %s contains duplicate key "%s". ' "Check lines %d and %d.", fname, key, diff --git a/pylintrc b/pylintrc index ff47af6087b8..44659ddb3769 100644 --- a/pylintrc +++ b/pylintrc @@ -3,6 +3,7 @@ ignore=tests # Use a conservative default here; 2 should speed up most setups and not hurt # any too bad. Override on command line as appropriate. jobs=2 +persistent=no [BASIC] good-names=id,i,j,k,ex,Run,_,fp @@ -23,6 +24,7 @@ good-names=id,i,j,k,ex,Run,_,fp # inconsistent-return-statements - doesn't handle raise # unnecessary-pass - readability for functions which only contain pass # import-outside-toplevel - TODO +# too-many-ancestors - it's too strict. disable= format, abstract-class-little-used, @@ -36,6 +38,7 @@ disable= not-context-manager, redefined-variable-type, too-few-public-methods, + too-many-ancestors, too-many-arguments, too-many-branches, too-many-instance-attributes, @@ -47,9 +50,11 @@ disable= too-many-boolean-expressions, unnecessary-pass, unused-argument +enable= + use-symbolic-message-instead [REPORTS] -reports=no +score=no [TYPECHECK] # For attrs diff --git a/requirements_all.txt b/requirements_all.txt index e8307161c093..9e2f31443e5e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -100,7 +100,7 @@ TwitterAPI==2.5.10 # VL53L1X2==0.1.5 # homeassistant.components.waze_travel_time -WazeRouteCalculator==0.10 +WazeRouteCalculator==0.12 # homeassistant.components.yessssms YesssSMS==0.4.1 @@ -126,6 +126,12 @@ afsapi==0.0.4 # homeassistant.components.geonetnz_quakes aio_geojson_geonetnz_quakes==0.11 +# homeassistant.components.geonetnz_volcano +aio_geojson_geonetnz_volcano==0.5 + +# homeassistant.components.nsw_rural_fire_service_feed +aio_geojson_nsw_rfs_incidents==0.1 + # homeassistant.components.ambient_station aioambient==0.3.2 @@ -142,7 +148,7 @@ aiobotocore==0.10.4 aiodns==2.0.0 # homeassistant.components.esphome -aioesphomeapi==2.5.0 +aioesphomeapi==2.6.1 # homeassistant.components.freebox aiofreepybox==0.0.8 @@ -200,7 +206,7 @@ aladdin_connect==0.3 alarmdecoder==1.13.2 # homeassistant.components.alpha_vantage -alpha_vantage==2.1.1 +alpha_vantage==2.1.2 # homeassistant.components.ambiclimate ambiclimate==0.2.1 @@ -209,7 +215,7 @@ ambiclimate==0.2.1 amcrest==1.5.3 # homeassistant.components.androidtv -androidtv==0.0.32 +androidtv==0.0.34 # homeassistant.components.anel_pwrctrl anel_pwrctrl-homeassistant==0.0.1.dev2 @@ -224,7 +230,7 @@ apcaccess==0.0.13 apns2==0.3.0 # homeassistant.components.apprise -apprise==0.8.1 +apprise==0.8.2 # homeassistant.components.aprs aprslib==0.6.46 @@ -245,6 +251,9 @@ asterisk_mbox==0.5.0 # homeassistant.components.upnp async-upnp-client==0.14.12 +# homeassistant.components.aten_pe +atenpdu==0.3.0 + # homeassistant.components.aurora_abb_powerone aurorapy==0.2.6 @@ -402,6 +411,7 @@ datapoint==0.4.3 # homeassistant.components.ihc # homeassistant.components.namecheapdns # homeassistant.components.ohmconnect +# homeassistant.components.ssdp defusedxml==0.6.0 # homeassistant.components.deluge @@ -417,7 +427,7 @@ directpy==0.5 discogs_client==2.2.2 # homeassistant.components.discord -discord.py==1.2.4 +discord.py==1.2.5 # homeassistant.components.updater distro==1.4.0 @@ -462,7 +472,7 @@ emulated_roku==0.1.8 enocean==0.50 # homeassistant.components.entur_public_transport -enturclient==0.2.0 +enturclient==0.2.1 # homeassistant.components.environment_canada env_canada==0.0.30 @@ -474,7 +484,7 @@ env_canada==0.0.30 envoy_reader==0.8.6 # homeassistant.components.season -ephem==3.7.6.0 +ephem==3.7.7.0 # homeassistant.components.epson epson-projector==0.1.3 @@ -483,7 +493,7 @@ epson-projector==0.1.3 epsonprinter==0.0.9 # homeassistant.components.netgear_lte -eternalegypt==0.0.10 +eternalegypt==0.0.11 # homeassistant.components.keyboard_remote # evdev==1.1.2 @@ -546,7 +556,6 @@ geizhals==0.0.9 geniushub-client==0.6.30 # homeassistant.components.geo_json_events -# homeassistant.components.nsw_rural_fire_service_feed # homeassistant.components.usgs_earthquakes_feed geojson_client==0.4 @@ -554,7 +563,7 @@ geojson_client==0.4 geopy==1.19.0 # homeassistant.components.geo_rss_events -georss_generic_client==0.2 +georss_generic_client==0.3 # homeassistant.components.ign_sismologia georss_ign_sismologia_client==0.2 @@ -625,7 +634,7 @@ habitipy==0.2.0 hangups==0.4.9 # homeassistant.components.cloud -hass-nabucasa==0.29 +hass-nabucasa==0.30 # homeassistant.components.mqtt hbmqtt==0.9.5 @@ -634,10 +643,10 @@ hbmqtt==0.9.5 hdate==0.9.3 # homeassistant.components.heatmiser -heatmiserV3==0.9.1 +heatmiserV3==1.1.18 # homeassistant.components.here_travel_time -herepy==0.6.3.1 +herepy==0.6.3.3 # homeassistant.components.hikvisioncam hikvision==0.4 @@ -655,10 +664,10 @@ hole==0.5.0 holidays==0.9.11 # homeassistant.components.frontend -home-assistant-frontend==20191119.6 +home-assistant-frontend==20191204.1 # homeassistant.components.zwave -homeassistant-pyozw==0.1.4 +homeassistant-pyozw==0.1.7 # homeassistant.components.homekit_controller homekit[IP]==0.15.0 @@ -674,7 +683,7 @@ horimote==0.4.1 httplib2==0.10.3 # homeassistant.components.huawei_lte -huawei-lte-api==1.4.3 +huawei-lte-api==1.4.4 # homeassistant.components.hydrawise hydrawiser==0.1.1 @@ -715,7 +724,7 @@ iperf3==0.1.11 ipify==1.0.0 # homeassistant.components.verisure -jsonpath==0.75 +jsonpath==0.82 # homeassistant.components.kodi jsonrpc-async==0.6 @@ -751,7 +760,7 @@ libpurecool==0.5.0 libpyfoscam==1.0 # homeassistant.components.vivotek -libpyvivotek==0.2.2 +libpyvivotek==0.3.1 # homeassistant.components.mikrotik librouteros==2.3.0 @@ -796,7 +805,7 @@ london-tube-status==0.2 luftdaten==0.6.3 # homeassistant.components.lupusec -lupupy==0.0.17 +lupupy==0.0.18 # homeassistant.components.lw12wifi lw12==0.9.2 @@ -838,13 +847,13 @@ millheater==0.3.4 minio==4.0.9 # homeassistant.components.mitemp_bt -mitemp_bt==0.0.1 +mitemp_bt==0.0.3 # homeassistant.components.mopar motorparts==1.1.0 # homeassistant.components.tts -mutagen==1.42.0 +mutagen==1.43.0 # homeassistant.components.mychevy mychevy==1.2.0 @@ -859,7 +868,7 @@ n26==0.2.7 nad_receiver==0.0.11 # homeassistant.components.keenetic_ndms2 -ndms2_client==0.0.10 +ndms2_client==0.0.11 # homeassistant.components.ness_alarm nessclient==0.9.15 @@ -893,7 +902,7 @@ nuheat==0.3.0 # homeassistant.components.opencv # homeassistant.components.tensorflow # homeassistant.components.trend -numpy==1.17.3 +numpy==1.17.4 # homeassistant.components.oasa_telematics oasatelematics==0.3 @@ -911,13 +920,13 @@ onkyo-eiscp==1.2.7 onvif-zeep-async==0.2.0 # homeassistant.components.opencv -# opencv-python-headless==4.1.1.26 +# opencv-python-headless==4.1.2.30 # homeassistant.components.openevse openevsewifi==0.4 # homeassistant.components.openhome -openhomedevice==0.4.2 +openhomedevice==0.6.3 # homeassistant.components.opensensemap opensensemap-api==0.1.5 @@ -986,7 +995,7 @@ plexapi==3.3.0 plexauth==0.0.5 # homeassistant.components.plex -plexwebsocket==0.0.5 +plexwebsocket==0.0.6 # homeassistant.components.plum_lightpad plumlightpad==0.0.11 @@ -1019,8 +1028,11 @@ prometheus_client==0.7.1 # homeassistant.components.tensorflow protobuf==3.6.1 +# homeassistant.components.proxmoxve +proxmoxer==1.0.3 + # homeassistant.components.systemmonitor -psutil==5.6.5 +psutil==5.6.7 # homeassistant.components.ptvsd ptvsd==4.2.8 @@ -1028,6 +1040,9 @@ ptvsd==4.2.8 # homeassistant.components.wink pubnubsub-handler==1.0.8 +# homeassistant.components.androidtv +pure-python-adb==0.2.2.dev0 + # homeassistant.components.pushbullet pushbullet.py==0.11.0 @@ -1066,13 +1081,13 @@ pyHS100==0.3.5 pyMetno==0.4.6 # homeassistant.components.rfxtrx -pyRFXtrx==0.23 +pyRFXtrx==0.24 # homeassistant.components.switchmate # pySwitchmate==0.4.6 # homeassistant.components.tibber -pyTibber==0.11.7 +pyTibber==0.12.0 # homeassistant.components.dlink pyW215==0.6.0 @@ -1089,6 +1104,9 @@ py_nextbusnext==0.1.4 # homeassistant.components.ads pyads==3.0.7 +# homeassistant.components.hisense_aehw4a1 +pyaehw4a1==0.3.1 + # homeassistant.components.aftership pyaftership==0.1.2 @@ -1212,6 +1230,9 @@ pyflexit==0.3 # homeassistant.components.flic pyflic-homeassistant==0.4.dev0 +# homeassistant.components.flume +pyflume==0.2.1 + # homeassistant.components.flunearyou pyflunearyou==1.0.3 @@ -1244,13 +1265,13 @@ pyhaversion==3.1.0 pyheos==0.6.0 # homeassistant.components.hikvision -pyhik==0.2.4 +pyhik==0.2.5 # homeassistant.components.hive pyhiveapi==0.2.19.3 # homeassistant.components.homematic -pyhomematic==0.1.61 +pyhomematic==0.1.62 # homeassistant.components.homeworks pyhomeworks==0.0.6 @@ -1307,7 +1328,7 @@ pylitejet==0.1 pyloopenergy==0.1.3 # homeassistant.components.lutron_caseta -pylutron-caseta==0.5.0 +pylutron-caseta==0.5.1 # homeassistant.components.lutron pylutron==0.2.5 @@ -1319,7 +1340,7 @@ pymailgunner==1.4 pymediaroom==0.6.4 # homeassistant.components.somfy -pymfy==0.6.1 +pymfy==0.7.1 # homeassistant.components.xiaomi_tv pymitv==1.4.3 @@ -1376,7 +1397,7 @@ pynzbgetapi==0.2.0 pyobihai==1.2.0 # homeassistant.components.ombi -pyombi==0.1.5 +pyombi==0.1.10 # homeassistant.components.openuv pyopenuv==1.0.9 @@ -1388,7 +1409,7 @@ pyoppleio==1.0.5 pyota==2.0.5 # homeassistant.components.opentherm_gw -pyotgw==0.5b0 +pyotgw==0.5b1 # homeassistant.auth.mfa_modules.notify # homeassistant.auth.mfa_modules.totp @@ -1402,7 +1423,7 @@ pyowlet==1.0.3 pyowm==2.10.0 # homeassistant.components.elv -pypca==0.0.5 +pypca==0.0.7 # homeassistant.components.lcn pypck==0.6.3 @@ -1414,7 +1435,7 @@ pypjlink2==1.2.0 pypoint==1.1.2 # homeassistant.components.ps4 -pyps4-2ndscreen==1.0.1 +pyps4-2ndscreen==1.0.3 # homeassistant.components.qwikswitch pyqwikswitch==0.93 @@ -1543,7 +1564,7 @@ python-izone==1.1.1 python-join-api==0.0.4 # homeassistant.components.juicenet -python-juicenet==0.1.5 +python-juicenet==0.1.6 # homeassistant.components.lirc # python-lirc==1.2.3 @@ -1645,6 +1666,9 @@ pyuptimerobot==0.0.5 # homeassistant.components.vera pyvera==0.3.6 +# homeassistant.components.versasense +pyversasense==0.0.6 + # homeassistant.components.vesync pyvesync==1.1.0 @@ -1718,10 +1742,10 @@ rjpl==0.3.5 rocketchat-API==0.6.1 # homeassistant.components.roku -roku==3.1 +roku==4.0.0 # homeassistant.components.roomba -roombapy==1.3.1 +roombapy==1.4.2 # homeassistant.components.rova rova==0.1.0 @@ -1763,13 +1787,13 @@ sense_energy==0.7.0 sharp_aquos_rc==0.3.2 # homeassistant.components.shodan -shodan==1.19.0 +shodan==1.20.0 # homeassistant.components.simplepush simplepush==1.1.4 # homeassistant.components.simplisafe -simplisafe-python==5.1.0 +simplisafe-python==5.3.5 # homeassistant.components.sisyphus sisyphus-control==2.2.1 @@ -1838,7 +1862,10 @@ spotipy-homeassistant==2.4.4.dev1 # homeassistant.components.recorder # homeassistant.components.sql -sqlalchemy==1.3.10 +sqlalchemy==1.3.11 + +# homeassistant.components.starline +starline==0.1.3 # homeassistant.components.starlingbank starlingbank==3.1 @@ -1898,7 +1925,7 @@ temperusb==1.5.3 # tensorflow==1.13.2 # homeassistant.components.tesla -teslajsonpy==0.0.26 +teslajsonpy==0.2.0 # homeassistant.components.thermoworks_smoke thermoworks_smoke==0.1.8 @@ -1937,7 +1964,7 @@ twilio==6.32.0 unifiled==0.11 # homeassistant.components.upcloud -upcloud-api==0.4.3 +upcloud-api==0.4.5 # homeassistant.components.huawei_lte url-normalize==1.4.1 @@ -1964,7 +1991,7 @@ volkszaehler==0.1.2 volvooncall==0.8.7 # homeassistant.components.verisure -vsure==1.5.2 +vsure==1.5.4 # homeassistant.components.vasttrafik vtjp==0.1.14 @@ -2045,13 +2072,13 @@ yeelight==0.5.0 yeelightsunflower==0.0.10 # homeassistant.components.media_extractor -youtube_dl==2019.11.05 +youtube_dl==2019.11.28 # homeassistant.components.zengge zengge==0.2 # homeassistant.components.zeroconf -zeroconf==0.23.0 +zeroconf==0.24.0 # homeassistant.components.zha zha-quirks==0.0.28 diff --git a/requirements_test.txt b/requirements_test.txt index 33fab3d6c6af..9d63b59f62ad 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -6,14 +6,14 @@ asynctest==0.13.0 codecov==2.0.15 mock-open==1.3.1 -mypy==0.740 +mypy==0.750 pre-commit==1.20.0 -pylint==2.4.3 -astroid==2.3.2 +pylint==2.4.4 +astroid==2.3.3 pytest-aiohttp==0.3.0 pytest-cov==2.8.1 pytest-sugar==0.9.2 pytest-timeout==1.3.3 -pytest==5.2.2 +pytest==5.3.0 requests_mock==1.7.0 responses==0.10.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2aa46e28f7a1..036de45e3429 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -37,6 +37,12 @@ adguardhome==0.3.0 # homeassistant.components.geonetnz_quakes aio_geojson_geonetnz_quakes==0.11 +# homeassistant.components.geonetnz_volcano +aio_geojson_geonetnz_volcano==0.5 + +# homeassistant.components.nsw_rural_fire_service_feed +aio_geojson_nsw_rfs_incidents==0.1 + # homeassistant.components.ambient_station aioambient==0.3.2 @@ -50,7 +56,7 @@ aioautomatic==0.6.5 aiobotocore==0.10.4 # homeassistant.components.esphome -aioesphomeapi==2.5.0 +aioesphomeapi==2.6.1 # homeassistant.components.emulated_hue # homeassistant.components.http @@ -78,13 +84,13 @@ airly==0.0.2 ambiclimate==0.2.1 # homeassistant.components.androidtv -androidtv==0.0.32 +androidtv==0.0.34 # homeassistant.components.apns apns2==0.3.0 # homeassistant.components.apprise -apprise==0.8.1 +apprise==0.8.2 # homeassistant.components.aprs aprslib==0.6.46 @@ -137,6 +143,7 @@ datadog==0.15.0 # homeassistant.components.ihc # homeassistant.components.namecheapdns # homeassistant.components.ohmconnect +# homeassistant.components.ssdp defusedxml==0.6.0 # homeassistant.components.directv @@ -155,7 +162,7 @@ eebrightbox==0.0.4 emulated_roku==0.1.8 # homeassistant.components.season -ephem==3.7.6.0 +ephem==3.7.7.0 # homeassistant.components.feedreader feedparser-homeassistant==5.2.2.dev1 @@ -167,7 +174,6 @@ foobot_async==0.3.1 gTTS-token==1.1.3 # homeassistant.components.geo_json_events -# homeassistant.components.nsw_rural_fire_service_feed # homeassistant.components.usgs_earthquakes_feed geojson_client==0.4 @@ -175,7 +181,7 @@ geojson_client==0.4 geopy==1.19.0 # homeassistant.components.geo_rss_events -georss_generic_client==0.2 +georss_generic_client==0.3 # homeassistant.components.ign_sismologia georss_ign_sismologia_client==0.2 @@ -204,7 +210,7 @@ ha-ffmpeg==2.0 hangups==0.4.9 # homeassistant.components.cloud -hass-nabucasa==0.29 +hass-nabucasa==0.30 # homeassistant.components.mqtt hbmqtt==0.9.5 @@ -213,7 +219,7 @@ hbmqtt==0.9.5 hdate==0.9.3 # homeassistant.components.here_travel_time -herepy==0.6.3.1 +herepy==0.6.3.3 # homeassistant.components.pi_hole hole==0.5.0 @@ -222,10 +228,10 @@ hole==0.5.0 holidays==0.9.11 # homeassistant.components.frontend -home-assistant-frontend==20191119.6 +home-assistant-frontend==20191204.1 # homeassistant.components.zwave -homeassistant-pyozw==0.1.4 +homeassistant-pyozw==0.1.7 # homeassistant.components.homekit_controller homekit[IP]==0.15.0 @@ -238,7 +244,7 @@ homematicip==0.10.13 httplib2==0.10.3 # homeassistant.components.huawei_lte -huawei-lte-api==1.4.3 +huawei-lte-api==1.4.4 # homeassistant.components.iaqualink iaqualink==0.3.0 @@ -247,7 +253,7 @@ iaqualink==0.3.0 influxdb==5.2.3 # homeassistant.components.verisure -jsonpath==0.75 +jsonpath==0.82 # homeassistant.scripts.keyring keyring==19.2.0 @@ -277,7 +283,7 @@ mficlient==0.3.0 minio==4.0.9 # homeassistant.components.tts -mutagen==1.42.0 +mutagen==1.43.0 # homeassistant.components.ness_alarm nessclient==0.9.15 @@ -296,7 +302,7 @@ nuheat==0.3.0 # homeassistant.components.opencv # homeassistant.components.tensorflow # homeassistant.components.trend -numpy==1.17.3 +numpy==1.17.4 # homeassistant.components.google oauth2client==4.0.0 @@ -326,7 +332,7 @@ plexapi==3.3.0 plexauth==0.0.5 # homeassistant.components.plex -plexwebsocket==0.0.5 +plexwebsocket==0.0.6 # homeassistant.components.mhz19 # homeassistant.components.serial_pm @@ -344,6 +350,9 @@ prometheus_client==0.7.1 # homeassistant.components.ptvsd ptvsd==4.2.8 +# homeassistant.components.androidtv +pure-python-adb==0.2.2.dev0 + # homeassistant.components.pushbullet pushbullet.py==0.11.0 @@ -364,11 +373,14 @@ pyHS100==0.3.5 pyMetno==0.4.6 # homeassistant.components.rfxtrx -pyRFXtrx==0.23 +pyRFXtrx==0.24 # homeassistant.components.nextbus py_nextbusnext==0.1.4 +# homeassistant.components.hisense_aehw4a1 +pyaehw4a1==0.3.1 + # homeassistant.components.almond pyalmond==0.0.2 @@ -415,7 +427,7 @@ pyhaversion==3.1.0 pyheos==0.6.0 # homeassistant.components.homematic -pyhomematic==0.1.61 +pyhomematic==0.1.62 # homeassistant.components.ipma pyipma==1.2.1 @@ -439,7 +451,7 @@ pylitejet==0.1 pymailgunner==1.4 # homeassistant.components.somfy -pymfy==0.6.1 +pymfy==0.7.1 # homeassistant.components.mochad pymochad==0.2.0 @@ -460,7 +472,7 @@ pynx584==0.4 pyopenuv==1.0.9 # homeassistant.components.opentherm_gw -pyotgw==0.5b0 +pyotgw==0.5b1 # homeassistant.auth.mfa_modules.notify # homeassistant.auth.mfa_modules.totp @@ -471,7 +483,7 @@ pyotp==2.3.0 pypoint==1.1.2 # homeassistant.components.ps4 -pyps4-2ndscreen==1.0.1 +pyps4-2ndscreen==1.0.3 # homeassistant.components.qwikswitch pyqwikswitch==0.93 @@ -546,7 +558,7 @@ rxv==0.6.0 samsungctl[websocket]==0.7.1 # homeassistant.components.simplisafe -simplisafe-python==5.1.0 +simplisafe-python==5.3.5 # homeassistant.components.sleepiq sleepyq==0.7 @@ -562,7 +574,10 @@ somecomfort==0.5.2 # homeassistant.components.recorder # homeassistant.components.sql -sqlalchemy==1.3.10 +sqlalchemy==1.3.11 + +# homeassistant.components.starline +starline==0.1.3 # homeassistant.components.statsd statsd==3.2.1 @@ -598,7 +613,7 @@ url-normalize==1.4.1 uvcclient==0.11.0 # homeassistant.components.verisure -vsure==1.5.2 +vsure==1.5.4 # homeassistant.components.vultr vultr==0.1.2 @@ -634,7 +649,7 @@ ya_ma==0.3.8 yahooweather==0.10 # homeassistant.components.zeroconf -zeroconf==0.23.0 +zeroconf==0.24.0 # homeassistant.components.zha zha-quirks==0.0.28 diff --git a/requirements_test_pre_commit.txt b/requirements_test_pre_commit.txt index 29380ca7cd28..3f4d05a49081 100644 --- a/requirements_test_pre_commit.txt +++ b/requirements_test_pre_commit.txt @@ -1,5 +1,6 @@ # Automatically generated from .pre-commit-config-all.yaml by gen_requirements_all.py, do not edit +bandit==1.6.2 black==19.10b0 flake8-docstrings==1.5.0 flake8==3.7.9 diff --git a/script/__init__.py b/script/__init__.py new file mode 100644 index 000000000000..e71d5b3763bf --- /dev/null +++ b/script/__init__.py @@ -0,0 +1 @@ +"""Home Assistant scripts.""" diff --git a/script/scaffold/generate.py b/script/scaffold/generate.py index a04cdb3ef5e0..59767249d297 100644 --- a/script/scaffold/generate.py +++ b/script/scaffold/generate.py @@ -154,7 +154,7 @@ def _custom_tasks(template, info) -> None: "pick_implementation": {"title": "Pick Authentication Method"} }, "abort": { - "missing_configuration": "The Somfy component is not configured. Please follow the documentation." + "missing_configuration": "The {info.name} component is not configured. Please follow the documentation." }, "create_entry": { "default": f"Successfully authenticated with {info.name}." diff --git a/script/scaffold/templates/integration/integration/manifest.json b/script/scaffold/templates/integration/integration/manifest.json index 0bc54519ce9d..a95991abef8f 100644 --- a/script/scaffold/templates/integration/integration/manifest.json +++ b/script/scaffold/templates/integration/integration/manifest.json @@ -4,7 +4,8 @@ "config_flow": false, "documentation": "https://www.home-assistant.io/integrations/NEW_DOMAIN", "requirements": [], - "ssdp": {}, + "ssdp": [], + "zeroconf": [], "homekit": {}, "dependencies": [], "codeowners": [] diff --git a/tests/bandit.yaml b/tests/bandit.yaml new file mode 100644 index 000000000000..79812cba56fc --- /dev/null +++ b/tests/bandit.yaml @@ -0,0 +1,11 @@ +# https://bandit.readthedocs.io/en/latest/config.html + +tests: + - B313 + - B314 + - B315 + - B316 + - B317 + - B318 + - B319 + - B320 diff --git a/tests/common.py b/tests/common.py index f40019c5d242..e652e10cc542 100644 --- a/tests/common.py +++ b/tests/common.py @@ -56,7 +56,7 @@ 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 -from homeassistant.components.device_automation import ( # noqa +from homeassistant.components.device_automation import ( # noqa: F401 _async_get_device_automations as async_get_device_automations, _async_get_device_automation_capabilities as async_get_device_automation_capabilities, ) @@ -1084,7 +1084,7 @@ class hashdict(dict): """ - def __key(self): # noqa: D105 no docstring + def __key(self): return tuple(sorted(self.items())) def __repr__(self): # noqa: D105 no docstring diff --git a/tests/components/alarm_control_panel/common.py b/tests/components/alarm_control_panel/common.py index 216f226ef14b..d06939dce9b3 100644 --- a/tests/components/alarm_control_panel/common.py +++ b/tests/components/alarm_control_panel/common.py @@ -13,11 +13,12 @@ SERVICE_ALARM_ARM_AWAY, SERVICE_ALARM_ARM_NIGHT, SERVICE_ALARM_ARM_CUSTOM_BYPASS, + ENTITY_MATCH_ALL, ) from homeassistant.loader import bind_hass -async def async_alarm_disarm(hass, code=None, entity_id=None): +async def async_alarm_disarm(hass, code=None, entity_id=ENTITY_MATCH_ALL): """Send the alarm the command for disarm.""" data = {} if code: @@ -29,7 +30,7 @@ async def async_alarm_disarm(hass, code=None, entity_id=None): @bind_hass -def alarm_disarm(hass, code=None, entity_id=None): +def alarm_disarm(hass, code=None, entity_id=ENTITY_MATCH_ALL): """Send the alarm the command for disarm.""" data = {} if code: @@ -40,7 +41,7 @@ def alarm_disarm(hass, code=None, entity_id=None): hass.services.call(DOMAIN, SERVICE_ALARM_DISARM, data) -async def async_alarm_arm_home(hass, code=None, entity_id=None): +async def async_alarm_arm_home(hass, code=None, entity_id=ENTITY_MATCH_ALL): """Send the alarm the command for disarm.""" data = {} if code: @@ -52,7 +53,7 @@ async def async_alarm_arm_home(hass, code=None, entity_id=None): @bind_hass -def alarm_arm_home(hass, code=None, entity_id=None): +def alarm_arm_home(hass, code=None, entity_id=ENTITY_MATCH_ALL): """Send the alarm the command for arm home.""" data = {} if code: @@ -63,7 +64,7 @@ def alarm_arm_home(hass, code=None, entity_id=None): hass.services.call(DOMAIN, SERVICE_ALARM_ARM_HOME, data) -async def async_alarm_arm_away(hass, code=None, entity_id=None): +async def async_alarm_arm_away(hass, code=None, entity_id=ENTITY_MATCH_ALL): """Send the alarm the command for disarm.""" data = {} if code: @@ -75,7 +76,7 @@ async def async_alarm_arm_away(hass, code=None, entity_id=None): @bind_hass -def alarm_arm_away(hass, code=None, entity_id=None): +def alarm_arm_away(hass, code=None, entity_id=ENTITY_MATCH_ALL): """Send the alarm the command for arm away.""" data = {} if code: @@ -86,7 +87,7 @@ def alarm_arm_away(hass, code=None, entity_id=None): hass.services.call(DOMAIN, SERVICE_ALARM_ARM_AWAY, data) -async def async_alarm_arm_night(hass, code=None, entity_id=None): +async def async_alarm_arm_night(hass, code=None, entity_id=ENTITY_MATCH_ALL): """Send the alarm the command for disarm.""" data = {} if code: @@ -98,7 +99,7 @@ async def async_alarm_arm_night(hass, code=None, entity_id=None): @bind_hass -def alarm_arm_night(hass, code=None, entity_id=None): +def alarm_arm_night(hass, code=None, entity_id=ENTITY_MATCH_ALL): """Send the alarm the command for arm night.""" data = {} if code: @@ -109,7 +110,7 @@ def alarm_arm_night(hass, code=None, entity_id=None): hass.services.call(DOMAIN, SERVICE_ALARM_ARM_NIGHT, data) -async def async_alarm_trigger(hass, code=None, entity_id=None): +async def async_alarm_trigger(hass, code=None, entity_id=ENTITY_MATCH_ALL): """Send the alarm the command for disarm.""" data = {} if code: @@ -121,7 +122,7 @@ async def async_alarm_trigger(hass, code=None, entity_id=None): @bind_hass -def alarm_trigger(hass, code=None, entity_id=None): +def alarm_trigger(hass, code=None, entity_id=ENTITY_MATCH_ALL): """Send the alarm the command for trigger.""" data = {} if code: @@ -132,7 +133,7 @@ def alarm_trigger(hass, code=None, entity_id=None): hass.services.call(DOMAIN, SERVICE_ALARM_TRIGGER, data) -async def async_alarm_arm_custom_bypass(hass, code=None, entity_id=None): +async def async_alarm_arm_custom_bypass(hass, code=None, entity_id=ENTITY_MATCH_ALL): """Send the alarm the command for disarm.""" data = {} if code: @@ -146,7 +147,7 @@ async def async_alarm_arm_custom_bypass(hass, code=None, entity_id=None): @bind_hass -def alarm_arm_custom_bypass(hass, code=None, entity_id=None): +def alarm_arm_custom_bypass(hass, code=None, entity_id=ENTITY_MATCH_ALL): """Send the alarm the command for arm custom bypass.""" data = {} if code: diff --git a/tests/components/alarm_control_panel/test_device_action.py b/tests/components/alarm_control_panel/test_device_action.py index c2dfcbd78b98..bc489dbe2511 100644 --- a/tests/components/alarm_control_panel/test_device_action.py +++ b/tests/components/alarm_control_panel/test_device_action.py @@ -46,6 +46,9 @@ async def test_get_actions(hass, device_reg, entity_reg): 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) + hass.states.async_set( + "alarm_control_panel.test_5678", "attributes", {"supported_features": 15} + ) expected_actions = [ { "domain": DOMAIN, @@ -82,6 +85,36 @@ async def test_get_actions(hass, device_reg, entity_reg): assert_lists_same(actions, expected_actions) +async def test_get_actions_arm_night_only(hass, device_reg, entity_reg): + """Test we get the expected actions from a alarm_control_panel.""" + 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) + hass.states.async_set( + "alarm_control_panel.test_5678", "attributes", {"supported_features": 4} + ) + expected_actions = [ + { + "domain": DOMAIN, + "type": "arm_night", + "device_id": device_entry.id, + "entity_id": "alarm_control_panel.test_5678", + }, + { + "domain": DOMAIN, + "type": "disarm", + "device_id": device_entry.id, + "entity_id": "alarm_control_panel.test_5678", + }, + ] + actions = await async_get_device_automations(hass, "action", device_entry.id) + assert_lists_same(actions, expected_actions) + + async def test_get_action_capabilities(hass, device_reg, entity_reg): """Test we get the expected capabilities from a sensor trigger.""" platform = getattr(hass.components, f"test.{DOMAIN}") diff --git a/tests/components/alarm_control_panel/test_device_trigger.py b/tests/components/alarm_control_panel/test_device_trigger.py new file mode 100644 index 000000000000..ec14cefc2915 --- /dev/null +++ b/tests/components/alarm_control_panel/test_device_trigger.py @@ -0,0 +1,257 @@ +"""The tests for Alarm control panel device triggers.""" +import pytest + +from homeassistant.components.alarm_control_panel import DOMAIN +import homeassistant.components.automation as automation +from homeassistant.const import ( + STATE_ALARM_ARMED_AWAY, + STATE_ALARM_ARMED_HOME, + STATE_ALARM_ARMED_NIGHT, + STATE_ALARM_DISARMED, + STATE_ALARM_PENDING, + STATE_ALARM_TRIGGERED, +) +from homeassistant.helpers import device_registry +from homeassistant.setup import async_setup_component + +from tests.common import ( + MockConfigEntry, + assert_lists_same, + async_get_device_automations, + async_mock_service, + mock_device_registry, + mock_registry, +) + + +@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 service.""" + 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 alarm_control_panel.""" + 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) + hass.states.async_set( + "alarm_control_panel.test_5678", "attributes", {"supported_features": 15} + ) + expected_triggers = [ + { + "platform": "device", + "domain": DOMAIN, + "type": "disarmed", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_5678", + }, + { + "platform": "device", + "domain": DOMAIN, + "type": "triggered", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_5678", + }, + { + "platform": "device", + "domain": DOMAIN, + "type": "armed_home", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_5678", + }, + { + "platform": "device", + "domain": DOMAIN, + "type": "armed_away", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_5678", + }, + { + "platform": "device", + "domain": DOMAIN, + "type": "armed_night", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_5678", + }, + ] + triggers = await async_get_device_automations(hass, "trigger", device_entry.id) + assert_lists_same(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("alarm_control_panel.entity", STATE_ALARM_PENDING) + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": { + "platform": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": "alarm_control_panel.entity", + "type": "triggered", + }, + "action": { + "service": "test.automation", + "data_template": { + "some": ( + "triggered - {{ trigger.platform}} - " + "{{ trigger.entity_id}} - {{ trigger.from_state.state}} - " + "{{ trigger.to_state.state}} - {{ trigger.for }}" + ) + }, + }, + }, + { + "trigger": { + "platform": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": "alarm_control_panel.entity", + "type": "disarmed", + }, + "action": { + "service": "test.automation", + "data_template": { + "some": ( + "disarmed - {{ trigger.platform}} - " + "{{ trigger.entity_id}} - {{ trigger.from_state.state}} - " + "{{ trigger.to_state.state}} - {{ trigger.for }}" + ) + }, + }, + }, + { + "trigger": { + "platform": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": "alarm_control_panel.entity", + "type": "armed_home", + }, + "action": { + "service": "test.automation", + "data_template": { + "some": ( + "armed_home - {{ trigger.platform}} - " + "{{ trigger.entity_id}} - {{ trigger.from_state.state}} - " + "{{ trigger.to_state.state}} - {{ trigger.for }}" + ) + }, + }, + }, + { + "trigger": { + "platform": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": "alarm_control_panel.entity", + "type": "armed_away", + }, + "action": { + "service": "test.automation", + "data_template": { + "some": ( + "armed_away - {{ trigger.platform}} - " + "{{ trigger.entity_id}} - {{ trigger.from_state.state}} - " + "{{ trigger.to_state.state}} - {{ trigger.for }}" + ) + }, + }, + }, + { + "trigger": { + "platform": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": "alarm_control_panel.entity", + "type": "armed_night", + }, + "action": { + "service": "test.automation", + "data_template": { + "some": ( + "armed_night - {{ trigger.platform}} - " + "{{ trigger.entity_id}} - {{ trigger.from_state.state}} - " + "{{ trigger.to_state.state}} - {{ trigger.for }}" + ) + }, + }, + }, + ] + }, + ) + + # Fake that the entity is triggered. + hass.states.async_set("alarm_control_panel.entity", STATE_ALARM_TRIGGERED) + await hass.async_block_till_done() + assert len(calls) == 1 + assert calls[0].data[ + "some" + ] == "triggered - device - {} - pending - triggered - None".format( + "alarm_control_panel.entity" + ) + + # Fake that the entity is disarmed. + hass.states.async_set("alarm_control_panel.entity", STATE_ALARM_DISARMED) + await hass.async_block_till_done() + assert len(calls) == 2 + assert calls[1].data[ + "some" + ] == "disarmed - device - {} - triggered - disarmed - None".format( + "alarm_control_panel.entity" + ) + + # Fake that the entity is armed home. + hass.states.async_set("alarm_control_panel.entity", STATE_ALARM_PENDING) + hass.states.async_set("alarm_control_panel.entity", STATE_ALARM_ARMED_HOME) + await hass.async_block_till_done() + assert len(calls) == 3 + assert calls[2].data[ + "some" + ] == "armed_home - device - {} - pending - armed_home - None".format( + "alarm_control_panel.entity" + ) + + # Fake that the entity is armed away. + hass.states.async_set("alarm_control_panel.entity", STATE_ALARM_PENDING) + hass.states.async_set("alarm_control_panel.entity", STATE_ALARM_ARMED_AWAY) + await hass.async_block_till_done() + assert len(calls) == 4 + assert calls[3].data[ + "some" + ] == "armed_away - device - {} - pending - armed_away - None".format( + "alarm_control_panel.entity" + ) + + # Fake that the entity is armed night. + hass.states.async_set("alarm_control_panel.entity", STATE_ALARM_PENDING) + hass.states.async_set("alarm_control_panel.entity", STATE_ALARM_ARMED_NIGHT) + await hass.async_block_till_done() + assert len(calls) == 5 + assert calls[4].data[ + "some" + ] == "armed_night - device - {} - pending - armed_night - None".format( + "alarm_control_panel.entity" + ) diff --git a/tests/components/alexa/__init__.py b/tests/components/alexa/__init__.py index 0fa1961ad61c..3752ad7d48fe 100644 --- a/tests/components/alexa/__init__.py +++ b/tests/components/alexa/__init__.py @@ -13,7 +13,11 @@ class MockConfig(config.AbstractConfig): """Mock Alexa config.""" - entity_config = {"binary_sensor.test_doorbell": {"display_categories": "DOORBELL"}} + entity_config = { + "binary_sensor.test_doorbell": {"display_categories": "DOORBELL"}, + "binary_sensor.test_contact_forced": {"display_categories": "CONTACT_SENSOR"}, + "binary_sensor.test_motion_forced": {"display_categories": "MOTION_SENSOR"}, + } @property def supports_auth(self): diff --git a/tests/components/alexa/test_smart_home.py b/tests/components/alexa/test_smart_home.py index 9b901288f26a..9a2eba21c0e2 100644 --- a/tests/components/alexa/test_smart_home.py +++ b/tests/components/alexa/test_smart_home.py @@ -157,7 +157,7 @@ async def test_switch(hass, events): assert appliance["displayCategories"][0] == "SWITCH" assert appliance["friendlyName"] == "Test switch" assert_endpoint_capabilities( - appliance, "Alexa.PowerController", "Alexa.EndpointHealth" + appliance, "Alexa.PowerController", "Alexa.EndpointHealth", "Alexa" ) await assert_power_controller_works( @@ -168,6 +168,23 @@ async def test_switch(hass, events): properties.assert_equal("Alexa.PowerController", "powerState", "ON") +async def test_outlet(hass, events): + """Test switch with device class outlet discovery.""" + device = ( + "switch.test", + "on", + {"friendly_name": "Test switch", "device_class": "outlet"}, + ) + appliance = await discovery_test(device, hass) + + assert appliance["endpointId"] == "switch#test" + assert appliance["displayCategories"][0] == "SMARTPLUG" + assert appliance["friendlyName"] == "Test switch" + assert_endpoint_capabilities( + appliance, "Alexa", "Alexa.PowerController", "Alexa.EndpointHealth" + ) + + async def test_light(hass): """Test light discovery.""" device = ("light.test_1", "on", {"friendly_name": "Test light 1"}) @@ -177,7 +194,7 @@ async def test_light(hass): assert appliance["displayCategories"][0] == "LIGHT" assert appliance["friendlyName"] == "Test light 1" assert_endpoint_capabilities( - appliance, "Alexa.PowerController", "Alexa.EndpointHealth" + appliance, "Alexa.PowerController", "Alexa.EndpointHealth", "Alexa" ) await assert_power_controller_works( @@ -203,6 +220,7 @@ async def test_dimmable_light(hass): "Alexa.BrightnessController", "Alexa.PowerController", "Alexa.EndpointHealth", + "Alexa", ) properties = await reported_properties(hass, "light#test_2") @@ -245,6 +263,7 @@ async def test_color_light(hass): "Alexa.ColorController", "Alexa.ColorTemperatureController", "Alexa.EndpointHealth", + "Alexa", ) # IncreaseColorTemperature and DecreaseColorTemperature have their own @@ -260,8 +279,11 @@ async def test_script(hass): assert appliance["displayCategories"][0] == "ACTIVITY_TRIGGER" assert appliance["friendlyName"] == "Test script" - (capability,) = assert_endpoint_capabilities(appliance, "Alexa.SceneController") - assert not capability["supportsDeactivation"] + capabilities = assert_endpoint_capabilities( + appliance, "Alexa.SceneController", "Alexa" + ) + scene_capability = get_capability(capabilities, "Alexa.SceneController") + assert not scene_capability["supportsDeactivation"] await assert_scene_controller_works("script#test", "script.turn_on", None, hass) @@ -276,8 +298,11 @@ async def test_cancelable_script(hass): appliance = await discovery_test(device, hass) assert appliance["endpointId"] == "script#test_2" - (capability,) = assert_endpoint_capabilities(appliance, "Alexa.SceneController") - assert capability["supportsDeactivation"] + capabilities = assert_endpoint_capabilities( + appliance, "Alexa.SceneController", "Alexa" + ) + scene_capability = get_capability(capabilities, "Alexa.SceneController") + assert scene_capability["supportsDeactivation"] await assert_scene_controller_works( "script#test_2", "script.turn_on", "script.turn_off", hass @@ -293,7 +318,7 @@ async def test_input_boolean(hass): assert appliance["displayCategories"][0] == "OTHER" assert appliance["friendlyName"] == "Test input boolean" assert_endpoint_capabilities( - appliance, "Alexa.PowerController", "Alexa.EndpointHealth" + appliance, "Alexa.PowerController", "Alexa.EndpointHealth", "Alexa" ) await assert_power_controller_works( @@ -310,8 +335,11 @@ async def test_scene(hass): assert appliance["displayCategories"][0] == "SCENE_TRIGGER" assert appliance["friendlyName"] == "Test scene" - (capability,) = assert_endpoint_capabilities(appliance, "Alexa.SceneController") - assert not capability["supportsDeactivation"] + capabilities = assert_endpoint_capabilities( + appliance, "Alexa.SceneController", "Alexa" + ) + scene_capability = get_capability(capabilities, "Alexa.SceneController") + assert not scene_capability["supportsDeactivation"] await assert_scene_controller_works("scene#test", "scene.turn_on", None, hass) @@ -325,7 +353,7 @@ async def test_fan(hass): assert appliance["displayCategories"][0] == "FAN" assert appliance["friendlyName"] == "Test fan 1" capabilities = assert_endpoint_capabilities( - appliance, "Alexa.PowerController", "Alexa.EndpointHealth" + appliance, "Alexa.PowerController", "Alexa.EndpointHealth", "Alexa" ) power_capability = get_capability(capabilities, "Alexa.PowerController") @@ -361,6 +389,7 @@ async def test_variable_fan(hass): "Alexa.PowerLevelController", "Alexa.RangeController", "Alexa.EndpointHealth", + "Alexa", ) range_capability = get_capability(capabilities, "Alexa.RangeController") @@ -444,6 +473,7 @@ async def test_oscillating_fan(hass): "Alexa.RangeController", "Alexa.ToggleController", "Alexa.EndpointHealth", + "Alexa", ) toggle_capability = get_capability(capabilities, "Alexa.ToggleController") @@ -508,6 +538,7 @@ async def test_direction_fan(hass): "Alexa.RangeController", "Alexa.ModeController", "Alexa.EndpointHealth", + "Alexa", ) mode_capability = get_capability(capabilities, "Alexa.ModeController") @@ -548,7 +579,7 @@ async def test_direction_fan(hass): }, } in supported_modes - call, _ = await assert_request_calls_service( + call, msg = await assert_request_calls_service( "Alexa.ModeController", "SetMode", "fan#test_4", @@ -558,6 +589,25 @@ async def test_direction_fan(hass): instance="fan.direction", ) assert call.data["direction"] == "reverse" + properties = msg["context"]["properties"][0] + assert properties["name"] == "mode" + assert properties["namespace"] == "Alexa.ModeController" + assert properties["value"] == "direction.reverse" + + call, msg = await assert_request_calls_service( + "Alexa.ModeController", + "SetMode", + "fan#test_4", + "fan.set_direction", + hass, + payload={"mode": "direction.forward"}, + instance="fan.direction", + ) + assert call.data["direction"] == "forward" + properties = msg["context"]["properties"][0] + assert properties["name"] == "mode" + assert properties["namespace"] == "Alexa.ModeController" + assert properties["value"] == "direction.forward" # Test for AdjustMode instance=None Error coverage with pytest.raises(AssertionError): @@ -601,6 +651,7 @@ async def test_fan_range(hass): "Alexa.PowerLevelController", "Alexa.RangeController", "Alexa.EndpointHealth", + "Alexa", ) range_capability = get_capability(capabilities, "Alexa.RangeController") @@ -678,7 +729,7 @@ async def test_lock(hass): assert appliance["displayCategories"][0] == "SMARTLOCK" assert appliance["friendlyName"] == "Test lock" assert_endpoint_capabilities( - appliance, "Alexa.LockController", "Alexa.EndpointHealth" + appliance, "Alexa.LockController", "Alexa.EndpointHealth", "Alexa" ) _, msg = await assert_request_calls_service( @@ -729,6 +780,7 @@ async def test_media_player(hass): capabilities = assert_endpoint_capabilities( appliance, + "Alexa", "Alexa.ChannelController", "Alexa.EndpointHealth", "Alexa.InputController", @@ -883,7 +935,7 @@ async def test_media_player(hass): "media_player#test", "media_player.play_media", hass, - payload={"channel": {"number": 24}}, + payload={"channel": {"number": "24"}, "channelMetadata": {"name": ""}}, ) call, _ = await assert_request_calls_service( @@ -892,7 +944,7 @@ async def test_media_player(hass): "media_player#test", "media_player.play_media", hass, - payload={"channel": {"callSign": "ABC"}}, + payload={"channel": {"callSign": "ABC"}, "channelMetadata": {"name": ""}}, ) call, _ = await assert_request_calls_service( @@ -901,7 +953,19 @@ async def test_media_player(hass): "media_player#test", "media_player.play_media", hass, - payload={"channel": {"affiliateCallSign": "ABC"}}, + payload={"channel": {"number": ""}, "channelMetadata": {"name": "ABC"}}, + ) + + call, _ = await assert_request_calls_service( + "Alexa.ChannelController", + "ChangeChannel", + "media_player#test", + "media_player.play_media", + hass, + payload={ + "channel": {"affiliateCallSign": "ABC"}, + "channelMetadata": {"name": ""}, + }, ) call, _ = await assert_request_calls_service( @@ -910,7 +974,7 @@ async def test_media_player(hass): "media_player#test", "media_player.play_media", hass, - payload={"channel": {"uri": "ABC"}}, + payload={"channel": {"uri": "ABC"}, "channelMetadata": {"name": ""}}, ) call, _ = await assert_request_calls_service( @@ -951,6 +1015,7 @@ async def test_media_player_power(hass): assert_endpoint_capabilities( appliance, + "Alexa", "Alexa.ChannelController", "Alexa.EndpointHealth", "Alexa.InputController", @@ -979,6 +1044,110 @@ async def test_media_player_power(hass): ) +async def test_media_player_inputs(hass): + """Test media player discovery with source list inputs.""" + device = ( + "media_player.test", + "on", + { + "friendly_name": "Test media player", + "supported_features": SUPPORT_SELECT_SOURCE, + "volume_level": 0.75, + "source_list": [ + "foo", + "foo_2", + "hdmi", + "hdmi_2", + "hdmi-3", + "hdmi4", + "hdmi 5", + "HDMI 6", + "hdmi_arc", + "aux", + "input 1", + "tv", + ], + }, + ) + appliance = await discovery_test(device, hass) + + assert appliance["endpointId"] == "media_player#test" + assert appliance["displayCategories"][0] == "TV" + assert appliance["friendlyName"] == "Test media player" + + capabilities = assert_endpoint_capabilities( + appliance, + "Alexa", + "Alexa.InputController", + "Alexa.PowerController", + "Alexa.EndpointHealth", + ) + + input_capability = get_capability(capabilities, "Alexa.InputController") + assert input_capability is not None + assert {"name": "AUX"} not in input_capability["inputs"] + assert {"name": "AUX 1"} in input_capability["inputs"] + assert {"name": "HDMI 1"} in input_capability["inputs"] + assert {"name": "HDMI 2"} in input_capability["inputs"] + assert {"name": "HDMI 3"} in input_capability["inputs"] + assert {"name": "HDMI 4"} in input_capability["inputs"] + assert {"name": "HDMI 5"} in input_capability["inputs"] + assert {"name": "HDMI 6"} in input_capability["inputs"] + assert {"name": "HDMI ARC"} in input_capability["inputs"] + assert {"name": "FOO 1"} not in input_capability["inputs"] + assert {"name": "TV"} in input_capability["inputs"] + + call, _ = await assert_request_calls_service( + "Alexa.InputController", + "SelectInput", + "media_player#test", + "media_player.select_source", + hass, + payload={"input": "HDMI 1"}, + ) + assert call.data["source"] == "hdmi" + + call, _ = await assert_request_calls_service( + "Alexa.InputController", + "SelectInput", + "media_player#test", + "media_player.select_source", + hass, + payload={"input": "HDMI 2"}, + ) + assert call.data["source"] == "hdmi_2" + + call, _ = await assert_request_calls_service( + "Alexa.InputController", + "SelectInput", + "media_player#test", + "media_player.select_source", + hass, + payload={"input": "HDMI 5"}, + ) + assert call.data["source"] == "hdmi 5" + + call, _ = await assert_request_calls_service( + "Alexa.InputController", + "SelectInput", + "media_player#test", + "media_player.select_source", + hass, + payload={"input": "HDMI 6"}, + ) + assert call.data["source"] == "HDMI 6" + + call, _ = await assert_request_calls_service( + "Alexa.InputController", + "SelectInput", + "media_player#test", + "media_player.select_source", + hass, + payload={"input": "TV"}, + ) + assert call.data["source"] == "tv" + + async def test_media_player_speaker(hass): """Test media player discovery with device class speaker.""" device = ( @@ -1018,6 +1187,7 @@ async def test_media_player_seek(hass): assert_endpoint_capabilities( appliance, + "Alexa", "Alexa.EndpointHealth", "Alexa.PowerController", "Alexa.SeekController", @@ -1121,7 +1291,7 @@ async def test_alert(hass): assert appliance["displayCategories"][0] == "OTHER" assert appliance["friendlyName"] == "Test alert" assert_endpoint_capabilities( - appliance, "Alexa.PowerController", "Alexa.EndpointHealth" + appliance, "Alexa.PowerController", "Alexa.EndpointHealth", "Alexa" ) await assert_power_controller_works( @@ -1138,7 +1308,7 @@ async def test_automation(hass): assert appliance["displayCategories"][0] == "OTHER" assert appliance["friendlyName"] == "Test automation" assert_endpoint_capabilities( - appliance, "Alexa.PowerController", "Alexa.EndpointHealth" + appliance, "Alexa.PowerController", "Alexa.EndpointHealth", "Alexa" ) await assert_power_controller_works( @@ -1155,7 +1325,7 @@ async def test_group(hass): assert appliance["displayCategories"][0] == "OTHER" assert appliance["friendlyName"] == "Test group" assert_endpoint_capabilities( - appliance, "Alexa.PowerController", "Alexa.EndpointHealth" + appliance, "Alexa.PowerController", "Alexa.EndpointHealth", "Alexa" ) await assert_power_controller_works( @@ -1173,14 +1343,16 @@ async def test_cover(hass): appliance = await discovery_test(device, hass) assert appliance["endpointId"] == "cover#test" - assert appliance["displayCategories"][0] == "DOOR" + assert appliance["displayCategories"][0] == "OTHER" assert appliance["friendlyName"] == "Test cover" assert_endpoint_capabilities( appliance, + "Alexa.ModeController", "Alexa.PercentageController", "Alexa.PowerController", "Alexa.EndpointHealth", + "Alexa", ) await assert_power_controller_works( @@ -1269,7 +1441,7 @@ async def test_temp_sensor(hass): assert appliance["friendlyName"] == "Test Temp Sensor" capabilities = assert_endpoint_capabilities( - appliance, "Alexa.TemperatureSensor", "Alexa.EndpointHealth" + appliance, "Alexa.TemperatureSensor", "Alexa.EndpointHealth", "Alexa" ) temp_sensor_capability = get_capability(capabilities, "Alexa.TemperatureSensor") @@ -1298,7 +1470,7 @@ async def test_contact_sensor(hass): assert appliance["friendlyName"] == "Test Contact Sensor" capabilities = assert_endpoint_capabilities( - appliance, "Alexa.ContactSensor", "Alexa.EndpointHealth" + appliance, "Alexa.ContactSensor", "Alexa.EndpointHealth", "Alexa" ) contact_sensor_capability = get_capability(capabilities, "Alexa.ContactSensor") @@ -1313,6 +1485,35 @@ async def test_contact_sensor(hass): properties.assert_equal("Alexa.EndpointHealth", "connectivity", {"value": "OK"}) +async def test_forced_contact_sensor(hass): + """Test contact sensor discovery with specified display_category.""" + device = ( + "binary_sensor.test_contact_forced", + "on", + {"friendly_name": "Test Contact Sensor With DisplayCategory"}, + ) + appliance = await discovery_test(device, hass) + + assert appliance["endpointId"] == "binary_sensor#test_contact_forced" + assert appliance["displayCategories"][0] == "CONTACT_SENSOR" + assert appliance["friendlyName"] == "Test Contact Sensor With DisplayCategory" + + capabilities = assert_endpoint_capabilities( + appliance, "Alexa.ContactSensor", "Alexa.EndpointHealth", "Alexa" + ) + + contact_sensor_capability = get_capability(capabilities, "Alexa.ContactSensor") + assert contact_sensor_capability is not None + properties = contact_sensor_capability["properties"] + assert properties["retrievable"] is True + assert {"name": "detectionState"} in properties["supported"] + + properties = await reported_properties(hass, "binary_sensor#test_contact_forced") + properties.assert_equal("Alexa.ContactSensor", "detectionState", "DETECTED") + + properties.assert_equal("Alexa.EndpointHealth", "connectivity", {"value": "OK"}) + + async def test_motion_sensor(hass): """Test motion sensor discovery.""" device = ( @@ -1327,7 +1528,7 @@ async def test_motion_sensor(hass): assert appliance["friendlyName"] == "Test Motion Sensor" capabilities = assert_endpoint_capabilities( - appliance, "Alexa.MotionSensor", "Alexa.EndpointHealth" + appliance, "Alexa.MotionSensor", "Alexa.EndpointHealth", "Alexa" ) motion_sensor_capability = get_capability(capabilities, "Alexa.MotionSensor") @@ -1340,6 +1541,35 @@ async def test_motion_sensor(hass): properties.assert_equal("Alexa.MotionSensor", "detectionState", "DETECTED") +async def test_forced_motion_sensor(hass): + """Test motion sensor discovery with specified display_category.""" + device = ( + "binary_sensor.test_motion_forced", + "on", + {"friendly_name": "Test Motion Sensor With DisplayCategory"}, + ) + appliance = await discovery_test(device, hass) + + assert appliance["endpointId"] == "binary_sensor#test_motion_forced" + assert appliance["displayCategories"][0] == "MOTION_SENSOR" + assert appliance["friendlyName"] == "Test Motion Sensor With DisplayCategory" + + capabilities = assert_endpoint_capabilities( + appliance, "Alexa.MotionSensor", "Alexa.EndpointHealth", "Alexa" + ) + + motion_sensor_capability = get_capability(capabilities, "Alexa.MotionSensor") + assert motion_sensor_capability is not None + properties = motion_sensor_capability["properties"] + assert properties["retrievable"] is True + assert {"name": "detectionState"} in properties["supported"] + + properties = await reported_properties(hass, "binary_sensor#test_motion_forced") + properties.assert_equal("Alexa.MotionSensor", "detectionState", "DETECTED") + + properties.assert_equal("Alexa.EndpointHealth", "connectivity", {"value": "OK"}) + + async def test_doorbell_sensor(hass): """Test doorbell sensor discovery.""" device = ( @@ -1354,7 +1584,7 @@ async def test_doorbell_sensor(hass): assert appliance["friendlyName"] == "Test Doorbell Sensor" capabilities = assert_endpoint_capabilities( - appliance, "Alexa.DoorbellEventSource", "Alexa.EndpointHealth" + appliance, "Alexa.DoorbellEventSource", "Alexa.EndpointHealth", "Alexa" ) doorbell_capability = get_capability(capabilities, "Alexa.DoorbellEventSource") @@ -1404,6 +1634,7 @@ async def test_thermostat(hass): "Alexa.ThermostatController", "Alexa.TemperatureSensor", "Alexa.EndpointHealth", + "Alexa", ) properties = await reported_properties(hass, "climate#test_thermostat") @@ -1800,7 +2031,7 @@ async def test_entity_config(hass): assert appliance["friendlyName"] == "Config name" assert appliance["description"] == "Config description via Home Assistant" assert_endpoint_capabilities( - appliance, "Alexa.PowerController", "Alexa.EndpointHealth" + appliance, "Alexa.PowerController", "Alexa.EndpointHealth", "Alexa" ) scene = msg["payload"]["endpoints"][1] @@ -1917,7 +2148,7 @@ async def test_alarm_control_panel_disarmed(hass): assert appliance["displayCategories"][0] == "SECURITY_PANEL" assert appliance["friendlyName"] == "Test Alarm Control Panel 1" capabilities = assert_endpoint_capabilities( - appliance, "Alexa.SecurityPanelController", "Alexa.EndpointHealth" + appliance, "Alexa.SecurityPanelController", "Alexa.EndpointHealth", "Alexa" ) security_panel_capability = get_capability( capabilities, "Alexa.SecurityPanelController" @@ -1984,7 +2215,7 @@ async def test_alarm_control_panel_armed(hass): assert appliance["displayCategories"][0] == "SECURITY_PANEL" assert appliance["friendlyName"] == "Test Alarm Control Panel 2" assert_endpoint_capabilities( - appliance, "Alexa.SecurityPanelController", "Alexa.EndpointHealth" + appliance, "Alexa.SecurityPanelController", "Alexa.EndpointHealth", "Alexa" ) properties = await reported_properties(hass, "alarm_control_panel#test_2") @@ -2059,3 +2290,99 @@ async def test_mode_unsupported_domain(hass): assert msg["header"]["name"] == "ErrorResponse" assert msg["header"]["namespace"] == "Alexa" assert msg["payload"]["type"] == "INVALID_DIRECTIVE" + + +async def test_cover_position(hass): + """Test cover position mode discovery.""" + device = ( + "cover.test", + "off", + {"friendly_name": "Test cover", "supported_features": 255, "position": 30}, + ) + appliance = await discovery_test(device, hass) + + assert appliance["endpointId"] == "cover#test" + assert appliance["displayCategories"][0] == "OTHER" + assert appliance["friendlyName"] == "Test cover" + + capabilities = assert_endpoint_capabilities( + appliance, + "Alexa", + "Alexa.ModeController", + "Alexa.PercentageController", + "Alexa.PowerController", + "Alexa.EndpointHealth", + ) + + mode_capability = get_capability(capabilities, "Alexa.ModeController") + assert mode_capability is not None + assert mode_capability["instance"] == "cover.position" + + properties = mode_capability["properties"] + assert properties["nonControllable"] is False + assert {"name": "mode"} in properties["supported"] + + capability_resources = mode_capability["capabilityResources"] + assert capability_resources is not None + assert { + "@type": "asset", + "value": {"assetId": "Alexa.Setting.Mode"}, + } in capability_resources["friendlyNames"] + + configuration = mode_capability["configuration"] + assert configuration is not None + assert configuration["ordered"] is False + + supported_modes = configuration["supportedModes"] + assert supported_modes is not None + assert { + "value": "position.open", + "modeResources": { + "friendlyNames": [ + {"@type": "text", "value": {"text": "open", "locale": "en-US"}}, + {"@type": "text", "value": {"text": "opened", "locale": "en-US"}}, + {"@type": "text", "value": {"text": "raise", "locale": "en-US"}}, + {"@type": "text", "value": {"text": "raised", "locale": "en-US"}}, + ] + }, + } in supported_modes + assert { + "value": "position.closed", + "modeResources": { + "friendlyNames": [ + {"@type": "text", "value": {"text": "close", "locale": "en-US"}}, + {"@type": "text", "value": {"text": "closed", "locale": "en-US"}}, + {"@type": "text", "value": {"text": "shut", "locale": "en-US"}}, + {"@type": "text", "value": {"text": "lower", "locale": "en-US"}}, + {"@type": "text", "value": {"text": "lowered", "locale": "en-US"}}, + ] + }, + } in supported_modes + + call, msg = await assert_request_calls_service( + "Alexa.ModeController", + "SetMode", + "cover#test", + "cover.close_cover", + hass, + payload={"mode": "position.closed"}, + instance="cover.position", + ) + properties = msg["context"]["properties"][0] + assert properties["name"] == "mode" + assert properties["namespace"] == "Alexa.ModeController" + assert properties["value"] == "position.closed" + + call, msg = await assert_request_calls_service( + "Alexa.ModeController", + "SetMode", + "cover#test", + "cover.open_cover", + hass, + payload={"mode": "position.open"}, + instance="cover.position", + ) + properties = msg["context"]["properties"][0] + assert properties["name"] == "mode" + assert properties["namespace"] == "Alexa.ModeController" + assert properties["value"] == "position.open" diff --git a/tests/components/androidtv/patchers.py b/tests/components/androidtv/patchers.py index 986180bf214e..0549ad995e18 100644 --- a/tests/components/androidtv/patchers.py +++ b/tests/components/androidtv/patchers.py @@ -1,6 +1,5 @@ """Define patches used for androidtv tests.""" -from socket import error as socket_error from unittest.mock import mock_open, patch @@ -25,7 +24,7 @@ def shell(self, cmd): class ClientFakeSuccess: - """A fake of the `adb_messenger.client.Client` class when the connection and shell commands succeed.""" + """A fake of the `ppadb.client.Client` class when the connection and shell commands succeed.""" def __init__(self, host="127.0.0.1", port=5037): """Initialize a `ClientFakeSuccess` instance.""" @@ -43,7 +42,7 @@ def device(self, serial): class ClientFakeFail: - """A fake of the `adb_messenger.client.Client` class when the connection and shell commands fail.""" + """A fake of the `ppadb.client.Client` class when the connection and shell commands fail.""" def __init__(self, host="127.0.0.1", port=5037): """Initialize a `ClientFakeFail` instance.""" @@ -59,7 +58,7 @@ def device(self, serial): class DeviceFake: - """A fake of the `adb_messenger.device.Device` class.""" + """A fake of the `ppadb.device.Device` class.""" def __init__(self, host): """Initialize a `DeviceFake` instance.""" @@ -75,7 +74,7 @@ def shell(self, cmd): def patch_connect(success): - """Mock the `adb_shell.adb_device.AdbDevice` and `adb_messenger.client.Client` classes.""" + """Mock the `adb_shell.adb_device.AdbDevice` and `ppadb.client.Client` classes.""" def connect_success_python(self, *args, **kwargs): """Mock the `AdbDeviceFake.connect` method when it succeeds.""" @@ -83,7 +82,7 @@ def connect_success_python(self, *args, **kwargs): def connect_fail_python(self, *args, **kwargs): """Mock the `AdbDeviceFake.connect` method when it fails.""" - raise socket_error + raise OSError if success: return { diff --git a/tests/components/apns/test_notify.py b/tests/components/apns/test_notify.py index 78f597c58ade..19d869ea6783 100644 --- a/tests/components/apns/test_notify.py +++ b/tests/components/apns/test_notify.py @@ -121,7 +121,7 @@ def fake_write(_out, device): self._setup_notify() assert self.hass.services.call( - notify.DOMAIN, + apns.DOMAIN, "apns_test_app", {"push_id": "1234", "name": "test device"}, blocking=True, @@ -153,7 +153,7 @@ def fake_write(_out, device): self._setup_notify() assert self.hass.services.call( - notify.DOMAIN, "apns_test_app", {"push_id": "1234"}, blocking=True + apns.DOMAIN, "apns_test_app", {"push_id": "1234"}, blocking=True ) devices = {dev.push_id: dev for dev in written_devices} @@ -183,7 +183,7 @@ def fake_write(_out, device): self._setup_notify() assert self.hass.services.call( - notify.DOMAIN, + apns.DOMAIN, "apns_test_app", {"push_id": "1234", "name": "updated device 1"}, blocking=True, @@ -222,7 +222,7 @@ def fake_write(_out, device): self._setup_notify() assert self.hass.services.call( - notify.DOMAIN, + apns.DOMAIN, "apns_test_app", {"push_id": "1234", "name": "updated device 1"}, blocking=True, diff --git a/tests/components/arcam_fmj/conftest.py b/tests/components/arcam_fmj/conftest.py new file mode 100644 index 000000000000..10405dbeb11d --- /dev/null +++ b/tests/components/arcam_fmj/conftest.py @@ -0,0 +1,52 @@ +"""Tests for the arcam_fmj component.""" +from arcam.fmj.client import Client +from arcam.fmj.state import State +from asynctest import Mock +import pytest + +from homeassistant.components.arcam_fmj import DEVICE_SCHEMA +from homeassistant.components.arcam_fmj.const import DOMAIN +from homeassistant.components.arcam_fmj.media_player import ArcamFmj +from homeassistant.const import CONF_HOST, CONF_PORT + +MOCK_HOST = "127.0.0.1" +MOCK_PORT = 1234 +MOCK_TURN_ON = { + "service": "switch.turn_on", + "data": {"entity_id": "switch.test"}, +} +MOCK_NAME = "dummy" +MOCK_CONFIG = DEVICE_SCHEMA({CONF_HOST: MOCK_HOST, CONF_PORT: MOCK_PORT}) + + +@pytest.fixture(name="config") +def config_fixture(): + """Create hass config fixture.""" + return {DOMAIN: [{CONF_HOST: MOCK_HOST, CONF_PORT: MOCK_PORT}]} + + +@pytest.fixture(name="client") +def client_fixture(): + """Get a mocked client.""" + client = Mock(Client) + client.host = MOCK_HOST + client.port = MOCK_PORT + return client + + +@pytest.fixture(name="state") +def state_fixture(client): + """Get a mocked state.""" + state = Mock(State) + state.client = client + state.zn = 1 + state.get_power.return_value = True + return state + + +@pytest.fixture(name="player") +def player_fixture(hass, state): + """Get standard player.""" + player = ArcamFmj(state, MOCK_NAME, None) + player.async_schedule_update_ha_state = Mock() + return player diff --git a/tests/components/arcam_fmj/test_config_flow.py b/tests/components/arcam_fmj/test_config_flow.py index 54fb34443a53..6df280fa92ef 100644 --- a/tests/components/arcam_fmj/test_config_flow.py +++ b/tests/components/arcam_fmj/test_config_flow.py @@ -1,41 +1,37 @@ """Tests for the Arcam FMJ config flow module.""" + import pytest + from homeassistant import data_entry_flow -from homeassistant.const import CONF_HOST, CONF_PORT - -from tests.common import MockConfigEntry, MockDependency - -with MockDependency("arcam"), MockDependency("arcam.fmj"), MockDependency( - "arcam.fmj.client" -): - from homeassistant.components.arcam_fmj import DEVICE_SCHEMA - from homeassistant.components.arcam_fmj.config_flow import ArcamFmjFlowHandler - from homeassistant.components.arcam_fmj.const import DOMAIN - - MOCK_HOST = "127.0.0.1" - MOCK_PORT = 1234 - MOCK_NAME = "Arcam FMJ" - MOCK_CONFIG = DEVICE_SCHEMA({CONF_HOST: MOCK_HOST, CONF_PORT: MOCK_PORT}) - - @pytest.fixture(name="config_entry") - def config_entry_fixture(): - """Create a mock HEOS config entry.""" - return MockConfigEntry(domain=DOMAIN, data=MOCK_CONFIG, title=MOCK_NAME) - - async def test_single_import_only(hass, config_entry): - """Test form is shown when host not provided.""" - config_entry.add_to_hass(hass) - flow = ArcamFmjFlowHandler() - flow.hass = hass - result = await flow.async_step_import(MOCK_CONFIG) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT - assert result["reason"] == "already_setup" - - async def test_import(hass): - """Test form is shown when host not provided.""" - flow = ArcamFmjFlowHandler() - flow.hass = hass - result = await flow.async_step_import(MOCK_CONFIG) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["title"] == MOCK_NAME - assert result["data"] == MOCK_CONFIG +from homeassistant.components.arcam_fmj.config_flow import ArcamFmjFlowHandler +from homeassistant.components.arcam_fmj.const import DOMAIN + +from .conftest import MOCK_CONFIG, MOCK_NAME + +from tests.common import MockConfigEntry + + +@pytest.fixture(name="config_entry") +def config_entry_fixture(): + """Create a mock Arcam config entry.""" + return MockConfigEntry(domain=DOMAIN, data=MOCK_CONFIG, title=MOCK_NAME) + + +async def test_single_import_only(hass, config_entry): + """Test form is shown when host not provided.""" + config_entry.add_to_hass(hass) + flow = ArcamFmjFlowHandler() + flow.hass = hass + result = await flow.async_step_import(MOCK_CONFIG) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "already_setup" + + +async def test_import(hass): + """Test form is shown when host not provided.""" + flow = ArcamFmjFlowHandler() + flow.hass = hass + result = await flow.async_step_import(MOCK_CONFIG) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "Arcam FMJ" + assert result["data"] == MOCK_CONFIG diff --git a/tests/components/arcam_fmj/test_media_player.py b/tests/components/arcam_fmj/test_media_player.py new file mode 100644 index 000000000000..2d2c14f8e188 --- /dev/null +++ b/tests/components/arcam_fmj/test_media_player.py @@ -0,0 +1,348 @@ +"""Tests for arcam fmj receivers.""" +from math import isclose + +from arcam.fmj import DecodeMode2CH, DecodeModeMCH, IncomingAudioFormat, SourceCodes +from asynctest.mock import ANY, MagicMock, Mock, PropertyMock, patch +import pytest + +from homeassistant.components.arcam_fmj.const import ( + DOMAIN, + SIGNAL_CLIENT_DATA, + SIGNAL_CLIENT_STARTED, + SIGNAL_CLIENT_STOPPED, +) +from homeassistant.components.arcam_fmj.media_player import ArcamFmj +from homeassistant.components.media_player.const import MEDIA_TYPE_MUSIC +from homeassistant.const import STATE_OFF, STATE_ON +from homeassistant.core import HomeAssistant + +from .conftest import MOCK_HOST, MOCK_NAME, MOCK_PORT + +MOCK_TURN_ON = { + "service": "switch.turn_on", + "data": {"entity_id": "switch.test"}, +} + + +async def test_properties(player, state): + """Test standard properties.""" + assert player.unique_id is None + assert player.device_info == { + "identifiers": {(DOMAIN, MOCK_HOST, MOCK_PORT)}, + "model": "FMJ", + "manufacturer": "Arcam", + } + assert not player.should_poll + + +async def test_powered_off(player, state): + """Test properties in powered off state.""" + state.get_source.return_value = None + state.get_power.return_value = None + assert player.source is None + assert player.state == STATE_OFF + + +async def test_powered_on(player, state): + """Test properties in powered on state.""" + state.get_source.return_value = SourceCodes.PVR + state.get_power.return_value = True + assert player.source == "PVR" + assert player.state == STATE_ON + + +async def test_supported_features_no_service(player, state): + """Test support when turn on service exist.""" + state.get_power.return_value = None + assert player.supported_features == 68876 + + state.get_power.return_value = False + assert player.supported_features == 69004 + + +async def test_supported_features_service(hass, state): + """Test support when turn on service exist.""" + player = ArcamFmj(state, "dummy", MOCK_TURN_ON) + state.get_power.return_value = None + assert player.supported_features == 69004 + + state.get_power.return_value = False + assert player.supported_features == 69004 + + +async def test_turn_on_without_service(player, state): + """Test turn on service.""" + state.get_power.return_value = None + await player.async_turn_on() + state.set_power.assert_not_called() + + state.get_power.return_value = False + await player.async_turn_on() + state.set_power.assert_called_with(True) + + +async def test_turn_on_with_service(hass, state): + """Test support when turn on service exist.""" + player = ArcamFmj(state, "dummy", MOCK_TURN_ON) + player.hass = Mock(HomeAssistant) + with patch( + "homeassistant.components.arcam_fmj.media_player.async_call_from_config" + ) as async_call_from_config: + + state.get_power.return_value = None + await player.async_turn_on() + state.set_power.assert_not_called() + async_call_from_config.assert_called_with( + player.hass, + MOCK_TURN_ON, + variables=None, + blocking=True, + validate_config=False, + ) + + +async def test_turn_off(player, state): + """Test command to turn off.""" + await player.async_turn_off() + state.set_power.assert_called_with(False) + + +@pytest.mark.parametrize("mute", [True, False]) +async def test_mute_volume(player, state, mute): + """Test mute functionallity.""" + await player.async_mute_volume(mute) + state.set_mute.assert_called_with(mute) + player.async_schedule_update_ha_state.assert_called_with() + + +async def test_name(player): + """Test name.""" + assert player.name == MOCK_NAME + + +async def test_update(player, state): + """Test update.""" + await player.async_update() + state.update.assert_called_with() + + +@pytest.mark.parametrize( + "fmt, result", + [ + (None, True), + (IncomingAudioFormat.PCM, True), + (IncomingAudioFormat.ANALOGUE_DIRECT, True), + (IncomingAudioFormat.DOLBY_DIGITAL, False), + ], +) +async def test_2ch(player, state, fmt, result): + """Test selection of 2ch mode.""" + state.get_incoming_audio_format.return_value = (fmt, None) + assert player._get_2ch() == result # pylint: disable=W0212 + + +@pytest.mark.parametrize( + "source, value", + [("PVR", SourceCodes.PVR), ("BD", SourceCodes.BD), ("INVALID", None)], +) +async def test_select_source(player, state, source, value): + """Test selection of source.""" + await player.async_select_source(source) + if value: + state.set_source.assert_called_with(value) + else: + state.set_source.assert_not_called() + + +async def test_source_list(player, state): + """Test source list.""" + state.get_source_list.return_value = [SourceCodes.BD] + assert player.source_list == ["BD"] + + +@pytest.mark.parametrize( + "mode, mode_sel, mode_2ch, mode_mch", + [ + ("STEREO", True, DecodeMode2CH.STEREO, None), + ("STEREO", False, None, None), + ("STEREO", False, None, None), + ], +) +async def test_select_sound_mode(player, state, mode, mode_sel, mode_2ch, mode_mch): + """Test selection sound mode.""" + player._get_2ch = Mock(return_value=mode_sel) # pylint: disable=W0212 + + await player.async_select_sound_mode(mode) + if mode_2ch: + state.set_decode_mode_2ch.assert_called_with(mode_2ch) + else: + state.set_decode_mode_2ch.assert_not_called() + + if mode_mch: + state.set_decode_mode_mch.assert_called_with(mode_mch) + else: + state.set_decode_mode_mch.assert_not_called() + + +async def test_volume_up(player, state): + """Test mute functionallity.""" + await player.async_volume_up() + state.inc_volume.assert_called_with() + player.async_schedule_update_ha_state.assert_called_with() + + +async def test_volume_down(player, state): + """Test mute functionallity.""" + await player.async_volume_down() + state.dec_volume.assert_called_with() + player.async_schedule_update_ha_state.assert_called_with() + + +@pytest.mark.parametrize( + "mode, mode_sel, mode_2ch, mode_mch", + [ + ("STEREO", True, DecodeMode2CH.STEREO, None), + ("STEREO_DOWNMIX", False, None, DecodeModeMCH.STEREO_DOWNMIX), + (None, False, None, None), + ], +) +async def test_sound_mode(player, state, mode, mode_sel, mode_2ch, mode_mch): + """Test selection sound mode.""" + player._get_2ch = Mock(return_value=mode_sel) # pylint: disable=W0212 + state.get_decode_mode_2ch.return_value = mode_2ch + state.get_decode_mode_mch.return_value = mode_mch + + assert player.sound_mode == mode + + +async def test_sound_mode_list(player, state): + """Test sound mode list.""" + player._get_2ch = Mock(return_value=True) # pylint: disable=W0212 + assert sorted(player.sound_mode_list) == sorted([x.name for x in DecodeMode2CH]) + player._get_2ch = Mock(return_value=False) # pylint: disable=W0212 + assert sorted(player.sound_mode_list) == sorted([x.name for x in DecodeModeMCH]) + + +async def test_sound_mode_zone_x(player, state): + """Test second zone sound mode.""" + state.zn = 2 + assert player.sound_mode is None + assert player.sound_mode_list is None + + +async def test_is_volume_muted(player, state): + """Test muted.""" + state.get_mute.return_value = True + assert player.is_volume_muted is True # pylint: disable=singleton-comparison + state.get_mute.return_value = False + assert player.is_volume_muted is False # pylint: disable=singleton-comparison + state.get_mute.return_value = None + assert player.is_volume_muted is None + + +async def test_volume_level(player, state): + """Test volume.""" + state.get_volume.return_value = 0 + assert isclose(player.volume_level, 0.0) + state.get_volume.return_value = 50 + assert isclose(player.volume_level, 50.0 / 99) + state.get_volume.return_value = 99 + assert isclose(player.volume_level, 1.0) + state.get_volume.return_value = None + assert player.volume_level is None + + +@pytest.mark.parametrize("volume, call", [(0.0, 0), (0.5, 50), (1.0, 99)]) +async def test_set_volume_level(player, state, volume, call): + """Test setting volume.""" + await player.async_set_volume_level(volume) + state.set_volume.assert_called_with(call) + + +@pytest.mark.parametrize( + "source, media_content_type", + [ + (SourceCodes.DAB, MEDIA_TYPE_MUSIC), + (SourceCodes.FM, MEDIA_TYPE_MUSIC), + (SourceCodes.PVR, None), + (None, None), + ], +) +async def test_media_content_type(player, state, source, media_content_type): + """Test content type deduction.""" + state.get_source.return_value = source + assert player.media_content_type == media_content_type + + +@pytest.mark.parametrize( + "source, dab, rds, channel", + [ + (SourceCodes.DAB, "dab", "rds", "dab"), + (SourceCodes.DAB, None, None, None), + (SourceCodes.FM, "dab", "rds", "rds"), + (SourceCodes.FM, None, None, None), + (SourceCodes.PVR, "dab", "rds", None), + ], +) +async def test_media_channel(player, state, source, dab, rds, channel): + """Test media channel.""" + state.get_dab_station.return_value = dab + state.get_rds_information.return_value = rds + state.get_source.return_value = source + assert player.media_channel == channel + + +@pytest.mark.parametrize( + "source, dls, artist", + [ + (SourceCodes.DAB, "dls", "dls"), + (SourceCodes.FM, "dls", None), + (SourceCodes.DAB, None, None), + ], +) +async def test_media_artist(player, state, source, dls, artist): + """Test media artist.""" + state.get_dls_pdt.return_value = dls + state.get_source.return_value = source + assert player.media_artist == artist + + +@pytest.mark.parametrize( + "source, channel, title", + [ + (SourceCodes.DAB, "channel", "DAB - channel"), + (SourceCodes.DAB, None, "DAB"), + (None, None, None), + ], +) +async def test_media_title(player, state, source, channel, title): + """Test media title.""" + state.get_source.return_value = source + with patch.object( + ArcamFmj, "media_channel", new_callable=PropertyMock + ) as media_channel: + media_channel.return_value = channel + assert player.media_title == title + + +async def test_added_to_hass(player, state): + """Test addition to hass.""" + connectors = {} + + def _connect(signal, fun): + connectors[signal] = fun + + player.hass = MagicMock() + player.hass.helpers.dispatcher.async_dispatcher_connect.side_effects = _connect + + await player.async_added_to_hass() + state.start.assert_called_with() + player.hass.helpers.dispatcher.async_dispatcher_connect.assert_any_call( + SIGNAL_CLIENT_DATA, ANY + ) + player.hass.helpers.dispatcher.async_dispatcher_connect.assert_any_call( + SIGNAL_CLIENT_STARTED, ANY + ) + player.hass.helpers.dispatcher.async_dispatcher_connect.assert_any_call( + SIGNAL_CLIENT_STOPPED, ANY + ) diff --git a/tests/components/asuswrt/test_device_tracker.py b/tests/components/asuswrt/test_device_tracker.py index a3fde3a68555..de999362f51e 100644 --- a/tests/components/asuswrt/test_device_tracker.py +++ b/tests/components/asuswrt/test_device_tracker.py @@ -1,4 +1,5 @@ """The tests for the ASUSWRT device tracker platform.""" +from unittest.mock import patch from homeassistant.setup import async_setup_component from homeassistant.components.asuswrt import ( @@ -10,7 +11,7 @@ ) from homeassistant.const import CONF_PLATFORM, CONF_PASSWORD, CONF_USERNAME, CONF_HOST -from tests.common import MockDependency, mock_coro_func +from tests.common import mock_coro_func FAKEFILE = None @@ -28,9 +29,9 @@ async def test_password_or_pub_key_required(hass): """Test creating an AsusWRT scanner without a pass or pubkey.""" - with MockDependency("aioasuswrt.asuswrt") as mocked_asus: - mocked_asus.AsusWrt().connection.async_connect = mock_coro_func() - mocked_asus.AsusWrt().is_connected = False + with patch("homeassistant.components.asuswrt.AsusWrt") as AsusWrt: + AsusWrt().connection.async_connect = mock_coro_func() + AsusWrt().is_connected = False result = await async_setup_component( hass, DOMAIN, {DOMAIN: {CONF_HOST: "fake_host", CONF_USERNAME: "fake_user"}} ) @@ -39,9 +40,9 @@ async def test_password_or_pub_key_required(hass): async def test_get_scanner_with_password_no_pubkey(hass): """Test creating an AsusWRT scanner with a password and no pubkey.""" - with MockDependency("aioasuswrt.asuswrt") as mocked_asus: - mocked_asus.AsusWrt().connection.async_connect = mock_coro_func() - mocked_asus.AsusWrt().connection.async_get_connected_devices = mock_coro_func( + with patch("homeassistant.components.asuswrt.AsusWrt") as AsusWrt: + AsusWrt().connection.async_connect = mock_coro_func() + AsusWrt().connection.async_get_connected_devices = mock_coro_func( return_value={} ) result = await async_setup_component( diff --git a/tests/components/automation/common.py b/tests/components/automation/common.py index 6fadbd14199d..c7aa8f1eced9 100644 --- a/tests/components/automation/common.py +++ b/tests/components/automation/common.py @@ -10,33 +10,34 @@ SERVICE_TURN_OFF, SERVICE_TOGGLE, SERVICE_RELOAD, + ENTITY_MATCH_ALL, ) from homeassistant.loader import bind_hass @bind_hass -async def async_turn_on(hass, entity_id=None): +async def async_turn_on(hass, entity_id=ENTITY_MATCH_ALL): """Turn on specified automation or all.""" data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} await hass.services.async_call(DOMAIN, SERVICE_TURN_ON, data) @bind_hass -async def async_turn_off(hass, entity_id=None): +async def async_turn_off(hass, entity_id=ENTITY_MATCH_ALL): """Turn off specified automation or all.""" data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} await hass.services.async_call(DOMAIN, SERVICE_TURN_OFF, data) @bind_hass -async def async_toggle(hass, entity_id=None): +async def async_toggle(hass, entity_id=ENTITY_MATCH_ALL): """Toggle specified automation or all.""" data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} await hass.services.async_call(DOMAIN, SERVICE_TOGGLE, data) @bind_hass -async def async_trigger(hass, entity_id=None): +async def async_trigger(hass, entity_id=ENTITY_MATCH_ALL): """Trigger specified automation or all.""" data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} await hass.services.async_call(DOMAIN, SERVICE_TRIGGER, data) diff --git a/tests/components/automation/test_litejet.py b/tests/components/automation/test_litejet.py index 2e6f578ef4c7..4c916d8ed965 100644 --- a/tests/components/automation/test_litejet.py +++ b/tests/components/automation/test_litejet.py @@ -33,7 +33,7 @@ def get_switch_name(number): @pytest.fixture def mock_lj(hass): """Initialize components.""" - with mock.patch("pylitejet.LiteJet") as mock_pylitejet: + with mock.patch("homeassistant.components.litejet.LiteJet") as mock_pylitejet: mock_lj = mock_pylitejet.return_value mock_lj.switch_pressed_callbacks = {} diff --git a/tests/components/blackbird/test_media_player.py b/tests/components/blackbird/test_media_player.py index 34309fdbcf30..0b6eda16c152 100644 --- a/tests/components/blackbird/test_media_player.py +++ b/tests/components/blackbird/test_media_player.py @@ -5,7 +5,6 @@ from collections import defaultdict from homeassistant.components.media_player.const import ( - DOMAIN, SUPPORT_TURN_ON, SUPPORT_TURN_OFF, SUPPORT_SELECT_SOURCE, @@ -16,9 +15,9 @@ from homeassistant.components.blackbird.media_player import ( DATA_BLACKBIRD, PLATFORM_SCHEMA, - SERVICE_SETALLZONES, setup_platform, ) +from homeassistant.components.blackbird.const import DOMAIN, SERVICE_SETALLZONES import pytest diff --git a/tests/components/camera/common.py b/tests/components/camera/common.py index 9d5d2dcd6fd4..971d723d2d54 100644 --- a/tests/components/camera/common.py +++ b/tests/components/camera/common.py @@ -13,20 +13,25 @@ DATA_CAMERA_PREFS, PREF_PRELOAD_STREAM, ) -from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON +from homeassistant.const import ( + ATTR_ENTITY_ID, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + ENTITY_MATCH_ALL, +) from homeassistant.core import callback from homeassistant.loader import bind_hass @bind_hass -async def async_turn_off(hass, entity_id=None): +async def async_turn_off(hass, entity_id=ENTITY_MATCH_ALL): """Turn off camera.""" data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} await hass.services.async_call(DOMAIN, SERVICE_TURN_OFF, data) @bind_hass -async def async_turn_on(hass, entity_id=None): +async def async_turn_on(hass, entity_id=ENTITY_MATCH_ALL): """Turn on camera, and set operation mode.""" data = {} if entity_id is not None: @@ -36,7 +41,7 @@ async def async_turn_on(hass, entity_id=None): @bind_hass -def enable_motion_detection(hass, entity_id=None): +def enable_motion_detection(hass, entity_id=ENTITY_MATCH_ALL): """Enable Motion Detection.""" data = {ATTR_ENTITY_ID: entity_id} if entity_id else None hass.async_add_job(hass.services.async_call(DOMAIN, SERVICE_ENABLE_MOTION, data)) @@ -44,7 +49,7 @@ def enable_motion_detection(hass, entity_id=None): @bind_hass @callback -def async_snapshot(hass, filename, entity_id=None): +def async_snapshot(hass, filename, entity_id=ENTITY_MATCH_ALL): """Make a snapshot from a camera.""" data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} data[ATTR_FILENAME] = filename diff --git a/tests/components/climate/common.py b/tests/components/climate/common.py index 9f1ef8b084a6..a5ea182f2b61 100644 --- a/tests/components/climate/common.py +++ b/tests/components/climate/common.py @@ -27,11 +27,12 @@ ATTR_TEMPERATURE, SERVICE_TURN_OFF, SERVICE_TURN_ON, + ENTITY_MATCH_ALL, ) from homeassistant.loader import bind_hass -async def async_set_preset_mode(hass, preset_mode, entity_id=None): +async def async_set_preset_mode(hass, preset_mode, entity_id=ENTITY_MATCH_ALL): """Set new preset mode.""" data = {ATTR_PRESET_MODE: preset_mode} @@ -42,7 +43,7 @@ async def async_set_preset_mode(hass, preset_mode, entity_id=None): @bind_hass -def set_preset_mode(hass, preset_mode, entity_id=None): +def set_preset_mode(hass, preset_mode, entity_id=ENTITY_MATCH_ALL): """Set new preset mode.""" data = {ATTR_PRESET_MODE: preset_mode} @@ -52,7 +53,7 @@ def set_preset_mode(hass, preset_mode, entity_id=None): hass.services.call(DOMAIN, SERVICE_SET_PRESET_MODE, data) -async def async_set_aux_heat(hass, aux_heat, entity_id=None): +async def async_set_aux_heat(hass, aux_heat, entity_id=ENTITY_MATCH_ALL): """Turn all or specified climate devices auxiliary heater on.""" data = {ATTR_AUX_HEAT: aux_heat} @@ -63,7 +64,7 @@ async def async_set_aux_heat(hass, aux_heat, entity_id=None): @bind_hass -def set_aux_heat(hass, aux_heat, entity_id=None): +def set_aux_heat(hass, aux_heat, entity_id=ENTITY_MATCH_ALL): """Turn all or specified climate devices auxiliary heater on.""" data = {ATTR_AUX_HEAT: aux_heat} @@ -76,7 +77,7 @@ def set_aux_heat(hass, aux_heat, entity_id=None): async def async_set_temperature( hass, temperature=None, - entity_id=None, + entity_id=ENTITY_MATCH_ALL, target_temp_high=None, target_temp_low=None, hvac_mode=None, @@ -103,7 +104,7 @@ async def async_set_temperature( def set_temperature( hass, temperature=None, - entity_id=None, + entity_id=ENTITY_MATCH_ALL, target_temp_high=None, target_temp_low=None, hvac_mode=None, @@ -124,7 +125,7 @@ def set_temperature( hass.services.call(DOMAIN, SERVICE_SET_TEMPERATURE, kwargs) -async def async_set_humidity(hass, humidity, entity_id=None): +async def async_set_humidity(hass, humidity, entity_id=ENTITY_MATCH_ALL): """Set new target humidity.""" data = {ATTR_HUMIDITY: humidity} @@ -135,7 +136,7 @@ async def async_set_humidity(hass, humidity, entity_id=None): @bind_hass -def set_humidity(hass, humidity, entity_id=None): +def set_humidity(hass, humidity, entity_id=ENTITY_MATCH_ALL): """Set new target humidity.""" data = {ATTR_HUMIDITY: humidity} @@ -145,7 +146,7 @@ def set_humidity(hass, humidity, entity_id=None): hass.services.call(DOMAIN, SERVICE_SET_HUMIDITY, data) -async def async_set_fan_mode(hass, fan, entity_id=None): +async def async_set_fan_mode(hass, fan, entity_id=ENTITY_MATCH_ALL): """Set all or specified climate devices fan mode on.""" data = {ATTR_FAN_MODE: fan} @@ -156,7 +157,7 @@ async def async_set_fan_mode(hass, fan, entity_id=None): @bind_hass -def set_fan_mode(hass, fan, entity_id=None): +def set_fan_mode(hass, fan, entity_id=ENTITY_MATCH_ALL): """Set all or specified climate devices fan mode on.""" data = {ATTR_FAN_MODE: fan} @@ -166,7 +167,7 @@ def set_fan_mode(hass, fan, entity_id=None): hass.services.call(DOMAIN, SERVICE_SET_FAN_MODE, data) -async def async_set_hvac_mode(hass, hvac_mode, entity_id=None): +async def async_set_hvac_mode(hass, hvac_mode, entity_id=ENTITY_MATCH_ALL): """Set new target operation mode.""" data = {ATTR_HVAC_MODE: hvac_mode} @@ -177,7 +178,7 @@ async def async_set_hvac_mode(hass, hvac_mode, entity_id=None): @bind_hass -def set_operation_mode(hass, hvac_mode, entity_id=None): +def set_operation_mode(hass, hvac_mode, entity_id=ENTITY_MATCH_ALL): """Set new target operation mode.""" data = {ATTR_HVAC_MODE: hvac_mode} @@ -187,7 +188,7 @@ def set_operation_mode(hass, hvac_mode, entity_id=None): hass.services.call(DOMAIN, SERVICE_SET_HVAC_MODE, data) -async def async_set_swing_mode(hass, swing_mode, entity_id=None): +async def async_set_swing_mode(hass, swing_mode, entity_id=ENTITY_MATCH_ALL): """Set new target swing mode.""" data = {ATTR_SWING_MODE: swing_mode} @@ -198,7 +199,7 @@ async def async_set_swing_mode(hass, swing_mode, entity_id=None): @bind_hass -def set_swing_mode(hass, swing_mode, entity_id=None): +def set_swing_mode(hass, swing_mode, entity_id=ENTITY_MATCH_ALL): """Set new target swing mode.""" data = {ATTR_SWING_MODE: swing_mode} @@ -208,7 +209,7 @@ def set_swing_mode(hass, swing_mode, entity_id=None): hass.services.call(DOMAIN, SERVICE_SET_SWING_MODE, data) -async def async_turn_on(hass, entity_id=None): +async def async_turn_on(hass, entity_id=ENTITY_MATCH_ALL): """Turn on device.""" data = {} @@ -218,7 +219,7 @@ async def async_turn_on(hass, entity_id=None): await hass.services.async_call(DOMAIN, SERVICE_TURN_ON, data, blocking=True) -async def async_turn_off(hass, entity_id=None): +async def async_turn_off(hass, entity_id=ENTITY_MATCH_ALL): """Turn off device.""" data = {} diff --git a/tests/components/climate/test_device_action.py b/tests/components/climate/test_device_action.py index 3eb1f38ec41d..46e8b3395c4c 100644 --- a/tests/components/climate/test_device_action.py +++ b/tests/components/climate/test_device_action.py @@ -39,6 +39,7 @@ async def test_get_actions(hass, device_reg, entity_reg): ) entity_reg.async_get_or_create(DOMAIN, "test", "5678", device_id=device_entry.id) hass.states.async_set("climate.test_5678", const.HVAC_MODE_COOL, {}) + hass.states.async_set("climate.test_5678", "attributes", {"supported_features": 17}) expected_actions = [ { "domain": DOMAIN, @@ -57,6 +58,29 @@ async def test_get_actions(hass, device_reg, entity_reg): assert_lists_same(actions, expected_actions) +async def test_get_action_hvac_only(hass, device_reg, entity_reg): + """Test we get the expected actions from a climate.""" + 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) + hass.states.async_set("climate.test_5678", const.HVAC_MODE_COOL, {}) + hass.states.async_set("climate.test_5678", "attributes", {"supported_features": 1}) + expected_actions = [ + { + "domain": DOMAIN, + "type": "set_hvac_mode", + "device_id": device_entry.id, + "entity_id": "climate.test_5678", + }, + ] + actions = await async_get_device_automations(hass, "action", device_entry.id) + assert_lists_same(actions, expected_actions) + + async def test_action(hass): """Test for actions.""" hass.states.async_set( diff --git a/tests/components/climate/test_device_condition.py b/tests/components/climate/test_device_condition.py index 82b6f595fb0b..b0a9c6c283a7 100644 --- a/tests/components/climate/test_device_condition.py +++ b/tests/components/climate/test_device_condition.py @@ -53,6 +53,7 @@ async def test_get_conditions(hass, device_reg, entity_reg): const.ATTR_PRESET_MODES: [const.PRESET_HOME, const.PRESET_AWAY], }, ) + hass.states.async_set("climate.test_5678", "attributes", {"supported_features": 17}) expected_conditions = [ { "condition": "device", @@ -73,6 +74,38 @@ async def test_get_conditions(hass, device_reg, entity_reg): assert_lists_same(conditions, expected_conditions) +async def test_get_conditions_hvac_only(hass, device_reg, entity_reg): + """Test we get the expected conditions from a climate.""" + 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) + hass.states.async_set( + f"{DOMAIN}.test_5678", + const.HVAC_MODE_COOL, + { + const.ATTR_HVAC_MODE: const.HVAC_MODE_COOL, + const.ATTR_PRESET_MODE: const.PRESET_AWAY, + const.ATTR_PRESET_MODES: [const.PRESET_HOME, const.PRESET_AWAY], + }, + ) + hass.states.async_set("climate.test_5678", "attributes", {"supported_features": 1}) + expected_conditions = [ + { + "condition": "device", + "domain": DOMAIN, + "type": "is_hvac_mode", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_5678", + } + ] + conditions = await async_get_device_automations(hass, "condition", device_entry.id) + assert_lists_same(conditions, expected_conditions) + + async def test_if_state(hass, calls): """Test for turn_on and turn_off conditions.""" hass.states.async_set( diff --git a/tests/components/climate/test_init.py b/tests/components/climate/test_init.py index c9d9f1f64250..b044753c8910 100644 --- a/tests/components/climate/test_init.py +++ b/tests/components/climate/test_init.py @@ -1,10 +1,16 @@ """The tests for the climate component.""" from unittest.mock import MagicMock +from typing import List import pytest import voluptuous as vol -from homeassistant.components.climate import SET_TEMPERATURE_SCHEMA, ClimateDevice +from homeassistant.components.climate import ( + SET_TEMPERATURE_SCHEMA, + ClimateDevice, + HVAC_MODE_HEAT, + HVAC_MODE_OFF, +) from tests.common import async_mock_service @@ -38,9 +44,29 @@ async def test_set_temp_schema(hass, caplog): assert calls[-1].data == data +class MockClimateDevice(ClimateDevice): + """Mock Climate device to use in tests.""" + + @property + def hvac_mode(self) -> str: + """Return hvac operation ie. heat, cool mode. + + Need to be one of HVAC_MODE_*. + """ + return HVAC_MODE_HEAT + + @property + def hvac_modes(self) -> List[str]: + """Return the list of available hvac operation modes. + + Need to be a subset of HVAC_MODES. + """ + return [HVAC_MODE_OFF, HVAC_MODE_HEAT] + + async def test_sync_turn_on(hass): - """Test if adding turn_on work.""" - climate = ClimateDevice() + """Test if async turn_on calls sync turn_on.""" + climate = MockClimateDevice() climate.hass = hass climate.turn_on = MagicMock() @@ -50,8 +76,8 @@ async def test_sync_turn_on(hass): async def test_sync_turn_off(hass): - """Test if adding turn_on work.""" - climate = ClimateDevice() + """Test if async turn_off calls sync turn_off.""" + climate = MockClimateDevice() climate.hass = hass climate.turn_off = MagicMock() diff --git a/tests/components/cloud/test_client.py b/tests/components/cloud/test_client.py index 054b38daffc2..955923c1e68e 100644 --- a/tests/components/cloud/test_client.py +++ b/tests/components/cloud/test_client.py @@ -7,6 +7,7 @@ from homeassistant.core import State from homeassistant.setup import async_setup_component from homeassistant.components.cloud import DOMAIN +from homeassistant.components.cloud.client import CloudClient from homeassistant.components.cloud.const import PREF_ENABLE_ALEXA, PREF_ENABLE_GOOGLE from tests.components.alexa import test_smart_home as test_alexa from tests.common import mock_coro @@ -101,16 +102,13 @@ async def test_handler_google_actions(hass): reqid = "5711642932632160983" data = {"requestId": reqid, "inputs": [{"intent": "action.devices.SYNC"}]} - with patch( - "hass_nabucasa.Cloud._decode_claims", - return_value={"cognito:username": "myUserName"}, - ): - resp = await cloud.client.async_google_message(data) + config = await cloud.client.get_google_config() + resp = await cloud.client.async_google_message(data) assert resp["requestId"] == reqid payload = resp["payload"] - assert payload["agentUserId"] == "myUserName" + assert payload["agentUserId"] == config.cloud_user devices = payload["devices"] assert len(devices) == 1 @@ -187,25 +185,42 @@ async def test_google_config_expose_entity(hass, mock_cloud_setup, mock_cloud_lo """Test Google config exposing entity method uses latest config.""" cloud_client = hass.data[DOMAIN].client state = State("light.kitchen", "on") + gconf = await cloud_client.get_google_config() - assert cloud_client.google_config.should_expose(state) + assert gconf.should_expose(state) await cloud_client.prefs.async_update_google_entity_config( entity_id="light.kitchen", should_expose=False ) - assert not cloud_client.google_config.should_expose(state) + assert not gconf.should_expose(state) async def test_google_config_should_2fa(hass, mock_cloud_setup, mock_cloud_login): """Test Google config disabling 2FA method uses latest config.""" cloud_client = hass.data[DOMAIN].client + gconf = await cloud_client.get_google_config() state = State("light.kitchen", "on") - assert cloud_client.google_config.should_2fa(state) + assert gconf.should_2fa(state) await cloud_client.prefs.async_update_google_entity_config( entity_id="light.kitchen", disable_2fa=True ) - assert not cloud_client.google_config.should_2fa(state) + assert not gconf.should_2fa(state) + + +async def test_set_username(hass): + """Test we set username during loggin.""" + prefs = MagicMock( + alexa_enabled=False, + google_enabled=False, + async_set_username=MagicMock(return_value=mock_coro()), + ) + client = CloudClient(hass, prefs, None, {}, {}) + client.cloud = MagicMock(is_logged_in=True, username="mock-username") + await client.logged_in() + + assert len(prefs.async_set_username.mock_calls) == 1 + assert prefs.async_set_username.mock_calls[0][1][0] == "mock-username" diff --git a/tests/components/cloud/test_google_config.py b/tests/components/cloud/test_google_config.py index 43914f489d6e..3510b4b8abd2 100644 --- a/tests/components/cloud/test_google_config.py +++ b/tests/components/cloud/test_google_config.py @@ -12,7 +12,15 @@ async def test_google_update_report_state(hass, cloud_prefs): """Test Google config responds to updating preference.""" - config = CloudGoogleConfig(hass, GACTIONS_SCHEMA({}), cloud_prefs, None) + config = CloudGoogleConfig( + hass, + GACTIONS_SCHEMA({}), + "mock-user-id", + cloud_prefs, + Mock(claims={"cognito:username": "abcdefghjkl"}), + ) + await config.async_initialize() + await config.async_connect_agent_user("mock-user-id") with patch.object( config, "async_sync_entities", side_effect=mock_coro @@ -32,6 +40,7 @@ async def test_sync_entities(aioclient_mock, hass, cloud_prefs): config = CloudGoogleConfig( hass, GACTIONS_SCHEMA({}), + "mock-user-id", cloud_prefs, Mock( google_actions_sync_url="http://example.com", @@ -39,12 +48,20 @@ async def test_sync_entities(aioclient_mock, hass, cloud_prefs): ), ) - assert await config.async_sync_entities() == 404 + assert await config.async_sync_entities("user") == 404 async def test_google_update_expose_trigger_sync(hass, cloud_prefs): """Test Google config responds to updating exposed entities.""" - config = CloudGoogleConfig(hass, GACTIONS_SCHEMA({}), cloud_prefs, None) + config = CloudGoogleConfig( + hass, + GACTIONS_SCHEMA({}), + "mock-user-id", + cloud_prefs, + Mock(claims={"cognito:username": "abcdefghjkl"}), + ) + await config.async_initialize() + await config.async_connect_agent_user("mock-user-id") with patch.object( config, "async_sync_entities", side_effect=mock_coro @@ -80,8 +97,10 @@ async def test_google_update_expose_trigger_sync(hass, cloud_prefs): async def test_google_entity_registry_sync(hass, mock_cloud_login, cloud_prefs): """Test Google config responds to entity registry.""" config = CloudGoogleConfig( - hass, GACTIONS_SCHEMA({}), cloud_prefs, hass.data["cloud"] + hass, GACTIONS_SCHEMA({}), "mock-user-id", cloud_prefs, hass.data["cloud"] ) + await config.async_initialize() + await config.async_connect_agent_user("mock-user-id") with patch.object( config, "async_sync_entities", side_effect=mock_coro diff --git a/tests/components/cloud/test_http_api.py b/tests/components/cloud/test_http_api.py index 8d05f1a14c37..440ad7a9c890 100644 --- a/tests/components/cloud/test_http_api.py +++ b/tests/components/cloud/test_http_api.py @@ -85,46 +85,36 @@ def mock_cognito(): yield mock_cog() -async def test_google_actions_sync(mock_cognito, cloud_client, aioclient_mock): +async def test_google_actions_sync( + mock_cognito, mock_cloud_login, cloud_client, aioclient_mock +): """Test syncing Google Actions.""" aioclient_mock.post(GOOGLE_ACTIONS_SYNC_URL) req = await cloud_client.post("/api/cloud/google_actions/sync") assert req.status == 200 -async def test_google_actions_sync_fails(mock_cognito, cloud_client, aioclient_mock): +async def test_google_actions_sync_fails( + mock_cognito, mock_cloud_login, cloud_client, aioclient_mock +): """Test syncing Google Actions gone bad.""" aioclient_mock.post(GOOGLE_ACTIONS_SYNC_URL, status=403) req = await cloud_client.post("/api/cloud/google_actions/sync") assert req.status == 403 -async def test_login_view(hass, cloud_client, mock_cognito): +async def test_login_view(hass, cloud_client): """Test logging in.""" - mock_cognito.id_token = jwt.encode( - {"email": "hello@home-assistant.io", "custom:sub-exp": "2018-01-03"}, "test" - ) - mock_cognito.access_token = "access_token" - mock_cognito.refresh_token = "refresh_token" + hass.data["cloud"] = MagicMock(login=MagicMock(return_value=mock_coro())) - with patch("hass_nabucasa.iot.CloudIoT.connect") as mock_connect, patch( - "hass_nabucasa.auth.CognitoAuth._authenticate", return_value=mock_cognito - ) as mock_auth: - req = await cloud_client.post( - "/api/cloud/login", json={"email": "my_username", "password": "my_password"} - ) + req = await cloud_client.post( + "/api/cloud/login", json={"email": "my_username", "password": "my_password"} + ) assert req.status == 200 result = await req.json() assert result == {"success": True} - assert len(mock_connect.mock_calls) == 1 - - assert len(mock_auth.mock_calls) == 1 - result_user, result_pass = mock_auth.mock_calls[0][1] - assert result_user == "my_username" - assert result_pass == "my_password" - async def test_login_view_random_exception(cloud_client): """Try logging in with invalid JSON.""" @@ -347,7 +337,6 @@ async def test_websocket_status( "cloud": "connected", "prefs": { "alexa_enabled": True, - "cloud_user": None, "cloudhooks": {}, "google_enabled": True, "google_entity_configs": {}, @@ -801,7 +790,7 @@ async def test_list_alexa_entities(hass, hass_ws_client, setup_api, mock_cloud_l assert response["result"][0] == { "entity_id": "light.kitchen", "display_categories": ["LIGHT"], - "interfaces": ["Alexa.PowerController", "Alexa.EndpointHealth"], + "interfaces": ["Alexa.PowerController", "Alexa.EndpointHealth", "Alexa"], } diff --git a/tests/components/cloud/test_init.py b/tests/components/cloud/test_init.py index e160ea8826ab..d039cdd1b0b8 100644 --- a/tests/components/cloud/test_init.py +++ b/tests/components/cloud/test_init.py @@ -5,7 +5,6 @@ from homeassistant.core import Context from homeassistant.exceptions import Unauthorized -from homeassistant.auth.const import GROUP_ID_ADMIN from homeassistant.components import cloud from homeassistant.components.cloud.const import DOMAIN from homeassistant.components.cloud.prefs import STORAGE_KEY @@ -142,68 +141,11 @@ async def test_setup_existing_cloud_user(hass, hass_storage): assert hass_storage[STORAGE_KEY]["data"]["cloud_user"] == user.id -async def test_setup_invalid_cloud_user(hass, hass_storage): - """Test setup with API push default data.""" - hass_storage[STORAGE_KEY] = {"version": 1, "data": {"cloud_user": "non-existing"}} - with patch("hass_nabucasa.Cloud.start", return_value=mock_coro()): - result = await async_setup_component( - hass, - "cloud", - { - "http": {}, - "cloud": { - cloud.CONF_MODE: cloud.MODE_DEV, - "cognito_client_id": "test-cognito_client_id", - "user_pool_id": "test-user_pool_id", - "region": "test-region", - "relayer": "test-relayer", - }, - }, - ) - assert result - - assert hass_storage[STORAGE_KEY]["data"]["cloud_user"] != "non-existing" - cloud_user = await hass.auth.async_get_user( - hass_storage[STORAGE_KEY]["data"]["cloud_user"] - ) - - assert cloud_user - assert cloud_user.groups[0].id == GROUP_ID_ADMIN - - -async def test_setup_setup_cloud_user(hass, hass_storage): - """Test setup with API push default data.""" - hass_storage[STORAGE_KEY] = {"version": 1, "data": {"cloud_user": None}} - with patch("hass_nabucasa.Cloud.start", return_value=mock_coro()): - result = await async_setup_component( - hass, - "cloud", - { - "http": {}, - "cloud": { - cloud.CONF_MODE: cloud.MODE_DEV, - "cognito_client_id": "test-cognito_client_id", - "user_pool_id": "test-user_pool_id", - "region": "test-region", - "relayer": "test-relayer", - }, - }, - ) - assert result - - cloud_user = await hass.auth.async_get_user( - hass_storage[STORAGE_KEY]["data"]["cloud_user"] - ) - - assert cloud_user - assert cloud_user.groups[0].id == GROUP_ID_ADMIN - - async def test_on_connect(hass, mock_cloud_fixture): """Test cloud on connect triggers.""" cl = hass.data["cloud"] - assert len(cl.iot._on_connect) == 4 + assert len(cl.iot._on_connect) == 3 assert len(hass.states.async_entity_ids("binary_sensor")) == 0 diff --git a/tests/components/cloud/test_prefs.py b/tests/components/cloud/test_prefs.py new file mode 100644 index 000000000000..1678757e52cf --- /dev/null +++ b/tests/components/cloud/test_prefs.py @@ -0,0 +1,80 @@ +"""Test Cloud preferences.""" +from unittest.mock import patch + +from homeassistant.auth.const import GROUP_ID_ADMIN +from homeassistant.components.cloud.prefs import CloudPreferences, STORAGE_KEY + + +async def test_set_username(hass): + """Test we clear config if we set different username.""" + prefs = CloudPreferences(hass) + await prefs.async_initialize() + + assert prefs.google_enabled + + await prefs.async_update(google_enabled=False) + + assert not prefs.google_enabled + + await prefs.async_set_username("new-username") + + assert prefs.google_enabled + + +async def test_set_username_migration(hass): + """Test we not clear config if we had no username.""" + prefs = CloudPreferences(hass) + + with patch.object(prefs, "_empty_config", return_value=prefs._empty_config(None)): + await prefs.async_initialize() + + assert prefs.google_enabled + + await prefs.async_update(google_enabled=False) + + assert not prefs.google_enabled + + await prefs.async_set_username("new-username") + + assert not prefs.google_enabled + + +async def test_load_invalid_cloud_user(hass, hass_storage): + """Test loading cloud user with invalid storage.""" + hass_storage[STORAGE_KEY] = {"version": 1, "data": {"cloud_user": "non-existing"}} + + prefs = CloudPreferences(hass) + await prefs.async_initialize() + + cloud_user_id = await prefs.get_cloud_user() + + assert cloud_user_id != "non-existing" + + cloud_user = await hass.auth.async_get_user( + hass_storage[STORAGE_KEY]["data"]["cloud_user"] + ) + + assert cloud_user + assert cloud_user.groups[0].id == GROUP_ID_ADMIN + + +async def test_setup_remove_cloud_user(hass, hass_storage): + """Test creating and removing cloud user.""" + hass_storage[STORAGE_KEY] = {"version": 1, "data": {"cloud_user": None}} + + prefs = CloudPreferences(hass) + await prefs.async_initialize() + await prefs.async_set_username("user1") + + cloud_user = await hass.auth.async_get_user(await prefs.get_cloud_user()) + + assert cloud_user + assert cloud_user.groups[0].id == GROUP_ID_ADMIN + + await prefs.async_set_username("user2") + + cloud_user2 = await hass.auth.async_get_user(await prefs.get_cloud_user()) + + assert cloud_user2 + assert cloud_user2.groups[0].id == GROUP_ID_ADMIN + assert cloud_user2.id != cloud_user.id diff --git a/tests/components/conversation/test_init.py b/tests/components/conversation/test_init.py index ff44eaccc8ea..45008ef94476 100644 --- a/tests/components/conversation/test_init.py +++ b/tests/components/conversation/test_init.py @@ -1,11 +1,9 @@ """The tests for the Conversation component.""" -# pylint: disable=protected-access import pytest -from homeassistant.core import DOMAIN as HASS_DOMAIN +from homeassistant.core import DOMAIN as HASS_DOMAIN, Context from homeassistant.setup import async_setup_component from homeassistant.components import conversation -from homeassistant.components.cover import SERVICE_OPEN_COVER from homeassistant.helpers import intent from tests.common import async_mock_intent, async_mock_service @@ -25,10 +23,13 @@ async def test_calling_intent(hass): ) assert result + context = Context() + await hass.services.async_call( "conversation", "process", {conversation.ATTR_TEXT: "I would like the Grolsch beer"}, + context=context, ) await hass.async_block_till_done() @@ -38,6 +39,7 @@ async def test_calling_intent(hass): assert intent.intent_type == "OrderBeer" assert intent.slots == {"type": {"value": "Grolsch"}} assert intent.text_input == "I would like the Grolsch beer" + assert intent.context is context async def test_register_before_setup(hass): @@ -80,7 +82,7 @@ async def test_register_before_setup(hass): assert intent.text_input == "I would like the Grolsch beer" -async def test_http_processing_intent(hass, hass_client): +async def test_http_processing_intent(hass, hass_client, hass_admin_user): """Test processing intent via HTTP API.""" class TestIntentHandler(intent.IntentHandler): @@ -90,6 +92,7 @@ class TestIntentHandler(intent.IntentHandler): async def async_handle(self, intent): """Handle the intent.""" + assert intent.context.user_id == hass_admin_user.id response = intent.create_response() response.async_set_speech( "I've ordered a {}!".format(intent.slots["type"]["value"]) @@ -148,32 +151,6 @@ async def test_turn_on_intent(hass, sentence): assert call.data == {"entity_id": "light.kitchen"} -async def test_cover_intents_loading(hass): - """Test Cover Intents Loading.""" - with pytest.raises(intent.UnknownIntent): - await intent.async_handle( - hass, "test", "HassOpenCover", {"name": {"value": "garage door"}} - ) - - result = await async_setup_component(hass, "cover", {}) - assert result - - hass.states.async_set("cover.garage_door", "closed") - calls = async_mock_service(hass, "cover", SERVICE_OPEN_COVER) - - response = await intent.async_handle( - hass, "test", "HassOpenCover", {"name": {"value": "garage door"}} - ) - await hass.async_block_till_done() - - assert response.speech["plain"]["speech"] == "Opened garage door" - assert len(calls) == 1 - call = calls[0] - assert call.domain == "cover" - assert call.service == "open_cover" - assert call.data == {"entity_id": "cover.garage_door"} - - @pytest.mark.parametrize("sentence", ("turn off kitchen", "turn kitchen off")) async def test_turn_off_intent(hass, sentence): """Test calling the turn on intent.""" @@ -263,7 +240,7 @@ async def test_http_api_wrong_data(hass, hass_client): assert resp.status == 400 -async def test_custom_agent(hass, hass_client): +async def test_custom_agent(hass, hass_client, hass_admin_user): """Test a custom conversation agent.""" calls = [] @@ -271,9 +248,9 @@ async def test_custom_agent(hass, hass_client): class MyAgent(conversation.AbstractConversationAgent): """Test Agent.""" - async def async_process(self, text, conversation_id): + async def async_process(self, text, context, conversation_id): """Process some text.""" - calls.append((text, conversation_id)) + calls.append((text, context, conversation_id)) response = intent.IntentResponse() response.async_set_speech("Test response") return response @@ -296,4 +273,5 @@ async def async_process(self, text, conversation_id): assert len(calls) == 1 assert calls[0][0] == "Test Text" - assert calls[0][1] == "test-conv-id" + assert calls[0][1].user_id == hass_admin_user.id + assert calls[0][2] == "test-conv-id" diff --git a/tests/components/cover/test_init.py b/tests/components/cover/test_intent.py similarity index 83% rename from tests/components/cover/test_init.py rename to tests/components/cover/test_intent.py index d1ca17d18f35..ce01e882941b 100644 --- a/tests/components/cover/test_init.py +++ b/tests/components/cover/test_intent.py @@ -1,15 +1,17 @@ """The tests for the cover platform.""" -from homeassistant.components.cover import SERVICE_OPEN_COVER, SERVICE_CLOSE_COVER +from homeassistant.components.cover import ( + SERVICE_OPEN_COVER, + SERVICE_CLOSE_COVER, + intent as cover_intent, +) from homeassistant.helpers import intent -import homeassistant.components as comps from tests.common import async_mock_service async def test_open_cover_intent(hass): """Test HassOpenCover intent.""" - result = await comps.cover.async_setup(hass, {}) - assert result + await cover_intent.async_setup_intents(hass) hass.states.async_set("cover.garage_door", "closed") calls = async_mock_service(hass, "cover", SERVICE_OPEN_COVER) @@ -29,8 +31,7 @@ async def test_open_cover_intent(hass): async def test_close_cover_intent(hass): """Test HassCloseCover intent.""" - result = await comps.cover.async_setup(hass, {}) - assert result + await cover_intent.async_setup_intents(hass) hass.states.async_set("cover.garage_door", "open") calls = async_mock_service(hass, "cover", SERVICE_CLOSE_COVER) diff --git a/tests/components/daikin/test_config_flow.py b/tests/components/daikin/test_config_flow.py index f9fba67d554c..aea78f17564a 100644 --- a/tests/components/daikin/test_config_flow.py +++ b/tests/components/daikin/test_config_flow.py @@ -1,6 +1,7 @@ -# pylint: disable=W0621 +# pylint: disable=redefined-outer-name """Tests for the Daikin config flow.""" import asyncio +from unittest.mock import patch import pytest @@ -9,7 +10,7 @@ from homeassistant.components.daikin.const import KEY_IP, KEY_MAC from homeassistant.const import CONF_HOST -from tests.common import MockConfigEntry, MockDependency +from tests.common import MockConfigEntry MAC = "AABBCCDDEEFF" HOST = "127.0.0.1" @@ -30,10 +31,10 @@ async def mock_daikin_init(): """Mock the init function in pydaikin.""" pass - with MockDependency("pydaikin.appliance") as mock_daikin_: - mock_daikin_.Appliance().values.get.return_value = "AABBCCDDEEFF" - mock_daikin_.Appliance().init = mock_daikin_init - yield mock_daikin_ + with patch("homeassistant.components.daikin.config_flow.Appliance") as Appliance: + Appliance().values.get.return_value = "AABBCCDDEEFF" + Appliance().init = mock_daikin_init + yield Appliance async def test_user(hass, mock_daikin): @@ -94,7 +95,7 @@ async def test_discovery(hass, mock_daikin): async def test_device_abort(hass, mock_daikin, s_effect, reason): """Test device abort.""" flow = init_config_flow(hass) - mock_daikin.Appliance.side_effect = s_effect + mock_daikin.side_effect = s_effect result = await flow.async_step_user({CONF_HOST: HOST}) assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT diff --git a/tests/components/datadog/test_init.py b/tests/components/datadog/test_init.py index 56d70b18d912..30541c8137a8 100644 --- a/tests/components/datadog/test_init.py +++ b/tests/components/datadog/test_init.py @@ -12,7 +12,7 @@ import homeassistant.components.datadog as datadog import homeassistant.core as ha -from tests.common import assert_setup_component, get_test_home_assistant, MockDependency +from tests.common import assert_setup_component, get_test_home_assistant class TestDatadog(unittest.TestCase): @@ -33,11 +33,11 @@ def test_invalid_config(self): self.hass, datadog.DOMAIN, {datadog.DOMAIN: {"host1": "host1"}} ) - @MockDependency("datadog", "beer") - def test_datadog_setup_full(self, mock_datadog): + @mock.patch("homeassistant.components.datadog.statsd") + @mock.patch("homeassistant.components.datadog.initialize") + def test_datadog_setup_full(self, mock_connection, mock_client): """Test setup with all data.""" self.hass.bus.listen = mock.MagicMock() - mock_connection = mock_datadog.initialize assert setup_component( self.hass, @@ -54,11 +54,11 @@ def test_datadog_setup_full(self, mock_datadog): assert EVENT_LOGBOOK_ENTRY == self.hass.bus.listen.call_args_list[0][0][0] assert EVENT_STATE_CHANGED == self.hass.bus.listen.call_args_list[1][0][0] - @MockDependency("datadog") - def test_datadog_setup_defaults(self, mock_datadog): + @mock.patch("homeassistant.components.datadog.statsd") + @mock.patch("homeassistant.components.datadog.initialize") + def test_datadog_setup_defaults(self, mock_connection, mock_client): """Test setup with defaults.""" self.hass.bus.listen = mock.MagicMock() - mock_connection = mock_datadog.initialize assert setup_component( self.hass, @@ -78,11 +78,11 @@ def test_datadog_setup_defaults(self, mock_datadog): ) assert self.hass.bus.listen.called - @MockDependency("datadog") - def test_logbook_entry(self, mock_datadog): + @mock.patch("homeassistant.components.datadog.statsd") + @mock.patch("homeassistant.components.datadog.initialize") + def test_logbook_entry(self, mock_connection, mock_client): """Test event listener.""" self.hass.bus.listen = mock.MagicMock() - mock_client = mock_datadog.statsd assert setup_component( self.hass, @@ -110,11 +110,11 @@ def test_logbook_entry(self, mock_datadog): mock_client.event.reset_mock() - @MockDependency("datadog") - def test_state_changed(self, mock_datadog): + @mock.patch("homeassistant.components.datadog.statsd") + @mock.patch("homeassistant.components.datadog.initialize") + def test_state_changed(self, mock_connection, mock_client): """Test event listener.""" self.hass.bus.listen = mock.MagicMock() - mock_client = mock_datadog.statsd assert setup_component( self.hass, diff --git a/tests/components/demo/test_light.py b/tests/components/demo/test_light.py index 10b407688af4..34b340b600ac 100644 --- a/tests/components/demo/test_light.py +++ b/tests/components/demo/test_light.py @@ -62,10 +62,14 @@ async def test_turn_off(hass): async def test_turn_off_without_entity_id(hass): """Test light turn off all lights.""" - await hass.services.async_call("light", "turn_on", {}, blocking=True) + await hass.services.async_call( + "light", "turn_on", {"entity_id": "all"}, blocking=True + ) assert light.is_on(hass, ENTITY_LIGHT) - await hass.services.async_call("light", "turn_off", {}, blocking=True) + await hass.services.async_call( + "light", "turn_off", {"entity_id": "all"}, blocking=True + ) assert not light.is_on(hass, ENTITY_LIGHT) diff --git a/tests/components/device_automation/test_init.py b/tests/components/device_automation/test_init.py index 3c0e3b1eca7a..bddef3286ac8 100644 --- a/tests/components/device_automation/test_init.py +++ b/tests/components/device_automation/test_init.py @@ -184,6 +184,9 @@ async def test_websocket_get_action_capabilities( entity_reg.async_get_or_create( "alarm_control_panel", "test", "5678", device_id=device_entry.id ) + hass.states.async_set( + "alarm_control_panel.test_5678", "attributes", {"supported_features": 15} + ) expected_capabilities = { "arm_away": {"extra_fields": []}, "arm_home": {"extra_fields": []}, diff --git a/tests/components/directv/test_media_player.py b/tests/components/directv/test_media_player.py index 85916cf6159f..7f802e1a94b7 100644 --- a/tests/components/directv/test_media_player.py +++ b/tests/components/directv/test_media_player.py @@ -58,7 +58,7 @@ from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util -from tests.common import MockDependency, async_fire_time_changed +from tests.common import async_fire_time_changed CLIENT_ENTITY_ID = "media_player.client_dvr" MAIN_ENTITY_ID = "media_player.main_dvr" @@ -179,8 +179,9 @@ def platforms(hass, dtv_side_effect, mock_now): ] } - with MockDependency("DirectPy"), patch( - "DirectPy.DIRECTV", side_effect=dtv_side_effect + with patch( + "homeassistant.components.directv.media_player.DIRECTV", + side_effect=dtv_side_effect, ), patch("homeassistant.util.dt.utcnow", return_value=mock_now): hass.loop.run_until_complete(async_setup_component(hass, DOMAIN, config)) hass.loop.run_until_complete(hass.async_block_till_done()) @@ -309,7 +310,9 @@ def tune_channel(self, source): async def test_setup_platform_config(hass): """Test setting up the platform from configuration.""" - with MockDependency("DirectPy"), patch("DirectPy.DIRECTV", new=MockDirectvClass): + with patch( + "homeassistant.components.directv.media_player.DIRECTV", new=MockDirectvClass + ): await async_setup_component(hass, DOMAIN, WORKING_CONFIG) await hass.async_block_till_done() @@ -321,7 +324,9 @@ async def test_setup_platform_config(hass): async def test_setup_platform_discover(hass): """Test setting up the platform from discovery.""" - with MockDependency("DirectPy"), patch("DirectPy.DIRECTV", new=MockDirectvClass): + with patch( + "homeassistant.components.directv.media_player.DIRECTV", new=MockDirectvClass + ): hass.async_create_task( async_load_platform( @@ -337,7 +342,9 @@ async def test_setup_platform_discover(hass): async def test_setup_platform_discover_duplicate(hass): """Test setting up the platform from discovery.""" - with MockDependency("DirectPy"), patch("DirectPy.DIRECTV", new=MockDirectvClass): + with patch( + "homeassistant.components.directv.media_player.DIRECTV", new=MockDirectvClass + ): await async_setup_component(hass, DOMAIN, WORKING_CONFIG) await hass.async_block_till_done() @@ -358,7 +365,9 @@ async def test_setup_platform_discover_client(hass): LOCATIONS.append({"locationName": "Client 1", "clientAddr": "1"}) LOCATIONS.append({"locationName": "Client 2", "clientAddr": "2"}) - with MockDependency("DirectPy"), patch("DirectPy.DIRECTV", new=MockDirectvClass): + with patch( + "homeassistant.components.directv.media_player.DIRECTV", new=MockDirectvClass + ): await async_setup_component(hass, DOMAIN, WORKING_CONFIG) await hass.async_block_till_done() diff --git a/tests/components/ee_brightbox/test_device_tracker.py b/tests/components/ee_brightbox/test_device_tracker.py index 6f732896399f..f862539f1dfb 100644 --- a/tests/components/ee_brightbox/test_device_tracker.py +++ b/tests/components/ee_brightbox/test_device_tracker.py @@ -2,6 +2,7 @@ from datetime import datetime from asynctest import patch +from eebrightbox import EEBrightBoxException import pytest from homeassistant.components.device_tracker import DOMAIN @@ -41,8 +42,6 @@ def _configure_mock_get_devices(eebrightbox_mock): def _configure_mock_failed_config_check(eebrightbox_mock): - from eebrightbox import EEBrightBoxException - eebrightbox_instance = eebrightbox_mock.return_value eebrightbox_instance.__enter__.side_effect = EEBrightBoxException( "Failed to connect to the router" @@ -55,7 +54,7 @@ def mock_dev_track(mock_device_tracker_conf): pass -@patch("eebrightbox.EEBrightBox") +@patch("homeassistant.components.ee_brightbox.device_tracker.EEBrightBox") async def test_missing_credentials(eebrightbox_mock, hass): """Test missing credentials.""" _configure_mock_get_devices(eebrightbox_mock) @@ -73,7 +72,7 @@ async def test_missing_credentials(eebrightbox_mock, hass): assert hass.states.get("device_tracker.hostnameff") is None -@patch("eebrightbox.EEBrightBox") +@patch("homeassistant.components.ee_brightbox.device_tracker.EEBrightBox") async def test_invalid_credentials(eebrightbox_mock, hass): """Test invalid credentials.""" _configure_mock_failed_config_check(eebrightbox_mock) @@ -93,7 +92,7 @@ async def test_invalid_credentials(eebrightbox_mock, hass): assert hass.states.get("device_tracker.hostnameff") is None -@patch("eebrightbox.EEBrightBox") +@patch("homeassistant.components.ee_brightbox.device_tracker.EEBrightBox") async def test_get_devices(eebrightbox_mock, hass): """Test valid configuration.""" _configure_mock_get_devices(eebrightbox_mock) diff --git a/tests/components/emulated_hue/test_hue_api.py b/tests/components/emulated_hue/test_hue_api.py index 02f24f5afba7..4f0d70d00469 100644 --- a/tests/components/emulated_hue/test_hue_api.py +++ b/tests/components/emulated_hue/test_hue_api.py @@ -205,20 +205,20 @@ def test_discover_lights(hue_client): devices = set(val["uniqueid"] for val in result_json.values()) # Make sure the lights we added to the config are there - assert "light.ceiling_lights" in devices - assert "light.bed_light" not in devices - assert "script.set_kitchen_light" in devices - assert "light.kitchen_lights" not in devices - assert "media_player.living_room" in devices - assert "media_player.bedroom" in devices - assert "media_player.walkman" in devices - assert "media_player.lounge_room" in devices - assert "fan.living_room_fan" in devices - assert "fan.ceiling_fan" not in devices - assert "cover.living_room_window" in devices - assert "climate.hvac" in devices - assert "climate.heatpump" in devices - assert "climate.ecobee" not in devices + assert "00:2f:d2:31:ce:c5:55:cc-ee" in devices # light.ceiling_lights + assert "00:b6:14:77:34:b7:bb:06-e8" not in devices # light.bed_light + assert "00:95:b7:51:16:58:6c:c0-c5" in devices # script.set_kitchen_light + assert "00:64:7b:e4:96:c3:fe:90-c3" not in devices # light.kitchen_lights + assert "00:7e:8a:42:35:66:db:86-c5" in devices # media_player.living_room + assert "00:05:44:c2:d6:0a:e5:17-b7" in devices # media_player.bedroom + assert "00:f3:5f:fa:31:f3:32:21-a8" in devices # media_player.walkman + assert "00:b4:06:2e:91:95:23:97-fb" in devices # media_player.lounge_room + assert "00:b2:bd:f9:2c:ad:22:ae-58" in devices # fan.living_room_fan + assert "00:77:4c:8a:23:7d:27:4b-7f" not in devices # fan.ceiling_fan + assert "00:02:53:b9:d5:1a:b3:67-b2" in devices # cover.living_room_window + assert "00:42:03:fe:97:58:2d:b1-50" in devices # climate.hvac + assert "00:7b:2a:c7:08:d6:66:bf-80" in devices # climate.heatpump + assert "00:57:77:a1:6a:8e:ef:b3-6c" not in devices # climate.ecobee @asyncio.coroutine @@ -232,6 +232,26 @@ def test_light_without_brightness_supported(hass_hue, hue_client): assert light_without_brightness_json["type"] == "On/off light" +@asyncio.coroutine +@pytest.mark.parametrize( + "state,is_reachable", + [ + (const.STATE_UNAVAILABLE, False), + (const.STATE_OK, True), + (const.STATE_UNKNOWN, True), + ], +) +def test_reachable_for_state(hass_hue, hue_client, state, is_reachable): + """Test that an entity is reported as unreachable if in unavailable state.""" + entity_id = "light.ceiling_lights" + + hass_hue.states.async_set(entity_id, state) + + state_json = yield from perform_get_light_state(hue_client, entity_id, 200) + + assert state_json["state"]["reachable"] == is_reachable, state_json + + @asyncio.coroutine def test_get_light_state(hass_hue, hue_client): """Test the getting of light state.""" @@ -280,15 +300,15 @@ def test_get_light_state(hass_hue, hue_client): ) assert office_json["state"][HUE_API_STATE_ON] is False - assert office_json["state"][HUE_API_STATE_BRI] == 0 + # Removed assert HUE_API_STATE_BRI == 0 as Hue API states bri must be 1..254 assert office_json["state"][HUE_API_STATE_HUE] == 0 assert office_json["state"][HUE_API_STATE_SAT] == 0 # Make sure bedroom light isn't accessible - yield from perform_get_light_state(hue_client, "light.bed_light", 404) + yield from perform_get_light_state(hue_client, "light.bed_light", 401) # Make sure kitchen light isn't accessible - yield from perform_get_light_state(hue_client, "light.kitchen_lights", 404) + yield from perform_get_light_state(hue_client, "light.kitchen_lights", 401) @asyncio.coroutine @@ -345,7 +365,7 @@ def test_put_light_state(hass_hue, hue_client): ceiling_json = yield from perform_get_light_state( hue_client, "light.ceiling_lights", 200 ) - assert ceiling_json["state"][HUE_API_STATE_BRI] == 0 + # Removed assert HUE_API_STATE_BRI == 0 as Hue API states bri must be 1..254 assert ceiling_json["state"][HUE_API_STATE_HUE] == 0 assert ceiling_json["state"][HUE_API_STATE_SAT] == 0 @@ -353,7 +373,7 @@ def test_put_light_state(hass_hue, hue_client): bedroom_result = yield from perform_put_light_state( hass_hue, hue_client, "light.bed_light", True ) - assert bedroom_result.status == 404 + assert bedroom_result.status == 401 # Make sure we can't change the kitchen light state kitchen_result = yield from perform_put_light_state( @@ -414,7 +434,7 @@ def test_put_light_state_climate_set_temperature(hass_hue, hue_client): ecobee_result = yield from perform_put_light_state( hass_hue, hue_client, "climate.ecobee", True ) - assert ecobee_result.status == 404 + assert ecobee_result.status == 401 @asyncio.coroutine @@ -749,4 +769,4 @@ async def test_external_ip_blocked(hue_client): ): result = await hue_client.get("/api/username/lights") - assert result.status == 400 + assert result.status == 401 diff --git a/tests/components/emulated_hue/test_upnp.py b/tests/components/emulated_hue/test_upnp.py index 44f72ba017b9..ead78ad56ca7 100644 --- a/tests/components/emulated_hue/test_upnp.py +++ b/tests/components/emulated_hue/test_upnp.py @@ -52,7 +52,7 @@ def tearDownClass(cls): def test_description_xml(self): """Test the description.""" - import xml.etree.ElementTree as ET + import defusedxml.ElementTree as ET result = requests.get(BRIDGE_URL_BASE.format("/description.xml"), timeout=5) diff --git a/tests/components/emulated_roku/test_binding.py b/tests/components/emulated_roku/test_binding.py index 19b014a57823..712c35f5a104 100644 --- a/tests/components/emulated_roku/test_binding.py +++ b/tests/components/emulated_roku/test_binding.py @@ -44,7 +44,9 @@ def instantiate( def listener(event): events.append(event) - with patch("emulated_roku.EmulatedRokuServer", instantiate): + with patch( + "homeassistant.components.emulated_roku.binding.EmulatedRokuServer", instantiate + ): hass.bus.async_listen(EVENT_ROKU_COMMAND, listener) assert await binding.setup() is True diff --git a/tests/components/emulated_roku/test_init.py b/tests/components/emulated_roku/test_init.py index f83bd2330c4b..92524f24d97a 100644 --- a/tests/components/emulated_roku/test_init.py +++ b/tests/components/emulated_roku/test_init.py @@ -10,7 +10,7 @@ async def test_config_required_fields(hass): """Test that configuration is successful with required fields.""" with patch.object(emulated_roku, "configured_servers", return_value=[]), patch( - "emulated_roku.EmulatedRokuServer", + "homeassistant.components.emulated_roku.binding.EmulatedRokuServer", return_value=Mock(start=mock_coro_func(), close=mock_coro_func()), ): assert ( @@ -35,7 +35,7 @@ async def test_config_required_fields(hass): async def test_config_already_registered_not_configured(hass): """Test that an already registered name causes the entry to be ignored.""" with patch( - "emulated_roku.EmulatedRokuServer", + "homeassistant.components.emulated_roku.binding.EmulatedRokuServer", return_value=Mock(start=mock_coro_func(), close=mock_coro_func()), ) as instantiate, patch.object( emulated_roku, "configured_servers", return_value=["Emulated Roku Test"] @@ -74,7 +74,7 @@ async def test_setup_entry_successful(hass): } with patch( - "emulated_roku.EmulatedRokuServer", + "homeassistant.components.emulated_roku.binding.EmulatedRokuServer", return_value=Mock(start=mock_coro_func(), close=mock_coro_func()), ) as instantiate: assert await emulated_roku.async_setup_entry(hass, entry) is True @@ -98,7 +98,7 @@ async def test_unload_entry(hass): entry.data = {"name": "Emulated Roku Test", "listen_port": 8060} with patch( - "emulated_roku.EmulatedRokuServer", + "homeassistant.components.emulated_roku.binding.EmulatedRokuServer", return_value=Mock(start=mock_coro_func(), close=mock_coro_func()), ): assert await emulated_roku.async_setup_entry(hass, entry) is True diff --git a/tests/components/esphome/test_config_flow.py b/tests/components/esphome/test_config_flow.py index 8c05b274dd05..4b951f9a3695 100644 --- a/tests/components/esphome/test_config_flow.py +++ b/tests/components/esphome/test_config_flow.py @@ -4,23 +4,17 @@ import pytest -from homeassistant.components.esphome import config_flow, DATA_KEY -from tests.common import mock_coro, MockConfigEntry - -MockDeviceInfo = namedtuple("DeviceInfo", ["uses_password", "name"]) +from homeassistant.components.esphome import DATA_KEY, config_flow +from tests.common import MockConfigEntry, mock_coro -@pytest.fixture(autouse=True) -def aioesphomeapi_mock(): - """Mock aioesphomeapi.""" - with patch.dict("sys.modules", {"aioesphomeapi": MagicMock()}): - yield +MockDeviceInfo = namedtuple("DeviceInfo", ["uses_password", "name"]) @pytest.fixture def mock_client(): """Mock APIClient.""" - with patch("aioesphomeapi.APIClient") as mock_client: + with patch("homeassistant.components.esphome.config_flow.APIClient") as mock_client: def mock_constructor(loop, host, port, password): """Fake the client constructor.""" @@ -40,7 +34,8 @@ def mock_constructor(loop, host, port, password): def mock_api_connection_error(): """Mock out the try login method.""" with patch( - "aioesphomeapi.APIConnectionError", new_callable=lambda: OSError + "homeassistant.components.esphome.config_flow.APIConnectionError", + new_callable=lambda: OSError, ) as mock_error: yield mock_error @@ -86,7 +81,8 @@ def __init__(self): super().__init__("Error resolving IP address") with patch( - "aioesphomeapi.APIConnectionError", new_callable=lambda: MockResolveError + "homeassistant.components.esphome.config_flow.APIConnectionError", + new_callable=lambda: MockResolveError, ) as exc: mock_client.device_info.side_effect = exc result = await flow.async_step_user( diff --git a/tests/components/facebox/test_image_processing.py b/tests/components/facebox/test_image_processing.py index edad90e41c71..9b4610e84b0d 100644 --- a/tests/components/facebox/test_image_processing.py +++ b/tests/components/facebox/test_image_processing.py @@ -261,7 +261,7 @@ async def test_teach_service( fb.FILE_PATH: MOCK_FILE_PATH, } await hass.services.async_call( - ip.DOMAIN, fb.SERVICE_TEACH_FACE, service_data=data + fb.DOMAIN, fb.SERVICE_TEACH_FACE, service_data=data ) await hass.async_block_till_done() @@ -275,7 +275,7 @@ async def test_teach_service( fb.FILE_PATH: MOCK_FILE_PATH, } await hass.services.async_call( - ip.DOMAIN, fb.SERVICE_TEACH_FACE, service_data=data + fb.DOMAIN, fb.SERVICE_TEACH_FACE, service_data=data ) await hass.async_block_till_done() assert "AuthenticationError on facebox" in caplog.text @@ -290,7 +290,7 @@ async def test_teach_service( fb.FILE_PATH: MOCK_FILE_PATH, } await hass.services.async_call( - ip.DOMAIN, fb.SERVICE_TEACH_FACE, service_data=data + fb.DOMAIN, fb.SERVICE_TEACH_FACE, service_data=data ) await hass.async_block_till_done() assert MOCK_ERROR_NO_FACE in caplog.text @@ -305,7 +305,7 @@ async def test_teach_service( fb.FILE_PATH: MOCK_FILE_PATH, } await hass.services.async_call( - ip.DOMAIN, fb.SERVICE_TEACH_FACE, service_data=data + fb.DOMAIN, fb.SERVICE_TEACH_FACE, service_data=data ) await hass.async_block_till_done() assert "ConnectionError: Is facebox running?" in caplog.text diff --git a/tests/components/fan/common.py b/tests/components/fan/common.py index 24a6868a372f..de645dac42e6 100644 --- a/tests/components/fan/common.py +++ b/tests/components/fan/common.py @@ -12,10 +12,15 @@ SERVICE_SET_DIRECTION, SERVICE_SET_SPEED, ) -from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_ON, SERVICE_TURN_OFF +from homeassistant.const import ( + ATTR_ENTITY_ID, + SERVICE_TURN_ON, + SERVICE_TURN_OFF, + ENTITY_MATCH_ALL, +) -async def async_turn_on(hass, entity_id: str = None, speed: str = None) -> None: +async def async_turn_on(hass, entity_id=ENTITY_MATCH_ALL, speed: str = None) -> None: """Turn all or specified fan on.""" data = { key: value @@ -26,7 +31,7 @@ async def async_turn_on(hass, entity_id: str = None, speed: str = None) -> None: await hass.services.async_call(DOMAIN, SERVICE_TURN_ON, data, blocking=True) -async def async_turn_off(hass, entity_id: str = None) -> None: +async def async_turn_off(hass, entity_id=ENTITY_MATCH_ALL) -> None: """Turn all or specified fan off.""" data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} @@ -34,7 +39,7 @@ async def async_turn_off(hass, entity_id: str = None) -> None: async def async_oscillate( - hass, entity_id: str = None, should_oscillate: bool = True + hass, entity_id=ENTITY_MATCH_ALL, should_oscillate: bool = True ) -> None: """Set oscillation on all or specified fan.""" data = { @@ -49,7 +54,7 @@ async def async_oscillate( await hass.services.async_call(DOMAIN, SERVICE_OSCILLATE, data, blocking=True) -async def async_set_speed(hass, entity_id: str = None, speed: str = None) -> None: +async def async_set_speed(hass, entity_id=ENTITY_MATCH_ALL, speed: str = None) -> None: """Set speed for all or specified fan.""" data = { key: value @@ -61,7 +66,7 @@ async def async_set_speed(hass, entity_id: str = None, speed: str = None) -> Non async def async_set_direction( - hass, entity_id: str = None, direction: str = None + hass, entity_id=ENTITY_MATCH_ALL, direction: str = None ) -> None: """Set direction for all or specified fan.""" data = { diff --git a/tests/components/geonetnz_volcano/__init__.py b/tests/components/geonetnz_volcano/__init__.py new file mode 100644 index 000000000000..708b69e00319 --- /dev/null +++ b/tests/components/geonetnz_volcano/__init__.py @@ -0,0 +1,25 @@ +"""The tests for the GeoNet NZ Volcano Feed integration.""" +from unittest.mock import MagicMock + + +def _generate_mock_feed_entry( + external_id, + title, + alert_level, + distance_to_home, + coordinates, + attribution=None, + activity=None, + hazards=None, +): + """Construct a mock feed entry for testing purposes.""" + feed_entry = MagicMock() + feed_entry.external_id = external_id + feed_entry.title = title + feed_entry.alert_level = alert_level + feed_entry.distance_to_home = distance_to_home + feed_entry.coordinates = coordinates + feed_entry.attribution = attribution + feed_entry.activity = activity + feed_entry.hazards = hazards + return feed_entry diff --git a/tests/components/geonetnz_volcano/conftest.py b/tests/components/geonetnz_volcano/conftest.py new file mode 100644 index 000000000000..55231cd31207 --- /dev/null +++ b/tests/components/geonetnz_volcano/conftest.py @@ -0,0 +1,28 @@ +"""Configuration for GeoNet NZ Volcano tests.""" +import pytest + +from homeassistant.components.geonetnz_volcano import DOMAIN +from homeassistant.const import ( + CONF_LATITUDE, + CONF_LONGITUDE, + CONF_RADIUS, + CONF_UNIT_SYSTEM, + CONF_SCAN_INTERVAL, +) +from tests.common import MockConfigEntry + + +@pytest.fixture +def config_entry(): + """Create a mock GeoNet NZ Volcano config entry.""" + return MockConfigEntry( + domain=DOMAIN, + data={ + CONF_LATITUDE: -41.2, + CONF_LONGITUDE: 174.7, + CONF_RADIUS: 25, + CONF_UNIT_SYSTEM: "metric", + CONF_SCAN_INTERVAL: 300.0, + }, + title="-41.2, 174.7", + ) diff --git a/tests/components/geonetnz_volcano/test_config_flow.py b/tests/components/geonetnz_volcano/test_config_flow.py new file mode 100644 index 000000000000..8f589aded903 --- /dev/null +++ b/tests/components/geonetnz_volcano/test_config_flow.py @@ -0,0 +1,81 @@ +"""Define tests for the GeoNet NZ Volcano config flow.""" +from datetime import timedelta + +from homeassistant import data_entry_flow +from homeassistant.components.geonetnz_volcano import config_flow +from homeassistant.const import ( + CONF_LATITUDE, + CONF_LONGITUDE, + CONF_RADIUS, + CONF_SCAN_INTERVAL, + CONF_UNIT_SYSTEM, +) + + +async def test_duplicate_error(hass, config_entry): + """Test that errors are shown when duplicates are added.""" + conf = {CONF_LATITUDE: -41.2, CONF_LONGITUDE: 174.7, CONF_RADIUS: 25} + + config_entry.add_to_hass(hass) + flow = config_flow.GeonetnzVolcanoFlowHandler() + flow.hass = hass + + result = await flow.async_step_user(user_input=conf) + assert result["errors"] == {"base": "identifier_exists"} + + +async def test_show_form(hass): + """Test that the form is served with no input.""" + flow = config_flow.GeonetnzVolcanoFlowHandler() + flow.hass = hass + + result = await flow.async_step_user(user_input=None) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "user" + + +async def test_step_import(hass): + """Test that the import step works.""" + conf = { + CONF_LATITUDE: -41.2, + CONF_LONGITUDE: 174.7, + CONF_RADIUS: 25, + CONF_UNIT_SYSTEM: "metric", + CONF_SCAN_INTERVAL: timedelta(minutes=4), + } + + flow = config_flow.GeonetnzVolcanoFlowHandler() + flow.hass = hass + + result = await flow.async_step_import(import_config=conf) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "-41.2, 174.7" + assert result["data"] == { + CONF_LATITUDE: -41.2, + CONF_LONGITUDE: 174.7, + CONF_RADIUS: 25, + CONF_UNIT_SYSTEM: "metric", + CONF_SCAN_INTERVAL: 240.0, + } + + +async def test_step_user(hass): + """Test that the user step works.""" + hass.config.latitude = -41.2 + hass.config.longitude = 174.7 + conf = {CONF_RADIUS: 25} + + flow = config_flow.GeonetnzVolcanoFlowHandler() + flow.hass = hass + + result = await flow.async_step_user(user_input=conf) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "-41.2, 174.7" + assert result["data"] == { + CONF_LATITUDE: -41.2, + CONF_LONGITUDE: 174.7, + CONF_RADIUS: 25, + CONF_UNIT_SYSTEM: "metric", + CONF_SCAN_INTERVAL: 300.0, + } diff --git a/tests/components/geonetnz_volcano/test_init.py b/tests/components/geonetnz_volcano/test_init.py new file mode 100644 index 000000000000..3e2566ffb817 --- /dev/null +++ b/tests/components/geonetnz_volcano/test_init.py @@ -0,0 +1,22 @@ +"""Define tests for the GeoNet NZ Volcano general setup.""" +from asynctest import CoroutineMock, patch + +from homeassistant.components.geonetnz_volcano import DOMAIN, FEED + + +async def test_component_unload_config_entry(hass, config_entry): + """Test that loading and unloading of a config entry works.""" + config_entry.add_to_hass(hass) + with patch( + "aio_geojson_geonetnz_volcano.GeonetnzVolcanoFeedManager.update", + new_callable=CoroutineMock, + ) as mock_feed_manager_update: + # Load config entry. + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + assert mock_feed_manager_update.call_count == 1 + assert hass.data[DOMAIN][FEED][config_entry.entry_id] is not None + # Unload config entry. + assert await hass.config_entries.async_unload(config_entry.entry_id) + await hass.async_block_till_done() + assert hass.data[DOMAIN][FEED].get(config_entry.entry_id) is None diff --git a/tests/components/geonetnz_volcano/test_sensor.py b/tests/components/geonetnz_volcano/test_sensor.py new file mode 100644 index 000000000000..8f71e3c47570 --- /dev/null +++ b/tests/components/geonetnz_volcano/test_sensor.py @@ -0,0 +1,168 @@ +"""The tests for the GeoNet NZ Volcano Feed integration.""" +from asynctest import CoroutineMock, patch + +from homeassistant.components import geonetnz_volcano +from homeassistant.components.geo_location import ATTR_DISTANCE +from homeassistant.components.geonetnz_volcano import DEFAULT_SCAN_INTERVAL +from homeassistant.components.geonetnz_volcano.const import ( + ATTR_ACTIVITY, + ATTR_EXTERNAL_ID, + ATTR_HAZARDS, +) +from homeassistant.const import ( + ATTR_ATTRIBUTION, + ATTR_FRIENDLY_NAME, + ATTR_ICON, + ATTR_LATITUDE, + ATTR_LONGITUDE, + ATTR_UNIT_OF_MEASUREMENT, + CONF_RADIUS, + EVENT_HOMEASSISTANT_START, +) +from homeassistant.setup import async_setup_component +import homeassistant.util.dt as dt_util +from homeassistant.util.unit_system import IMPERIAL_SYSTEM + +from tests.common import async_fire_time_changed +from tests.components.geonetnz_volcano import _generate_mock_feed_entry + +CONFIG = {geonetnz_volcano.DOMAIN: {CONF_RADIUS: 200}} + + +async def test_setup(hass): + """Test the general setup of the integration.""" + # Set up some mock feed entries for this test. + mock_entry_1 = _generate_mock_feed_entry( + "1234", + "Title 1", + 1, + 15.5, + (38.0, -3.0), + attribution="Attribution 1", + activity="Activity 1", + hazards="Hazards 1", + ) + mock_entry_2 = _generate_mock_feed_entry("2345", "Title 2", 0, 20.5, (38.1, -3.1)) + mock_entry_3 = _generate_mock_feed_entry("3456", "Title 3", 2, 25.5, (38.2, -3.2)) + mock_entry_4 = _generate_mock_feed_entry("4567", "Title 4", 1, 12.5, (38.3, -3.3)) + + # Patching 'utcnow' to gain more control over the timed update. + utcnow = dt_util.utcnow() + with patch("homeassistant.util.dt.utcnow", return_value=utcnow), patch( + "aio_geojson_client.feed.GeoJsonFeed.update", new_callable=CoroutineMock + ) as mock_feed_update: + mock_feed_update.return_value = "OK", [mock_entry_1, mock_entry_2, mock_entry_3] + assert await async_setup_component(hass, geonetnz_volcano.DOMAIN, CONFIG) + # Artificially trigger update and collect events. + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + await hass.async_block_till_done() + + all_states = hass.states.async_all() + # 3 sensor entities + assert len(all_states) == 3 + + state = hass.states.get("sensor.volcano_title_1") + assert state is not None + assert state.name == "Volcano Title 1" + assert int(state.state) == 1 + assert state.attributes[ATTR_EXTERNAL_ID] == "1234" + assert state.attributes[ATTR_LATITUDE] == 38.0 + assert state.attributes[ATTR_LONGITUDE] == -3.0 + assert state.attributes[ATTR_DISTANCE] == 15.5 + assert state.attributes[ATTR_FRIENDLY_NAME] == "Volcano Title 1" + assert state.attributes[ATTR_ATTRIBUTION] == "Attribution 1" + assert state.attributes[ATTR_ACTIVITY] == "Activity 1" + assert state.attributes[ATTR_HAZARDS] == "Hazards 1" + assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == "alert level" + assert state.attributes[ATTR_ICON] == "mdi:image-filter-hdr" + + state = hass.states.get("sensor.volcano_title_2") + assert state is not None + assert state.name == "Volcano Title 2" + assert int(state.state) == 0 + assert state.attributes[ATTR_EXTERNAL_ID] == "2345" + assert state.attributes[ATTR_LATITUDE] == 38.1 + assert state.attributes[ATTR_LONGITUDE] == -3.1 + assert state.attributes[ATTR_DISTANCE] == 20.5 + assert state.attributes[ATTR_FRIENDLY_NAME] == "Volcano Title 2" + + state = hass.states.get("sensor.volcano_title_3") + assert state is not None + assert state.name == "Volcano Title 3" + assert int(state.state) == 2 + assert state.attributes[ATTR_EXTERNAL_ID] == "3456" + assert state.attributes[ATTR_LATITUDE] == 38.2 + assert state.attributes[ATTR_LONGITUDE] == -3.2 + assert state.attributes[ATTR_DISTANCE] == 25.5 + assert state.attributes[ATTR_FRIENDLY_NAME] == "Volcano Title 3" + + # Simulate an update - two existing, one new entry, one outdated entry + mock_feed_update.return_value = "OK", [mock_entry_1, mock_entry_4, mock_entry_3] + async_fire_time_changed(hass, utcnow + DEFAULT_SCAN_INTERVAL) + await hass.async_block_till_done() + + all_states = hass.states.async_all() + assert len(all_states) == 4 + + # Simulate an update - empty data, but successful update, + # so no changes to entities. + mock_feed_update.return_value = "OK_NO_DATA", None + async_fire_time_changed(hass, utcnow + 2 * DEFAULT_SCAN_INTERVAL) + await hass.async_block_till_done() + + all_states = hass.states.async_all() + assert len(all_states) == 4 + + # Simulate an update - empty data, keep all entities + mock_feed_update.return_value = "ERROR", None + async_fire_time_changed(hass, utcnow + 3 * DEFAULT_SCAN_INTERVAL) + await hass.async_block_till_done() + + all_states = hass.states.async_all() + assert len(all_states) == 4 + + # Simulate an update - regular data for 3 entries + mock_feed_update.return_value = "OK", [mock_entry_1, mock_entry_2, mock_entry_3] + async_fire_time_changed(hass, utcnow + 4 * DEFAULT_SCAN_INTERVAL) + await hass.async_block_till_done() + + all_states = hass.states.async_all() + assert len(all_states) == 4 + + +async def test_setup_imperial(hass): + """Test the setup of the integration using imperial unit system.""" + hass.config.units = IMPERIAL_SYSTEM + # Set up some mock feed entries for this test. + mock_entry_1 = _generate_mock_feed_entry("1234", "Title 1", 1, 15.5, (38.0, -3.0)) + + # Patching 'utcnow' to gain more control over the timed update. + utcnow = dt_util.utcnow() + with patch("homeassistant.util.dt.utcnow", return_value=utcnow), patch( + "aio_geojson_client.feed.GeoJsonFeed.update", new_callable=CoroutineMock + ) as mock_feed_update, patch( + "aio_geojson_client.feed.GeoJsonFeed.__init__" + ) as mock_feed_init: + mock_feed_update.return_value = "OK", [mock_entry_1] + assert await async_setup_component(hass, geonetnz_volcano.DOMAIN, CONFIG) + # Artificially trigger update and collect events. + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + await hass.async_block_till_done() + + all_states = hass.states.async_all() + assert len(all_states) == 1 + + # Test conversion of 200 miles to kilometers. + assert mock_feed_init.call_args[1].get("filter_radius") == 321.8688 + + state = hass.states.get("sensor.volcano_title_1") + assert state is not None + assert state.name == "Volcano Title 1" + assert int(state.state) == 1 + assert state.attributes[ATTR_EXTERNAL_ID] == "1234" + assert state.attributes[ATTR_LATITUDE] == 38.0 + assert state.attributes[ATTR_LONGITUDE] == -3.0 + assert state.attributes[ATTR_DISTANCE] == 9.6 + assert state.attributes[ATTR_FRIENDLY_NAME] == "Volcano Title 1" + assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == "alert level" + assert state.attributes[ATTR_ICON] == "mdi:image-filter-hdr" diff --git a/tests/components/google_assistant/__init__.py b/tests/components/google_assistant/__init__.py index 09522e9c86fc..657bf930ed60 100644 --- a/tests/components/google_assistant/__init__.py +++ b/tests/components/google_assistant/__init__.py @@ -1,7 +1,18 @@ """Tests for the Google Assistant integration.""" +from asynctest.mock import MagicMock from homeassistant.components.google_assistant import helpers +def mock_google_config_store(agent_user_ids=None): + """Fake a storage for google assistant.""" + store = MagicMock(spec=helpers.GoogleConfigStore) + if agent_user_ids is not None: + store.agent_user_ids = agent_user_ids + else: + store.agent_user_ids = {} + return store + + class MockConfig(helpers.AbstractConfig): """Fake config that always exposes everything.""" @@ -15,6 +26,7 @@ def __init__( local_sdk_webhook_id=None, local_sdk_user_id=None, enabled=True, + agent_user_ids=None, ): """Initialize config.""" super().__init__(hass) @@ -24,6 +36,7 @@ def __init__( self._local_sdk_webhook_id = local_sdk_webhook_id self._local_sdk_user_id = local_sdk_user_id self._enabled = enabled + self._store = mock_google_config_store(agent_user_ids) @property def enabled(self): diff --git a/tests/components/google_assistant/test_helpers.py b/tests/components/google_assistant/test_helpers.py index 497b7b1f0ae8..eb479a3b6b52 100644 --- a/tests/components/google_assistant/test_helpers.py +++ b/tests/components/google_assistant/test_helpers.py @@ -1,11 +1,18 @@ """Test Google Assistant helpers.""" -from unittest.mock import Mock +from asynctest.mock import Mock, patch, call +from datetime import timedelta +import pytest from homeassistant.setup import async_setup_component from homeassistant.components.google_assistant import helpers from homeassistant.components.google_assistant.const import EVENT_COMMAND_RECEIVED +from homeassistant.util import dt from . import MockConfig -from tests.common import async_capture_events, async_mock_service +from tests.common import ( + async_capture_events, + async_mock_service, + async_fire_time_changed, +) async def test_google_entity_sync_serialize_with_local_sdk(hass): @@ -19,13 +26,13 @@ async def test_google_entity_sync_serialize_with_local_sdk(hass): ) entity = helpers.GoogleEntity(hass, config, hass.states.get("light.ceiling_lights")) - serialized = await entity.sync_serialize() + serialized = await entity.sync_serialize(None) assert "otherDeviceIds" not in serialized assert "customData" not in serialized config.async_enable_local_sdk() - serialized = await entity.sync_serialize() + serialized = await entity.sync_serialize(None) assert serialized["otherDeviceIds"] == [{"deviceId": "light.ceiling_lights"}] assert serialized["customData"] == { "httpPort": 1234, @@ -128,3 +135,84 @@ async def test_config_local_sdk_if_disabled(hass, hass_client): resp = await client.post("/api/webhook/mock-webhook-id") assert resp.status == 200 assert await resp.read() == b"" + + +async def test_agent_user_id_storage(hass, hass_storage): + """Test a disconnect message.""" + + hass_storage["google_assistant"] = { + "version": 1, + "key": "google_assistant", + "data": {"agent_user_ids": {"agent_1": {}}}, + } + + store = helpers.GoogleConfigStore(hass) + await store.async_load() + + assert hass_storage["google_assistant"] == { + "version": 1, + "key": "google_assistant", + "data": {"agent_user_ids": {"agent_1": {}}}, + } + + async def _check_after_delay(data): + async_fire_time_changed(hass, dt.utcnow() + timedelta(seconds=2)) + await hass.async_block_till_done() + + assert hass_storage["google_assistant"] == { + "version": 1, + "key": "google_assistant", + "data": data, + } + + store.add_agent_user_id("agent_2") + await _check_after_delay({"agent_user_ids": {"agent_1": {}, "agent_2": {}}}) + + store.pop_agent_user_id("agent_1") + await _check_after_delay({"agent_user_ids": {"agent_2": {}}}) + + +async def test_agent_user_id_connect(): + """Test the connection and disconnection of users.""" + config = MockConfig() + store = config._store + + await config.async_connect_agent_user("agent_2") + assert store.add_agent_user_id.call_args == call("agent_2") + + await config.async_connect_agent_user("agent_1") + assert store.add_agent_user_id.call_args == call("agent_1") + + await config.async_disconnect_agent_user("agent_2") + assert store.pop_agent_user_id.call_args == call("agent_2") + + await config.async_disconnect_agent_user("agent_1") + assert store.pop_agent_user_id.call_args == call("agent_1") + + +@pytest.mark.parametrize("agents", [{}, {"1"}, {"1", "2"}]) +async def test_report_state_all(agents): + """Test a disconnect message.""" + config = MockConfig(agent_user_ids=agents) + data = {} + with patch.object(config, "async_report_state") as mock: + await config.async_report_state_all(data) + assert sorted(mock.mock_calls) == sorted( + [call(data, agent) for agent in agents] + ) + + +@pytest.mark.parametrize( + "agents, result", [({}, 204), ({"1": 200}, 200), ({"1": 200, "2": 300}, 300)], +) +async def test_sync_entities_all(agents, result): + """Test sync entities .""" + config = MockConfig(agent_user_ids=set(agents.keys())) + with patch.object( + config, + "async_sync_entities", + side_effect=lambda agent_user_id: agents[agent_user_id], + ) as mock: + res = await config.async_sync_entities_all() + assert sorted(mock.mock_calls) == sorted([call(agent) for agent in agents]) + assert res == result diff --git a/tests/components/google_assistant/test_http.py b/tests/components/google_assistant/test_http.py index 4b26bbeba7f3..86ffcc87ac0a 100644 --- a/tests/components/google_assistant/test_http.py +++ b/tests/components/google_assistant/test_http.py @@ -12,7 +12,6 @@ REPORT_STATE_BASE_URL, HOMEGRAPH_TOKEN_URL, ) -from homeassistant.auth.models import User DUMMY_CONFIG = GOOGLE_ASSISTANT_SCHEMA( { @@ -67,6 +66,7 @@ async def test_update_access_token(hass): jwt = "dummyjwt" config = GoogleConfig(hass, DUMMY_CONFIG) + await config.async_initialize() base_time = datetime(2019, 10, 14, tzinfo=timezone.utc) with patch( @@ -99,6 +99,8 @@ async def test_update_access_token(hass): async def test_call_homegraph_api(hass, aioclient_mock, hass_storage): """Test the function to call the homegraph api.""" config = GoogleConfig(hass, DUMMY_CONFIG) + await config.async_initialize() + with patch( "homeassistant.components.google_assistant.http._get_homegraph_token" ) as mock_get_token: @@ -106,7 +108,8 @@ async def test_call_homegraph_api(hass, aioclient_mock, hass_storage): aioclient_mock.post(MOCK_URL, status=200, json={}) - await config.async_call_homegraph_api(MOCK_URL, MOCK_JSON) + res = await config.async_call_homegraph_api(MOCK_URL, MOCK_JSON) + assert res == 200 assert mock_get_token.call_count == 1 assert aioclient_mock.call_count == 1 @@ -119,6 +122,8 @@ async def test_call_homegraph_api(hass, aioclient_mock, hass_storage): async def test_call_homegraph_api_retry(hass, aioclient_mock, hass_storage): """Test the that the calls get retried with new token on 401.""" config = GoogleConfig(hass, DUMMY_CONFIG) + await config.async_initialize() + with patch( "homeassistant.components.google_assistant.http._get_homegraph_token" ) as mock_get_token: @@ -139,19 +144,50 @@ async def test_call_homegraph_api_retry(hass, aioclient_mock, hass_storage): assert call[3] == MOCK_HEADER +async def test_call_homegraph_api_key(hass, aioclient_mock, hass_storage): + """Test the function to call the homegraph api.""" + config = GoogleConfig( + hass, GOOGLE_ASSISTANT_SCHEMA({"project_id": "1234", "api_key": "dummy_key"}), + ) + await config.async_initialize() + + aioclient_mock.post(MOCK_URL, status=200, json={}) + + res = await config.async_call_homegraph_api_key(MOCK_URL, MOCK_JSON) + assert res == 200 + assert aioclient_mock.call_count == 1 + + call = aioclient_mock.mock_calls[0] + assert call[1].query == {"key": "dummy_key"} + assert call[2] == MOCK_JSON + + +async def test_call_homegraph_api_key_fail(hass, aioclient_mock, hass_storage): + """Test the function to call the homegraph api.""" + config = GoogleConfig( + hass, GOOGLE_ASSISTANT_SCHEMA({"project_id": "1234", "api_key": "dummy_key"}), + ) + await config.async_initialize() + + aioclient_mock.post(MOCK_URL, status=666, json={}) + + res = await config.async_call_homegraph_api_key(MOCK_URL, MOCK_JSON) + assert res == 666 + assert aioclient_mock.call_count == 1 + + async def test_report_state(hass, aioclient_mock, hass_storage): """Test the report state function.""" + agent_user_id = "user" config = GoogleConfig(hass, DUMMY_CONFIG) - message = {"devices": {}} - owner = User(name="Test User", perm_lookup=None, groups=[], is_owner=True) + await config.async_initialize() - with patch.object(config, "async_call_homegraph_api") as mock_call, patch.object( - hass.auth, "async_get_owner" - ) as mock_get_owner: - mock_get_owner.return_value = owner + await config.async_connect_agent_user(agent_user_id) + message = {"devices": {}} - await config.async_report_state(message) + with patch.object(config, "async_call_homegraph_api") as mock_call: + await config.async_report_state(message, agent_user_id) mock_call.assert_called_once_with( REPORT_STATE_BASE_URL, - {"requestId": ANY, "agentUserId": owner.id, "payload": message}, + {"requestId": ANY, "agentUserId": agent_user_id, "payload": message}, ) diff --git a/tests/components/google_assistant/test_report_state.py b/tests/components/google_assistant/test_report_state.py index 6ab88286a695..ce624c9ca95e 100644 --- a/tests/components/google_assistant/test_report_state.py +++ b/tests/components/google_assistant/test_report_state.py @@ -16,7 +16,7 @@ async def test_report_state(hass, caplog): hass.states.async_set("switch.ac", "on") with patch.object( - BASIC_CONFIG, "async_report_state", side_effect=mock_coro + BASIC_CONFIG, "async_report_state_all", side_effect=mock_coro ) as mock_report, patch.object(report_state, "INITIAL_REPORT_DELAY", 0): unsub = report_state.async_enable_report_state(hass, BASIC_CONFIG) @@ -35,7 +35,7 @@ async def test_report_state(hass, caplog): } with patch.object( - BASIC_CONFIG, "async_report_state", side_effect=mock_coro + BASIC_CONFIG, "async_report_state_all", side_effect=mock_coro ) as mock_report: hass.states.async_set("light.kitchen", "on") await hass.async_block_till_done() @@ -48,7 +48,7 @@ async def test_report_state(hass, caplog): # Test that state changes that change something that Google doesn't care about # do not trigger a state report. with patch.object( - BASIC_CONFIG, "async_report_state", side_effect=mock_coro + BASIC_CONFIG, "async_report_state_all", side_effect=mock_coro ) as mock_report: hass.states.async_set( "light.kitchen", "on", {"irrelevant": "should_be_ignored"} @@ -59,7 +59,7 @@ async def test_report_state(hass, caplog): # Test that entities that we can't query don't report a state with patch.object( - BASIC_CONFIG, "async_report_state", side_effect=mock_coro + BASIC_CONFIG, "async_report_state_all", side_effect=mock_coro ) as mock_report, patch( "homeassistant.components.google_assistant.report_state.GoogleEntity.query_serialize", side_effect=error.SmartHomeError("mock-error", "mock-msg"), @@ -73,7 +73,7 @@ async def test_report_state(hass, caplog): unsub() with patch.object( - BASIC_CONFIG, "async_report_state", side_effect=mock_coro + BASIC_CONFIG, "async_report_state_all", side_effect=mock_coro ) as mock_report: hass.states.async_set("light.kitchen", "on") await hass.async_block_till_done() diff --git a/tests/components/google_assistant/test_smart_home.py b/tests/components/google_assistant/test_smart_home.py index 3c3801b35c69..c144ffee6def 100644 --- a/tests/components/google_assistant/test_smart_home.py +++ b/tests/components/google_assistant/test_smart_home.py @@ -455,7 +455,7 @@ async def test_serialize_input_boolean(hass): state = State("input_boolean.bla", "on") # pylint: disable=protected-access entity = sh.GoogleEntity(hass, BASIC_CONFIG, state) - result = await entity.sync_serialize() + result = await entity.sync_serialize(None) assert result == { "id": "input_boolean.bla", "attributes": {}, @@ -466,14 +466,17 @@ async def test_serialize_input_boolean(hass): } -async def test_unavailable_state_doesnt_sync(hass): - """Test that an unavailable entity does not sync over.""" - light = DemoLight(None, "Demo Light", state=False) +async def test_unavailable_state_does_sync(hass): + """Test that an unavailable entity does sync over.""" + light = DemoLight(None, "Demo Light", state=False, hs_color=(180, 75)) light.hass = hass light.entity_id = "light.demo_light" light._available = False # pylint: disable=protected-access await light.async_update_ha_state() + events = [] + hass.bus.async_listen(EVENT_SYNC_RECEIVED, events.append) + result = await sh.async_handle_message( hass, BASIC_CONFIG, @@ -483,8 +486,35 @@ async def test_unavailable_state_doesnt_sync(hass): assert result == { "requestId": REQ_ID, - "payload": {"agentUserId": "test-agent", "devices": []}, + "payload": { + "agentUserId": "test-agent", + "devices": [ + { + "id": "light.demo_light", + "name": {"name": "Demo Light"}, + "traits": [ + trait.TRAIT_BRIGHTNESS, + trait.TRAIT_ONOFF, + trait.TRAIT_COLOR_SETTING, + ], + "type": const.TYPE_LIGHT, + "willReportState": False, + "attributes": { + "colorModel": "hsv", + "colorTemperatureRange": { + "temperatureMinK": 2000, + "temperatureMaxK": 6535, + }, + }, + } + ], + }, } + await hass.async_block_till_done() + + assert len(events) == 1 + assert events[0].event_type == EVENT_SYNC_RECEIVED + assert events[0].data == {"request_id": REQ_ID} @pytest.mark.parametrize( @@ -498,6 +528,7 @@ async def test_unavailable_state_doesnt_sync(hass): async def test_device_class_switch(hass, device_class, google_type): """Test that a cover entity syncs to the correct device type.""" sensor = DemoSwitch( + None, "Demo Sensor", state=False, icon="mdi:switch", @@ -545,7 +576,9 @@ async def test_device_class_switch(hass, device_class, google_type): ) async def test_device_class_binary_sensor(hass, device_class, google_type): """Test that a binary entity syncs to the correct device type.""" - sensor = DemoBinarySensor("Demo Sensor", state=False, device_class=device_class) + sensor = DemoBinarySensor( + None, "Demo Sensor", state=False, device_class=device_class + ) sensor.hass = hass sensor.entity_id = "binary_sensor.demo_sensor" await sensor.async_update_ha_state() @@ -585,7 +618,7 @@ async def test_device_class_binary_sensor(hass, device_class, google_type): ) async def test_device_class_cover(hass, device_class, google_type): """Test that a binary entity syncs to the correct device type.""" - sensor = DemoCover(hass, "Demo Sensor", device_class=device_class) + sensor = DemoCover(None, hass, "Demo Sensor", device_class=device_class) sensor.hass = hass sensor.entity_id = "cover.demo_sensor" await sensor.async_update_ha_state() @@ -661,8 +694,8 @@ async def test_query_disconnect(hass): config.async_enable_report_state() assert config._unsub_report_state is not None with patch.object( - config, "async_deactivate_report_state", side_effect=mock_coro - ) as mock_deactivate: + config, "async_disconnect_agent_user", side_effect=mock_coro + ) as mock_disconnect: result = await sh.async_handle_message( hass, config, @@ -670,7 +703,7 @@ async def test_query_disconnect(hass): {"inputs": [{"intent": "action.devices.DISCONNECT"}], "requestId": REQ_ID}, ) assert result is None - assert len(mock_deactivate.mock_calls) == 1 + assert len(mock_disconnect.mock_calls) == 1 async def test_trait_execute_adding_query_data(hass): @@ -738,10 +771,12 @@ async def test_trait_execute_adding_query_data(hass): async def test_identify(hass): """Test identify message.""" + user_agent_id = "mock-user-id" + proxy_device_id = user_agent_id result = await sh.async_handle_message( hass, BASIC_CONFIG, - None, + user_agent_id, { "requestId": REQ_ID, "inputs": [ @@ -775,7 +810,7 @@ async def test_identify(hass): "customData": { "httpPort": 8123, "httpSSL": False, - "proxyDeviceId": BASIC_CONFIG.agent_user_id, + "proxyDeviceId": proxy_device_id, "webhookId": "dde3b9800a905e886cc4d38e226a6e7e3f2a6993d2b9b9f63d13e42ee7de3219", }, } @@ -787,7 +822,7 @@ async def test_identify(hass): "requestId": REQ_ID, "payload": { "device": { - "id": BASIC_CONFIG.agent_user_id, + "id": proxy_device_id, "isLocalOnly": True, "isProxy": True, "deviceInfo": { @@ -819,10 +854,13 @@ async def test_reachable_devices(hass): should_expose=lambda state: state.entity_id != "light.not_expose" ) + user_agent_id = "mock-user-id" + proxy_device_id = user_agent_id + result = await sh.async_handle_message( hass, config, - None, + user_agent_id, { "requestId": REQ_ID, "inputs": [ @@ -831,7 +869,7 @@ async def test_reachable_devices(hass): "payload": { "device": { "proxyDevice": { - "id": "6a04f0f7-6125-4356-a846-861df7e01497", + "id": proxy_device_id, "customData": "{}", "proxyData": "{}", } @@ -846,7 +884,7 @@ async def test_reachable_devices(hass): "customData": { "httpPort": 8123, "httpSSL": False, - "proxyDeviceId": BASIC_CONFIG.agent_user_id, + "proxyDeviceId": proxy_device_id, "webhookId": "dde3b9800a905e886cc4d38e226a6e7e3f2a6993d2b9b9f63d13e42ee7de3219", }, }, @@ -855,11 +893,11 @@ async def test_reachable_devices(hass): "customData": { "httpPort": 8123, "httpSSL": False, - "proxyDeviceId": BASIC_CONFIG.agent_user_id, + "proxyDeviceId": proxy_device_id, "webhookId": "dde3b9800a905e886cc4d38e226a6e7e3f2a6993d2b9b9f63d13e42ee7de3219", }, }, - {"id": BASIC_CONFIG.agent_user_id, "customData": {}}, + {"id": proxy_device_id, "customData": {}}, ], }, ) diff --git a/tests/components/google_assistant/test_trait.py b/tests/components/google_assistant/test_trait.py index d6ec24a78679..6d24aa0942f8 100644 --- a/tests/components/google_assistant/test_trait.py +++ b/tests/components/google_assistant/test_trait.py @@ -1629,3 +1629,29 @@ async def test_temperature_setting_sensor(hass): assert trt.query_attributes() == {"thermostatTemperatureAmbient": 21.1} hass.config.units.temperature_unit = TEMP_CELSIUS + + +async def test_humidity_setting_sensor(hass): + """Test HumiditySetting trait support for humidity sensor.""" + assert ( + helpers.get_google_type(sensor.DOMAIN, sensor.DEVICE_CLASS_HUMIDITY) is not None + ) + assert not trait.HumiditySettingTrait.supported( + sensor.DOMAIN, 0, sensor.DEVICE_CLASS_TEMPERATURE + ) + assert trait.HumiditySettingTrait.supported( + sensor.DOMAIN, 0, sensor.DEVICE_CLASS_HUMIDITY + ) + + trt = trait.HumiditySettingTrait( + hass, + State("sensor.test", "70", {ATTR_DEVICE_CLASS: sensor.DEVICE_CLASS_HUMIDITY}), + BASIC_CONFIG, + ) + + assert trt.sync_attributes() == {"queryOnlyHumiditySetting": True} + assert trt.query_attributes() == {"humidityAmbientPercent": 70} + + with pytest.raises(helpers.SmartHomeError) as err: + await trt.execute(trait.COMMAND_ONOFF, BASIC_DATA, {"on": False}, {}) + assert err.value.code == const.ERR_NOT_SUPPORTED diff --git a/tests/components/google_translate/test_tts.py b/tests/components/google_translate/test_tts.py index e9d19a8f4f1a..13f9eb88fce5 100644 --- a/tests/components/google_translate/test_tts.py +++ b/tests/components/google_translate/test_tts.py @@ -14,7 +14,7 @@ from tests.common import get_test_home_assistant, assert_setup_component, mock_service -from tests.components.tts.test_init import mutagen_mock # noqa +from tests.components.tts.test_init import mutagen_mock # noqa: F401 class TestTTSGooglePlatform: diff --git a/tests/components/hangouts/test_config_flow.py b/tests/components/hangouts/test_config_flow.py index 29585db5f615..93f909d3bd4d 100644 --- a/tests/components/hangouts/test_config_flow.py +++ b/tests/components/hangouts/test_config_flow.py @@ -4,6 +4,10 @@ from homeassistant import data_entry_flow from homeassistant.components.hangouts import config_flow +from homeassistant.const import CONF_EMAIL, CONF_PASSWORD + +EMAIL = "test@test.com" +PASSWORD = "1232456" async def test_flow_works(hass, aioclient_mock): @@ -12,12 +16,12 @@ async def test_flow_works(hass, aioclient_mock): flow.hass = hass - with patch("hangups.get_auth"): + with patch("homeassistant.components.hangouts.config_flow.get_auth"): result = await flow.async_step_user( - {"email": "test@test.com", "password": "1232456"} + {CONF_EMAIL: EMAIL, CONF_PASSWORD: PASSWORD} ) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["title"] == "test@test.com" + assert result["title"] == EMAIL async def test_flow_works_with_authcode(hass, aioclient_mock): @@ -26,16 +30,16 @@ async def test_flow_works_with_authcode(hass, aioclient_mock): flow.hass = hass - with patch("hangups.get_auth"): + with patch("homeassistant.components.hangouts.config_flow.get_auth"): result = await flow.async_step_user( { - "email": "test@test.com", - "password": "1232456", + CONF_EMAIL: EMAIL, + CONF_PASSWORD: PASSWORD, "authorization_code": "c29tZXJhbmRvbXN0cmluZw==", } ) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["title"] == "test@test.com" + assert result["title"] == EMAIL async def test_flow_works_with_2fa(hass, aioclient_mock): @@ -46,17 +50,20 @@ async def test_flow_works_with_2fa(hass, aioclient_mock): flow.hass = hass - with patch("hangups.get_auth", side_effect=Google2FAError): + with patch( + "homeassistant.components.hangouts.config_flow.get_auth", + side_effect=Google2FAError, + ): result = await flow.async_step_user( - {"email": "test@test.com", "password": "1232456"} + {CONF_EMAIL: EMAIL, CONF_PASSWORD: PASSWORD} ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "2fa" - with patch("hangups.get_auth"): + with patch("homeassistant.components.hangouts.config_flow.get_auth"): result = await flow.async_step_2fa({"2fa": 123456}) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["title"] == "test@test.com" + assert result["title"] == EMAIL async def test_flow_with_unknown_2fa(hass, aioclient_mock): @@ -68,11 +75,11 @@ async def test_flow_with_unknown_2fa(hass, aioclient_mock): flow.hass = hass with patch( - "hangups.get_auth", + "homeassistant.components.hangouts.config_flow.get_auth", side_effect=GoogleAuthError("Unknown verification code input"), ): result = await flow.async_step_user( - {"email": "test@test.com", "password": "1232456"} + {CONF_EMAIL: EMAIL, CONF_PASSWORD: PASSWORD} ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["errors"]["base"] == "invalid_2fa_method" @@ -86,9 +93,12 @@ async def test_flow_invalid_login(hass, aioclient_mock): flow.hass = hass - with patch("hangups.get_auth", side_effect=GoogleAuthError): + with patch( + "homeassistant.components.hangouts.config_flow.get_auth", + side_effect=GoogleAuthError, + ): result = await flow.async_step_user( - {"email": "test@test.com", "password": "1232456"} + {CONF_EMAIL: EMAIL, CONF_PASSWORD: PASSWORD} ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["errors"]["base"] == "invalid_login" @@ -102,14 +112,20 @@ async def test_flow_invalid_2fa(hass, aioclient_mock): flow.hass = hass - with patch("hangups.get_auth", side_effect=Google2FAError): + with patch( + "homeassistant.components.hangouts.config_flow.get_auth", + side_effect=Google2FAError, + ): result = await flow.async_step_user( - {"email": "test@test.com", "password": "1232456"} + {CONF_EMAIL: EMAIL, CONF_PASSWORD: PASSWORD} ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "2fa" - with patch("hangups.get_auth", side_effect=Google2FAError): + with patch( + "homeassistant.components.hangouts.config_flow.get_auth", + side_effect=Google2FAError, + ): result = await flow.async_step_2fa({"2fa": 123456}) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM diff --git a/tests/components/here_travel_time/test_sensor.py b/tests/components/here_travel_time/test_sensor.py index 1d788c93c663..296efbdad532 100644 --- a/tests/components/here_travel_time/test_sensor.py +++ b/tests/components/here_travel_time/test_sensor.py @@ -71,8 +71,8 @@ def _build_mock_url(origin, destination, modes, app_id, app_code, departure): """Construct a url for HERE.""" base_url = "https://route.cit.api.here.com/routing/7.2/calculateroute.json?" parameters = { - "waypoint0": origin, - "waypoint1": destination, + "waypoint0": f"geo!{origin}", + "waypoint1": f"geo!{destination}", "mode": ";".join(str(herepy.RouteMode[mode]) for mode in modes), "app_id": app_id, "app_code": app_code, diff --git a/tests/components/hisense_aehw4a1/__init__.py b/tests/components/hisense_aehw4a1/__init__.py new file mode 100644 index 000000000000..1365294626ee --- /dev/null +++ b/tests/components/hisense_aehw4a1/__init__.py @@ -0,0 +1 @@ +"""Tests for the hisense_aehw4a1 component.""" diff --git a/tests/components/hisense_aehw4a1/test_init.py b/tests/components/hisense_aehw4a1/test_init.py new file mode 100644 index 000000000000..638fbe8f943a --- /dev/null +++ b/tests/components/hisense_aehw4a1/test_init.py @@ -0,0 +1,89 @@ +"""Tests for the Hisense AEH-W4A1 init file.""" +from unittest.mock import patch + +from pyaehw4a1 import exceptions + +from homeassistant import config_entries, data_entry_flow +from homeassistant.components import hisense_aehw4a1 +from homeassistant.setup import async_setup_component + +from tests.common import mock_coro + + +async def test_creating_entry_sets_up_climate_discovery(hass): + """Test setting up Hisense AEH-W4A1 loads the climate component.""" + with patch( + "homeassistant.components.hisense_aehw4a1.config_flow.AehW4a1.discovery", + return_value=mock_coro(["1.2.3.4"]), + ): + with patch( + "homeassistant.components.hisense_aehw4a1.climate.async_setup_entry", + return_value=mock_coro(True), + ) as mock_setup: + result = await hass.config_entries.flow.async_init( + hisense_aehw4a1.DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + # Confirmation form + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + + await hass.async_block_till_done() + + assert len(mock_setup.mock_calls) == 1 + + +async def test_configuring_hisense_w4a1_create_entry(hass): + """Test that specifying config will create an entry.""" + with patch( + "homeassistant.components.hisense_aehw4a1.config_flow.AehW4a1.check", + return_value=mock_coro(True), + ): + with patch( + "homeassistant.components.hisense_aehw4a1.async_setup_entry", + return_value=mock_coro(True), + ) as mock_setup: + await async_setup_component( + hass, + hisense_aehw4a1.DOMAIN, + {"hisense_aehw4a1": {"ip_address": ["1.2.3.4"]}}, + ) + await hass.async_block_till_done() + + assert len(mock_setup.mock_calls) == 1 + + +async def test_configuring_hisense_w4a1_not_creates_entry_for_device_not_found(hass): + """Test that specifying config will not create an entry.""" + with patch( + "homeassistant.components.hisense_aehw4a1.config_flow.AehW4a1.check", + side_effect=exceptions.ConnectionError, + ): + with patch( + "homeassistant.components.hisense_aehw4a1.async_setup_entry", + return_value=mock_coro(True), + ) as mock_setup: + await async_setup_component( + hass, + hisense_aehw4a1.DOMAIN, + {"hisense_aehw4a1": {"ip_address": ["1.2.3.4"]}}, + ) + await hass.async_block_till_done() + + assert len(mock_setup.mock_calls) == 0 + + +async def test_configuring_hisense_w4a1_not_creates_entry_for_empty_import(hass): + """Test that specifying config will not create an entry.""" + with patch( + "homeassistant.components.hisense_aehw4a1.async_setup_entry", + return_value=mock_coro(True), + ) as mock_setup: + await async_setup_component(hass, hisense_aehw4a1.DOMAIN, {}) + await hass.async_block_till_done() + + assert len(mock_setup.mock_calls) == 0 diff --git a/tests/components/homeassistant/test_scene.py b/tests/components/homeassistant/test_scene.py index 25ce6088a511..d3bbac44df88 100644 --- a/tests/components/homeassistant/test_scene.py +++ b/tests/components/homeassistant/test_scene.py @@ -1,8 +1,13 @@ """Test Home Assistant scenes.""" from unittest.mock import patch +import pytest +import voluptuous as vol + from homeassistant.setup import async_setup_component +from tests.common import async_mock_service + async def test_reload_config_service(hass): """Test the reload config service.""" @@ -63,6 +68,16 @@ async def test_create_service(hass, caplog): assert hass.states.get("scene.hallo") is None assert hass.states.get("scene.hallo_2") is not None + assert await hass.services.async_call( + "scene", + "create", + {"scene_id": "hallo", "entities": {}, "snapshot_entities": []}, + blocking=True, + ) + await hass.async_block_till_done() + assert "Empty scenes are not allowed" in caplog.text + assert hass.states.get("scene.hallo") is None + assert await hass.services.async_call( "scene", "create", @@ -117,3 +132,80 @@ async def test_create_service(hass, caplog): assert scene.name == "hallo_2" assert scene.state == "scening" assert scene.attributes.get("entity_id") == ["light.kitchen"] + + +async def test_snapshot_service(hass, caplog): + """Test the snapshot option.""" + assert await async_setup_component(hass, "scene", {"scene": {}}) + hass.states.async_set("light.my_light", "on", {"hs_color": (345, 75)}) + assert hass.states.get("scene.hallo") is None + + assert await hass.services.async_call( + "scene", + "create", + {"scene_id": "hallo", "snapshot_entities": ["light.my_light"]}, + blocking=True, + ) + await hass.async_block_till_done() + scene = hass.states.get("scene.hallo") + assert scene is not None + assert scene.attributes.get("entity_id") == ["light.my_light"] + + hass.states.async_set("light.my_light", "off", {"hs_color": (123, 45)}) + turn_on_calls = async_mock_service(hass, "light", "turn_on") + assert await hass.services.async_call( + "scene", "turn_on", {"entity_id": "scene.hallo"}, blocking=True + ) + await hass.async_block_till_done() + assert len(turn_on_calls) == 1 + assert turn_on_calls[0].data.get("entity_id") == "light.my_light" + assert turn_on_calls[0].data.get("hs_color") == (345, 75) + + assert await hass.services.async_call( + "scene", + "create", + {"scene_id": "hallo_2", "snapshot_entities": ["light.not_existent"]}, + blocking=True, + ) + await hass.async_block_till_done() + assert hass.states.get("scene.hallo_2") is None + assert ( + "Entity light.not_existent does not exist and therefore cannot be snapshotted" + in caplog.text + ) + + assert await hass.services.async_call( + "scene", + "create", + { + "scene_id": "hallo_3", + "entities": {"light.bed_light": {"state": "on", "brightness": 50}}, + "snapshot_entities": ["light.my_light"], + }, + blocking=True, + ) + await hass.async_block_till_done() + scene = hass.states.get("scene.hallo_3") + assert scene is not None + assert "light.my_light" in scene.attributes.get("entity_id") + assert "light.bed_light" in scene.attributes.get("entity_id") + + +async def test_ensure_no_intersection(hass): + """Test that entities and snapshot_entities do not overlap.""" + assert await async_setup_component(hass, "scene", {"scene": {}}) + + with pytest.raises(vol.MultipleInvalid) as ex: + assert await hass.services.async_call( + "scene", + "create", + { + "scene_id": "hallo", + "entities": {"light.my_light": {"state": "on", "brightness": 50}}, + "snapshot_entities": ["light.my_light"], + }, + blocking=True, + ) + await hass.async_block_till_done() + assert "entities and snapshot_entities must not overlap" in str(ex.value) + assert hass.states.get("scene.hallo") is None diff --git a/tests/components/homekit/test_accessories.py b/tests/components/homekit/test_accessories.py index fcc0e05b570c..883d84339e5c 100644 --- a/tests/components/homekit/test_accessories.py +++ b/tests/components/homekit/test_accessories.py @@ -245,6 +245,33 @@ async def test_linked_battery_sensor(hass, hk_driver, caplog): assert acc._char_charging.value == 0 +async def test_missing_linked_battery_sensor(hass, hk_driver, caplog): + """Test battery service with mising linked_battery_sensor.""" + entity_id = "homekit.accessory" + linked_battery = "sensor.battery" + hass.states.async_set(entity_id, "open") + await hass.async_block_till_done() + + acc = HomeAccessory( + hass, + hk_driver, + "Battery Service", + entity_id, + 2, + {CONF_LINKED_BATTERY_SENSOR: linked_battery}, + ) + acc.update_state = lambda x: None + assert not acc.linked_battery_sensor + + await hass.async_add_job(acc.run) + await hass.async_block_till_done() + + assert not acc.linked_battery_sensor + assert not hasattr(acc, "_char_battery") + assert not hasattr(acc, "_char_low_battery") + assert not hasattr(acc, "_char_charging") + + async def test_call_service(hass, hk_driver, events): """Test call_service method.""" entity_id = "homekit.accessory" diff --git a/tests/components/homekit/test_type_thermostats.py b/tests/components/homekit/test_type_thermostats.py index c896ad211e86..9f9ebcdfd32e 100644 --- a/tests/components/homekit/test_type_thermostats.py +++ b/tests/components/homekit/test_type_thermostats.py @@ -24,6 +24,8 @@ HVAC_MODE_HEAT, HVAC_MODE_HEAT_COOL, HVAC_MODE_OFF, + HVAC_MODE_FAN_ONLY, + HVAC_MODE_AUTO, ) from homeassistant.components.homekit.const import ( ATTR_VALUE, @@ -64,7 +66,20 @@ async def test_thermostat(hass, hk_driver, cls, events): """Test if accessory and HA are updated accordingly.""" entity_id = "climate.test" - hass.states.async_set(entity_id, HVAC_MODE_OFF) + hass.states.async_set( + entity_id, + HVAC_MODE_OFF, + { + ATTR_HVAC_MODES: [ + HVAC_MODE_HEAT, + HVAC_MODE_HEAT_COOL, + HVAC_MODE_FAN_ONLY, + HVAC_MODE_COOL, + HVAC_MODE_OFF, + HVAC_MODE_AUTO, + ], + }, + ) await hass.async_block_till_done() acc = cls.thermostat(hass, hk_driver, "Climate", entity_id, 2, None) await hass.async_add_job(acc.run) @@ -120,7 +135,7 @@ async def test_thermostat(hass, hk_driver, cls, events): hass.states.async_set( entity_id, - HVAC_MODE_COOL, + HVAC_MODE_FAN_ONLY, { ATTR_TEMPERATURE: 20.0, ATTR_CURRENT_TEMPERATURE: 25.0, @@ -164,9 +179,8 @@ async def test_thermostat(hass, hk_driver, cls, events): hass.states.async_set( entity_id, - HVAC_MODE_HEAT_COOL, + HVAC_MODE_AUTO, { - ATTR_HVAC_MODES: [HVAC_MODE_HEAT, HVAC_MODE_COOL], ATTR_TEMPERATURE: 22.0, ATTR_CURRENT_TEMPERATURE: 18.0, ATTR_HVAC_ACTION: CURRENT_HVAC_HEAT, @@ -183,7 +197,6 @@ async def test_thermostat(hass, hk_driver, cls, events): entity_id, HVAC_MODE_HEAT_COOL, { - ATTR_HVAC_MODES: [HVAC_MODE_HEAT, HVAC_MODE_COOL], ATTR_TEMPERATURE: 22.0, ATTR_CURRENT_TEMPERATURE: 25.0, ATTR_HVAC_ACTION: CURRENT_HVAC_COOL, @@ -198,9 +211,8 @@ async def test_thermostat(hass, hk_driver, cls, events): hass.states.async_set( entity_id, - HVAC_MODE_HEAT_COOL, + HVAC_MODE_AUTO, { - ATTR_HVAC_MODES: [HVAC_MODE_HEAT, HVAC_MODE_COOL], ATTR_TEMPERATURE: 22.0, ATTR_CURRENT_TEMPERATURE: 22.0, ATTR_HVAC_ACTION: CURRENT_HVAC_IDLE, @@ -226,14 +238,23 @@ async def test_thermostat(hass, hk_driver, cls, events): assert len(events) == 1 assert events[-1].data[ATTR_VALUE] == "19.0°C" - await hass.async_add_job(acc.char_target_heat_cool.client_update_value, 1) + await hass.async_add_job(acc.char_target_heat_cool.client_update_value, 2) await hass.async_block_till_done() assert call_set_hvac_mode assert call_set_hvac_mode[0].data[ATTR_ENTITY_ID] == entity_id - assert call_set_hvac_mode[0].data[ATTR_HVAC_MODE] == HVAC_MODE_HEAT - assert acc.char_target_heat_cool.value == 1 + assert call_set_hvac_mode[0].data[ATTR_HVAC_MODE] == HVAC_MODE_COOL + assert acc.char_target_heat_cool.value == 2 assert len(events) == 2 - assert events[-1].data[ATTR_VALUE] == HVAC_MODE_HEAT + assert events[-1].data[ATTR_VALUE] == HVAC_MODE_COOL + + await hass.async_add_job(acc.char_target_heat_cool.client_update_value, 3) + await hass.async_block_till_done() + assert call_set_hvac_mode + assert call_set_hvac_mode[1].data[ATTR_ENTITY_ID] == entity_id + assert call_set_hvac_mode[1].data[ATTR_HVAC_MODE] == HVAC_MODE_AUTO + assert acc.char_target_heat_cool.value == 3 + assert len(events) == 3 + assert events[-1].data[ATTR_VALUE] == HVAC_MODE_AUTO async def test_thermostat_auto(hass, hk_driver, cls, events): @@ -261,7 +282,6 @@ async def test_thermostat_auto(hass, hk_driver, cls, events): entity_id, HVAC_MODE_HEAT_COOL, { - ATTR_HVAC_MODE: HVAC_MODE_HEAT_COOL, ATTR_TARGET_TEMP_HIGH: 22.0, ATTR_TARGET_TEMP_LOW: 20.0, ATTR_CURRENT_TEMPERATURE: 18.0, @@ -278,9 +298,8 @@ async def test_thermostat_auto(hass, hk_driver, cls, events): hass.states.async_set( entity_id, - HVAC_MODE_HEAT_COOL, + HVAC_MODE_COOL, { - ATTR_HVAC_MODE: HVAC_MODE_HEAT_COOL, ATTR_TARGET_TEMP_HIGH: 23.0, ATTR_TARGET_TEMP_LOW: 19.0, ATTR_CURRENT_TEMPERATURE: 24.0, @@ -291,15 +310,14 @@ async def test_thermostat_auto(hass, hk_driver, cls, events): assert acc.char_heating_thresh_temp.value == 19.0 assert acc.char_cooling_thresh_temp.value == 23.0 assert acc.char_current_heat_cool.value == 2 - assert acc.char_target_heat_cool.value == 3 + assert acc.char_target_heat_cool.value == 2 assert acc.char_current_temp.value == 24.0 assert acc.char_display_units.value == 0 hass.states.async_set( entity_id, - HVAC_MODE_HEAT_COOL, + HVAC_MODE_AUTO, { - ATTR_HVAC_MODE: HVAC_MODE_HEAT_COOL, ATTR_TARGET_TEMP_HIGH: 23.0, ATTR_TARGET_TEMP_LOW: 19.0, ATTR_CURRENT_TEMPERATURE: 21.0, @@ -346,7 +364,6 @@ async def test_thermostat_power_state(hass, hk_driver, cls, events): HVAC_MODE_HEAT, { ATTR_SUPPORTED_FEATURES: 4096, - ATTR_HVAC_MODE: HVAC_MODE_HEAT, ATTR_TEMPERATURE: 23.0, ATTR_CURRENT_TEMPERATURE: 18.0, ATTR_HVAC_ACTION: CURRENT_HVAC_HEAT, @@ -364,7 +381,6 @@ async def test_thermostat_power_state(hass, hk_driver, cls, events): entity_id, HVAC_MODE_OFF, { - ATTR_HVAC_MODE: HVAC_MODE_HEAT, ATTR_TEMPERATURE: 23.0, ATTR_CURRENT_TEMPERATURE: 18.0, ATTR_HVAC_ACTION: CURRENT_HVAC_IDLE, @@ -378,7 +394,6 @@ async def test_thermostat_power_state(hass, hk_driver, cls, events): entity_id, HVAC_MODE_OFF, { - ATTR_HVAC_MODE: HVAC_MODE_OFF, ATTR_TEMPERATURE: 23.0, ATTR_CURRENT_TEMPERATURE: 18.0, ATTR_HVAC_ACTION: CURRENT_HVAC_IDLE, @@ -423,7 +438,6 @@ async def test_thermostat_fahrenheit(hass, hk_driver, cls, events): entity_id, HVAC_MODE_HEAT_COOL, { - ATTR_HVAC_MODE: HVAC_MODE_HEAT_COOL, ATTR_TARGET_TEMP_HIGH: 75.2, ATTR_TARGET_TEMP_LOW: 68.1, ATTR_TEMPERATURE: 71.6, @@ -503,6 +517,34 @@ async def test_thermostat_temperature_step_whole(hass, hk_driver, cls): assert acc.char_target_temp.properties[PROP_MIN_STEP] == 1.0 +async def test_thermostat_hvac_modes(hass, hk_driver, cls): + """Test if unsupported HVAC modes are deactivated in HomeKit.""" + entity_id = "climate.test" + + hass.states.async_set( + entity_id, HVAC_MODE_OFF, {ATTR_HVAC_MODES: [HVAC_MODE_HEAT, HVAC_MODE_OFF]} + ) + + await hass.async_block_till_done() + acc = cls.thermostat(hass, hk_driver, "Climate", entity_id, 2, None) + await hass.async_add_job(acc.run) + await hass.async_block_till_done() + + with pytest.raises(ValueError): + await hass.async_add_job(acc.char_target_heat_cool.set_value, 3) + await hass.async_block_till_done() + assert acc.char_target_heat_cool.value == 0 + + await hass.async_add_job(acc.char_target_heat_cool.set_value, 1) + await hass.async_block_till_done() + assert acc.char_target_heat_cool.value == 1 + + with pytest.raises(ValueError): + await hass.async_add_job(acc.char_target_heat_cool.set_value, 2) + await hass.async_block_till_done() + assert acc.char_target_heat_cool.value == 1 + + async def test_water_heater(hass, hk_driver, cls, events): """Test if accessory and HA are updated accordingly.""" entity_id = "water_heater.test" @@ -571,7 +613,8 @@ async def test_water_heater(hass, hk_driver, cls, events): await hass.async_block_till_done() assert acc.char_target_heat_cool.value == 1 - await hass.async_add_job(acc.char_target_heat_cool.client_update_value, 3) + with pytest.raises(ValueError): + await hass.async_add_job(acc.char_target_heat_cool.set_value, 3) await hass.async_block_till_done() assert acc.char_target_heat_cool.value == 1 diff --git a/tests/components/homematicip_cloud/conftest.py b/tests/components/homematicip_cloud/conftest.py index f60f8d659b54..fa19f573c7c1 100644 --- a/tests/components/homematicip_cloud/conftest.py +++ b/tests/components/homematicip_cloud/conftest.py @@ -27,7 +27,9 @@ def mock_connection_fixture() -> AsyncConnection: def _rest_call_side_effect(path, body=None): return path, body - connection._restCall.side_effect = _rest_call_side_effect # pylint: disable=W0212 + connection._restCall.side_effect = ( # pylint: disable=protected-access + _rest_call_side_effect + ) connection.api_call.return_value = mock_coro(True) connection.init.side_effect = mock_coro(True) diff --git a/tests/components/homematicip_cloud/helper.py b/tests/components/homematicip_cloud/helper.py index 494ec2dc90bc..42ff2061698c 100644 --- a/tests/components/homematicip_cloud/helper.py +++ b/tests/components/homematicip_cloud/helper.py @@ -58,7 +58,9 @@ async def async_manipulate_test_data( fire_target = hmip_device if fire_device is None else fire_device if isinstance(fire_target, AsyncHome): - fire_target.fire_update_event(fire_target._rawJSONData) # pylint: disable=W0212 + fire_target.fire_update_event( + fire_target._rawJSONData # pylint: disable=protected-access + ) else: fire_target.fire_update_event() @@ -136,7 +138,9 @@ def get_async_home_mock(self): def _get_mock(instance): """Create a mock and copy instance attributes over mock.""" if isinstance(instance, Mock): - instance.__dict__.update(instance._mock_wraps.__dict__) # pylint: disable=W0212 + instance.__dict__.update( + instance._mock_wraps.__dict__ # pylint: disable=protected-access + ) return instance mock = Mock(spec=instance, wraps=instance) diff --git a/tests/components/homematicip_cloud/test_alarm_control_panel.py b/tests/components/homematicip_cloud/test_alarm_control_panel.py index 78bc0a09ea50..cf85e805143d 100644 --- a/tests/components/homematicip_cloud/test_alarm_control_panel.py +++ b/tests/components/homematicip_cloud/test_alarm_control_panel.py @@ -18,7 +18,7 @@ async def _async_manipulate_security_zones( hass, home, internal_active=False, external_active=False, alarm_triggered=False ): """Set new values on hmip security zones.""" - json = home._rawJSONData # pylint: disable=W0212 + json = home._rawJSONData # pylint: disable=protected-access json["functionalHomes"]["SECURITY_AND_ALARM"]["alarmActive"] = alarm_triggered external_zone_id = json["functionalHomes"]["SECURITY_AND_ALARM"]["securityZones"][ "EXTERNAL" diff --git a/tests/components/homematicip_cloud/test_climate.py b/tests/components/homematicip_cloud/test_climate.py index 2b233a6dee2c..db0529294748 100644 --- a/tests/components/homematicip_cloud/test_climate.py +++ b/tests/components/homematicip_cloud/test_climate.py @@ -7,8 +7,11 @@ from homeassistant.components.climate import DOMAIN as CLIMATE_DOMAIN from homeassistant.components.climate.const import ( ATTR_CURRENT_TEMPERATURE, + ATTR_HVAC_ACTION, ATTR_PRESET_MODE, ATTR_PRESET_MODES, + CURRENT_HVAC_HEAT, + CURRENT_HVAC_IDLE, HVAC_MODE_AUTO, HVAC_MODE_COOL, HVAC_MODE_HEAT, @@ -215,6 +218,17 @@ async def test_hmip_heating_group_heat(hass, default_mock_hap): # Only fire event from last async_manipulate_test_data available. assert hmip_device.mock_calls[-1][0] == "fire_update_event" + await async_manipulate_test_data(hass, hmip_device, "floorHeatingMode", "RADIATOR") + await async_manipulate_test_data(hass, hmip_device, "valvePosition", 0.1) + ha_state = hass.states.get(entity_id) + assert ha_state.state == HVAC_MODE_AUTO + assert ha_state.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_HEAT + await async_manipulate_test_data(hass, hmip_device, "floorHeatingMode", "RADIATOR") + await async_manipulate_test_data(hass, hmip_device, "valvePosition", 0.0) + ha_state = hass.states.get(entity_id) + assert ha_state.state == HVAC_MODE_AUTO + assert ha_state.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_IDLE + async def test_hmip_heating_group_cool(hass, default_mock_hap): """Test HomematicipHeatingGroup.""" @@ -367,7 +381,7 @@ async def test_hmip_climate_services(hass, mock_hap_with_service): ) assert home.mock_calls[-1][0] == "activate_absence_with_duration" assert home.mock_calls[-1][1] == (60,) - assert len(home._connection.mock_calls) == 1 # pylint: disable=W0212 + assert len(home._connection.mock_calls) == 1 # pylint: disable=protected-access await hass.services.async_call( "homematicip_cloud", @@ -377,7 +391,7 @@ async def test_hmip_climate_services(hass, mock_hap_with_service): ) assert home.mock_calls[-1][0] == "activate_absence_with_duration" assert home.mock_calls[-1][1] == (60,) - assert len(home._connection.mock_calls) == 2 # pylint: disable=W0212 + assert len(home._connection.mock_calls) == 2 # pylint: disable=protected-access await hass.services.async_call( "homematicip_cloud", @@ -387,7 +401,7 @@ async def test_hmip_climate_services(hass, mock_hap_with_service): ) assert home.mock_calls[-1][0] == "activate_absence_with_period" assert home.mock_calls[-1][1] == (datetime.datetime(2019, 2, 17, 14, 0),) - assert len(home._connection.mock_calls) == 3 # pylint: disable=W0212 + assert len(home._connection.mock_calls) == 3 # pylint: disable=protected-access await hass.services.async_call( "homematicip_cloud", @@ -397,7 +411,7 @@ async def test_hmip_climate_services(hass, mock_hap_with_service): ) assert home.mock_calls[-1][0] == "activate_absence_with_period" assert home.mock_calls[-1][1] == (datetime.datetime(2019, 2, 17, 14, 0),) - assert len(home._connection.mock_calls) == 4 # pylint: disable=W0212 + assert len(home._connection.mock_calls) == 4 # pylint: disable=protected-access await hass.services.async_call( "homematicip_cloud", @@ -407,7 +421,7 @@ async def test_hmip_climate_services(hass, mock_hap_with_service): ) assert home.mock_calls[-1][0] == "activate_vacation" assert home.mock_calls[-1][1] == (datetime.datetime(2019, 2, 17, 14, 0), 18.5) - assert len(home._connection.mock_calls) == 5 # pylint: disable=W0212 + assert len(home._connection.mock_calls) == 5 # pylint: disable=protected-access await hass.services.async_call( "homematicip_cloud", @@ -417,7 +431,7 @@ async def test_hmip_climate_services(hass, mock_hap_with_service): ) assert home.mock_calls[-1][0] == "activate_vacation" assert home.mock_calls[-1][1] == (datetime.datetime(2019, 2, 17, 14, 0), 18.5) - assert len(home._connection.mock_calls) == 6 # pylint: disable=W0212 + assert len(home._connection.mock_calls) == 6 # pylint: disable=protected-access await hass.services.async_call( "homematicip_cloud", @@ -427,14 +441,14 @@ async def test_hmip_climate_services(hass, mock_hap_with_service): ) assert home.mock_calls[-1][0] == "deactivate_absence" assert home.mock_calls[-1][1] == () - assert len(home._connection.mock_calls) == 7 # pylint: disable=W0212 + assert len(home._connection.mock_calls) == 7 # pylint: disable=protected-access await hass.services.async_call( "homematicip_cloud", "deactivate_eco_mode", blocking=True ) assert home.mock_calls[-1][0] == "deactivate_absence" assert home.mock_calls[-1][1] == () - assert len(home._connection.mock_calls) == 8 # pylint: disable=W0212 + assert len(home._connection.mock_calls) == 8 # pylint: disable=protected-access await hass.services.async_call( "homematicip_cloud", @@ -444,14 +458,14 @@ async def test_hmip_climate_services(hass, mock_hap_with_service): ) assert home.mock_calls[-1][0] == "deactivate_vacation" assert home.mock_calls[-1][1] == () - assert len(home._connection.mock_calls) == 9 # pylint: disable=W0212 + assert len(home._connection.mock_calls) == 9 # pylint: disable=protected-access await hass.services.async_call( "homematicip_cloud", "deactivate_vacation", blocking=True ) assert home.mock_calls[-1][0] == "deactivate_vacation" assert home.mock_calls[-1][1] == () - assert len(home._connection.mock_calls) == 10 # pylint: disable=W0212 + assert len(home._connection.mock_calls) == 10 # pylint: disable=protected-access not_existing_hap_id = "5555F7110000000000000001" await hass.services.async_call( @@ -463,7 +477,7 @@ async def test_hmip_climate_services(hass, mock_hap_with_service): assert home.mock_calls[-1][0] == "deactivate_vacation" assert home.mock_calls[-1][1] == () # There is no further call on connection. - assert len(home._connection.mock_calls) == 10 # pylint: disable=W0212 + assert len(home._connection.mock_calls) == 10 # pylint: disable=protected-access async def test_hmip_heating_group_services(hass, mock_hap_with_service): @@ -485,7 +499,9 @@ async def test_hmip_heating_group_services(hass, mock_hap_with_service): ) assert hmip_device.mock_calls[-1][0] == "set_active_profile" assert hmip_device.mock_calls[-1][1] == (1,) - assert len(hmip_device._connection.mock_calls) == 2 # pylint: disable=W0212 + assert ( + len(hmip_device._connection.mock_calls) == 2 # pylint: disable=protected-access + ) await hass.services.async_call( "homematicip_cloud", @@ -495,4 +511,7 @@ async def test_hmip_heating_group_services(hass, mock_hap_with_service): ) assert hmip_device.mock_calls[-1][0] == "set_active_profile" assert hmip_device.mock_calls[-1][1] == (1,) - assert len(hmip_device._connection.mock_calls) == 12 # pylint: disable=W0212 + assert ( + len(hmip_device._connection.mock_calls) # pylint: disable=protected-access + == 12 + ) diff --git a/tests/components/homematicip_cloud/test_device.py b/tests/components/homematicip_cloud/test_device.py index 812f32a33442..77f99655c989 100644 --- a/tests/components/homematicip_cloud/test_device.py +++ b/tests/components/homematicip_cloud/test_device.py @@ -105,7 +105,7 @@ async def test_hap_reconnected(hass, default_mock_hap): ha_state = hass.states.get(entity_id) assert ha_state.state == STATE_UNAVAILABLE - default_mock_hap._accesspoint_connected = False # pylint: disable=W0212 + default_mock_hap._accesspoint_connected = False # pylint: disable=protected-access await async_manipulate_test_data(hass, default_mock_hap.home, "connected", True) await hass.async_block_till_done() ha_state = hass.states.get(entity_id) diff --git a/tests/components/hue/test_config_flow.py b/tests/components/hue/test_config_flow.py index 54082464a7c3..a6d221ef3234 100644 --- a/tests/components/hue/test_config_flow.py +++ b/tests/components/hue/test_config_flow.py @@ -230,6 +230,26 @@ async def test_bridge_ssdp_emulated_hue(hass): ) assert result["type"] == "abort" + assert result["reason"] == "not_hue_bridge" + + +async def test_bridge_ssdp_espalexa(hass): + """Test if discovery info is from an Espalexa based device.""" + flow = config_flow.HueFlowHandler() + flow.hass = hass + flow.context = {} + + result = await flow.async_step_ssdp( + { + "name": "Espalexa (0.0.0.0)", + "host": "0.0.0.0", + "serial": "1234", + "manufacturerURL": config_flow.HUE_MANUFACTURERURL, + } + ) + + assert result["type"] == "abort" + assert result["reason"] == "not_hue_bridge" async def test_bridge_ssdp_already_configured(hass): diff --git a/tests/components/hue/test_light.py b/tests/components/hue/test_light.py index 88c527a50ca8..5b10ff9446cd 100644 --- a/tests/components/hue/test_light.py +++ b/tests/components/hue/test_light.py @@ -204,6 +204,10 @@ async def mock_request(method, path, **kwargs): return bridge.mock_group_responses.popleft() return None + async def async_request_call(coro): + await coro + + bridge.async_request_call = async_request_call bridge.api.config.apiversion = "9.9.9" bridge.api.lights = Lights({}, mock_request) bridge.api.groups = Groups({}, mock_request) diff --git a/tests/components/hue/test_sensor_base.py b/tests/components/hue/test_sensor_base.py index ba259dccf710..ad927767c307 100644 --- a/tests/components/hue/test_sensor_base.py +++ b/tests/components/hue/test_sensor_base.py @@ -277,6 +277,10 @@ async def mock_request(method, path, **kwargs): return bridge.mock_sensor_responses.popleft() return None + async def async_request_call(coro): + await coro + + bridge.async_request_call = async_request_call bridge.api.config.apiversion = "9.9.9" bridge.api.sensors = Sensors({}, mock_request) return bridge diff --git a/tests/components/image_processing/common.py b/tests/components/image_processing/common.py index b767884503dc..8522353d3f29 100644 --- a/tests/components/image_processing/common.py +++ b/tests/components/image_processing/common.py @@ -4,20 +4,20 @@ components. Instead call the service directly. """ from homeassistant.components.image_processing import DOMAIN, SERVICE_SCAN -from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.const import ATTR_ENTITY_ID, ENTITY_MATCH_ALL from homeassistant.core import callback from homeassistant.loader import bind_hass @bind_hass -def scan(hass, entity_id=None): +def scan(hass, entity_id=ENTITY_MATCH_ALL): """Force process of all cameras or given entity.""" hass.add_job(async_scan, hass, entity_id) @callback @bind_hass -def async_scan(hass, entity_id=None): +def async_scan(hass, entity_id=ENTITY_MATCH_ALL): """Force process of all cameras or given entity.""" data = {ATTR_ENTITY_ID: entity_id} if entity_id else None hass.async_add_job(hass.services.async_call(DOMAIN, SERVICE_SCAN, data)) diff --git a/tests/components/input_datetime/test_reproduce_state.py b/tests/components/input_datetime/test_reproduce_state.py index 71f0658923c3..428b5189117c 100644 --- a/tests/components/input_datetime/test_reproduce_state.py +++ b/tests/components/input_datetime/test_reproduce_state.py @@ -36,10 +36,19 @@ async def test_reproducing_states(hass, caplog): # Test invalid state is handled await hass.helpers.state.async_reproduce_state( - [State("input_datetime.entity_datetime", "not_supported")], blocking=True + [ + State("input_datetime.entity_datetime", "not_supported"), + State("input_datetime.entity_datetime", "not-valid-date"), + State("input_datetime.entity_datetime", "not:valid:time"), + State("input_datetime.entity_datetime", "1234-56-78 90:12:34"), + ], + blocking=True, ) assert "not_supported" in caplog.text + assert "not-valid-date" in caplog.text + assert "not:valid:time" in caplog.text + assert "1234-56-78 90:12:34" in caplog.text assert len(datetime_calls) == 0 # Make sure correct services are called diff --git a/tests/components/intent/__init__.py b/tests/components/intent/__init__.py new file mode 100644 index 000000000000..463f53d921c1 --- /dev/null +++ b/tests/components/intent/__init__.py @@ -0,0 +1 @@ +"""Tests for the Intent integration.""" diff --git a/tests/components/intent/test_init.py b/tests/components/intent/test_init.py new file mode 100644 index 000000000000..76a0399c6887 --- /dev/null +++ b/tests/components/intent/test_init.py @@ -0,0 +1,76 @@ +"""Tests for Intent component.""" +import pytest + +from homeassistant.setup import async_setup_component +from homeassistant.helpers import intent +from homeassistant.components.cover import SERVICE_OPEN_COVER + +from tests.common import async_mock_service + + +async def test_http_handle_intent(hass, hass_client, hass_admin_user): + """Test handle intent via HTTP API.""" + + class TestIntentHandler(intent.IntentHandler): + """Test Intent Handler.""" + + intent_type = "OrderBeer" + + async def async_handle(self, intent): + """Handle the intent.""" + assert intent.context.user_id == hass_admin_user.id + response = intent.create_response() + response.async_set_speech( + "I've ordered a {}!".format(intent.slots["type"]["value"]) + ) + response.async_set_card( + "Beer ordered", "You chose a {}.".format(intent.slots["type"]["value"]) + ) + return response + + intent.async_register(hass, TestIntentHandler()) + + result = await async_setup_component(hass, "intent", {}) + assert result + + client = await hass_client() + resp = await client.post( + "/api/intent/handle", json={"name": "OrderBeer", "data": {"type": "Belgian"}} + ) + + assert resp.status == 200 + data = await resp.json() + + assert data == { + "card": { + "simple": {"content": "You chose a Belgian.", "title": "Beer ordered"} + }, + "speech": {"plain": {"extra_data": None, "speech": "I've ordered a Belgian!"}}, + } + + +async def test_cover_intents_loading(hass): + """Test Cover Intents Loading.""" + assert await async_setup_component(hass, "intent", {}) + + with pytest.raises(intent.UnknownIntent): + await intent.async_handle( + hass, "test", "HassOpenCover", {"name": {"value": "garage door"}} + ) + + assert await async_setup_component(hass, "cover", {}) + + hass.states.async_set("cover.garage_door", "closed") + calls = async_mock_service(hass, "cover", SERVICE_OPEN_COVER) + + response = await intent.async_handle( + hass, "test", "HassOpenCover", {"name": {"value": "garage door"}} + ) + await hass.async_block_till_done() + + assert response.speech["plain"]["speech"] == "Opened garage door" + assert len(calls) == 1 + call = calls[0] + assert call.domain == "cover" + assert call.service == "open_cover" + assert call.data == {"entity_id": "cover.garage_door"} diff --git a/tests/components/jewish_calendar/test_sensor.py b/tests/components/jewish_calendar/test_sensor.py index 07e0b7cb192a..60ffdea8c708 100644 --- a/tests/components/jewish_calendar/test_sensor.py +++ b/tests/components/jewish_calendar/test_sensor.py @@ -180,8 +180,9 @@ async def test_jewish_calendar_sensor( assert sensor_object.state == str(result) if sensor == "holiday": - assert sensor_object.attributes.get("type") == "YOM_TOV" assert sensor_object.attributes.get("id") == "rosh_hashana_i" + assert sensor_object.attributes.get("type") == "YOM_TOV" + assert sensor_object.attributes.get("type_id") == 1 SHABBAT_PARAMS = [ diff --git a/tests/components/light/common.py b/tests/components/light/common.py index 416c02884dcc..32678bf4daa0 100644 --- a/tests/components/light/common.py +++ b/tests/components/light/common.py @@ -24,6 +24,7 @@ SERVICE_TOGGLE, SERVICE_TURN_OFF, SERVICE_TURN_ON, + ENTITY_MATCH_ALL, ) from homeassistant.loader import bind_hass @@ -31,7 +32,7 @@ @bind_hass def turn_on( hass, - entity_id=None, + entity_id=ENTITY_MATCH_ALL, transition=None, brightness=None, brightness_pct=None, @@ -69,7 +70,7 @@ def turn_on( async def async_turn_on( hass, - entity_id=None, + entity_id=ENTITY_MATCH_ALL, transition=None, brightness=None, brightness_pct=None, @@ -110,12 +111,12 @@ async def async_turn_on( @bind_hass -def turn_off(hass, entity_id=None, transition=None): +def turn_off(hass, entity_id=ENTITY_MATCH_ALL, transition=None): """Turn all or specified light off.""" hass.add_job(async_turn_off, hass, entity_id, transition) -async def async_turn_off(hass, entity_id=None, transition=None): +async def async_turn_off(hass, entity_id=ENTITY_MATCH_ALL, transition=None): """Turn all or specified light off.""" data = { key: value @@ -127,12 +128,12 @@ async def async_turn_off(hass, entity_id=None, transition=None): @bind_hass -def toggle(hass, entity_id=None, transition=None): +def toggle(hass, entity_id=ENTITY_MATCH_ALL, transition=None): """Toggle all or specified light.""" hass.add_job(async_toggle, hass, entity_id, transition) -async def async_toggle(hass, entity_id=None, transition=None): +async def async_toggle(hass, entity_id=ENTITY_MATCH_ALL, transition=None): """Toggle all or specified light.""" data = { key: value diff --git a/tests/components/light/test_init.py b/tests/components/light/test_init.py index 8ceda6cbd3ef..2cf13369bd98 100644 --- a/tests/components/light/test_init.py +++ b/tests/components/light/test_init.py @@ -18,13 +18,10 @@ SERVICE_TURN_ON, SERVICE_TURN_OFF, SERVICE_TOGGLE, - ATTR_SUPPORTED_FEATURES, ) from homeassistant.components import light -from homeassistant.helpers.intent import IntentHandleError from tests.common import ( - async_mock_service, mock_service, get_test_home_assistant, mock_storage, @@ -433,89 +430,10 @@ def _mock_open(path, *args, **kwargs): assert {light.ATTR_HS_COLOR: (50.353, 100), light.ATTR_BRIGHTNESS: 100} == data -async def test_intent_set_color(hass): - """Test the set color intent.""" - hass.states.async_set( - "light.hello_2", "off", {ATTR_SUPPORTED_FEATURES: light.SUPPORT_COLOR} - ) - hass.states.async_set("switch.hello", "off") - calls = async_mock_service(hass, light.DOMAIN, light.SERVICE_TURN_ON) - hass.helpers.intent.async_register(light.SetIntentHandler()) - - result = await hass.helpers.intent.async_handle( - "test", - light.INTENT_SET, - {"name": {"value": "Hello"}, "color": {"value": "blue"}}, - ) - await hass.async_block_till_done() - - assert result.speech["plain"]["speech"] == "Changed hello 2 to the color blue" - - assert len(calls) == 1 - call = calls[0] - assert call.domain == light.DOMAIN - assert call.service == SERVICE_TURN_ON - assert call.data.get(ATTR_ENTITY_ID) == "light.hello_2" - assert call.data.get(light.ATTR_RGB_COLOR) == (0, 0, 255) - - -async def test_intent_set_color_tests_feature(hass): - """Test the set color intent.""" - hass.states.async_set("light.hello", "off") - calls = async_mock_service(hass, light.DOMAIN, light.SERVICE_TURN_ON) - hass.helpers.intent.async_register(light.SetIntentHandler()) - - try: - await hass.helpers.intent.async_handle( - "test", - light.INTENT_SET, - {"name": {"value": "Hello"}, "color": {"value": "blue"}}, - ) - assert False, "handling intent should have raised" - except IntentHandleError as err: - assert str(err) == "Entity hello does not support changing colors" - - assert len(calls) == 0 - - -async def test_intent_set_color_and_brightness(hass): - """Test the set color intent.""" - hass.states.async_set( - "light.hello_2", - "off", - {ATTR_SUPPORTED_FEATURES: (light.SUPPORT_COLOR | light.SUPPORT_BRIGHTNESS)}, - ) - hass.states.async_set("switch.hello", "off") - calls = async_mock_service(hass, light.DOMAIN, light.SERVICE_TURN_ON) - hass.helpers.intent.async_register(light.SetIntentHandler()) - - result = await hass.helpers.intent.async_handle( - "test", - light.INTENT_SET, - { - "name": {"value": "Hello"}, - "color": {"value": "blue"}, - "brightness": {"value": "20"}, - }, - ) - await hass.async_block_till_done() - - assert ( - result.speech["plain"]["speech"] - == "Changed hello 2 to the color blue and 20% brightness" - ) - - assert len(calls) == 1 - call = calls[0] - assert call.domain == light.DOMAIN - assert call.service == SERVICE_TURN_ON - assert call.data.get(ATTR_ENTITY_ID) == "light.hello_2" - assert call.data.get(light.ATTR_RGB_COLOR) == (0, 0, 255) - assert call.data.get(light.ATTR_BRIGHTNESS_PCT) == 20 - - async def test_light_context(hass, hass_admin_user): """Test that light context works.""" + platform = getattr(hass.components, "test.light") + platform.init() assert await async_setup_component(hass, "light", {"light": {"platform": "test"}}) state = hass.states.get("light.ceiling") @@ -537,6 +455,8 @@ async def test_light_context(hass, hass_admin_user): async def test_light_turn_on_auth(hass, hass_admin_user): """Test that light context works.""" + platform = getattr(hass.components, "test.light") + platform.init() assert await async_setup_component(hass, "light", {"light": {"platform": "test"}}) state = hass.states.get("light.ceiling") diff --git a/tests/components/light/test_intent.py b/tests/components/light/test_intent.py new file mode 100644 index 000000000000..594c9a5d1fc7 --- /dev/null +++ b/tests/components/light/test_intent.py @@ -0,0 +1,88 @@ +"""Tests for the light intents.""" +from homeassistant.helpers.intent import IntentHandleError + +from homeassistant.const import ATTR_SUPPORTED_FEATURES, SERVICE_TURN_ON, ATTR_ENTITY_ID +from homeassistant.components import light +from homeassistant.components.light import intent +from tests.common import async_mock_service + + +async def test_intent_set_color(hass): + """Test the set color intent.""" + hass.states.async_set( + "light.hello_2", "off", {ATTR_SUPPORTED_FEATURES: light.SUPPORT_COLOR} + ) + hass.states.async_set("switch.hello", "off") + calls = async_mock_service(hass, light.DOMAIN, light.SERVICE_TURN_ON) + await intent.async_setup_intents(hass) + + result = await hass.helpers.intent.async_handle( + "test", + intent.INTENT_SET, + {"name": {"value": "Hello"}, "color": {"value": "blue"}}, + ) + await hass.async_block_till_done() + + assert result.speech["plain"]["speech"] == "Changed hello 2 to the color blue" + + assert len(calls) == 1 + call = calls[0] + assert call.domain == light.DOMAIN + assert call.service == SERVICE_TURN_ON + assert call.data.get(ATTR_ENTITY_ID) == "light.hello_2" + assert call.data.get(light.ATTR_RGB_COLOR) == (0, 0, 255) + + +async def test_intent_set_color_tests_feature(hass): + """Test the set color intent.""" + hass.states.async_set("light.hello", "off") + calls = async_mock_service(hass, light.DOMAIN, light.SERVICE_TURN_ON) + await intent.async_setup_intents(hass) + + try: + await hass.helpers.intent.async_handle( + "test", + intent.INTENT_SET, + {"name": {"value": "Hello"}, "color": {"value": "blue"}}, + ) + assert False, "handling intent should have raised" + except IntentHandleError as err: + assert str(err) == "Entity hello does not support changing colors" + + assert len(calls) == 0 + + +async def test_intent_set_color_and_brightness(hass): + """Test the set color intent.""" + hass.states.async_set( + "light.hello_2", + "off", + {ATTR_SUPPORTED_FEATURES: (light.SUPPORT_COLOR | light.SUPPORT_BRIGHTNESS)}, + ) + hass.states.async_set("switch.hello", "off") + calls = async_mock_service(hass, light.DOMAIN, light.SERVICE_TURN_ON) + await intent.async_setup_intents(hass) + + result = await hass.helpers.intent.async_handle( + "test", + intent.INTENT_SET, + { + "name": {"value": "Hello"}, + "color": {"value": "blue"}, + "brightness": {"value": "20"}, + }, + ) + await hass.async_block_till_done() + + assert ( + result.speech["plain"]["speech"] + == "Changed hello 2 to the color blue and 20% brightness" + ) + + assert len(calls) == 1 + call = calls[0] + assert call.domain == light.DOMAIN + assert call.service == SERVICE_TURN_ON + assert call.data.get(ATTR_ENTITY_ID) == "light.hello_2" + assert call.data.get(light.ATTR_RGB_COLOR) == (0, 0, 255) + assert call.data.get(light.ATTR_BRIGHTNESS_PCT) == 20 diff --git a/tests/components/litejet/test_light.py b/tests/components/litejet/test_light.py index 1fc6f1df94e4..e4ca1c2106ea 100644 --- a/tests/components/litejet/test_light.py +++ b/tests/components/litejet/test_light.py @@ -21,7 +21,7 @@ class TestLiteJetLight(unittest.TestCase): """Test the litejet component.""" - @mock.patch("pylitejet.LiteJet") + @mock.patch("homeassistant.components.litejet.LiteJet") def setup_method(self, method, mock_pylitejet): """Set up things to be run when tests are started.""" self.hass = get_test_home_assistant() diff --git a/tests/components/litejet/test_scene.py b/tests/components/litejet/test_scene.py index d9ab561b6e14..0f42ac40cdf8 100644 --- a/tests/components/litejet/test_scene.py +++ b/tests/components/litejet/test_scene.py @@ -20,7 +20,7 @@ class TestLiteJetScene(unittest.TestCase): """Test the litejet component.""" - @mock.patch("pylitejet.LiteJet") + @mock.patch("homeassistant.components.litejet.LiteJet") def setup_method(self, method, mock_pylitejet): """Set up things to be run when tests are started.""" self.hass = get_test_home_assistant() diff --git a/tests/components/litejet/test_switch.py b/tests/components/litejet/test_switch.py index b7f980872548..a9cf54dc1f68 100644 --- a/tests/components/litejet/test_switch.py +++ b/tests/components/litejet/test_switch.py @@ -21,7 +21,7 @@ class TestLiteJetSwitch(unittest.TestCase): """Test the litejet component.""" - @mock.patch("pylitejet.LiteJet") + @mock.patch("homeassistant.components.litejet.LiteJet") def setup_method(self, method, mock_pylitejet): """Set up things to be run when tests are started.""" self.hass = get_test_home_assistant() diff --git a/tests/components/local_file/test_camera.py b/tests/components/local_file/test_camera.py index ae71954bf4ae..042b0f764001 100644 --- a/tests/components/local_file/test_camera.py +++ b/tests/components/local_file/test_camera.py @@ -2,8 +2,7 @@ import asyncio from unittest import mock -from homeassistant.components.camera.const import DOMAIN -from homeassistant.components.local_file.camera import SERVICE_UPDATE_FILE_PATH +from homeassistant.components.local_file.const import DOMAIN, SERVICE_UPDATE_FILE_PATH from homeassistant.setup import async_setup_component from tests.common import mock_registry diff --git a/tests/components/lock/common.py b/tests/components/lock/common.py index 2ad8f075bce5..a658befca55d 100644 --- a/tests/components/lock/common.py +++ b/tests/components/lock/common.py @@ -10,12 +10,13 @@ SERVICE_LOCK, SERVICE_UNLOCK, SERVICE_OPEN, + ENTITY_MATCH_ALL, ) from homeassistant.loader import bind_hass @bind_hass -def lock(hass, entity_id=None, code=None): +def lock(hass, entity_id=ENTITY_MATCH_ALL, code=None): """Lock all or specified locks.""" data = {} if code: @@ -26,7 +27,7 @@ def lock(hass, entity_id=None, code=None): hass.services.call(DOMAIN, SERVICE_LOCK, data) -async def async_lock(hass, entity_id=None, code=None): +async def async_lock(hass, entity_id=ENTITY_MATCH_ALL, code=None): """Lock all or specified locks.""" data = {} if code: @@ -38,7 +39,7 @@ async def async_lock(hass, entity_id=None, code=None): @bind_hass -def unlock(hass, entity_id=None, code=None): +def unlock(hass, entity_id=ENTITY_MATCH_ALL, code=None): """Unlock all or specified locks.""" data = {} if code: @@ -49,7 +50,7 @@ def unlock(hass, entity_id=None, code=None): hass.services.call(DOMAIN, SERVICE_UNLOCK, data) -async def async_unlock(hass, entity_id=None, code=None): +async def async_unlock(hass, entity_id=ENTITY_MATCH_ALL, code=None): """Lock all or specified locks.""" data = {} if code: @@ -61,7 +62,7 @@ async def async_unlock(hass, entity_id=None, code=None): @bind_hass -def open_lock(hass, entity_id=None, code=None): +def open_lock(hass, entity_id=ENTITY_MATCH_ALL, code=None): """Open all or specified locks.""" data = {} if code: @@ -72,7 +73,7 @@ def open_lock(hass, entity_id=None, code=None): hass.services.call(DOMAIN, SERVICE_OPEN, data) -async def async_open_lock(hass, entity_id=None, code=None): +async def async_open_lock(hass, entity_id=ENTITY_MATCH_ALL, code=None): """Lock all or specified locks.""" data = {} if code: diff --git a/tests/components/logi_circle/test_config_flow.py b/tests/components/logi_circle/test_config_flow.py index ebfae2af4513..0e2ef29da94e 100644 --- a/tests/components/logi_circle/test_config_flow.py +++ b/tests/components/logi_circle/test_config_flow.py @@ -9,14 +9,11 @@ from homeassistant.components.logi_circle.config_flow import ( DOMAIN, LogiCircleAuthCallbackView, + AuthorizationFailed, ) from homeassistant.setup import async_setup_component -from tests.common import MockDependency, mock_coro - - -class AuthorizationFailed(Exception): - """Dummy Exception.""" +from tests.common import mock_coro class MockRequest: @@ -40,7 +37,7 @@ def init_config_flow(hass): sensors=None, ) flow = config_flow.LogiCircleFlowHandler() - flow._get_authorization_url = Mock( # pylint: disable=W0212 + flow._get_authorization_url = Mock( # pylint: disable=protected-access return_value="http://example.com" ) flow.hass = hass @@ -50,22 +47,20 @@ def init_config_flow(hass): @pytest.fixture def mock_logi_circle(): """Mock logi_circle.""" - with MockDependency("logi_circle", "exception") as mock_logi_circle_: - mock_logi_circle_.exception.AuthorizationFailed = AuthorizationFailed - mock_logi_circle_.LogiCircle().authorize = Mock( - return_value=mock_coro(return_value=True) - ) - mock_logi_circle_.LogiCircle().close = Mock( - return_value=mock_coro(return_value=True) - ) - mock_logi_circle_.LogiCircle().account = mock_coro( - return_value={"accountId": "testId"} - ) - mock_logi_circle_.LogiCircle().authorize_url = "http://authorize.url" - yield mock_logi_circle_ - - -async def test_step_import(hass, mock_logi_circle): # pylint: disable=W0621 + with patch( + "homeassistant.components.logi_circle.config_flow.LogiCircle" + ) as logi_circle: + LogiCircle = logi_circle() + LogiCircle.authorize = Mock(return_value=mock_coro(return_value=True)) + LogiCircle.close = Mock(return_value=mock_coro(return_value=True)) + LogiCircle.account = mock_coro(return_value={"accountId": "testId"}) + LogiCircle.authorize_url = "http://authorize.url" + yield LogiCircle + + +async def test_step_import( + hass, mock_logi_circle # pylint: disable=redefined-outer-name +): """Test that we trigger import when configuring with client.""" flow = init_config_flow(hass) @@ -75,8 +70,8 @@ async def test_step_import(hass, mock_logi_circle): # pylint: disable=W0621 async def test_full_flow_implementation( - hass, mock_logi_circle -): # noqa pylint: disable=W0621 + hass, mock_logi_circle # pylint: disable=redefined-outer-name +): """Test registering an implementation and finishing flow works.""" config_flow.register_flow_implementation( hass, @@ -154,10 +149,10 @@ async def test_abort_if_already_setup(hass): ) async def test_abort_if_authorize_fails( hass, mock_logi_circle, side_effect, error -): # noqa pylint: disable=W0621 +): # pylint: disable=redefined-outer-name """Test we abort if authorizing fails.""" flow = init_config_flow(hass) - mock_logi_circle.LogiCircle().authorize.side_effect = side_effect + mock_logi_circle.authorize.side_effect = side_effect result = await flow.async_step_code("123ABC") assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT @@ -176,7 +171,9 @@ async def test_not_pick_implementation_if_only_one(hass): assert result["step_id"] == "auth" -async def test_gen_auth_url(hass, mock_logi_circle): # pylint: disable=W0621 +async def test_gen_auth_url( + hass, mock_logi_circle +): # pylint: disable=redefined-outer-name """Test generating authorize URL from Logi Circle API.""" config_flow.register_flow_implementation( hass, @@ -192,7 +189,7 @@ async def test_gen_auth_url(hass, mock_logi_circle): # pylint: disable=W0621 flow.flow_impl = "test-auth-url" await async_setup_component(hass, "http", {}) - result = flow._get_authorization_url() # pylint: disable=W0212 + result = flow._get_authorization_url() # pylint: disable=protected-access assert result == "http://authorize.url" @@ -206,7 +203,7 @@ async def test_callback_view_rejects_missing_code(hass): async def test_callback_view_accepts_code( hass, mock_logi_circle -): # noqa pylint: disable=W0621 +): # pylint: disable=redefined-outer-name """Test the auth callback view handles requests with auth code.""" init_config_flow(hass) view = LogiCircleAuthCallbackView() @@ -215,4 +212,4 @@ async def test_callback_view_accepts_code( assert resp.status == 200 await hass.async_block_till_done() - mock_logi_circle.LogiCircle.return_value.authorize.assert_called_with("456") + mock_logi_circle.authorize.assert_called_with("456") diff --git a/tests/components/marytts/test_tts.py b/tests/components/marytts/test_tts.py index 275cf17e22af..5692d72d388a 100644 --- a/tests/components/marytts/test_tts.py +++ b/tests/components/marytts/test_tts.py @@ -12,7 +12,7 @@ from tests.common import get_test_home_assistant, assert_setup_component, mock_service -from tests.components.tts.test_init import mutagen_mock # noqa +from tests.components.tts.test_init import mutagen_mock # noqa: F401 class TestTTSMaryTTSPlatform: diff --git a/tests/components/media_player/common.py b/tests/components/media_player/common.py index 1d1f811c91ec..177f8169ff95 100644 --- a/tests/components/media_player/common.py +++ b/tests/components/media_player/common.py @@ -32,47 +32,48 @@ SERVICE_VOLUME_MUTE, SERVICE_VOLUME_SET, SERVICE_VOLUME_UP, + ENTITY_MATCH_ALL, ) from homeassistant.loader import bind_hass @bind_hass -def turn_on(hass, entity_id=None): +def turn_on(hass, entity_id=ENTITY_MATCH_ALL): """Turn on specified media player or all.""" data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} hass.services.call(DOMAIN, SERVICE_TURN_ON, data) @bind_hass -def turn_off(hass, entity_id=None): +def turn_off(hass, entity_id=ENTITY_MATCH_ALL): """Turn off specified media player or all.""" data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} hass.services.call(DOMAIN, SERVICE_TURN_OFF, data) @bind_hass -def toggle(hass, entity_id=None): +def toggle(hass, entity_id=ENTITY_MATCH_ALL): """Toggle specified media player or all.""" data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} hass.services.call(DOMAIN, SERVICE_TOGGLE, data) @bind_hass -def volume_up(hass, entity_id=None): +def volume_up(hass, entity_id=ENTITY_MATCH_ALL): """Send the media player the command for volume up.""" data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} hass.services.call(DOMAIN, SERVICE_VOLUME_UP, data) @bind_hass -def volume_down(hass, entity_id=None): +def volume_down(hass, entity_id=ENTITY_MATCH_ALL): """Send the media player the command for volume down.""" data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} hass.services.call(DOMAIN, SERVICE_VOLUME_DOWN, data) @bind_hass -def mute_volume(hass, mute, entity_id=None): +def mute_volume(hass, mute, entity_id=ENTITY_MATCH_ALL): """Send the media player the command for muting the volume.""" data = {ATTR_MEDIA_VOLUME_MUTED: mute} @@ -83,7 +84,7 @@ def mute_volume(hass, mute, entity_id=None): @bind_hass -def set_volume_level(hass, volume, entity_id=None): +def set_volume_level(hass, volume, entity_id=ENTITY_MATCH_ALL): """Send the media player the command for setting the volume.""" data = {ATTR_MEDIA_VOLUME_LEVEL: volume} @@ -94,49 +95,49 @@ def set_volume_level(hass, volume, entity_id=None): @bind_hass -def media_play_pause(hass, entity_id=None): +def media_play_pause(hass, entity_id=ENTITY_MATCH_ALL): """Send the media player the command for play/pause.""" data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} hass.services.call(DOMAIN, SERVICE_MEDIA_PLAY_PAUSE, data) @bind_hass -def media_play(hass, entity_id=None): +def media_play(hass, entity_id=ENTITY_MATCH_ALL): """Send the media player the command for play/pause.""" data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} hass.services.call(DOMAIN, SERVICE_MEDIA_PLAY, data) @bind_hass -def media_pause(hass, entity_id=None): +def media_pause(hass, entity_id=ENTITY_MATCH_ALL): """Send the media player the command for pause.""" data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} hass.services.call(DOMAIN, SERVICE_MEDIA_PAUSE, data) @bind_hass -def media_stop(hass, entity_id=None): +def media_stop(hass, entity_id=ENTITY_MATCH_ALL): """Send the media player the command for stop.""" data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} hass.services.call(DOMAIN, SERVICE_MEDIA_STOP, data) @bind_hass -def media_next_track(hass, entity_id=None): +def media_next_track(hass, entity_id=ENTITY_MATCH_ALL): """Send the media player the command for next track.""" data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} hass.services.call(DOMAIN, SERVICE_MEDIA_NEXT_TRACK, data) @bind_hass -def media_previous_track(hass, entity_id=None): +def media_previous_track(hass, entity_id=ENTITY_MATCH_ALL): """Send the media player the command for prev track.""" data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} hass.services.call(DOMAIN, SERVICE_MEDIA_PREVIOUS_TRACK, data) @bind_hass -def media_seek(hass, position, entity_id=None): +def media_seek(hass, position, entity_id=ENTITY_MATCH_ALL): """Send the media player the command to seek in current playing media.""" data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} data[ATTR_MEDIA_SEEK_POSITION] = position @@ -144,7 +145,7 @@ def media_seek(hass, position, entity_id=None): @bind_hass -def play_media(hass, media_type, media_id, entity_id=None, enqueue=None): +def play_media(hass, media_type, media_id, entity_id=ENTITY_MATCH_ALL, enqueue=None): """Send the media player the command for playing media.""" data = {ATTR_MEDIA_CONTENT_TYPE: media_type, ATTR_MEDIA_CONTENT_ID: media_id} @@ -158,7 +159,7 @@ def play_media(hass, media_type, media_id, entity_id=None, enqueue=None): @bind_hass -def select_source(hass, source, entity_id=None): +def select_source(hass, source, entity_id=ENTITY_MATCH_ALL): """Send the media player the command to select input source.""" data = {ATTR_INPUT_SOURCE: source} @@ -169,7 +170,7 @@ def select_source(hass, source, entity_id=None): @bind_hass -def clear_playlist(hass, entity_id=None): +def clear_playlist(hass, entity_id=ENTITY_MATCH_ALL): """Send the media player the command for clear playlist.""" data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} hass.services.call(DOMAIN, SERVICE_CLEAR_PLAYLIST, data) diff --git a/tests/components/mfi/test_sensor.py b/tests/components/mfi/test_sensor.py index da472308fc2f..6849578bbb9a 100644 --- a/tests/components/mfi/test_sensor.py +++ b/tests/components/mfi/test_sensor.py @@ -3,6 +3,7 @@ import unittest.mock as mock import requests +from mficlient.client import FailedToLogin from homeassistant.setup import setup_component import homeassistant.components.sensor as sensor @@ -38,28 +39,26 @@ def teardown_method(self, method): """Stop everything that was started.""" self.hass.stop() - @mock.patch("mficlient.client.MFiClient") + @mock.patch("homeassistant.components.mfi.sensor.MFiClient") def test_setup_missing_config(self, mock_client): """Test setup with missing configuration.""" config = {"sensor": {"platform": "mfi"}} assert setup_component(self.hass, "sensor", config) assert not mock_client.called - @mock.patch("mficlient.client.MFiClient") + @mock.patch("homeassistant.components.mfi.sensor.MFiClient") def test_setup_failed_login(self, mock_client): """Test setup with login failure.""" - from mficlient.client import FailedToLogin - mock_client.side_effect = FailedToLogin assert not self.PLATFORM.setup_platform(self.hass, dict(self.GOOD_CONFIG), None) - @mock.patch("mficlient.client.MFiClient") + @mock.patch("homeassistant.components.mfi.sensor.MFiClient") def test_setup_failed_connect(self, mock_client): """Test setup with connection failure.""" mock_client.side_effect = requests.exceptions.ConnectionError assert not self.PLATFORM.setup_platform(self.hass, dict(self.GOOD_CONFIG), None) - @mock.patch("mficlient.client.MFiClient") + @mock.patch("homeassistant.components.mfi.sensor.MFiClient") def test_setup_minimum(self, mock_client): """Test setup with minimum configuration.""" config = dict(self.GOOD_CONFIG) @@ -70,7 +69,7 @@ def test_setup_minimum(self, mock_client): "foo", "user", "pass", port=6443, use_tls=True, verify=True ) - @mock.patch("mficlient.client.MFiClient") + @mock.patch("homeassistant.components.mfi.sensor.MFiClient") def test_setup_with_port(self, mock_client): """Test setup with port.""" config = dict(self.GOOD_CONFIG) @@ -81,7 +80,7 @@ def test_setup_with_port(self, mock_client): "foo", "user", "pass", port=6123, use_tls=True, verify=True ) - @mock.patch("mficlient.client.MFiClient") + @mock.patch("homeassistant.components.mfi.sensor.MFiClient") def test_setup_with_tls_disabled(self, mock_client): """Test setup without TLS.""" config = dict(self.GOOD_CONFIG) @@ -94,7 +93,7 @@ def test_setup_with_tls_disabled(self, mock_client): "foo", "user", "pass", port=6080, use_tls=False, verify=False ) - @mock.patch("mficlient.client.MFiClient") + @mock.patch("homeassistant.components.mfi.sensor.MFiClient") @mock.patch("homeassistant.components.mfi.sensor.MfiSensor") def test_setup_adds_proper_devices(self, mock_sensor, mock_client): """Test if setup adds devices.""" diff --git a/tests/components/mfi/test_switch.py b/tests/components/mfi/test_switch.py index 11a6c402ad69..ebddc8c5bc25 100644 --- a/tests/components/mfi/test_switch.py +++ b/tests/components/mfi/test_switch.py @@ -5,12 +5,11 @@ from homeassistant.setup import setup_component import homeassistant.components.switch as switch import homeassistant.components.mfi.switch as mfi -from tests.components.mfi import test_sensor as test_mfi_sensor from tests.common import get_test_home_assistant -class TestMfiSwitchSetup(test_mfi_sensor.TestMfiSensorSetup): +class TestMfiSwitchSetup(unittest.TestCase): """Test the mFi switch.""" PLATFORM = mfi @@ -28,7 +27,15 @@ class TestMfiSwitchSetup(test_mfi_sensor.TestMfiSensorSetup): } } - @mock.patch("mficlient.client.MFiClient") + def setup_method(self, method): + """Set up things to be run when tests are started.""" + self.hass = get_test_home_assistant() + + def teardown_method(self, method): + """Stop everything that was started.""" + self.hass.stop() + + @mock.patch("homeassistant.components.mfi.switch.MFiClient") @mock.patch("homeassistant.components.mfi.switch.MfiSwitch") def test_setup_adds_proper_devices(self, mock_switch, mock_client): """Test if setup adds devices.""" diff --git a/tests/components/mobile_app/conftest.py b/tests/components/mobile_app/conftest.py index 33af3c2b4a76..1d653b73ba3a 100644 --- a/tests/components/mobile_app/conftest.py +++ b/tests/components/mobile_app/conftest.py @@ -18,7 +18,7 @@ def registry(hass): @pytest.fixture -async def create_registrations(authed_api_client): +async def create_registrations(hass, authed_api_client): """Return two new registrations.""" enc_reg = await authed_api_client.post( "/api/mobile_app/registrations", json=REGISTER @@ -34,6 +34,8 @@ async def create_registrations(authed_api_client): assert clear_reg.status == 201 clear_reg_json = await clear_reg.json() + await hass.async_block_till_done() + return (enc_reg_json, clear_reg_json) diff --git a/tests/components/mobile_app/test_entity.py b/tests/components/mobile_app/test_entity.py index f2693f25585a..0db9d42048fc 100644 --- a/tests/components/mobile_app/test_entity.py +++ b/tests/components/mobile_app/test_entity.py @@ -1,13 +1,13 @@ """Entity tests for mobile_app.""" -# pylint: disable=redefined-outer-name,unused-import + import logging +from homeassistant.helpers import device_registry + _LOGGER = logging.getLogger(__name__) -async def test_sensor( - hass, create_registrations, webhook_client -): # noqa: F401, F811, E501 +async def test_sensor(hass, create_registrations, webhook_client): """Test that sensors can be registered and updated.""" webhook_id = create_registrations[1]["webhook_id"] webhook_url = "/api/webhook/{}".format(webhook_id) @@ -66,10 +66,11 @@ async def test_sensor( updated_entity = hass.states.get("sensor.battery_state") assert updated_entity.state == "123" + dev_reg = await device_registry.async_get_registry(hass) + assert len(dev_reg.devices) == len(create_registrations) + -async def test_sensor_must_register( - hass, create_registrations, webhook_client # noqa: F401, F811, E501 -): # noqa: F401, F811, E501 +async def test_sensor_must_register(hass, create_registrations, webhook_client): """Test that sensors must be registered before updating.""" webhook_id = create_registrations[1]["webhook_id"] webhook_url = "/api/webhook/{}".format(webhook_id) @@ -88,9 +89,7 @@ async def test_sensor_must_register( assert json["battery_state"]["error"]["code"] == "not_registered" -async def test_sensor_id_no_dupes( - hass, create_registrations, webhook_client # noqa: F401, F811, E501 -): # noqa: F401, F811, E501 +async def test_sensor_id_no_dupes(hass, create_registrations, webhook_client): """Test that sensors must have a unique ID.""" webhook_id = create_registrations[1]["webhook_id"] webhook_url = "/api/webhook/{}".format(webhook_id) diff --git a/tests/components/mobile_app/test_http_api.py b/tests/components/mobile_app/test_http_api.py index 3c05f495e403..158d3ffe2135 100644 --- a/tests/components/mobile_app/test_http_api.py +++ b/tests/components/mobile_app/test_http_api.py @@ -12,9 +12,8 @@ async def test_registration(hass, hass_client): """Test that registrations happen.""" try: - # pylint: disable=unused-import - from nacl.secret import SecretBox # noqa: F401 - from nacl.encoding import Base64Encoder # noqa: F401 + from nacl.secret import SecretBox + from nacl.encoding import Base64Encoder except (ImportError, OSError): pytest.skip("libnacl/libsodium is not installed") return diff --git a/tests/components/mobile_app/test_webhook.py b/tests/components/mobile_app/test_webhook.py index 9274aa60a841..6e8efe15dd0d 100644 --- a/tests/components/mobile_app/test_webhook.py +++ b/tests/components/mobile_app/test_webhook.py @@ -1,5 +1,5 @@ """Webhook tests for mobile_app.""" -# pylint: disable=redefined-outer-name,unused-import + import logging import pytest @@ -29,9 +29,7 @@ async def test_webhook_handle_render_template(create_registrations, webhook_clie assert json == {"one": "Hello world"} -async def test_webhook_handle_call_services( - hass, create_registrations, webhook_client -): # noqa: E501 F811 +async def test_webhook_handle_call_services(hass, create_registrations, webhook_client): """Test that we call services properly.""" calls = async_mock_service(hass, "test", "mobile_app") @@ -68,9 +66,7 @@ def store_event(event): assert events[0].data["hello"] == "yo world" -async def test_webhook_update_registration( - webhook_client, hass_client -): # noqa: E501 F811 +async def test_webhook_update_registration(webhook_client, hass_client): """Test that a we can update an existing registration via webhook.""" authed_api_client = await hass_client() register_resp = await authed_api_client.post( @@ -156,7 +152,7 @@ async def test_webhook_handle_get_config(hass, create_registrations, webhook_cli async def test_webhook_returns_error_incorrect_json( webhook_client, create_registrations, caplog -): # noqa: E501 F811 +): """Test that an error is returned when JSON is invalid.""" resp = await webhook_client.post( "/api/webhook/{}".format(create_registrations[1]["webhook_id"]), data="not json" @@ -171,9 +167,8 @@ async def test_webhook_returns_error_incorrect_json( async def test_webhook_handle_decryption(webhook_client, create_registrations): """Test that we can encrypt/decrypt properly.""" try: - # pylint: disable=unused-import - from nacl.secret import SecretBox # noqa: F401 - from nacl.encoding import Base64Encoder # noqa: F401 + from nacl.secret import SecretBox + from nacl.encoding import Base64Encoder except (ImportError, OSError): pytest.skip("libnacl/libsodium is not installed") return @@ -221,3 +216,23 @@ async def test_webhook_requires_encryption(webhook_client, create_registrations) assert "error" in webhook_json assert webhook_json["success"] is False assert webhook_json["error"]["code"] == "encryption_required" + + +async def test_webhook_update_location(hass, webhook_client, create_registrations): + """Test that encrypted registrations only accept encrypted data.""" + resp = await webhook_client.post( + "/api/webhook/{}".format(create_registrations[1]["webhook_id"]), + json={ + "type": "update_location", + "data": {"gps": [1, 2], "gps_accuracy": 10, "altitude": -10}, + }, + ) + + assert resp.status == 200 + + state = hass.states.get("device_tracker.test_1_2") + assert state is not None + assert state.attributes["latitude"] == 1.0 + assert state.attributes["longitude"] == 2.0 + assert state.attributes["gps_accuracy"] == 10 + assert state.attributes["altitude"] == -10 diff --git a/tests/components/monoprice/test_media_player.py b/tests/components/monoprice/test_media_player.py index 36110e6d909d..a33b85539083 100644 --- a/tests/components/monoprice/test_media_player.py +++ b/tests/components/monoprice/test_media_player.py @@ -5,7 +5,6 @@ from collections import defaultdict from homeassistant.components.media_player.const import ( - DOMAIN, SUPPORT_TURN_ON, SUPPORT_TURN_OFF, SUPPORT_VOLUME_MUTE, @@ -19,10 +18,13 @@ from homeassistant.components.monoprice.media_player import ( DATA_MONOPRICE, PLATFORM_SCHEMA, - SERVICE_SNAPSHOT, - SERVICE_RESTORE, setup_platform, ) +from homeassistant.components.monoprice.const import ( + DOMAIN, + SERVICE_RESTORE, + SERVICE_SNAPSHOT, +) import pytest diff --git a/tests/components/mqtt/test_climate.py b/tests/components/mqtt/test_climate.py index 3f4fc6571864..648448a6494b 100644 --- a/tests/components/mqtt/test_climate.py +++ b/tests/components/mqtt/test_climate.py @@ -23,6 +23,7 @@ HVAC_MODE_FAN_ONLY, SUPPORT_TARGET_TEMPERATURE_RANGE, PRESET_NONE, + PRESET_ECO, ) from homeassistant.components.mqtt.discovery import async_start from homeassistant.const import STATE_OFF, STATE_UNAVAILABLE @@ -446,6 +447,19 @@ async def test_set_away_mode(hass, mqtt_mock): state = hass.states.get(ENTITY_CLIMATE) assert state.attributes.get("preset_mode") is None + await common.async_set_preset_mode(hass, "hold-on", ENTITY_CLIMATE) + mqtt_mock.async_publish.reset_mock() + + await common.async_set_preset_mode(hass, "away", ENTITY_CLIMATE) + mqtt_mock.async_publish.assert_has_calls( + [ + unittest.mock.call("hold-topic", "off", 0, False), + unittest.mock.call("away-mode-topic", "AN", 0, False), + ] + ) + state = hass.states.get(ENTITY_CLIMATE) + assert state.attributes.get("preset_mode") == "away" + async def test_set_hvac_action(hass, mqtt_mock): """Test setting of the HVAC action.""" @@ -495,6 +509,12 @@ async def test_set_hold(hass, mqtt_mock): state = hass.states.get(ENTITY_CLIMATE) assert state.attributes.get("preset_mode") == "hold-on" + await common.async_set_preset_mode(hass, PRESET_ECO, ENTITY_CLIMATE) + mqtt_mock.async_publish.assert_called_once_with("hold-topic", "eco", 0, False) + mqtt_mock.async_publish.reset_mock() + state = hass.states.get(ENTITY_CLIMATE) + assert state.attributes.get("preset_mode") == PRESET_ECO + await common.async_set_preset_mode(hass, PRESET_NONE, ENTITY_CLIMATE) mqtt_mock.async_publish.assert_called_once_with("hold-topic", "off", 0, False) state = hass.states.get(ENTITY_CLIMATE) diff --git a/tests/components/mqtt/test_server.py b/tests/components/mqtt/test_server.py index 71dff7ef3acc..3627c95040e0 100644 --- a/tests/components/mqtt/test_server.py +++ b/tests/components/mqtt/test_server.py @@ -19,13 +19,9 @@ def teardown_method(self, method): """Stop everything that was started.""" self.hass.stop() - @patch( - "homeassistant.components.mqtt.server.custom_app_context", Mock(return_value="") - ) + @patch("passlib.apps.custom_app_context", Mock(return_value="")) @patch("tempfile.NamedTemporaryFile", Mock(return_value=MagicMock())) - @patch( - "homeassistant.components.mqtt.server.Broker", Mock(return_value=MagicMock()) - ) + @patch("hbmqtt.broker.Broker", Mock(return_value=MagicMock())) @patch("hbmqtt.broker.Broker.start", Mock(return_value=mock_coro())) @patch("homeassistant.components.mqtt.MQTT") def test_creating_config_with_pass_and_no_http_pass(self, mock_mqtt): @@ -45,13 +41,9 @@ def test_creating_config_with_pass_and_no_http_pass(self, mock_mqtt): assert mock_mqtt.mock_calls[1][2]["username"] == "homeassistant" assert mock_mqtt.mock_calls[1][2]["password"] == password - @patch( - "homeassistant.components.mqtt.server.custom_app_context", Mock(return_value="") - ) + @patch("passlib.apps.custom_app_context", Mock(return_value="")) @patch("tempfile.NamedTemporaryFile", Mock(return_value=MagicMock())) - @patch( - "homeassistant.components.mqtt.server.Broker", Mock(return_value=MagicMock()) - ) + @patch("hbmqtt.broker.Broker", Mock(return_value=MagicMock())) @patch("hbmqtt.broker.Broker.start", Mock(return_value=mock_coro())) @patch("homeassistant.components.mqtt.MQTT") def test_creating_config_with_pass_and_http_pass(self, mock_mqtt): diff --git a/tests/components/mqtt/test_state_vacuum.py b/tests/components/mqtt/test_state_vacuum.py index 572c3b057524..7919e07767d5 100644 --- a/tests/components/mqtt/test_state_vacuum.py +++ b/tests/components/mqtt/test_state_vacuum.py @@ -28,6 +28,7 @@ CONF_PLATFORM, STATE_UNAVAILABLE, STATE_UNKNOWN, + ENTITY_MATCH_ALL, ) from homeassistant.setup import async_setup_component @@ -75,29 +76,41 @@ async def test_all_commands(hass, mqtt_mock): assert await async_setup_component(hass, vacuum.DOMAIN, {vacuum.DOMAIN: config}) - await hass.services.async_call(DOMAIN, SERVICE_START, blocking=True) + await hass.services.async_call( + DOMAIN, SERVICE_START, {"entity_id": ENTITY_MATCH_ALL}, blocking=True + ) mqtt_mock.async_publish.assert_called_once_with(COMMAND_TOPIC, "start", 0, False) mqtt_mock.async_publish.reset_mock() - await hass.services.async_call(DOMAIN, SERVICE_STOP, blocking=True) + await hass.services.async_call( + DOMAIN, SERVICE_STOP, {"entity_id": ENTITY_MATCH_ALL}, blocking=True + ) mqtt_mock.async_publish.assert_called_once_with(COMMAND_TOPIC, "stop", 0, False) mqtt_mock.async_publish.reset_mock() - await hass.services.async_call(DOMAIN, SERVICE_PAUSE, blocking=True) + await hass.services.async_call( + DOMAIN, SERVICE_PAUSE, {"entity_id": ENTITY_MATCH_ALL}, blocking=True + ) mqtt_mock.async_publish.assert_called_once_with(COMMAND_TOPIC, "pause", 0, False) mqtt_mock.async_publish.reset_mock() - await hass.services.async_call(DOMAIN, SERVICE_LOCATE, blocking=True) + await hass.services.async_call( + DOMAIN, SERVICE_LOCATE, {"entity_id": ENTITY_MATCH_ALL}, blocking=True + ) mqtt_mock.async_publish.assert_called_once_with(COMMAND_TOPIC, "locate", 0, False) mqtt_mock.async_publish.reset_mock() - await hass.services.async_call(DOMAIN, SERVICE_CLEAN_SPOT, blocking=True) + await hass.services.async_call( + DOMAIN, SERVICE_CLEAN_SPOT, {"entity_id": ENTITY_MATCH_ALL}, blocking=True + ) mqtt_mock.async_publish.assert_called_once_with( COMMAND_TOPIC, "clean_spot", 0, False ) mqtt_mock.async_publish.reset_mock() - await hass.services.async_call(DOMAIN, SERVICE_RETURN_TO_BASE, blocking=True) + await hass.services.async_call( + DOMAIN, SERVICE_RETURN_TO_BASE, {"entity_id": ENTITY_MATCH_ALL}, blocking=True + ) mqtt_mock.async_publish.assert_called_once_with( COMMAND_TOPIC, "return_to_base", 0, False ) @@ -134,27 +147,39 @@ async def test_commands_without_supported_features(hass, mqtt_mock): assert await async_setup_component(hass, vacuum.DOMAIN, {vacuum.DOMAIN: config}) - await hass.services.async_call(DOMAIN, SERVICE_START, blocking=True) + await hass.services.async_call( + DOMAIN, SERVICE_START, {"entity_id": ENTITY_MATCH_ALL}, blocking=True + ) mqtt_mock.async_publish.assert_not_called() mqtt_mock.async_publish.reset_mock() - await hass.services.async_call(DOMAIN, SERVICE_PAUSE, blocking=True) + await hass.services.async_call( + DOMAIN, SERVICE_PAUSE, {"entity_id": ENTITY_MATCH_ALL}, blocking=True + ) mqtt_mock.async_publish.assert_not_called() mqtt_mock.async_publish.reset_mock() - await hass.services.async_call(DOMAIN, SERVICE_STOP, blocking=True) + await hass.services.async_call( + DOMAIN, SERVICE_STOP, {"entity_id": ENTITY_MATCH_ALL}, blocking=True + ) mqtt_mock.async_publish.assert_not_called() mqtt_mock.async_publish.reset_mock() - await hass.services.async_call(DOMAIN, SERVICE_RETURN_TO_BASE, blocking=True) + await hass.services.async_call( + DOMAIN, SERVICE_RETURN_TO_BASE, {"entity_id": ENTITY_MATCH_ALL}, blocking=True + ) mqtt_mock.async_publish.assert_not_called() mqtt_mock.async_publish.reset_mock() - await hass.services.async_call(DOMAIN, SERVICE_LOCATE, blocking=True) + await hass.services.async_call( + DOMAIN, SERVICE_LOCATE, {"entity_id": ENTITY_MATCH_ALL}, blocking=True + ) mqtt_mock.async_publish.assert_not_called() mqtt_mock.async_publish.reset_mock() - await hass.services.async_call(DOMAIN, SERVICE_CLEAN_SPOT, blocking=True) + await hass.services.async_call( + DOMAIN, SERVICE_CLEAN_SPOT, {"entity_id": ENTITY_MATCH_ALL}, blocking=True + ) mqtt_mock.async_publish.assert_not_called() mqtt_mock.async_publish.reset_mock() diff --git a/tests/components/nsw_rural_fire_service_feed/test_geo_location.py b/tests/components/nsw_rural_fire_service_feed/test_geo_location.py index f5f88087010a..274ef3d37431 100644 --- a/tests/components/nsw_rural_fire_service_feed/test_geo_location.py +++ b/tests/components/nsw_rural_fire_service_feed/test_geo_location.py @@ -1,5 +1,8 @@ -"""The tests for the geojson platform.""" +"""The tests for the NSW Rural Fire Service Feeds platform.""" import datetime +from unittest.mock import ANY + +from aio_geojson_nsw_rfs_incidents import NswRuralFireServiceIncidentsFeed from asynctest.mock import patch, MagicMock, call from homeassistant.components import geo_location @@ -20,6 +23,7 @@ from homeassistant.const import ( ATTR_ATTRIBUTION, ATTR_FRIENDLY_NAME, + ATTR_ICON, ATTR_LATITUDE, ATTR_LONGITUDE, ATTR_UNIT_OF_MEASUREMENT, @@ -27,7 +31,7 @@ CONF_LONGITUDE, CONF_RADIUS, EVENT_HOMEASSISTANT_START, - ATTR_ICON, + EVENT_HOMEASSISTANT_STOP, ) from homeassistant.setup import async_setup_component from tests.common import assert_setup_component, async_fire_time_changed @@ -110,12 +114,12 @@ async def test_setup(hass): mock_entry_3 = _generate_mock_feed_entry("3456", "Title 3", 25.5, (-31.2, 150.2)) mock_entry_4 = _generate_mock_feed_entry("4567", "Title 4", 12.5, (-31.3, 150.3)) - utcnow = dt_util.utcnow() # Patching 'utcnow' to gain more control over the timed update. + utcnow = dt_util.utcnow() with patch("homeassistant.util.dt.utcnow", return_value=utcnow), patch( - "geojson_client.nsw_rural_fire_service_feed." "NswRuralFireServiceFeed" - ) as mock_feed: - mock_feed.return_value.update.return_value = ( + "aio_geojson_client.feed.GeoJsonFeed.update" + ) as mock_feed_update: + mock_feed_update.return_value = ( "OK", [mock_entry_1, mock_entry_2, mock_entry_3], ) @@ -187,7 +191,7 @@ async def test_setup(hass): # Simulate an update - one existing, one new entry, # one outdated entry - mock_feed.return_value.update.return_value = ( + mock_feed_update.return_value = ( "OK", [mock_entry_1, mock_entry_4, mock_entry_3], ) @@ -199,7 +203,7 @@ async def test_setup(hass): # Simulate an update - empty data, but successful update, # so no changes to entities. - mock_feed.return_value.update.return_value = "OK_NO_DATA", None + mock_feed_update.return_value = "OK_NO_DATA", None async_fire_time_changed(hass, utcnow + 2 * SCAN_INTERVAL) await hass.async_block_till_done() @@ -207,13 +211,18 @@ async def test_setup(hass): assert len(all_states) == 3 # Simulate an update - empty data, removes all entities - mock_feed.return_value.update.return_value = "ERROR", None + mock_feed_update.return_value = "ERROR", None async_fire_time_changed(hass, utcnow + 3 * SCAN_INTERVAL) await hass.async_block_till_done() all_states = hass.states.async_all() assert len(all_states) == 0 + # Artificially trigger update. + hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) + # Collect events. + await hass.async_block_till_done() + async def test_setup_with_custom_location(hass): """Test the setup with a custom location.""" @@ -221,9 +230,12 @@ async def test_setup_with_custom_location(hass): mock_entry_1 = _generate_mock_feed_entry("1234", "Title 1", 20.5, (-31.1, 150.1)) with patch( - "geojson_client.nsw_rural_fire_service_feed." "NswRuralFireServiceFeed" - ) as mock_feed: - mock_feed.return_value.update.return_value = "OK", [mock_entry_1] + "aio_geojson_nsw_rfs_incidents.feed_manager.NswRuralFireServiceIncidentsFeed", + wraps=NswRuralFireServiceIncidentsFeed, + ) as mock_feed_manager, patch( + "aio_geojson_client.feed.GeoJsonFeed.update" + ) as mock_feed_update: + mock_feed_update.return_value = "OK", [mock_entry_1] with assert_setup_component(1, geo_location.DOMAIN): assert await async_setup_component( @@ -238,6 +250,6 @@ async def test_setup_with_custom_location(hass): all_states = hass.states.async_all() assert len(all_states) == 1 - assert mock_feed.call_args == call( - (15.1, 25.2), filter_categories=[], filter_radius=200.0 + assert mock_feed_manager.call_args == call( + ANY, (15.1, 25.2), filter_categories=[], filter_radius=200.0 ) diff --git a/tests/components/nuheat/test_climate.py b/tests/components/nuheat/test_climate.py index 1ab791dfd374..c35497968ace 100644 --- a/tests/components/nuheat/test_climate.py +++ b/tests/components/nuheat/test_climate.py @@ -67,7 +67,7 @@ def test_setup_platform(self, mocked_thermostat): thermostat = mocked_thermostat(self.api, "12345", "F") thermostats = [thermostat] - self.hass.data[nuheat.NUHEAT_DOMAIN] = (self.api, ["12345"]) + self.hass.data[nuheat.DOMAIN] = (self.api, ["12345"]) config = {} add_entities = Mock() @@ -85,12 +85,12 @@ def test_resume_program_service(self, mocked_thermostat): thermostat.schedule_update_ha_state = Mock() thermostat.entity_id = "climate.master_bathroom" - self.hass.data[nuheat.NUHEAT_DOMAIN] = (self.api, ["12345"]) + self.hass.data[nuheat.DOMAIN] = (self.api, ["12345"]) nuheat.setup_platform(self.hass, {}, Mock(), {}) # Explicit entity self.hass.services.call( - nuheat.NUHEAT_DOMAIN, + nuheat.DOMAIN, nuheat.SERVICE_RESUME_PROGRAM, {"entity_id": "climate.master_bathroom"}, True, @@ -103,9 +103,7 @@ def test_resume_program_service(self, mocked_thermostat): thermostat.schedule_update_ha_state.reset_mock() # All entities - self.hass.services.call( - nuheat.NUHEAT_DOMAIN, nuheat.SERVICE_RESUME_PROGRAM, {}, True - ) + self.hass.services.call(nuheat.DOMAIN, nuheat.SERVICE_RESUME_PROGRAM, {}, True) thermostat.resume_program.assert_called_with() thermostat.schedule_update_ha_state.assert_called_with(True) diff --git a/tests/components/onboarding/test_views.py b/tests/components/onboarding/test_views.py index bcb430b828d1..7881b75ee99a 100644 --- a/tests/components/onboarding/test_views.py +++ b/tests/components/onboarding/test_views.py @@ -9,13 +9,13 @@ from homeassistant.components.onboarding import const, views from tests.common import CLIENT_ID, register_auth_provider -from tests.components.met.conftest import mock_weather # noqa +from tests.components.met.conftest import mock_weather # noqa: F401 from . import mock_storage @pytest.fixture(autouse=True) -def always_mock_weather(mock_weather): # noqa +def always_mock_weather(mock_weather): # noqa: F811 """Mock the Met weather provider.""" pass diff --git a/tests/components/owntracks/test_device_tracker.py b/tests/components/owntracks/test_device_tracker.py index 5ba68474513a..3a7f3c030ed2 100644 --- a/tests/components/owntracks/test_device_tracker.py +++ b/tests/components/owntracks/test_device_tracker.py @@ -1406,6 +1406,25 @@ def config_context(hass, setup_comp): patch_save.stop() +@pytest.fixture(name="not_supports_encryption") +def mock_not_supports_encryption(): + """Mock non successful nacl import.""" + with patch( + "homeassistant.components.owntracks.messages.supports_encryption", + return_value=False, + ): + yield + + +@pytest.fixture(name="get_cipher_error") +def mock_get_cipher_error(): + """Mock non successful cipher.""" + with patch( + "homeassistant.components.owntracks.messages.get_cipher", side_effect=OSError() + ): + yield + + @patch("homeassistant.components.owntracks.messages.get_cipher", mock_cipher) async def test_encrypted_payload(hass, setup_comp): """Test encrypted payload.""" @@ -1422,6 +1441,22 @@ async def test_encrypted_payload_topic_key(hass, setup_comp): assert_location_latitude(hass, LOCATION_MESSAGE["lat"]) +async def test_encrypted_payload_not_supports_encryption( + hass, setup_comp, not_supports_encryption +): + """Test encrypted payload with no supported encryption.""" + await setup_owntracks(hass, {CONF_SECRET: TEST_SECRET_KEY}) + await send_message(hass, LOCATION_TOPIC, MOCK_ENCRYPTED_LOCATION_MESSAGE) + assert hass.states.get(DEVICE_TRACKER_STATE) is None + + +async def test_encrypted_payload_get_cipher_error(hass, setup_comp, get_cipher_error): + """Test encrypted payload with no supported encryption.""" + await setup_owntracks(hass, {CONF_SECRET: TEST_SECRET_KEY}) + await send_message(hass, LOCATION_TOPIC, MOCK_ENCRYPTED_LOCATION_MESSAGE) + assert hass.states.get(DEVICE_TRACKER_STATE) is None + + @patch("homeassistant.components.owntracks.messages.get_cipher", mock_cipher) async def test_encrypted_payload_no_key(hass, setup_comp): """Test encrypted payload with no key, .""" @@ -1460,8 +1495,7 @@ async def test_encrypted_payload_no_topic_key(hass, setup_comp): async def test_encrypted_payload_libsodium(hass, setup_comp): """Test sending encrypted message payload.""" try: - # pylint: disable=unused-import - import nacl # noqa: F401 + import nacl # noqa: F401 pylint: disable=unused-import except (ImportError, OSError): pytest.skip("PyNaCl/libsodium is not installed") return diff --git a/tests/components/owntracks/test_helper.py b/tests/components/owntracks/test_helper.py new file mode 100644 index 000000000000..f870ce82dd38 --- /dev/null +++ b/tests/components/owntracks/test_helper.py @@ -0,0 +1,29 @@ +"""Test the owntracks_http platform.""" +from unittest.mock import patch +import pytest + +from homeassistant.components.owntracks import helper + + +@pytest.fixture(name="nacl_imported") +def mock_nacl_imported(): + """Mock a successful import.""" + with patch("homeassistant.components.owntracks.helper.nacl"): + yield + + +@pytest.fixture(name="nacl_not_imported") +def mock_nacl_not_imported(): + """Mock non successful import.""" + with patch("homeassistant.components.owntracks.helper.nacl", new=None): + yield + + +def test_supports_encryption(nacl_imported): + """Test if env supports encryption.""" + assert helper.supports_encryption() + + +def test_supports_encryption_failed(nacl_not_imported): + """Test if env does not support encryption.""" + assert not helper.supports_encryption() diff --git a/tests/components/plex/mock_classes.py b/tests/components/plex/mock_classes.py index 69e6a84df636..1a680e6af0f0 100644 --- a/tests/components/plex/mock_classes.py +++ b/tests/components/plex/mock_classes.py @@ -30,7 +30,7 @@ def __init__(self, index): self.provides = ["server"] self._mock_plex_server = MockPlexServer(index) - def connect(self): + def connect(self, timeout): """Mock the resource connect method.""" return self._mock_plex_server diff --git a/tests/components/plex/test_config_flow.py b/tests/components/plex/test_config_flow.py index c0d14f1efdcb..668ac3b2a172 100644 --- a/tests/components/plex/test_config_flow.py +++ b/tests/components/plex/test_config_flow.py @@ -178,9 +178,8 @@ async def test_import_bad_hostname(hass): CONF_URL: f"http://{MOCK_SERVERS[0][CONF_HOST]}:{MOCK_SERVERS[0][CONF_PORT]}", }, ) - assert result["type"] == "form" - assert result["step_id"] == "start_website_auth" - assert result["errors"]["base"] == "not_found" + assert result["type"] == "abort" + assert result["reason"] == "non-interactive" async def test_unknown_exception(hass): @@ -384,12 +383,14 @@ async def test_already_configured(hass): mock_plex_server = MockPlexServer() flow = init_config_flow(hass) + flow.context = {"source": "import"} MockConfigEntry( domain=config_flow.DOMAIN, data={ + config_flow.CONF_SERVER: MOCK_SERVERS[0][config_flow.CONF_SERVER], config_flow.CONF_SERVER_IDENTIFIER: MOCK_SERVERS[0][ config_flow.CONF_SERVER_IDENTIFIER - ] + ], }, ).add_to_hass(hass) @@ -530,3 +531,20 @@ async def test_callback_view(hass, aiohttp_client): resp = await client.get(forward_url) assert resp.status == 200 + + +async def test_multiple_servers_with_import(hass): + """Test importing a config with multiple servers available.""" + + with patch( + "plexapi.myplex.MyPlexAccount", return_value=MockPlexAccount(servers=2) + ), asynctest.patch("plexauth.PlexAuth.initiate_auth"), asynctest.patch( + "plexauth.PlexAuth.token", return_value=MOCK_TOKEN + ): + result = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, + context={"source": "import"}, + data={CONF_TOKEN: MOCK_TOKEN}, + ) + assert result["type"] == "abort" + assert result["reason"] == "non-interactive" diff --git a/tests/components/point/test_config_flow.py b/tests/components/point/test_config_flow.py index 664cb412f755..c1c705e752de 100644 --- a/tests/components/point/test_config_flow.py +++ b/tests/components/point/test_config_flow.py @@ -7,14 +7,14 @@ from homeassistant import data_entry_flow from homeassistant.components.point import DOMAIN, config_flow -from tests.common import MockDependency, mock_coro +from tests.common import mock_coro def init_config_flow(hass, side_effect=None): """Init a configuration flow.""" config_flow.register_flow_implementation(hass, DOMAIN, "id", "secret") flow = config_flow.PointFlowHandler() - flow._get_authorization_url = Mock( # pylint: disable=W0212 + flow._get_authorization_url = Mock( # pylint: disable=protected-access return_value=mock_coro("https://example.com"), side_effect=side_effect ) flow.hass = hass @@ -28,17 +28,17 @@ def is_authorized(): @pytest.fixture -def mock_pypoint(is_authorized): # pylint: disable=W0621 +def mock_pypoint(is_authorized): # pylint: disable=redefined-outer-name """Mock pypoint.""" - with MockDependency("pypoint") as mock_pypoint_: - mock_pypoint_.PointSession().get_access_token.return_value = { + with patch( + "homeassistant.components.point.config_flow.PointSession" + ) as PointSession: + PointSession.return_value.get_access_token.return_value = { "access_token": "boo" } - mock_pypoint_.PointSession().is_authorized = is_authorized - mock_pypoint_.PointSession().user.return_value = { - "email": "john.doe@example.com" - } - yield mock_pypoint_ + PointSession.return_value.is_authorized = is_authorized + PointSession.return_value.user.return_value = {"email": "john.doe@example.com"} + yield PointSession async def test_abort_if_no_implementation_registered(hass): @@ -67,8 +67,8 @@ async def test_abort_if_already_setup(hass): async def test_full_flow_implementation( - hass, mock_pypoint -): # noqa pylint: disable=W0621 + hass, mock_pypoint # pylint: disable=redefined-outer-name +): """Test registering an implementation and finishing flow works.""" config_flow.register_flow_implementation(hass, "test-other", None, None) flow = init_config_flow(hass) @@ -94,7 +94,7 @@ async def test_full_flow_implementation( assert result["data"]["token"] == {"access_token": "boo"} -async def test_step_import(hass, mock_pypoint): # pylint: disable=W0621 +async def test_step_import(hass, mock_pypoint): # pylint: disable=redefined-outer-name """Test that we trigger import when configuring with client.""" flow = init_config_flow(hass) @@ -106,7 +106,7 @@ async def test_step_import(hass, mock_pypoint): # pylint: disable=W0621 @pytest.mark.parametrize("is_authorized", [False]) async def test_wrong_code_flow_implementation( hass, mock_pypoint -): # noqa pylint: disable=W0621 +): # pylint: disable=redefined-outer-name """Test wrong code.""" flow = init_config_flow(hass) diff --git a/tests/components/prometheus/test_init.py b/tests/components/prometheus/test_init.py index 4ec40731c5d2..cf1ff7489f6d 100644 --- a/tests/components/prometheus/test_init.py +++ b/tests/components/prometheus/test_init.py @@ -24,24 +24,26 @@ async def prometheus_client(loop, hass, hass_client): hass, climate.DOMAIN, {"climate": [{"platform": "demo"}]} ) - sensor1 = DemoSensor("Television Energy", 74, None, ENERGY_KILO_WATT_HOUR, None) + sensor1 = DemoSensor( + None, "Television Energy", 74, None, ENERGY_KILO_WATT_HOUR, None + ) sensor1.hass = hass sensor1.entity_id = "sensor.television_energy" await sensor1.async_update_ha_state() sensor2 = DemoSensor( - "Radio Energy", 14, DEVICE_CLASS_POWER, ENERGY_KILO_WATT_HOUR, None + None, "Radio Energy", 14, DEVICE_CLASS_POWER, ENERGY_KILO_WATT_HOUR, None ) sensor2.hass = hass sensor2.entity_id = "sensor.radio_energy" await sensor2.async_update_ha_state() - sensor3 = DemoSensor("Electricity price", 0.123, None, "SEK/kWh", None) + sensor3 = DemoSensor(None, "Electricity price", 0.123, None, "SEK/kWh", None) sensor3.hass = hass sensor3.entity_id = "sensor.electricity_price" await sensor3.async_update_ha_state() - sensor4 = DemoSensor("Wind Direction", 25, None, "°", None) + sensor4 = DemoSensor(None, "Wind Direction", 25, None, "°", None) sensor4.hass = hass sensor4.entity_id = "sensor.wind_direction" await sensor4.async_update_ha_state() diff --git a/tests/components/qwikswitch/test_init.py b/tests/components/qwikswitch/test_init.py index 126e4c91c393..e573e8cc293a 100644 --- a/tests/components/qwikswitch/test_init.py +++ b/tests/components/qwikswitch/test_init.py @@ -57,7 +57,7 @@ def aioclient_mock(): yield mock_session -async def test_binary_sensor_device(hass, aioclient_mock): # noqa +async def test_binary_sensor_device(hass, aioclient_mock): # noqa: F811 """Test a binary sensor device.""" config = { "qwikswitch": { @@ -86,7 +86,7 @@ async def test_binary_sensor_device(hass, aioclient_mock): # noqa assert state_obj.state == "off" -async def test_sensor_device(hass, aioclient_mock): # noqa +async def test_sensor_device(hass, aioclient_mock): # noqa: F811 """Test a sensor device.""" config = { "qwikswitch": { diff --git a/tests/components/rainmachine/test_config_flow.py b/tests/components/rainmachine/test_config_flow.py index f888f9d4642b..cfde51408220 100644 --- a/tests/components/rainmachine/test_config_flow.py +++ b/tests/components/rainmachine/test_config_flow.py @@ -1,6 +1,8 @@ """Define tests for the OpenUV config flow.""" from unittest.mock import patch +from regenmaschine.errors import RainMachineError + from homeassistant import data_entry_flow from homeassistant.components.rainmachine import DOMAIN, config_flow from homeassistant.const import ( @@ -33,8 +35,6 @@ async def test_duplicate_error(hass): async def test_invalid_password(hass): """Test that an invalid password throws an error.""" - from regenmaschine.errors import RainMachineError - conf = { CONF_IP_ADDRESS: "192.168.1.100", CONF_PASSWORD: "bad_password", @@ -46,7 +46,8 @@ async def test_invalid_password(hass): flow.hass = hass with patch( - "regenmaschine.login", return_value=mock_coro(exception=RainMachineError) + "homeassistant.components.rainmachine.config_flow.login", + return_value=mock_coro(exception=RainMachineError), ): result = await flow.async_step_user(user_input=conf) assert result["errors"] == {CONF_PASSWORD: "invalid_credentials"} @@ -75,7 +76,10 @@ async def test_step_import(hass): flow = config_flow.RainMachineFlowHandler() flow.hass = hass - with patch("regenmaschine.login", return_value=mock_coro(True)): + with patch( + "homeassistant.components.rainmachine.config_flow.login", + return_value=mock_coro(True), + ): result = await flow.async_step_import(import_config=conf) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY @@ -101,7 +105,10 @@ async def test_step_user(hass): flow = config_flow.RainMachineFlowHandler() flow.hass = hass - with patch("regenmaschine.login", return_value=mock_coro(True)): + with patch( + "homeassistant.components.rainmachine.config_flow.login", + return_value=mock_coro(True), + ): result = await flow.async_step_user(user_input=conf) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY diff --git a/tests/components/random/test_binary_sensor.py b/tests/components/random/test_binary_sensor.py index 5bc332e6da3c..a11b571dd83b 100644 --- a/tests/components/random/test_binary_sensor.py +++ b/tests/components/random/test_binary_sensor.py @@ -18,7 +18,7 @@ def teardown_method(self, method): """Stop everything that was started.""" self.hass.stop() - @patch("random.getrandbits", return_value=1) + @patch("homeassistant.components.random.binary_sensor.getrandbits", return_value=1) def test_random_binary_sensor_on(self, mocked): """Test the Random binary sensor.""" config = {"binary_sensor": {"platform": "random", "name": "test"}} @@ -29,7 +29,9 @@ def test_random_binary_sensor_on(self, mocked): assert state.state == "on" - @patch("random.getrandbits", return_value=False) + @patch( + "homeassistant.components.random.binary_sensor.getrandbits", return_value=False + ) def test_random_binary_sensor_off(self, mocked): """Test the Random binary sensor.""" config = {"binary_sensor": {"platform": "random", "name": "test"}} diff --git a/tests/components/remote/common.py b/tests/components/remote/common.py index a35489f1780f..d972640487aa 100644 --- a/tests/components/remote/common.py +++ b/tests/components/remote/common.py @@ -15,12 +15,17 @@ SERVICE_LEARN_COMMAND, SERVICE_SEND_COMMAND, ) -from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON +from homeassistant.const import ( + ATTR_ENTITY_ID, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + ENTITY_MATCH_ALL, +) from homeassistant.loader import bind_hass @bind_hass -def turn_on(hass, activity=None, entity_id=None): +def turn_on(hass, activity=None, entity_id=ENTITY_MATCH_ALL): """Turn all or specified remote on.""" data = { key: value @@ -31,7 +36,7 @@ def turn_on(hass, activity=None, entity_id=None): @bind_hass -def turn_off(hass, activity=None, entity_id=None): +def turn_off(hass, activity=None, entity_id=ENTITY_MATCH_ALL): """Turn all or specified remote off.""" data = {} if activity: @@ -45,7 +50,12 @@ def turn_off(hass, activity=None, entity_id=None): @bind_hass def send_command( - hass, command, entity_id=None, device=None, num_repeats=None, delay_secs=None + hass, + command, + entity_id=ENTITY_MATCH_ALL, + device=None, + num_repeats=None, + delay_secs=None, ): """Send a command to a device.""" data = {ATTR_COMMAND: command} @@ -66,7 +76,12 @@ def send_command( @bind_hass def learn_command( - hass, entity_id=None, device=None, command=None, alternative=None, timeout=None + hass, + entity_id=ENTITY_MATCH_ALL, + device=None, + command=None, + alternative=None, + timeout=None, ): """Learn a command from a device.""" data = {} diff --git a/tests/components/rest/test_binary_sensor.py b/tests/components/rest/test_binary_sensor.py index 63a7d3ff273f..8993be6a7a10 100644 --- a/tests/components/rest/test_binary_sensor.py +++ b/tests/components/rest/test_binary_sensor.py @@ -4,7 +4,7 @@ from unittest.mock import patch, Mock import requests -from requests.exceptions import Timeout, MissingSchema +from requests.exceptions import Timeout import requests_mock from homeassistant.exceptions import PlatformNotReady @@ -47,7 +47,7 @@ def test_setup_missing_config(self): def test_setup_missing_schema(self): """Test setup with resource missing schema.""" - with pytest.raises(MissingSchema): + with pytest.raises(PlatformNotReady): rest.setup_platform( self.hass, {"platform": "rest", "resource": "localhost", "method": "GET"}, @@ -60,7 +60,7 @@ def test_setup_failed_connect(self, mock_req): with raises(PlatformNotReady): rest.setup_platform( self.hass, - {"platform": "rest", "resource": "http://localhost"}, + {"platform": "rest", "resource": "http://localhost", "method": "GET"}, self.add_devices, None, ) @@ -72,7 +72,7 @@ def test_setup_timeout(self, mock_req): with raises(PlatformNotReady): rest.setup_platform( self.hass, - {"platform": "rest", "resource": "http://localhost"}, + {"platform": "rest", "resource": "http://localhost", "method": "GET"}, self.add_devices, None, ) diff --git a/tests/components/rest/test_sensor.py b/tests/components/rest/test_sensor.py index 50acb0533476..d770f21a403e 100644 --- a/tests/components/rest/test_sensor.py +++ b/tests/components/rest/test_sensor.py @@ -4,7 +4,7 @@ from unittest.mock import patch, Mock import requests -from requests.exceptions import Timeout, MissingSchema, RequestException +from requests.exceptions import Timeout, RequestException import requests_mock from homeassistant.exceptions import PlatformNotReady @@ -37,7 +37,7 @@ def test_setup_missing_config(self): def test_setup_missing_schema(self): """Test setup with resource missing schema.""" - with pytest.raises(MissingSchema): + with pytest.raises(PlatformNotReady): rest.setup_platform( self.hass, {"platform": "rest", "resource": "localhost", "method": "GET"}, @@ -50,7 +50,7 @@ def test_setup_failed_connect(self, mock_req): with raises(PlatformNotReady): rest.setup_platform( self.hass, - {"platform": "rest", "resource": "http://localhost"}, + {"platform": "rest", "resource": "http://localhost", "method": "GET"}, lambda devices, update=True: None, ) @@ -60,7 +60,7 @@ def test_setup_timeout(self, mock_req): with raises(PlatformNotReady): rest.setup_platform( self.hass, - {"platform": "rest", "resource": "http://localhost"}, + {"platform": "rest", "resource": "http://localhost", "method": "GET"}, lambda devices, update=True: None, ) @@ -284,6 +284,26 @@ def test_update_with_json_attrs(self): self.sensor.update() assert "some_json_value" == self.sensor.device_state_attributes["key"] + def test_update_with_json_attrs_list_dict(self): + """Test attributes get extracted from a JSON list[0] result.""" + self.rest.update = Mock( + "rest.RestData.update", + side_effect=self.update_side_effect('[{ "key": "another_value" }]'), + ) + self.sensor = rest.RestSensor( + self.hass, + self.rest, + self.name, + self.unit_of_measurement, + self.device_class, + None, + ["key"], + self.force_update, + self.resource_template, + ) + self.sensor.update() + assert "another_value" == self.sensor.device_state_attributes["key"] + @patch("homeassistant.components.rest.sensor._LOGGER") def test_update_with_json_attrs_no_data(self, mock_logger): """Test attributes when no JSON result fetched.""" @@ -397,7 +417,7 @@ def test_update(self, mock_req): self.rest.update() assert "test data" == self.rest.data - @patch("requests.Session", side_effect=RequestException) + @patch("requests.request", side_effect=RequestException) def test_update_request_exception(self, mock_req): """Test update when a request exception occurs.""" self.rest.update() diff --git a/tests/components/rest_command/test_init.py b/tests/components/rest_command/test_init.py index b7ac5a4be8a2..ba63091041d6 100644 --- a/tests/components/rest_command/test_init.py +++ b/tests/components/rest_command/test_init.py @@ -236,6 +236,19 @@ def test_rest_command_headers(self, aioclient_mock): }, "content_type": "text/plain", }, + "headers_template_test": { + "headers": { + "Accept": "application/json", + "User-Agent": "Mozilla/{{ 3 + 2 }}.0", + } + }, + "headers_and_content_type_override_template_test": { + "headers": { + "Accept": "application/{{ 1 + 1 }}json", + aiohttp.hdrs.CONTENT_TYPE: "application/pdf", + }, + "content_type": "text/json", + }, } } @@ -245,7 +258,7 @@ def test_rest_command_headers(self, aioclient_mock): {"url": self.url, "method": "post", "payload": "test data"} ) - with assert_setup_component(5): + with assert_setup_component(7): setup_component(self.hass, rc.DOMAIN, header_config_variations) # provide post request data @@ -257,11 +270,13 @@ def test_rest_command_headers(self, aioclient_mock): "headers_test", "headers_and_content_type_test", "headers_and_content_type_override_test", + "headers_template_test", + "headers_and_content_type_override_template_test", ]: self.hass.services.call(rc.DOMAIN, test_service, {}) self.hass.block_till_done() - assert len(aioclient_mock.mock_calls) == 5 + assert len(aioclient_mock.mock_calls) == 7 # no_headers_test assert aioclient_mock.mock_calls[0][3] is None @@ -293,3 +308,16 @@ def test_rest_command_headers(self, aioclient_mock): == "text/plain" ) assert aioclient_mock.mock_calls[4][3].get("Accept") == "application/json" + + # headers_template_test + assert len(aioclient_mock.mock_calls[5][3]) == 2 + assert aioclient_mock.mock_calls[5][3].get("Accept") == "application/json" + assert aioclient_mock.mock_calls[5][3].get("User-Agent") == "Mozilla/5.0" + + # headers_and_content_type_override_template_test + assert len(aioclient_mock.mock_calls[6][3]) == 2 + assert ( + aioclient_mock.mock_calls[6][3].get(aiohttp.hdrs.CONTENT_TYPE) + == "text/json" + ) + assert aioclient_mock.mock_calls[6][3].get("Accept") == "application/2json" diff --git a/tests/components/rss_feed_template/test_init.py b/tests/components/rss_feed_template/test_init.py index 294d84987b2a..b07cc8aa9b3c 100644 --- a/tests/components/rss_feed_template/test_init.py +++ b/tests/components/rss_feed_template/test_init.py @@ -1,7 +1,7 @@ """The tests for the rss_feed_api component.""" import asyncio -from xml.etree import ElementTree +from defusedxml import ElementTree import pytest from homeassistant.setup import async_setup_component diff --git a/tests/components/samsungtv/test_media_player.py b/tests/components/samsungtv/test_media_player.py index 2b5e377c617a..918e30ef4e7a 100644 --- a/tests/components/samsungtv/test_media_player.py +++ b/tests/components/samsungtv/test_media_player.py @@ -8,6 +8,7 @@ import pytest from samsungctl import exceptions from tests.common import async_fire_time_changed +from websocket import WebSocketException from homeassistant.components.media_player import DEVICE_CLASS_TV from homeassistant.components.media_player.const import ( @@ -32,6 +33,7 @@ ATTR_ENTITY_ID, ATTR_FRIENDLY_NAME, ATTR_SUPPORTED_FEATURES, + CONF_BROADCAST_ADDRESS, CONF_HOST, CONF_MAC, CONF_NAME, @@ -67,6 +69,19 @@ } } +ENTITY_ID_BROADCAST = f"{DOMAIN}.fake_broadcast" +MOCK_CONFIG_BROADCAST = { + DOMAIN: { + CONF_PLATFORM: SAMSUNGTV_DOMAIN, + CONF_HOST: "fake_broadcast", + CONF_NAME: "fake_broadcast", + CONF_PORT: 8001, + CONF_TIMEOUT: 10, + CONF_MAC: "38:f9:d3:82:b4:f1", + CONF_BROADCAST_ADDRESS: "192.168.5.255", + } +} + ENTITY_ID_NOMAC = f"{DOMAIN}.fake_nomac" MOCK_CONFIG_NOMAC = { DOMAIN: { @@ -373,6 +388,17 @@ async def test_send_key_unhandled_response(hass, remote): assert state.state == STATE_ON +async def test_send_key_websocketexception(hass, remote): + """Testing unhandled response exception.""" + await setup_samsungtv(hass, MOCK_CONFIG) + remote.control = mock.Mock(side_effect=WebSocketException("Boom")) + assert await hass.services.async_call( + DOMAIN, SERVICE_VOLUME_UP, {ATTR_ENTITY_ID: ENTITY_ID}, True + ) + state = hass.states.get(ENTITY_ID) + assert state.state == STATE_ON + + async def test_send_key_os_error(hass, remote): """Testing broken pipe Exception.""" await setup_samsungtv(hass, MOCK_CONFIG) @@ -543,7 +569,7 @@ async def test_media_next_track(hass, remote): ) # key and update called assert remote.control.call_count == 2 - assert remote.control.call_args_list == [call("KEY_FF"), call("KEY")] + assert remote.control.call_args_list == [call("KEY_CHUP"), call("KEY")] async def test_media_previous_track(hass, remote): @@ -554,7 +580,7 @@ async def test_media_previous_track(hass, remote): ) # key and update called assert remote.control.call_count == 2 - assert remote.control.call_args_list == [call("KEY_REWIND"), call("KEY")] + assert remote.control.call_args_list == [call("KEY_CHDOWN"), call("KEY")] async def test_turn_on_with_mac(hass, remote, wakeonlan): @@ -565,7 +591,22 @@ async def test_turn_on_with_mac(hass, remote, wakeonlan): ) # key and update called assert wakeonlan.send_magic_packet.call_count == 1 - assert wakeonlan.send_magic_packet.call_args_list == [call("38:f9:d3:82:b4:f1")] + assert wakeonlan.send_magic_packet.call_args_list == [ + call("38:f9:d3:82:b4:f1", ip_address="255.255.255.255") + ] + + +async def test_turn_on_with_mac_and_broadcast(hass, remote, wakeonlan): + """Test turn on.""" + await setup_samsungtv(hass, MOCK_CONFIG_BROADCAST) + assert await hass.services.async_call( + DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: ENTITY_ID_BROADCAST}, True + ) + # key and update called + assert wakeonlan.send_magic_packet.call_count == 1 + assert wakeonlan.send_magic_packet.call_args_list == [ + call("38:f9:d3:82:b4:f1", ip_address="192.168.5.255") + ] async def test_turn_on_without_mac(hass, remote): diff --git a/tests/components/scene/common.py b/tests/components/scene/common.py index 4f8123ca6380..5da0cc21db08 100644 --- a/tests/components/scene/common.py +++ b/tests/components/scene/common.py @@ -4,12 +4,12 @@ components. Instead call the service directly. """ from homeassistant.components.scene import DOMAIN -from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_ON +from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_ON, ENTITY_MATCH_ALL from homeassistant.loader import bind_hass @bind_hass -def activate(hass, entity_id=None): +def activate(hass, entity_id=ENTITY_MATCH_ALL): """Activate a scene.""" data = {} diff --git a/tests/components/seventeentrack/test_sensor.py b/tests/components/seventeentrack/test_sensor.py index dff3cc40b9e2..45ab8a622254 100644 --- a/tests/components/seventeentrack/test_sensor.py +++ b/tests/components/seventeentrack/test_sensor.py @@ -119,7 +119,10 @@ def fixture_mock_py17track(): @pytest.fixture(autouse=True, name="mock_client") def fixture_mock_client(mock_py17track): """Mock py17track client.""" - with mock.patch("py17track.Client", new=ClientMock): + with mock.patch( + "homeassistant.components.seventeentrack.sensor.SeventeenTrackClient", + new=ClientMock, + ): yield ProfileMock.reset() diff --git a/tests/components/shopping_list/conftest.py b/tests/components/shopping_list/conftest.py new file mode 100644 index 000000000000..646b3bee4c01 --- /dev/null +++ b/tests/components/shopping_list/conftest.py @@ -0,0 +1,23 @@ +"""Shopping list test helpers.""" +from unittest.mock import patch + +import pytest + +from homeassistant.setup import async_setup_component +from homeassistant.components.shopping_list import intent as sl_intent + + +@pytest.fixture(autouse=True) +def mock_shopping_list_io(): + """Stub out the persistence.""" + with patch("homeassistant.components.shopping_list.ShoppingData.save"), patch( + "homeassistant.components.shopping_list." "ShoppingData.async_load" + ): + yield + + +@pytest.fixture +async def sl_setup(hass): + """Set up the shopping list.""" + assert await async_setup_component(hass, "shopping_list", {}) + await sl_intent.async_setup_intents(hass) diff --git a/tests/components/shopping_list/test_init.py b/tests/components/shopping_list/test_init.py index 1d42fa60d9c2..4394a835f494 100644 --- a/tests/components/shopping_list/test_init.py +++ b/tests/components/shopping_list/test_init.py @@ -1,27 +1,13 @@ """Test shopping list component.""" import asyncio -from unittest.mock import patch -import pytest - -from homeassistant.bootstrap import async_setup_component from homeassistant.helpers import intent from homeassistant.components.websocket_api.const import TYPE_RESULT -@pytest.fixture(autouse=True) -def mock_shopping_list_io(): - """Stub out the persistence.""" - with patch("homeassistant.components.shopping_list.ShoppingData.save"), patch( - "homeassistant.components.shopping_list." "ShoppingData.async_load" - ): - yield - - @asyncio.coroutine -def test_add_item(hass): +def test_add_item(hass, sl_setup): """Test adding an item intent.""" - yield from async_setup_component(hass, "shopping_list", {}) response = yield from intent.async_handle( hass, "test", "HassShoppingListAddItem", {"item": {"value": "beer"}} @@ -31,9 +17,8 @@ def test_add_item(hass): @asyncio.coroutine -def test_recent_items_intent(hass): +def test_recent_items_intent(hass, sl_setup): """Test recent items.""" - yield from async_setup_component(hass, "shopping_list", {}) yield from intent.async_handle( hass, "test", "HassShoppingListAddItem", {"item": {"value": "beer"}} @@ -54,9 +39,8 @@ def test_recent_items_intent(hass): @asyncio.coroutine -def test_deprecated_api_get_all(hass, hass_client): +def test_deprecated_api_get_all(hass, hass_client, sl_setup): """Test the API.""" - yield from async_setup_component(hass, "shopping_list", {}) yield from intent.async_handle( hass, "test", "HassShoppingListAddItem", {"item": {"value": "beer"}} @@ -77,9 +61,8 @@ def test_deprecated_api_get_all(hass, hass_client): assert not data[1]["complete"] -async def test_ws_get_items(hass, hass_ws_client): +async def test_ws_get_items(hass, hass_ws_client, sl_setup): """Test get shopping_list items websocket command.""" - await async_setup_component(hass, "shopping_list", {}) await intent.async_handle( hass, "test", "HassShoppingListAddItem", {"item": {"value": "beer"}} @@ -106,9 +89,8 @@ async def test_ws_get_items(hass, hass_ws_client): @asyncio.coroutine -def test_deprecated_api_update(hass, hass_client): +def test_deprecated_api_update(hass, hass_client, sl_setup): """Test the API.""" - yield from async_setup_component(hass, "shopping_list", {}) yield from intent.async_handle( hass, "test", "HassShoppingListAddItem", {"item": {"value": "beer"}} @@ -142,9 +124,8 @@ def test_deprecated_api_update(hass, hass_client): assert wine == {"id": wine_id, "name": "wine", "complete": True} -async def test_ws_update_item(hass, hass_ws_client): +async def test_ws_update_item(hass, hass_ws_client, sl_setup): """Test update shopping_list item websocket command.""" - await async_setup_component(hass, "shopping_list", {}) await intent.async_handle( hass, "test", "HassShoppingListAddItem", {"item": {"value": "beer"}} ) @@ -186,9 +167,8 @@ async def test_ws_update_item(hass, hass_ws_client): @asyncio.coroutine -def test_api_update_fails(hass, hass_client): +def test_api_update_fails(hass, hass_client, sl_setup): """Test the API.""" - yield from async_setup_component(hass, "shopping_list", {}) yield from intent.async_handle( hass, "test", "HassShoppingListAddItem", {"item": {"value": "beer"}} @@ -209,9 +189,8 @@ def test_api_update_fails(hass, hass_client): assert resp.status == 400 -async def test_ws_update_item_fail(hass, hass_ws_client): +async def test_ws_update_item_fail(hass, hass_ws_client, sl_setup): """Test failure of update shopping_list item websocket command.""" - await async_setup_component(hass, "shopping_list", {}) await intent.async_handle( hass, "test", "HassShoppingListAddItem", {"item": {"value": "beer"}} ) @@ -234,9 +213,8 @@ async def test_ws_update_item_fail(hass, hass_ws_client): @asyncio.coroutine -def test_deprecated_api_clear_completed(hass, hass_client): +def test_deprecated_api_clear_completed(hass, hass_client, sl_setup): """Test the API.""" - yield from async_setup_component(hass, "shopping_list", {}) yield from intent.async_handle( hass, "test", "HassShoppingListAddItem", {"item": {"value": "beer"}} @@ -265,9 +243,8 @@ def test_deprecated_api_clear_completed(hass, hass_client): assert items[0] == {"id": wine_id, "name": "wine", "complete": False} -async def test_ws_clear_items(hass, hass_ws_client): +async def test_ws_clear_items(hass, hass_ws_client, sl_setup): """Test clearing shopping_list items websocket command.""" - await async_setup_component(hass, "shopping_list", {}) await intent.async_handle( hass, "test", "HassShoppingListAddItem", {"item": {"value": "beer"}} ) @@ -296,9 +273,8 @@ async def test_ws_clear_items(hass, hass_ws_client): @asyncio.coroutine -def test_deprecated_api_create(hass, hass_client): +def test_deprecated_api_create(hass, hass_client, sl_setup): """Test the API.""" - yield from async_setup_component(hass, "shopping_list", {}) client = yield from hass_client() resp = yield from client.post("/api/shopping_list/item", json={"name": "soda"}) @@ -315,9 +291,8 @@ def test_deprecated_api_create(hass, hass_client): @asyncio.coroutine -def test_deprecated_api_create_fail(hass, hass_client): +def test_deprecated_api_create_fail(hass, hass_client, sl_setup): """Test the API.""" - yield from async_setup_component(hass, "shopping_list", {}) client = yield from hass_client() resp = yield from client.post("/api/shopping_list/item", json={"name": 1234}) @@ -326,9 +301,8 @@ def test_deprecated_api_create_fail(hass, hass_client): assert len(hass.data["shopping_list"].items) == 0 -async def test_ws_add_item(hass, hass_ws_client): +async def test_ws_add_item(hass, hass_ws_client, sl_setup): """Test adding shopping_list item websocket command.""" - await async_setup_component(hass, "shopping_list", {}) client = await hass_ws_client(hass) await client.send_json({"id": 5, "type": "shopping_list/items/add", "name": "soda"}) msg = await client.receive_json() @@ -342,9 +316,8 @@ async def test_ws_add_item(hass, hass_ws_client): assert items[0]["complete"] is False -async def test_ws_add_item_fail(hass, hass_ws_client): +async def test_ws_add_item_fail(hass, hass_ws_client, sl_setup): """Test adding shopping_list item failure websocket command.""" - await async_setup_component(hass, "shopping_list", {}) client = await hass_ws_client(hass) await client.send_json({"id": 5, "type": "shopping_list/items/add", "name": 123}) msg = await client.receive_json() diff --git a/tests/components/shopping_list/test_intent.py b/tests/components/shopping_list/test_intent.py new file mode 100644 index 000000000000..d0bcb1d837c2 --- /dev/null +++ b/tests/components/shopping_list/test_intent.py @@ -0,0 +1,22 @@ +"""Test Shopping List intents.""" +from homeassistant.helpers import intent + + +async def test_recent_items_intent(hass, sl_setup): + """Test recent items.""" + await intent.async_handle( + hass, "test", "HassShoppingListAddItem", {"item": {"value": "beer"}} + ) + await intent.async_handle( + hass, "test", "HassShoppingListAddItem", {"item": {"value": "wine"}} + ) + await intent.async_handle( + hass, "test", "HassShoppingListAddItem", {"item": {"value": "soda"}} + ) + + response = await intent.async_handle(hass, "test", "HassShoppingListLastItems") + + assert ( + response.speech["plain"]["speech"] + == "These are the top 3 items on your shopping list: soda, wine, beer" + ) diff --git a/tests/components/smartthings/test_climate.py b/tests/components/smartthings/test_climate.py index 630174a06613..79919a376cd0 100644 --- a/tests/components/smartthings/test_climate.py +++ b/tests/components/smartthings/test_climate.py @@ -551,7 +551,9 @@ async def test_set_turn_off(hass, air_conditioner): await setup_platform(hass, CLIMATE_DOMAIN, devices=[air_conditioner]) state = hass.states.get("climate.air_conditioner") assert state.state == HVAC_MODE_HEAT_COOL - await hass.services.async_call(CLIMATE_DOMAIN, SERVICE_TURN_OFF, blocking=True) + await hass.services.async_call( + CLIMATE_DOMAIN, SERVICE_TURN_OFF, {"entity_id": "all"}, blocking=True + ) state = hass.states.get("climate.air_conditioner") assert state.state == HVAC_MODE_OFF @@ -562,7 +564,9 @@ async def test_set_turn_on(hass, air_conditioner): await setup_platform(hass, CLIMATE_DOMAIN, devices=[air_conditioner]) state = hass.states.get("climate.air_conditioner") assert state.state == HVAC_MODE_OFF - await hass.services.async_call(CLIMATE_DOMAIN, SERVICE_TURN_ON, blocking=True) + await hass.services.async_call( + CLIMATE_DOMAIN, SERVICE_TURN_ON, {"entity_id": "all"}, blocking=True + ) state = hass.states.get("climate.air_conditioner") assert state.state == HVAC_MODE_HEAT_COOL diff --git a/tests/components/smartthings/test_cover.py b/tests/components/smartthings/test_cover.py index 19a2ec3463a6..26b68c0cb1f6 100644 --- a/tests/components/smartthings/test_cover.py +++ b/tests/components/smartthings/test_cover.py @@ -114,7 +114,10 @@ async def test_set_cover_position(hass, device_factory): await setup_platform(hass, COVER_DOMAIN, devices=[device]) # Act await hass.services.async_call( - COVER_DOMAIN, SERVICE_SET_COVER_POSITION, {ATTR_POSITION: 50}, blocking=True + COVER_DOMAIN, + SERVICE_SET_COVER_POSITION, + {ATTR_POSITION: 50, "entity_id": "all"}, + blocking=True, ) state = hass.states.get("cover.shade") @@ -136,7 +139,10 @@ async def test_set_cover_position_unsupported(hass, device_factory): await setup_platform(hass, COVER_DOMAIN, devices=[device]) # Act await hass.services.async_call( - COVER_DOMAIN, SERVICE_SET_COVER_POSITION, {ATTR_POSITION: 50}, blocking=True + COVER_DOMAIN, + SERVICE_SET_COVER_POSITION, + {"entity_id": "all", ATTR_POSITION: 50}, + blocking=True, ) state = hass.states.get("cover.shade") diff --git a/tests/components/smhi/common.py b/tests/components/smhi/common.py index 74c8a99b51fc..6f2158403243 100644 --- a/tests/components/smhi/common.py +++ b/tests/components/smhi/common.py @@ -5,7 +5,7 @@ class AsyncMock(Mock): """Implements Mock async.""" - # pylint: disable=W0235 + # pylint: disable=useless-super-delegation async def __call__(self, *args, **kwargs): """Hack for async support for Mock.""" return super().__call__(*args, **kwargs) diff --git a/tests/components/smhi/test_config_flow.py b/tests/components/smhi/test_config_flow.py index ab317fb829ac..e5e1d392419e 100644 --- a/tests/components/smhi/test_config_flow.py +++ b/tests/components/smhi/test_config_flow.py @@ -9,7 +9,7 @@ from homeassistant.components.smhi import config_flow -# pylint: disable=W0212 +# pylint: disable=protected-access async def test_homeassistant_location_exists() -> None: """Test if homeassistant location exists it should return True.""" hass = Mock() diff --git a/tests/components/smhi/test_weather.py b/tests/components/smhi/test_weather.py index 76b32895758a..6cb7d690c1c4 100644 --- a/tests/components/smhi/test_weather.py +++ b/tests/components/smhi/test_weather.py @@ -99,7 +99,7 @@ def test_properties_no_data(hass: HomeAssistant) -> None: assert weather.temperature_unit == TEMP_CELSIUS -# pylint: disable=W0212 +# pylint: disable=protected-access def test_properties_unknown_symbol() -> None: """Test behaviour when unknown symbol from API.""" hass = Mock() @@ -152,7 +152,7 @@ def test_properties_unknown_symbol() -> None: assert forecast[ATTR_FORECAST_CONDITION] is None -# pylint: disable=W0212 +# pylint: disable=protected-access async def test_refresh_weather_forecast_exceeds_retries(hass) -> None: """Test the refresh weather forecast function.""" from smhi.smhi_lib import SmhiForecastException diff --git a/tests/components/starline/__init__.py b/tests/components/starline/__init__.py new file mode 100644 index 000000000000..58f50c0f1b9d --- /dev/null +++ b/tests/components/starline/__init__.py @@ -0,0 +1 @@ +"""Tests for the StarLine component.""" diff --git a/tests/components/starline/test_config_flow.py b/tests/components/starline/test_config_flow.py new file mode 100644 index 000000000000..31bdf98b4043 --- /dev/null +++ b/tests/components/starline/test_config_flow.py @@ -0,0 +1,126 @@ +"""Tests for StarLine config flow.""" +import requests_mock +from homeassistant.components.starline import config_flow + +TEST_APP_ID = "666" +TEST_APP_SECRET = "appsecret" +TEST_APP_CODE = "appcode" +TEST_APP_TOKEN = "apptoken" +TEST_APP_SLNET = "slnettoken" +TEST_APP_SLID = "slidtoken" +TEST_APP_UID = "123" +TEST_APP_USERNAME = "sluser" +TEST_APP_PASSWORD = "slpassword" + + +async def test_flow_works(hass): + """Test that config flow works.""" + with requests_mock.Mocker() as mock: + mock.get( + "https://id.starline.ru/apiV3/application/getCode/", + text='{"state": 1, "desc": {"code": "' + TEST_APP_CODE + '"}}', + ) + mock.get( + "https://id.starline.ru/apiV3/application/getToken/", + text='{"state": 1, "desc": {"token": "' + TEST_APP_TOKEN + '"}}', + ) + mock.post( + "https://id.starline.ru/apiV3/user/login/", + text='{"state": 1, "desc": {"user_token": "' + TEST_APP_SLID + '"}}', + ) + mock.post( + "https://developer.starline.ru/json/v2/auth.slid", + text='{"code": 200, "user_id": "' + TEST_APP_UID + '"}', + cookies={"slnet": TEST_APP_SLNET}, + ) + mock.get( + "https://developer.starline.ru/json/v2/user/{}/user_info".format( + TEST_APP_UID + ), + text='{"code": 200, "devices": [{"device_id": "123", "imei": "123", "alias": "123", "battery": "123", "ctemp": "123", "etemp": "123", "fw_version": "123", "gsm_lvl": "123", "phone": "123", "status": "1", "ts_activity": "123", "typename": "123", "balance": {}, "car_state": {}, "car_alr_state": {}, "functions": [], "position": {}}], "shared_devices": []}', + ) + + result = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, context={"source": "user"} + ) + assert result["type"] == "form" + assert result["step_id"] == "auth_app" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + config_flow.CONF_APP_ID: TEST_APP_ID, + config_flow.CONF_APP_SECRET: TEST_APP_SECRET, + }, + ) + assert result["type"] == "form" + assert result["step_id"] == "auth_user" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + config_flow.CONF_USERNAME: TEST_APP_USERNAME, + config_flow.CONF_PASSWORD: TEST_APP_PASSWORD, + }, + ) + assert result["type"] == "create_entry" + assert result["title"] == "Application {}".format(TEST_APP_ID) + + +async def test_step_auth_app_code_falls(hass): + """Test config flow works when app auth code fails.""" + with requests_mock.Mocker() as mock: + mock.get( + "https://id.starline.ru/apiV3/application/getCode/", text='{"state": 0}}' + ) + result = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, + context={"source": "user"}, + data={ + config_flow.CONF_APP_ID: TEST_APP_ID, + config_flow.CONF_APP_SECRET: TEST_APP_SECRET, + }, + ) + assert result["type"] == "form" + assert result["step_id"] == "auth_app" + assert result["errors"] == {"base": "error_auth_app"} + + +async def test_step_auth_app_token_falls(hass): + """Test config flow works when app auth token fails.""" + with requests_mock.Mocker() as mock: + mock.get( + "https://id.starline.ru/apiV3/application/getCode/", + text='{"state": 1, "desc": {"code": "' + TEST_APP_CODE + '"}}', + ) + mock.get( + "https://id.starline.ru/apiV3/application/getToken/", text='{"state": 0}' + ) + result = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, + context={"source": "user"}, + data={ + config_flow.CONF_APP_ID: TEST_APP_ID, + config_flow.CONF_APP_SECRET: TEST_APP_SECRET, + }, + ) + assert result["type"] == "form" + assert result["step_id"] == "auth_app" + assert result["errors"] == {"base": "error_auth_app"} + + +async def test_step_auth_user_falls(hass): + """Test config flow works when user fails.""" + with requests_mock.Mocker() as mock: + mock.post("https://id.starline.ru/apiV3/user/login/", text='{"state": 0}') + flow = config_flow.StarlineFlowHandler() + flow.hass = hass + result = await flow.async_step_auth_user( + user_input={ + config_flow.CONF_USERNAME: TEST_APP_USERNAME, + config_flow.CONF_PASSWORD: TEST_APP_PASSWORD, + } + ) + assert result["type"] == "form" + assert result["step_id"] == "auth_user" + assert result["errors"] == {"base": "error_auth_user"} diff --git a/tests/components/switch/common.py b/tests/components/switch/common.py index 4491b07cd73a..2c6b1ccd0d2b 100644 --- a/tests/components/switch/common.py +++ b/tests/components/switch/common.py @@ -4,29 +4,34 @@ components. Instead call the service directly. """ from homeassistant.components.switch import DOMAIN -from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON +from homeassistant.const import ( + ATTR_ENTITY_ID, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + ENTITY_MATCH_ALL, +) from homeassistant.loader import bind_hass @bind_hass -def turn_on(hass, entity_id=None): +def turn_on(hass, entity_id=ENTITY_MATCH_ALL): """Turn all or specified switch on.""" hass.add_job(async_turn_on, hass, entity_id) -async def async_turn_on(hass, entity_id=None): +async def async_turn_on(hass, entity_id=ENTITY_MATCH_ALL): """Turn all or specified switch on.""" data = {ATTR_ENTITY_ID: entity_id} if entity_id else None await hass.services.async_call(DOMAIN, SERVICE_TURN_ON, data, blocking=True) @bind_hass -def turn_off(hass, entity_id=None): +def turn_off(hass, entity_id=ENTITY_MATCH_ALL): """Turn all or specified switch off.""" hass.add_job(async_turn_off, hass, entity_id) -async def async_turn_off(hass, entity_id=None): +async def async_turn_off(hass, entity_id=ENTITY_MATCH_ALL): """Turn all or specified switch off.""" data = {ATTR_ENTITY_ID: entity_id} if entity_id else None await hass.services.async_call(DOMAIN, SERVICE_TURN_OFF, data, blocking=True) diff --git a/tests/components/template/test_binary_sensor.py b/tests/components/template/test_binary_sensor.py index 143811da2099..b17b98f2f109 100644 --- a/tests/components/template/test_binary_sensor.py +++ b/tests/components/template/test_binary_sensor.py @@ -576,22 +576,22 @@ async def test_no_update_template_match_all(hass, caplog): await hass.async_block_till_done() assert len(hass.states.async_all()) == 5 assert ( - "Template binary sensor all_state has no entity ids " + "Template binary sensor 'all_state' has no entity ids " "configured to track nor were we able to extract the entities to " "track from the value template" ) in caplog.text assert ( - "Template binary sensor all_icon has no entity ids " + "Template binary sensor 'all_icon' has no entity ids " "configured to track nor were we able to extract the entities to " "track from the icon template" ) in caplog.text assert ( - "Template binary sensor all_entity_picture has no entity ids " + "Template binary sensor 'all_entity_picture' has no entity ids " "configured to track nor were we able to extract the entities to " "track from the entity_picture template" ) in caplog.text assert ( - "Template binary sensor all_attribute has no entity ids " + "Template binary sensor 'all_attribute' has no entity ids " "configured to track nor were we able to extract the entities to " "track from the test_attribute template" ) in caplog.text diff --git a/tests/components/template/test_lock.py b/tests/components/template/test_lock.py index d1d302073754..32a34411b336 100644 --- a/tests/components/template/test_lock.py +++ b/tests/components/template/test_lock.py @@ -254,10 +254,9 @@ def test_no_template_match_all(self, caplog): assert state.state == lock.STATE_UNLOCKED assert ( - "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 " - "be able to be updated manually." + "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(s). This entity will only be able to be updated manually" ) in caplog.text self.hass.states.set("lock.template_lock", lock.STATE_LOCKED) @@ -343,7 +342,7 @@ async def test_available_template_with_entities(hass): { "lock": { "platform": "template", - "value_template": "{{ 'on' }}", + "value_template": "{{ states('switch.test_state') }}", "lock": {"service": "switch.turn_on", "entity_id": "switch.test_state"}, "unlock": { "service": "switch.turn_off", diff --git a/tests/components/template/test_sensor.py b/tests/components/template/test_sensor.py index b3813da17663..7ae34b04c000 100644 --- a/tests/components/template/test_sensor.py +++ b/tests/components/template/test_sensor.py @@ -377,6 +377,49 @@ def test_setup_valid_device_class(self): state = self.hass.states.get("sensor.test2") assert "device_class" not in state.attributes + def test_available_template_with_entities(self): + """Test availability tempalates with values from other entities.""" + + with assert_setup_component(1): + assert setup_component( + self.hass, + "sensor", + { + "sensor": { + "platform": "template", + "sensors": { + "test_template_sensor": { + "value_template": "{{ states.sensor.test_state.state }}", + "availability_template": "{{ is_state('availability_boolean.state', 'on') }}", + } + }, + } + }, + ) + + self.hass.start() + self.hass.block_till_done() + + # When template returns true.. + self.hass.states.set("availability_boolean.state", STATE_ON) + self.hass.block_till_done() + + # Device State should not be unavailable + assert ( + self.hass.states.get("sensor.test_template_sensor").state + != STATE_UNAVAILABLE + ) + + # When Availability template returns false + self.hass.states.set("availability_boolean.state", STATE_OFF) + self.hass.block_till_done() + + # device state should be unavailable + assert ( + self.hass.states.get("sensor.test_template_sensor").state + == STATE_UNAVAILABLE + ) + async def test_available_template_with_entities(hass): """Test availability tempalates with values from other entities.""" @@ -511,27 +554,27 @@ async def test_no_template_match_all(hass, caplog): await hass.async_block_till_done() assert len(hass.states.async_all()) == 6 assert ( - "Template sensor invalid_state has no entity ids " + "Template sensor 'invalid_state' has no entity ids " "configured to track nor were we able to extract the entities to " "track from the value template" ) in caplog.text assert ( - "Template sensor invalid_icon has no entity ids " + "Template sensor 'invalid_icon' has no entity ids " "configured to track nor were we able to extract the entities to " "track from the icon template" ) in caplog.text assert ( - "Template sensor invalid_entity_picture has no entity ids " + "Template sensor 'invalid_entity_picture' has no entity ids " "configured to track nor were we able to extract the entities to " "track from the entity_picture template" ) in caplog.text assert ( - "Template sensor invalid_friendly_name has no entity ids " + "Template sensor 'invalid_friendly_name' has no entity ids " "configured to track nor were we able to extract the entities to " "track from the friendly_name template" ) in caplog.text assert ( - "Template sensor invalid_attribute has no entity ids " + "Template sensor 'invalid_attribute' has no entity ids " "configured to track nor were we able to extract the entities to " "track from the test_attribute template" ) in caplog.text diff --git a/tests/components/transmission/test_config_flow.py b/tests/components/transmission/test_config_flow.py index 28fbed9ff42f..80e6bd550173 100644 --- a/tests/components/transmission/test_config_flow.py +++ b/tests/components/transmission/test_config_flow.py @@ -98,7 +98,6 @@ async def test_flow_works(hass, api): 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(MOCK_ENTRY) @@ -110,7 +109,6 @@ async def test_flow_works(hass, api): 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): diff --git a/tests/components/unifi/test_device_tracker.py b/tests/components/unifi/test_device_tracker.py index 29b165537571..580365bb8bd4 100644 --- a/tests/components/unifi/test_device_tracker.py +++ b/tests/components/unifi/test_device_tracker.py @@ -2,6 +2,8 @@ from copy import copy from datetime import timedelta +from asynctest import patch + from homeassistant import config_entries from homeassistant.components import unifi from homeassistant.components.unifi.const import ( @@ -18,8 +20,6 @@ from .test_controller import ENTRY_CONFIG, SITES, setup_unifi_integration -DEFAULT_DETECTION_TIME = timedelta(seconds=300) - CLIENT_1 = { "essid": "ssid", "hostname": "client_1", @@ -203,7 +203,20 @@ async def test_wireless_client_go_wired_issue(hass): await hass.async_block_till_done() client_1 = hass.states.get("device_tracker.client_1") - assert client_1.state == "not_home" + assert client_1.state == "home" + + with patch.object( + unifi.device_tracker.dt_util, + "utcnow", + return_value=(dt_util.utcnow() + timedelta(minutes=5)), + ): + controller.mock_client_responses.append([client_1_client]) + controller.mock_device_responses.append({}) + await 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()) diff --git a/tests/components/universal/test_media_player.py b/tests/components/universal/test_media_player.py index 67d826f576b5..22ec96c3a217 100644 --- a/tests/components/universal/test_media_player.py +++ b/tests/components/universal/test_media_player.py @@ -539,10 +539,10 @@ def test_volume_level_children_and_attr(self): ump = universal.UniversalMediaPlayer(self.hass, **config) - assert "0" == ump.volume_level + assert 0 == ump.volume_level self.hass.states.set(self.mock_volume_id, 100) - assert "100" == ump.volume_level + assert 100 == ump.volume_level def test_is_volume_muted_children_and_attr(self): """Test is volume muted property w/ children and attrs.""" diff --git a/tests/components/vacuum/common.py b/tests/components/vacuum/common.py index 7d9f645449f9..59f600590b19 100644 --- a/tests/components/vacuum/common.py +++ b/tests/components/vacuum/common.py @@ -23,137 +23,138 @@ SERVICE_TOGGLE, SERVICE_TURN_OFF, SERVICE_TURN_ON, + ENTITY_MATCH_ALL, ) from homeassistant.loader import bind_hass @bind_hass -def turn_on(hass, entity_id=None): +def turn_on(hass, entity_id=ENTITY_MATCH_ALL): """Turn all or specified vacuum on.""" hass.add_job(async_turn_on, hass, entity_id) -async def async_turn_on(hass, entity_id=None): +async def async_turn_on(hass, entity_id=ENTITY_MATCH_ALL): """Turn all or specified vacuum on.""" data = {ATTR_ENTITY_ID: entity_id} if entity_id else None await hass.services.async_call(DOMAIN, SERVICE_TURN_ON, data, blocking=True) @bind_hass -def turn_off(hass, entity_id=None): +def turn_off(hass, entity_id=ENTITY_MATCH_ALL): """Turn all or specified vacuum off.""" hass.add_job(async_turn_off, hass, entity_id) -async def async_turn_off(hass, entity_id=None): +async def async_turn_off(hass, entity_id=ENTITY_MATCH_ALL): """Turn all or specified vacuum off.""" data = {ATTR_ENTITY_ID: entity_id} if entity_id else None await hass.services.async_call(DOMAIN, SERVICE_TURN_OFF, data, blocking=True) @bind_hass -def toggle(hass, entity_id=None): +def toggle(hass, entity_id=ENTITY_MATCH_ALL): """Toggle all or specified vacuum.""" hass.add_job(async_toggle, hass, entity_id) -async def async_toggle(hass, entity_id=None): +async def async_toggle(hass, entity_id=ENTITY_MATCH_ALL): """Toggle all or specified vacuum.""" data = {ATTR_ENTITY_ID: entity_id} if entity_id else None await hass.services.async_call(DOMAIN, SERVICE_TOGGLE, data, blocking=True) @bind_hass -def locate(hass, entity_id=None): +def locate(hass, entity_id=ENTITY_MATCH_ALL): """Locate all or specified vacuum.""" hass.add_job(async_locate, hass, entity_id) -async def async_locate(hass, entity_id=None): +async def async_locate(hass, entity_id=ENTITY_MATCH_ALL): """Locate all or specified vacuum.""" data = {ATTR_ENTITY_ID: entity_id} if entity_id else None await hass.services.async_call(DOMAIN, SERVICE_LOCATE, data, blocking=True) @bind_hass -def clean_spot(hass, entity_id=None): +def clean_spot(hass, entity_id=ENTITY_MATCH_ALL): """Tell all or specified vacuum to perform a spot clean-up.""" hass.add_job(async_clean_spot, hass, entity_id) -async def async_clean_spot(hass, entity_id=None): +async def async_clean_spot(hass, entity_id=ENTITY_MATCH_ALL): """Tell all or specified vacuum to perform a spot clean-up.""" data = {ATTR_ENTITY_ID: entity_id} if entity_id else None await hass.services.async_call(DOMAIN, SERVICE_CLEAN_SPOT, data, blocking=True) @bind_hass -def return_to_base(hass, entity_id=None): +def return_to_base(hass, entity_id=ENTITY_MATCH_ALL): """Tell all or specified vacuum to return to base.""" hass.add_job(async_return_to_base, hass, entity_id) -async def async_return_to_base(hass, entity_id=None): +async def async_return_to_base(hass, entity_id=ENTITY_MATCH_ALL): """Tell all or specified vacuum to return to base.""" data = {ATTR_ENTITY_ID: entity_id} if entity_id else None await hass.services.async_call(DOMAIN, SERVICE_RETURN_TO_BASE, data, blocking=True) @bind_hass -def start_pause(hass, entity_id=None): +def start_pause(hass, entity_id=ENTITY_MATCH_ALL): """Tell all or specified vacuum to start or pause the current task.""" hass.add_job(async_start_pause, hass, entity_id) -async def async_start_pause(hass, entity_id=None): +async def async_start_pause(hass, entity_id=ENTITY_MATCH_ALL): """Tell all or specified vacuum to start or pause the current task.""" data = {ATTR_ENTITY_ID: entity_id} if entity_id else None await hass.services.async_call(DOMAIN, SERVICE_START_PAUSE, data, blocking=True) @bind_hass -def start(hass, entity_id=None): +def start(hass, entity_id=ENTITY_MATCH_ALL): """Tell all or specified vacuum to start or resume the current task.""" hass.add_job(async_start, hass, entity_id) -async def async_start(hass, entity_id=None): +async def async_start(hass, entity_id=ENTITY_MATCH_ALL): """Tell all or specified vacuum to start or resume the current task.""" data = {ATTR_ENTITY_ID: entity_id} if entity_id else None await hass.services.async_call(DOMAIN, SERVICE_START, data, blocking=True) @bind_hass -def pause(hass, entity_id=None): +def pause(hass, entity_id=ENTITY_MATCH_ALL): """Tell all or the specified vacuum to pause the current task.""" hass.add_job(async_pause, hass, entity_id) -async def async_pause(hass, entity_id=None): +async def async_pause(hass, entity_id=ENTITY_MATCH_ALL): """Tell all or the specified vacuum to pause the current task.""" data = {ATTR_ENTITY_ID: entity_id} if entity_id else None await hass.services.async_call(DOMAIN, SERVICE_PAUSE, data, blocking=True) @bind_hass -def stop(hass, entity_id=None): +def stop(hass, entity_id=ENTITY_MATCH_ALL): """Stop all or specified vacuum.""" hass.add_job(async_stop, hass, entity_id) -async def async_stop(hass, entity_id=None): +async def async_stop(hass, entity_id=ENTITY_MATCH_ALL): """Stop all or specified vacuum.""" data = {ATTR_ENTITY_ID: entity_id} if entity_id else None await hass.services.async_call(DOMAIN, SERVICE_STOP, data, blocking=True) @bind_hass -def set_fan_speed(hass, fan_speed, entity_id=None): +def set_fan_speed(hass, fan_speed, entity_id=ENTITY_MATCH_ALL): """Set fan speed for all or specified vacuum.""" hass.add_job(async_set_fan_speed, hass, fan_speed, entity_id) -async def async_set_fan_speed(hass, fan_speed, entity_id=None): +async def async_set_fan_speed(hass, fan_speed, entity_id=ENTITY_MATCH_ALL): """Set fan speed for all or specified vacuum.""" data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} data[ATTR_FAN_SPEED] = fan_speed @@ -161,12 +162,12 @@ async def async_set_fan_speed(hass, fan_speed, entity_id=None): @bind_hass -def send_command(hass, command, params=None, entity_id=None): +def send_command(hass, command, params=None, entity_id=ENTITY_MATCH_ALL): """Send command to all or specified vacuum.""" hass.add_job(async_send_command, hass, command, params, entity_id) -async def async_send_command(hass, command, params=None, entity_id=None): +async def async_send_command(hass, command, params=None, entity_id=ENTITY_MATCH_ALL): """Send command to all or specified vacuum.""" data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} data[ATTR_COMMAND] = command diff --git a/tests/components/verisure/test_ethernet_status.py b/tests/components/verisure/test_ethernet_status.py new file mode 100644 index 000000000000..71c7df94ae54 --- /dev/null +++ b/tests/components/verisure/test_ethernet_status.py @@ -0,0 +1,67 @@ +"""Test Verisure ethernet status.""" +from contextlib import contextmanager +from unittest.mock import patch + +from homeassistant.const import STATE_UNAVAILABLE +from homeassistant.setup import async_setup_component +from homeassistant.components.verisure import DOMAIN as VERISURE_DOMAIN + +CONFIG = { + "verisure": { + "username": "test", + "password": "test", + "alarm": False, + "door_window": False, + "hygrometers": False, + "mouse": False, + "smartplugs": False, + "thermometers": False, + "smartcam": False, + } +} + + +@contextmanager +def mock_hub(config, response): + """Extensively mock out a verisure hub.""" + hub_prefix = "homeassistant.components.verisure.binary_sensor.hub" + verisure_prefix = "verisure.Session" + with patch(verisure_prefix) as session, patch(hub_prefix) as hub: + session.login.return_value = True + + hub.config = config["verisure"] + hub.get.return_value = response + hub.get_first.return_value = response.get("ethernetConnectedNow", None) + + yield hub + + +async def setup_verisure(hass, config, response): + """Set up mock verisure.""" + with mock_hub(config, response): + await async_setup_component(hass, VERISURE_DOMAIN, config) + await hass.async_block_till_done() + + +async def test_verisure_no_ethernet_status(hass): + """Test no data from API.""" + await setup_verisure(hass, CONFIG, {}) + assert len(hass.states.async_all()) == 1 + entity_id = hass.states.async_entity_ids()[0] + assert hass.states.get(entity_id).state == STATE_UNAVAILABLE + + +async def test_verisure_ethernet_status_disconnected(hass): + """Test disconnected.""" + await setup_verisure(hass, CONFIG, {"ethernetConnectedNow": False}) + assert len(hass.states.async_all()) == 1 + entity_id = hass.states.async_entity_ids()[0] + assert hass.states.get(entity_id).state == "off" + + +async def test_verisure_ethernet_status_connected(hass): + """Test connected.""" + await setup_verisure(hass, CONFIG, {"ethernetConnectedNow": True}) + assert len(hass.states.async_all()) == 1 + entity_id = hass.states.async_entity_ids()[0] + assert hass.states.get(entity_id).state == "on" diff --git a/tests/components/verisure/test_lock.py b/tests/components/verisure/test_lock.py index db277285819c..ac03e0d9fb61 100644 --- a/tests/components/verisure/test_lock.py +++ b/tests/components/verisure/test_lock.py @@ -50,6 +50,9 @@ def mock_hub(config, get_response=LOCKS[0]): """Extensively mock out a verisure hub.""" hub_prefix = "homeassistant.components.verisure.lock.hub" + # Since there is no conf to disable ethernet status, mock hub for + # binary sensor too + hub_binary_sensor = "homeassistant.components.verisure.binary_sensor.hub" verisure_prefix = "verisure.Session" with patch(verisure_prefix) as session, patch(hub_prefix) as hub: session.login.return_value = True @@ -62,7 +65,8 @@ def mock_hub(config, get_response=LOCKS[0]): } hub.session.get_lock_state_transaction.return_value = {"result": "OK"} - yield hub + with patch(hub_binary_sensor, hub): + yield hub async def setup_verisure_locks(hass, config): @@ -70,8 +74,8 @@ async def setup_verisure_locks(hass, config): with mock_hub(config): await async_setup_component(hass, VERISURE_DOMAIN, config) await hass.async_block_till_done() - # lock.door_lock, group.all_locks - assert len(hass.states.async_all()) == 2 + # lock.door_lock, group.all_locks, ethernet_status + assert len(hass.states.async_all()) == 3 async def test_verisure_no_default_code(hass): diff --git a/tests/components/voicerss/test_tts.py b/tests/components/voicerss/test_tts.py index 01ac6049ac58..0f2a06180963 100644 --- a/tests/components/voicerss/test_tts.py +++ b/tests/components/voicerss/test_tts.py @@ -13,7 +13,7 @@ from tests.common import get_test_home_assistant, assert_setup_component, mock_service -from tests.components.tts.test_init import mutagen_mock # noqa +from tests.components.tts.test_init import mutagen_mock # noqa: F401 class TestTTSVoiceRSSPlatform: diff --git a/tests/components/wake_on_lan/test_switch.py b/tests/components/wake_on_lan/test_switch.py index 2d319c2b5e71..1fbcc9cc273f 100644 --- a/tests/components/wake_on_lan/test_switch.py +++ b/tests/components/wake_on_lan/test_switch.py @@ -30,7 +30,7 @@ def system(): return "Windows" -class TestWOLSwitch(unittest.TestCase): +class TestWolSwitch(unittest.TestCase): """Test the wol switch.""" def setUp(self): @@ -53,7 +53,7 @@ def test_valid_hostname(self): { "switch": { "platform": "wake_on_lan", - "mac_address": "00-01-02-03-04-05", + "mac": "00-01-02-03-04-05", "host": "validhostname", } }, @@ -89,7 +89,7 @@ def test_valid_hostname_windows(self): { "switch": { "platform": "wake_on_lan", - "mac_address": "00-01-02-03-04-05", + "mac": "00-01-02-03-04-05", "host": "validhostname", } }, @@ -113,7 +113,7 @@ def test_minimal_config(self): assert setup_component( self.hass, switch.DOMAIN, - {"switch": {"platform": "wake_on_lan", "mac_address": "00-01-02-03-04-05"}}, + {"switch": {"platform": "wake_on_lan", "mac": "00-01-02-03-04-05"}}, ) @patch("wakeonlan.send_magic_packet", new=send_magic_packet) @@ -126,7 +126,7 @@ def test_broadcast_config(self): { "switch": { "platform": "wake_on_lan", - "mac_address": "00-01-02-03-04-05", + "mac": "00-01-02-03-04-05", "broadcast_address": "255.255.255.255", } }, @@ -150,7 +150,7 @@ def test_off_script(self): { "switch": { "platform": "wake_on_lan", - "mac_address": "00-01-02-03-04-05", + "mac": "00-01-02-03-04-05", "host": "validhostname", "turn_off": {"service": "shell_command.turn_off_target"}, } @@ -192,7 +192,7 @@ def test_invalid_hostname_windows(self): { "switch": { "platform": "wake_on_lan", - "mac_address": "00-01-02-03-04-05", + "mac": "00-01-02-03-04-05", "host": "invalidhostname", } }, diff --git a/tests/components/water_heater/common.py b/tests/components/water_heater/common.py index 3fb010ab55cc..0808e3e3daca 100644 --- a/tests/components/water_heater/common.py +++ b/tests/components/water_heater/common.py @@ -12,12 +12,12 @@ SERVICE_SET_TEMPERATURE, SERVICE_SET_OPERATION_MODE, ) -from homeassistant.const import ATTR_ENTITY_ID, ATTR_TEMPERATURE +from homeassistant.const import ATTR_ENTITY_ID, ATTR_TEMPERATURE, ENTITY_MATCH_ALL from homeassistant.loader import bind_hass @bind_hass -def set_away_mode(hass, away_mode, entity_id=None): +def set_away_mode(hass, away_mode, entity_id=ENTITY_MATCH_ALL): """Turn all or specified water_heater devices away mode on.""" data = {ATTR_AWAY_MODE: away_mode} @@ -28,7 +28,9 @@ def set_away_mode(hass, away_mode, entity_id=None): @bind_hass -def set_temperature(hass, temperature=None, entity_id=None, operation_mode=None): +def set_temperature( + hass, temperature=None, entity_id=ENTITY_MATCH_ALL, operation_mode=None +): """Set new target temperature.""" kwargs = { key: value @@ -44,7 +46,7 @@ def set_temperature(hass, temperature=None, entity_id=None, operation_mode=None) @bind_hass -def set_operation_mode(hass, operation_mode, entity_id=None): +def set_operation_mode(hass, operation_mode, entity_id=ENTITY_MATCH_ALL): """Set new target operation mode.""" data = {ATTR_OPERATION_MODE: operation_mode} diff --git a/tests/components/wled/test_light.py b/tests/components/wled/test_light.py index 185a25b05071..037081608af4 100644 --- a/tests/components/wled/test_light.py +++ b/tests/components/wled/test_light.py @@ -8,6 +8,7 @@ ATTR_HS_COLOR, ATTR_RGB_COLOR, ATTR_TRANSITION, + ATTR_WHITE_VALUE, DOMAIN as LIGHT_DOMAIN, ) from homeassistant.components.wled.const import ( @@ -164,7 +165,8 @@ async def test_rgbw_light( state = hass.states.get("light.wled_rgbw_light") assert state.state == STATE_ON - assert state.attributes.get(ATTR_HS_COLOR) == (0.0, 64.706) + assert state.attributes.get(ATTR_HS_COLOR) == (0.0, 100.0) + assert state.attributes.get(ATTR_WHITE_VALUE) == 139 await hass.services.async_call( LIGHT_DOMAIN, @@ -177,3 +179,17 @@ async def test_rgbw_light( state = hass.states.get("light.wled_rgbw_light") assert state.state == STATE_ON assert state.attributes.get(ATTR_HS_COLOR) == (28.874, 72.522) + assert state.attributes.get(ATTR_WHITE_VALUE) == 139 + + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "light.wled_rgbw_light", ATTR_WHITE_VALUE: 100}, + blocking=True, + ) + await hass.async_block_till_done() + + state = hass.states.get("light.wled_rgbw_light") + assert state.state == STATE_ON + assert state.attributes.get(ATTR_HS_COLOR) == (28.874, 72.522) + assert state.attributes.get(ATTR_WHITE_VALUE) == 100 diff --git a/tests/components/xiaomi_miio/test_vacuum.py b/tests/components/xiaomi_miio/test_vacuum.py index 18da270960c0..5b6ce578c8b5 100644 --- a/tests/components/xiaomi_miio/test_vacuum.py +++ b/tests/components/xiaomi_miio/test_vacuum.py @@ -36,6 +36,7 @@ CONF_HOST, CONF_NAME, CONF_TOKEN, + DOMAIN as XIAOMI_DOMAIN, SERVICE_MOVE_REMOTE_CONTROL, SERVICE_MOVE_REMOTE_CONTROL_STEP, SERVICE_START_REMOTE_CONTROL, @@ -212,6 +213,7 @@ def test_xiaomi_vacuum_services(hass, caplog, mock_mirobo_is_got_error): "Balanced", "Turbo", "Max", + "Gentle", ] assert state.attributes.get(ATTR_MAIN_BRUSH_LEFT) == 12 assert state.attributes.get(ATTR_SIDE_BRUSH_LEFT) == 12 @@ -354,6 +356,7 @@ def test_xiaomi_specific_services(hass, caplog, mock_mirobo_is_on): "Balanced", "Turbo", "Max", + "Gentle", ] assert state.attributes.get(ATTR_MAIN_BRUSH_LEFT) == 11 assert state.attributes.get(ATTR_SIDE_BRUSH_LEFT) == 11 @@ -364,7 +367,10 @@ def test_xiaomi_specific_services(hass, caplog, mock_mirobo_is_on): # Xiaomi vacuum specific services: yield from hass.services.async_call( - DOMAIN, SERVICE_START_REMOTE_CONTROL, {ATTR_ENTITY_ID: entity_id}, blocking=True + XIAOMI_DOMAIN, + SERVICE_START_REMOTE_CONTROL, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, ) mock_mirobo_is_on.assert_has_calls([mock.call.manual_start()], any_order=True) @@ -373,7 +379,7 @@ def test_xiaomi_specific_services(hass, caplog, mock_mirobo_is_on): control = {"duration": 1000, "rotation": -40, "velocity": -0.1} yield from hass.services.async_call( - DOMAIN, SERVICE_MOVE_REMOTE_CONTROL, control, blocking=True + XIAOMI_DOMAIN, SERVICE_MOVE_REMOTE_CONTROL, control, blocking=True ) mock_mirobo_is_on.manual_control.assert_has_calls( [mock.call(**control)], any_order=True @@ -382,7 +388,7 @@ def test_xiaomi_specific_services(hass, caplog, mock_mirobo_is_on): mock_mirobo_is_on.reset_mock() yield from hass.services.async_call( - DOMAIN, SERVICE_STOP_REMOTE_CONTROL, {}, blocking=True + XIAOMI_DOMAIN, SERVICE_STOP_REMOTE_CONTROL, {}, blocking=True ) mock_mirobo_is_on.assert_has_calls([mock.call.manual_stop()], any_order=True) mock_mirobo_is_on.assert_has_calls(STATUS_CALLS, any_order=True) @@ -390,7 +396,7 @@ def test_xiaomi_specific_services(hass, caplog, mock_mirobo_is_on): control_once = {"duration": 2000, "rotation": 120, "velocity": 0.1} yield from hass.services.async_call( - DOMAIN, SERVICE_MOVE_REMOTE_CONTROL_STEP, control_once, blocking=True + XIAOMI_DOMAIN, SERVICE_MOVE_REMOTE_CONTROL_STEP, control_once, blocking=True ) mock_mirobo_is_on.manual_control_once.assert_has_calls( [mock.call(**control_once)], any_order=True @@ -400,7 +406,7 @@ def test_xiaomi_specific_services(hass, caplog, mock_mirobo_is_on): control = {"zone": [[123, 123, 123, 123]], "repeats": 2} yield from hass.services.async_call( - DOMAIN, SERVICE_CLEAN_ZONE, control, blocking=True + XIAOMI_DOMAIN, SERVICE_CLEAN_ZONE, control, blocking=True ) mock_mirobo_is_on.zoned_clean.assert_has_calls( [mock.call([[123, 123, 123, 123, 2]])], any_order=True diff --git a/tests/components/yandextts/test_tts.py b/tests/components/yandextts/test_tts.py index 4af259e1355b..c532732ccc52 100644 --- a/tests/components/yandextts/test_tts.py +++ b/tests/components/yandextts/test_tts.py @@ -11,7 +11,7 @@ ) from tests.common import get_test_home_assistant, assert_setup_component, mock_service -from tests.components.tts.test_init import mutagen_mock # noqa +from tests.components.tts.test_init import mutagen_mock # noqa: F401 class TestTTSYandexPlatform: diff --git a/tests/components/zwave/test_climate.py b/tests/components/zwave/test_climate.py index 2f13d95fb9f0..c9fe123af82c 100644 --- a/tests/components/zwave/test_climate.py +++ b/tests/components/zwave/test_climate.py @@ -9,6 +9,7 @@ HVAC_MODE_HEAT, HVAC_MODE_HEAT_COOL, HVAC_MODE_OFF, + PRESET_AWAY, PRESET_BOOST, PRESET_ECO, PRESET_NONE, @@ -16,6 +17,9 @@ SUPPORT_PRESET_MODE, SUPPORT_SWING_MODE, SUPPORT_TARGET_TEMPERATURE, + SUPPORT_TARGET_TEMPERATURE_RANGE, + ATTR_TARGET_TEMP_LOW, + ATTR_TARGET_TEMP_HIGH, ) from homeassistant.components.zwave import climate from homeassistant.components.zwave.climate import DEFAULT_HVAC_MODES @@ -29,9 +33,7 @@ def device(hass, mock_openzwave): """Fixture to provide a precreated climate device.""" node = MockNode() values = MockEntityValues( - primary=MockValue(data=1, node=node), - temperature=MockValue(data=5, node=node, units=None), - mode=MockValue( + primary=MockValue( data=HVAC_MODE_HEAT, data_items=[ HVAC_MODE_OFF, @@ -41,6 +43,9 @@ def device(hass, mock_openzwave): ], node=node, ), + setpoint_heating=MockValue(data=1, node=node), + setpoint_cooling=MockValue(data=10, node=node), + temperature=MockValue(data=5, node=node, units=None), fan_mode=MockValue(data="test2", data_items=[3, 4, 5], node=node), operating_state=MockValue(data=CURRENT_HVAC_HEAT, node=node), fan_action=MockValue(data=7, node=node), @@ -56,9 +61,7 @@ def device_zxt_120(hass, mock_openzwave): node = MockNode(manufacturer_id="5254", product_id="8377") values = MockEntityValues( - primary=MockValue(data=1, node=node), - temperature=MockValue(data=5, node=node, units=None), - mode=MockValue( + primary=MockValue( data=HVAC_MODE_HEAT, data_items=[ HVAC_MODE_OFF, @@ -68,6 +71,9 @@ def device_zxt_120(hass, mock_openzwave): ], node=node, ), + setpoint_heating=MockValue(data=1, node=node), + setpoint_cooling=MockValue(data=10, node=node), + temperature=MockValue(data=5, node=node, units=None), fan_mode=MockValue(data="test2", data_items=[3, 4, 5], node=node), operating_state=MockValue(data=CURRENT_HVAC_HEAT, node=node), fan_action=MockValue(data=7, node=node), @@ -83,13 +89,14 @@ def device_mapping(hass, mock_openzwave): """Fixture to provide a precreated climate device. Test state mapping.""" node = MockNode() values = MockEntityValues( - primary=MockValue(data=1, node=node), - temperature=MockValue(data=5, node=node, units=None), - mode=MockValue( + primary=MockValue( data="Heat", - data_items=["Off", "Cool", "Heat", "Full Power", "heat_cool"], + data_items=["Off", "Cool", "Heat", "Full Power", "Auto"], node=node, ), + setpoint_heating=MockValue(data=1, node=node), + setpoint_cooling=MockValue(data=10, node=node), + temperature=MockValue(data=5, node=node, units=None), fan_mode=MockValue(data="test2", data_items=[3, 4, 5], node=node), operating_state=MockValue(data="heating", node=node), fan_action=MockValue(data=7, node=node), @@ -104,13 +111,14 @@ def device_unknown(hass, mock_openzwave): """Fixture to provide a precreated climate device. Test state unknown.""" node = MockNode() values = MockEntityValues( - primary=MockValue(data=1, node=node), - temperature=MockValue(data=5, node=node, units=None), - mode=MockValue( + primary=MockValue( data="Heat", data_items=["Off", "Cool", "Heat", "heat_cool", "Abcdefg"], node=node, ), + setpoint_heating=MockValue(data=1, node=node), + setpoint_cooling=MockValue(data=10, node=node), + temperature=MockValue(data=5, node=node, units=None), fan_mode=MockValue(data="test2", data_items=[3, 4, 5], node=node), operating_state=MockValue(data="test4", node=node), fan_action=MockValue(data=7, node=node), @@ -125,9 +133,7 @@ def device_heat_cool(hass, mock_openzwave): """Fixture to provide a precreated climate device. Test state heat only.""" node = MockNode() values = MockEntityValues( - primary=MockValue(data=1, node=node), - temperature=MockValue(data=5, node=node, units=None), - mode=MockValue( + primary=MockValue( data=HVAC_MODE_HEAT, data_items=[ HVAC_MODE_OFF, @@ -138,6 +144,88 @@ def device_heat_cool(hass, mock_openzwave): ], node=node, ), + setpoint_heating=MockValue(data=1, node=node), + setpoint_cooling=MockValue(data=10, node=node), + temperature=MockValue(data=5, node=node, units=None), + fan_mode=MockValue(data="test2", data_items=[3, 4, 5], node=node), + operating_state=MockValue(data="test4", node=node), + fan_action=MockValue(data=7, node=node), + ) + device = climate.get_device(hass, node=node, values=values, node_config={}) + + yield device + + +@pytest.fixture +def device_heat_cool_range(hass, mock_openzwave): + """Fixture to provide a precreated climate device. Target range mode.""" + node = MockNode() + values = MockEntityValues( + primary=MockValue( + data=HVAC_MODE_HEAT_COOL, + data_items=[ + HVAC_MODE_OFF, + HVAC_MODE_HEAT, + HVAC_MODE_COOL, + HVAC_MODE_HEAT_COOL, + ], + node=node, + ), + setpoint_heating=MockValue(data=1, node=node), + setpoint_cooling=MockValue(data=10, node=node), + temperature=MockValue(data=5, node=node, units=None), + fan_mode=MockValue(data="test2", data_items=[3, 4, 5], node=node), + operating_state=MockValue(data="test4", node=node), + fan_action=MockValue(data=7, node=node), + ) + device = climate.get_device(hass, node=node, values=values, node_config={}) + + yield device + + +@pytest.fixture +def device_heat_cool_away(hass, mock_openzwave): + """Fixture to provide a precreated climate device. Target range mode.""" + node = MockNode() + values = MockEntityValues( + primary=MockValue( + data=HVAC_MODE_HEAT_COOL, + data_items=[ + HVAC_MODE_OFF, + HVAC_MODE_HEAT, + HVAC_MODE_COOL, + HVAC_MODE_HEAT_COOL, + PRESET_AWAY, + ], + node=node, + ), + setpoint_heating=MockValue(data=2, node=node), + setpoint_cooling=MockValue(data=9, node=node), + setpoint_away_heating=MockValue(data=1, node=node), + setpoint_away_cooling=MockValue(data=10, node=node), + temperature=MockValue(data=5, node=node, units=None), + fan_mode=MockValue(data="test2", data_items=[3, 4, 5], node=node), + operating_state=MockValue(data="test4", node=node), + fan_action=MockValue(data=7, node=node), + ) + device = climate.get_device(hass, node=node, values=values, node_config={}) + + yield device + + +@pytest.fixture +def device_heat_eco(hass, mock_openzwave): + """Fixture to provide a precreated climate device. heat/heat eco.""" + node = MockNode() + values = MockEntityValues( + primary=MockValue( + data=HVAC_MODE_HEAT, + data_items=[HVAC_MODE_OFF, HVAC_MODE_HEAT, "heat econ"], + node=node, + ), + setpoint_heating=MockValue(data=2, node=node), + setpoint_eco_heating=MockValue(data=1, node=node), + temperature=MockValue(data=5, node=node, units=None), fan_mode=MockValue(data="test2", data_items=[3, 4, 5], node=node), operating_state=MockValue(data="test4", node=node), fan_action=MockValue(data=7, node=node), @@ -155,7 +243,23 @@ def test_default_hvac_modes(): def test_supported_features(device): """Test supported features flags.""" - assert device.supported_features == SUPPORT_FAN_MODE + SUPPORT_TARGET_TEMPERATURE + assert ( + device.supported_features + == SUPPORT_FAN_MODE + + SUPPORT_TARGET_TEMPERATURE + + SUPPORT_TARGET_TEMPERATURE_RANGE + ) + + +def test_supported_features_temp_range(device_heat_cool_range): + """Test supported features flags with target temp range.""" + device = device_heat_cool_range + assert ( + device.supported_features + == SUPPORT_FAN_MODE + + SUPPORT_TARGET_TEMPERATURE + + SUPPORT_TARGET_TEMPERATURE_RANGE + ) def test_supported_features_preset_mode(device_mapping): @@ -163,7 +267,10 @@ def test_supported_features_preset_mode(device_mapping): device = device_mapping assert ( device.supported_features - == SUPPORT_FAN_MODE + SUPPORT_TARGET_TEMPERATURE + SUPPORT_PRESET_MODE + == SUPPORT_FAN_MODE + + SUPPORT_TARGET_TEMPERATURE + + SUPPORT_TARGET_TEMPERATURE_RANGE + + SUPPORT_PRESET_MODE ) @@ -172,7 +279,10 @@ def test_supported_features_swing_mode(device_zxt_120): device = device_zxt_120 assert ( device.supported_features - == SUPPORT_FAN_MODE + SUPPORT_TARGET_TEMPERATURE + SUPPORT_SWING_MODE + == SUPPORT_FAN_MODE + + SUPPORT_TARGET_TEMPERATURE + + SUPPORT_TARGET_TEMPERATURE_RANGE + + SUPPORT_SWING_MODE ) @@ -207,14 +317,6 @@ def test_temperature_unit(device): assert device.temperature_unit == TEMP_CELSIUS -def test_default_target_temperature(device): - """Test default setting of target temperature.""" - assert device.target_temperature == 1 - device.values.primary.data = 0 - value_changed(device.values.primary) - assert device.target_temperature == 5 # Current Temperature - - def test_data_lists(device): """Test data lists from zwave value items.""" assert device.fan_modes == [3, 4, 5] @@ -225,7 +327,7 @@ def test_data_lists(device): HVAC_MODE_HEAT_COOL, ] assert device.preset_modes == [] - device.values.mode = None + device.values.primary = None assert device.preset_modes == [] @@ -234,71 +336,126 @@ def test_data_lists_mapping(device_mapping): device = device_mapping assert device.hvac_modes == ["off", "cool", "heat", "heat_cool"] assert device.preset_modes == ["boost", "none"] - device.values.mode = None + device.values.primary = None assert device.preset_modes == [] def test_target_value_set(device): """Test values changed for climate device.""" - assert device.values.primary.data == 1 + assert device.values.setpoint_heating.data == 1 + assert device.values.setpoint_cooling.data == 10 device.set_temperature() - assert device.values.primary.data == 1 + assert device.values.setpoint_heating.data == 1 + assert device.values.setpoint_cooling.data == 10 device.set_temperature(**{ATTR_TEMPERATURE: 2}) - assert device.values.primary.data == 2 + assert device.values.setpoint_heating.data == 2 + assert device.values.setpoint_cooling.data == 10 + device.set_hvac_mode(HVAC_MODE_COOL) + value_changed(device.values.primary) + assert device.values.setpoint_heating.data == 2 + assert device.values.setpoint_cooling.data == 10 + device.set_temperature(**{ATTR_TEMPERATURE: 9}) + assert device.values.setpoint_heating.data == 2 + assert device.values.setpoint_cooling.data == 9 + + +def test_target_value_set_range(device_heat_cool_range): + """Test values changed for climate device.""" + device = device_heat_cool_range + assert device.values.setpoint_heating.data == 1 + assert device.values.setpoint_cooling.data == 10 + device.set_temperature() + assert device.values.setpoint_heating.data == 1 + assert device.values.setpoint_cooling.data == 10 + device.set_temperature(**{ATTR_TARGET_TEMP_LOW: 2}) + assert device.values.setpoint_heating.data == 2 + assert device.values.setpoint_cooling.data == 10 + device.set_temperature(**{ATTR_TARGET_TEMP_HIGH: 9}) + assert device.values.setpoint_heating.data == 2 + assert device.values.setpoint_cooling.data == 9 + device.set_temperature(**{ATTR_TARGET_TEMP_LOW: 3, ATTR_TARGET_TEMP_HIGH: 8}) + assert device.values.setpoint_heating.data == 3 + assert device.values.setpoint_cooling.data == 8 + + +def test_target_value_set_range_away(device_heat_cool_away): + """Test values changed for climate device.""" + device = device_heat_cool_away + assert device.values.setpoint_heating.data == 2 + assert device.values.setpoint_cooling.data == 9 + assert device.values.setpoint_away_heating.data == 1 + assert device.values.setpoint_away_cooling.data == 10 + device.set_preset_mode(PRESET_AWAY) + device.set_temperature(**{ATTR_TARGET_TEMP_LOW: 0, ATTR_TARGET_TEMP_HIGH: 11}) + assert device.values.setpoint_heating.data == 2 + assert device.values.setpoint_cooling.data == 9 + assert device.values.setpoint_away_heating.data == 0 + assert device.values.setpoint_away_cooling.data == 11 + + +def test_target_value_set_eco(device_heat_eco): + """Test values changed for climate device.""" + device = device_heat_eco + assert device.values.setpoint_heating.data == 2 + assert device.values.setpoint_eco_heating.data == 1 + device.set_preset_mode("heat econ") + device.set_temperature(**{ATTR_TEMPERATURE: 0}) + assert device.values.setpoint_heating.data == 2 + assert device.values.setpoint_eco_heating.data == 0 def test_operation_value_set(device): """Test values changed for climate device.""" - assert device.values.mode.data == HVAC_MODE_HEAT + assert device.values.primary.data == HVAC_MODE_HEAT device.set_hvac_mode(HVAC_MODE_COOL) - assert device.values.mode.data == HVAC_MODE_COOL + assert device.values.primary.data == HVAC_MODE_COOL device.set_preset_mode(PRESET_ECO) - assert device.values.mode.data == PRESET_ECO + assert device.values.primary.data == PRESET_ECO device.set_preset_mode(PRESET_NONE) - assert device.values.mode.data == HVAC_MODE_HEAT_COOL - device.values.mode = None + assert device.values.primary.data == HVAC_MODE_HEAT_COOL + device.values.primary = None device.set_hvac_mode("test_set_failes") - assert device.values.mode is None + assert device.values.primary is None device.set_preset_mode("test_set_failes") - assert device.values.mode is None + assert device.values.primary is None def test_operation_value_set_mapping(device_mapping): """Test values changed for climate device. Mapping.""" device = device_mapping - assert device.values.mode.data == "Heat" + assert device.values.primary.data == "Heat" device.set_hvac_mode(HVAC_MODE_COOL) - assert device.values.mode.data == "Cool" + assert device.values.primary.data == "Cool" device.set_hvac_mode(HVAC_MODE_OFF) - assert device.values.mode.data == "Off" + assert device.values.primary.data == "Off" device.set_preset_mode(PRESET_BOOST) - assert device.values.mode.data == "Full Power" + assert device.values.primary.data == "Full Power" device.set_preset_mode(PRESET_ECO) - assert device.values.mode.data == "eco" + assert device.values.primary.data == "eco" def test_operation_value_set_unknown(device_unknown): """Test values changed for climate device. Unknown.""" device = device_unknown - assert device.values.mode.data == "Heat" + assert device.values.primary.data == "Heat" device.set_preset_mode("Abcdefg") - assert device.values.mode.data == "Abcdefg" + assert device.values.primary.data == "Abcdefg" device.set_preset_mode(PRESET_NONE) - assert device.values.mode.data == HVAC_MODE_HEAT_COOL + assert device.values.primary.data == HVAC_MODE_HEAT_COOL def test_operation_value_set_heat_cool(device_heat_cool): """Test values changed for climate device. Heat/Cool only.""" device = device_heat_cool - assert device.values.mode.data == HVAC_MODE_HEAT + assert device.values.primary.data == HVAC_MODE_HEAT device.set_preset_mode("Heat Eco") - assert device.values.mode.data == "Heat Eco" + assert device.values.primary.data == "Heat Eco" device.set_preset_mode(PRESET_NONE) - assert device.values.mode.data == HVAC_MODE_HEAT + assert device.values.primary.data == HVAC_MODE_HEAT device.set_preset_mode("Cool Eco") - assert device.values.mode.data == "Cool Eco" + assert device.values.primary.data == "Cool Eco" device.set_preset_mode(PRESET_NONE) - assert device.values.mode.data == HVAC_MODE_COOL + assert device.values.primary.data == HVAC_MODE_COOL def test_fan_mode_value_set(device): @@ -314,11 +471,81 @@ def test_fan_mode_value_set(device): def test_target_value_changed(device): """Test values changed for climate device.""" assert device.target_temperature == 1 - device.values.primary.data = 2 + device.values.setpoint_heating.data = 2 + value_changed(device.values.setpoint_heating) + assert device.target_temperature == 2 + device.values.primary.data = HVAC_MODE_COOL + value_changed(device.values.primary) + assert device.target_temperature == 10 + device.values.setpoint_cooling.data = 9 + value_changed(device.values.setpoint_cooling) + assert device.target_temperature == 9 + + +def test_target_range_changed(device_heat_cool_range): + """Test values changed for climate device.""" + device = device_heat_cool_range + assert device.target_temperature_low == 1 + assert device.target_temperature_high == 10 + device.values.setpoint_heating.data = 2 + value_changed(device.values.setpoint_heating) + assert device.target_temperature_low == 2 + assert device.target_temperature_high == 10 + device.values.setpoint_cooling.data = 9 + value_changed(device.values.setpoint_cooling) + assert device.target_temperature_low == 2 + assert device.target_temperature_high == 9 + + +def test_target_changed_preset_range(device_heat_cool_away): + """Test values changed for climate device.""" + device = device_heat_cool_away + assert device.target_temperature_low == 2 + assert device.target_temperature_high == 9 + device.values.primary.data = PRESET_AWAY + value_changed(device.values.primary) + assert device.target_temperature_low == 1 + assert device.target_temperature_high == 10 + device.values.setpoint_away_heating.data = 0 + value_changed(device.values.setpoint_away_heating) + device.values.setpoint_away_cooling.data = 11 + value_changed(device.values.setpoint_away_cooling) + assert device.target_temperature_low == 0 + assert device.target_temperature_high == 11 + device.values.primary.data = HVAC_MODE_HEAT_COOL + value_changed(device.values.primary) + assert device.target_temperature_low == 2 + assert device.target_temperature_high == 9 + + +def test_target_changed_eco(device_heat_eco): + """Test values changed for climate device.""" + device = device_heat_eco + assert device.target_temperature == 2 + device.values.primary.data = "heat econ" + value_changed(device.values.primary) + assert device.target_temperature == 1 + device.values.setpoint_eco_heating.data = 0 + value_changed(device.values.setpoint_eco_heating) + assert device.target_temperature == 0 + device.values.primary.data = HVAC_MODE_HEAT value_changed(device.values.primary) assert device.target_temperature == 2 +def test_target_changed_with_mode(device): + """Test values changed for climate device.""" + assert device.hvac_mode == HVAC_MODE_HEAT + assert device.target_temperature == 1 + device.values.primary.data = HVAC_MODE_COOL + value_changed(device.values.primary) + assert device.target_temperature == 10 + device.values.primary.data = HVAC_MODE_HEAT_COOL + value_changed(device.values.primary) + assert device.target_temperature_low == 1 + assert device.target_temperature_high == 10 + + def test_temperature_value_changed(device): """Test values changed for climate device.""" assert device.current_temperature == 5 @@ -331,15 +558,15 @@ def test_operation_value_changed(device): """Test values changed for climate device.""" assert device.hvac_mode == HVAC_MODE_HEAT assert device.preset_mode == PRESET_NONE - device.values.mode.data = HVAC_MODE_COOL - value_changed(device.values.mode) + device.values.primary.data = HVAC_MODE_COOL + value_changed(device.values.primary) assert device.hvac_mode == HVAC_MODE_COOL assert device.preset_mode == PRESET_NONE - device.values.mode.data = HVAC_MODE_OFF - value_changed(device.values.mode) + device.values.primary.data = HVAC_MODE_OFF + value_changed(device.values.primary) assert device.hvac_mode == HVAC_MODE_OFF assert device.preset_mode == PRESET_NONE - device.values.mode = None + device.values.primary = None assert device.hvac_mode == HVAC_MODE_HEAT_COOL assert device.preset_mode == PRESET_NONE @@ -349,8 +576,8 @@ def test_operation_value_changed_preset(device_mapping): device = device_mapping assert device.hvac_mode == HVAC_MODE_HEAT assert device.preset_mode == PRESET_NONE - device.values.mode.data = PRESET_ECO - value_changed(device.values.mode) + device.values.primary.data = PRESET_ECO + value_changed(device.values.primary) assert device.hvac_mode == HVAC_MODE_HEAT_COOL assert device.preset_mode == PRESET_ECO @@ -360,12 +587,12 @@ def test_operation_value_changed_mapping(device_mapping): device = device_mapping assert device.hvac_mode == HVAC_MODE_HEAT assert device.preset_mode == PRESET_NONE - device.values.mode.data = "Off" - value_changed(device.values.mode) + device.values.primary.data = "Off" + value_changed(device.values.primary) assert device.hvac_mode == HVAC_MODE_OFF assert device.preset_mode == PRESET_NONE - device.values.mode.data = "Cool" - value_changed(device.values.mode) + device.values.primary.data = "Cool" + value_changed(device.values.primary) assert device.hvac_mode == HVAC_MODE_COOL assert device.preset_mode == PRESET_NONE @@ -375,11 +602,11 @@ def test_operation_value_changed_mapping_preset(device_mapping): device = device_mapping assert device.hvac_mode == HVAC_MODE_HEAT assert device.preset_mode == PRESET_NONE - device.values.mode.data = "Full Power" - value_changed(device.values.mode) + device.values.primary.data = "Full Power" + value_changed(device.values.primary) assert device.hvac_mode == HVAC_MODE_HEAT_COOL assert device.preset_mode == PRESET_BOOST - device.values.mode = None + device.values.primary = None assert device.hvac_mode == HVAC_MODE_HEAT_COOL assert device.preset_mode == PRESET_NONE @@ -389,8 +616,8 @@ def test_operation_value_changed_unknown(device_unknown): device = device_unknown assert device.hvac_mode == HVAC_MODE_HEAT assert device.preset_mode == PRESET_NONE - device.values.mode.data = "Abcdefg" - value_changed(device.values.mode) + device.values.primary.data = "Abcdefg" + value_changed(device.values.primary) assert device.hvac_mode == HVAC_MODE_HEAT_COOL assert device.preset_mode == "Abcdefg" @@ -400,12 +627,12 @@ def test_operation_value_changed_heat_cool(device_heat_cool): device = device_heat_cool assert device.hvac_mode == HVAC_MODE_HEAT assert device.preset_mode == PRESET_NONE - device.values.mode.data = "Cool Eco" - value_changed(device.values.mode) + device.values.primary.data = "Cool Eco" + value_changed(device.values.primary) assert device.hvac_mode == HVAC_MODE_COOL assert device.preset_mode == "Cool Eco" - device.values.mode.data = "Heat Eco" - value_changed(device.values.mode) + device.values.primary.data = "Heat Eco" + value_changed(device.values.primary) assert device.hvac_mode == HVAC_MODE_HEAT assert device.preset_mode == "Heat Eco" diff --git a/tests/components/zwave/test_init.py b/tests/components/zwave/test_init.py index 1de69249bfea..7038d6b61147 100644 --- a/tests/components/zwave/test_init.py +++ b/tests/components/zwave/test_init.py @@ -574,18 +574,33 @@ def mock_connect(receiver, signal, *args, **kwargs): assert len(mock_receivers) == 1 node = MockNode(node_id=11, generic=const.GENERIC_TYPE_THERMOSTAT) - setpoint = MockValue( + thermostat_mode = MockValue( + data="Heat", + data_items=["Off", "Heat"], + node=node, + command_class=const.COMMAND_CLASS_THERMOSTAT_MODE, + genre=const.GENRE_USER, + ) + setpoint_heating = MockValue( data=22.0, node=node, - index=12, - instance=13, command_class=const.COMMAND_CLASS_THERMOSTAT_SETPOINT, + index=1, genre=const.GENRE_USER, - units="C", ) - hass.async_add_job(mock_receivers[0], node, setpoint) + + hass.async_add_job(mock_receivers[0], node, thermostat_mode) await hass.async_block_till_done() + def mock_update(self): + self.hass.add_job(self.async_update_ha_state) + + with patch.object( + zwave.node_entity.ZWaveBaseEntity, "maybe_schedule_update", new=mock_update + ): + hass.async_add_job(mock_receivers[0], node, setpoint_heating) + await hass.async_block_till_done() + assert ( hass.states.get("climate.mock_node_mock_value").attributes["temperature"] == 22.0 @@ -597,9 +612,6 @@ def mock_connect(receiver, signal, *args, **kwargs): is None ) - def mock_update(self): - self.hass.add_job(self.async_update_ha_state) - with patch.object( zwave.node_entity.ZWaveBaseEntity, "maybe_schedule_update", new=mock_update ): @@ -607,7 +619,6 @@ def mock_update(self): data=23.5, node=node, index=1, - instance=13, command_class=const.COMMAND_CLASS_SENSOR_MULTILEVEL, genre=const.GENRE_USER, units="C", diff --git a/tests/conftest.py b/tests/conftest.py index 5e1bbc76fb56..a6683ecad3ab 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,8 +1,6 @@ """Set up some common test helper things.""" -import asyncio import functools import logging -import os from unittest.mock import patch import pytest @@ -26,10 +24,6 @@ mock_aiohttp_client, ) # noqa: E402 module level import not at top of file -if os.environ.get("UVLOOP") == "1": - import uvloop - - asyncio.set_event_loop_policy(uvloop.EventLoopPolicy()) logging.basicConfig(level=logging.DEBUG) logging.getLogger("sqlalchemy.engine").setLevel(logging.INFO) diff --git a/tests/helpers/test_check_config.py b/tests/helpers/test_check_config.py index 5228f0d48822..0b34b263cafa 100644 --- a/tests/helpers/test_check_config.py +++ b/tests/helpers/test_check_config.py @@ -1,6 +1,5 @@ """Test check_config helper.""" import logging -import os # noqa: F401 pylint: disable=unused-import from unittest.mock import patch from homeassistant.helpers.check_config import ( diff --git a/tests/helpers/test_config_validation.py b/tests/helpers/test_config_validation.py index 1f5d6ddfc402..57554d37bb16 100644 --- a/tests/helpers/test_config_validation.py +++ b/tests/helpers/test_config_validation.py @@ -395,7 +395,7 @@ def test_template_complex(): """Test template_complex validator.""" schema = vol.Schema(cv.template_complex) - for value in (None, "{{ partial_print }", "{% if True %}Hello"): + for value in ("{{ partial_print }", "{% if True %}Hello"): with pytest.raises(vol.MultipleInvalid): schema(value) @@ -420,6 +420,10 @@ def test_template_complex(): ["{{ beer }}", 1], ) + # Ensure we don't mutate non-string types that cannot be templates. + for value in (1, True, None): + assert schema(value) == value + def test_time_zone(): """Test time zone validation.""" diff --git a/tests/helpers/test_entity.py b/tests/helpers/test_entity.py index 9d05920f78b7..cd852f5bfc06 100644 --- a/tests/helpers/test_entity.py +++ b/tests/helpers/test_entity.py @@ -9,7 +9,7 @@ from homeassistant.helpers import entity, entity_registry from homeassistant.core import Context -from homeassistant.const import ATTR_HIDDEN, ATTR_DEVICE_CLASS +from homeassistant.const import ATTR_HIDDEN, ATTR_DEVICE_CLASS, STATE_UNAVAILABLE from homeassistant.config import DATA_CUSTOMIZE from homeassistant.helpers.entity_values import EntityValues @@ -641,3 +641,23 @@ async def test_disabled_in_entity_registry(hass): assert entry3 != entry2 assert ent.registry_entry == entry3 assert ent.enabled is False + + +async def test_capability_attrs(hass): + """Test we still include capabilities even when unavailable.""" + with patch.object( + entity.Entity, "available", PropertyMock(return_value=False) + ), patch.object( + entity.Entity, + "capability_attributes", + PropertyMock(return_value={"always": "there"}), + ): + ent = entity.Entity() + ent.hass = hass + ent.entity_id = "hello.world" + ent.async_write_ha_state() + + state = hass.states.get("hello.world") + assert state is not None + assert state.state == STATE_UNAVAILABLE + assert state.attributes["always"] == "there" diff --git a/tests/helpers/test_entity_component.py b/tests/helpers/test_entity_component.py index 0d52f430ff5d..350aa88b6af2 100644 --- a/tests/helpers/test_entity_component.py +++ b/tests/helpers/test_entity_component.py @@ -9,6 +9,7 @@ import pytest import homeassistant.core as ha +from homeassistant.const import ENTITY_MATCH_ALL from homeassistant.exceptions import PlatformNotReady from homeassistant.components import group from homeassistant.helpers.entity_component import EntityComponent @@ -194,7 +195,7 @@ async def test_extract_from_service_available_device(hass): ] ) - call_1 = ha.ServiceCall("test", "service") + call_1 = ha.ServiceCall("test", "service", data={"entity_id": ENTITY_MATCH_ALL}) assert ["test_domain.test_1", "test_domain.test_3"] == sorted( ent.entity_id for ent in (await component.async_extract_from_service(call_1)) @@ -250,7 +251,7 @@ async def test_platform_not_ready(hass): assert "test_domain.mod1" in hass.config.components -async def test_extract_from_service_returns_all_if_no_entity_id(hass): +async def test_extract_from_service_fails_if_no_entity_id(hass): """Test the extraction of everything from service.""" component = EntityComponent(_LOGGER, DOMAIN, hass) await component.async_add_entities( @@ -259,7 +260,7 @@ async def test_extract_from_service_returns_all_if_no_entity_id(hass): call = ha.ServiceCall("test", "service") - assert ["test_domain.test_1", "test_domain.test_2"] == sorted( + assert [] == sorted( ent.entity_id for ent in (await component.async_extract_from_service(call)) ) @@ -445,12 +446,9 @@ async def test_extract_all_omit_entity_id(hass, caplog): call = ha.ServiceCall("test", "service") - assert ["test_domain.test_1", "test_domain.test_2"] == sorted( + assert [] == sorted( ent.entity_id for ent in await component.async_extract_from_service(call) ) - assert ( - "Not passing an entity ID to a service to target all entities is " "deprecated" - ) in caplog.text async def test_extract_all_use_match_all(hass, caplog): diff --git a/tests/helpers/test_icon.py b/tests/helpers/test_icon.py index ce6e95110c96..4f1d4cb223fe 100644 --- a/tests/helpers/test_icon.py +++ b/tests/helpers/test_icon.py @@ -44,3 +44,15 @@ def test_battery_icon(): postfix = "" assert iconbase + postfix == icon_for_battery_level(level, False) assert iconbase + postfix_charging == icon_for_battery_level(level, True) + + +def test_signal_icon(): + """Test icon generator for signal sensor.""" + from homeassistant.helpers.icon import icon_for_signal_level + + assert icon_for_signal_level(None) == "mdi:signal-cellular-outline" + assert icon_for_signal_level(0) == "mdi:signal-cellular-outline" + assert icon_for_signal_level(5) == "mdi:signal-cellular-1" + assert icon_for_signal_level(40) == "mdi:signal-cellular-2" + assert icon_for_signal_level(80) == "mdi:signal-cellular-3" + assert icon_for_signal_level(100) == "mdi:signal-cellular-3" diff --git a/tests/helpers/test_service.py b/tests/helpers/test_service.py index 22611b9f601c..20be7db42f54 100644 --- a/tests/helpers/test_service.py +++ b/tests/helpers/test_service.py @@ -9,9 +9,9 @@ import pytest # To prevent circular import when running just this file -import homeassistant.components # noqa +import homeassistant.components # noqa: F401 from homeassistant import core as ha, exceptions -from homeassistant.const import STATE_ON, STATE_OFF, ATTR_ENTITY_ID +from homeassistant.const import STATE_ON, STATE_OFF, ATTR_ENTITY_ID, ENTITY_MATCH_ALL from homeassistant.setup import async_setup_component import homeassistant.helpers.config_validation as cv from homeassistant.auth.permissions import PolicyPermissions @@ -334,7 +334,10 @@ async def test_call_context_target_all(hass, mock_service_platform_call, mock_en [Mock(entities=mock_entities)], Mock(), ha.ServiceCall( - "test_domain", "test_service", context=ha.Context(user_id="mock-id") + "test_domain", + "test_service", + data={"entity_id": ENTITY_MATCH_ALL}, + context=ha.Context(user_id="mock-id"), ), ) @@ -407,7 +410,9 @@ async def test_call_no_context_target_all( hass, [Mock(entities=mock_entities)], Mock(), - ha.ServiceCall("test_domain", "test_service"), + ha.ServiceCall( + "test_domain", "test_service", data={"entity_id": ENTITY_MATCH_ALL} + ), ) assert len(mock_service_platform_call.mock_calls) == 1 @@ -458,9 +463,9 @@ async def test_call_with_match_all( async def test_call_with_omit_entity_id( - hass, mock_service_platform_call, mock_entities, caplog + hass, mock_service_platform_call, mock_entities ): - """Check we only target allowed entities if targetting all.""" + """Check service call if we do not pass an entity ID.""" await service.entity_service_call( hass, [Mock(entities=mock_entities)], @@ -470,13 +475,7 @@ async def test_call_with_omit_entity_id( assert len(mock_service_platform_call.mock_calls) == 1 entities = mock_service_platform_call.mock_calls[0][1][2] - assert entities == [ - mock_entities["light.kitchen"], - mock_entities["light.living_room"], - ] - assert ( - "Not passing an entity ID to a service to target " "all entities is deprecated" - ) in caplog.text + assert entities == [] async def test_register_admin_service(hass, hass_read_only_user, hass_admin_user): diff --git a/tests/helpers/test_template.py b/tests/helpers/test_template.py index b69fdb17e35a..f463149bc282 100644 --- a/tests/helpers/test_template.py +++ b/tests/helpers/test_template.py @@ -227,6 +227,13 @@ def test_rounding_value(hass): == "12.8" ) + assert ( + template.Template( + '{{ states.sensor.temperature.state | round(1, "half") }}', hass + ).async_render() + == "13.0" + ) + def test_rounding_value_get_original_value_on_error(hass): """Test rounding value get original value on error.""" @@ -1789,3 +1796,10 @@ def test_length_of_states(hass): tpl = template.Template("{{ states.sensor | length }}", hass) assert tpl.async_render() == "2" + + +def test_render_complex_handling_non_template_values(hass): + """Test that we can render non-template fields.""" + assert template.render_complex( + {True: 1, False: template.Template("{{ hello }}", hass)}, {"hello": 2} + ) == {True: 1, False: "2"} diff --git a/tests/scripts/test_check_config.py b/tests/scripts/test_check_config.py index 5199f01807f5..8e1ffe63e845 100644 --- a/tests/scripts/test_check_config.py +++ b/tests/scripts/test_check_config.py @@ -1,6 +1,5 @@ """Test check_config script.""" import logging -import os # noqa: F401 pylint: disable=unused-import from unittest.mock import patch import homeassistant.scripts.check_config as check_config diff --git a/tests/testing_config/custom_components/test/alarm_control_panel.py b/tests/testing_config/custom_components/test/alarm_control_panel.py index 0e2842f86956..1ffa52086e7b 100644 --- a/tests/testing_config/custom_components/test/alarm_control_panel.py +++ b/tests/testing_config/custom_components/test/alarm_control_panel.py @@ -11,6 +11,12 @@ STATE_ALARM_DISARMED, STATE_ALARM_TRIGGERED, ) +from homeassistant.components.alarm_control_panel.const import ( + SUPPORT_ALARM_ARM_HOME, + SUPPORT_ALARM_ARM_AWAY, + SUPPORT_ALARM_ARM_NIGHT, + SUPPORT_ALARM_TRIGGER, +) from tests.common import MockEntity ENTITIES = {} @@ -64,6 +70,16 @@ def state(self): """Return the state of the device.""" return self._state + @property + def supported_features(self) -> int: + """Return the list of supported features.""" + return ( + SUPPORT_ALARM_ARM_HOME + | SUPPORT_ALARM_ARM_AWAY + | SUPPORT_ALARM_ARM_NIGHT + | SUPPORT_ALARM_TRIGGER + ) + def alarm_arm_away(self, code=None): """Send arm away command.""" self._state = STATE_ALARM_ARMED_AWAY diff --git a/tests/testing_config/custom_components/test_package/__init__.py b/tests/testing_config/custom_components/test_package/__init__.py index 0a02ef1e45f2..f5cd2c34edfd 100644 --- a/tests/testing_config/custom_components/test_package/__init__.py +++ b/tests/testing_config/custom_components/test_package/__init__.py @@ -1,5 +1,5 @@ """Provide a mock package component.""" -from .const import TEST # noqa +from .const import TEST # noqa: F401 DOMAIN = "test_package" diff --git a/tests/util/test_async.py b/tests/util/test_async.py index 8dede61869c3..9cda40c1b8b4 100644 --- a/tests/util/test_async.py +++ b/tests/util/test_async.py @@ -165,7 +165,7 @@ def test_run_callback_threadsafe_with_exception(self): def test_run_callback_threadsafe_with_invalid(self): """Test callback submission from thread to event loop on invalid.""" - callback = lambda: self.target_callback(invalid=True) # noqa + callback = lambda: self.target_callback(invalid=True) # noqa: E731 future = self.loop.run_in_executor(None, callback) with self.assertRaises(ValueError) as exc_context: self.loop.run_until_complete(future) diff --git a/tox.ini b/tox.ini index dc2a9f79b90d..17253e1d1e14 100644 --- a/tox.ini +++ b/tox.ini @@ -37,6 +37,7 @@ commands = python -m script.gen_requirements_all validate python -m script.hassfest validate pre-commit run flake8 {posargs: --all-files} + pre-commit run bandit {posargs: --all-files} [testenv:typing] deps =