diff --git a/.coveragerc b/.coveragerc index 8e5b61136c02c..bbed9b7e742a7 100644 --- a/.coveragerc +++ b/.coveragerc @@ -136,6 +136,7 @@ omit = homeassistant/components/device_tracker/tplink.py homeassistant/components/device_tracker/traccar.py homeassistant/components/device_tracker/trackr.py + homeassistant/components/device_tracker/ubee.py homeassistant/components/device_tracker/ubus.py homeassistant/components/digital_ocean/* homeassistant/components/dominos/* @@ -204,6 +205,7 @@ omit = homeassistant/components/insteon/* homeassistant/components/ios/* homeassistant/components/iota/* + homeassistant/components/iperf3/* homeassistant/components/isy994/* homeassistant/components/joaoapps_join/* homeassistant/components/juicenet/* @@ -317,6 +319,8 @@ omit = homeassistant/components/media_player/yamaha_musiccast.py homeassistant/components/media_player/yamaha.py homeassistant/components/media_player/ziggo_mediabox_xl.py + homeassistant/components/meteo_france/* + homeassistant/components/mobile_app/* homeassistant/components/mochad/* homeassistant/components/modbus/* homeassistant/components/mychevy/* @@ -326,6 +330,7 @@ omit = homeassistant/components/nest/* homeassistant/components/netatmo/* homeassistant/components/netgear_lte/* + homeassistant/components/nissan_leaf/* homeassistant/components/notify/aws_lambda.py homeassistant/components/notify/aws_sns.py homeassistant/components/notify/aws_sqs.py @@ -374,10 +379,13 @@ omit = homeassistant/components/openuv/__init__.py homeassistant/components/openuv/binary_sensor.py homeassistant/components/openuv/sensor.py + homeassistant/components/owlet/* homeassistant/components/pilight/* homeassistant/components/plum_lightpad/* homeassistant/components/point/* homeassistant/components/prometheus/* + homeassistant/components/ps4/__init__.py + homeassistant/components/ps4/media_player.py homeassistant/components/qwikswitch/* homeassistant/components/rachio/* homeassistant/components/rainbird/* @@ -388,6 +396,7 @@ omit = homeassistant/components/rainmachine/switch.py homeassistant/components/raspihats/* homeassistant/components/raspyrfm/* + homeassistant/components/reddit/* homeassistant/components/remember_the_milk/__init__.py homeassistant/components/remote/harmony.py homeassistant/components/remote/itach.py @@ -470,7 +479,6 @@ omit = homeassistant/components/sensor/imap_email_content.py homeassistant/components/sensor/imap.py homeassistant/components/sensor/influxdb.py - homeassistant/components/sensor/iperf3.py homeassistant/components/sensor/irish_rail_transport.py homeassistant/components/sensor/kwb.py homeassistant/components/sensor/lacrosse.py @@ -482,7 +490,6 @@ omit = homeassistant/components/sensor/loopenergy.py homeassistant/components/sensor/lyft.py homeassistant/components/sensor/magicseaweed.py - homeassistant/components/sensor/meteo_france.py homeassistant/components/sensor/metoffice.py homeassistant/components/sensor/miflora.py homeassistant/components/sensor/mitemp_bt.py @@ -609,6 +616,7 @@ omit = homeassistant/components/switch/rest.py homeassistant/components/switch/rpi_rf.py homeassistant/components/switch/snmp.py + homeassistant/components/switch/sony_projector.py homeassistant/components/switch/switchbot.py homeassistant/components/switch/switchmate.py homeassistant/components/switch/telnet.py diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index e58659c876a1b..57244b44d9a87 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -2,6 +2,7 @@ - If you need additional help with this template please refer to https://www.home-assistant.io/help/reporting_issues/ - Make sure you are running the latest version of Home Assistant before reporting an issue: https://github.com/home-assistant/home-assistant/releases - Frontend issues should be submitted to the home-assistant-polymer repository: https://github.com/home-assistant/home-assistant-polymer/issues +- iOS issues should be submitted to the home-assistant-iOS repository: https://github.com/home-assistant/home-assistant-iOS/issues - Do not report issues for components if you are using custom components: files in /custom_components - This is for bugs only. Feature and enhancement requests should go in our community forum: https://community.home-assistant.io/c/feature-requests - Provide as many details as possible. Paste logs, configuration sample and code into the backticks. Do not delete any text from this template! diff --git a/.github/ISSUE_TEMPLATE/Bug_report.md b/.github/ISSUE_TEMPLATE/Bug_report.md index d9ab141d61c22..2abfa6f9b6f66 100644 --- a/.github/ISSUE_TEMPLATE/Bug_report.md +++ b/.github/ISSUE_TEMPLATE/Bug_report.md @@ -8,6 +8,7 @@ about: Create a report to help us improve - If you need additional help with this template please refer to https://www.home-assistant.io/help/reporting_issues/ - Make sure you are running the latest version of Home Assistant before reporting an issue: https://github.com/home-assistant/home-assistant/releases - Frontend issues should be submitted to the home-assistant-polymer repository: https://github.com/home-assistant/home-assistant-polymer/issues +- iOS issues should be submitted to the home-assistant-iOS repository: https://github.com/home-assistant/home-assistant-iOS/issues - Do not report issues for components if you are using custom components: files in /custom_components - This is for bugs only. Feature and enhancement requests should go in our community forum: https://community.home-assistant.io/c/feature-requests - Provide as many details as possible. Paste logs, configuration sample and code into the backticks. Do not delete any text from this template! diff --git a/.travis.yml b/.travis.yml index 920e8b5704774..be00f989290c0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -34,7 +34,7 @@ cache: - $HOME/.cache/pip install: pip install -U tox coveralls language: python -script: travis_wait 30 tox --develop +script: travis_wait 40 tox --develop services: - docker before_deploy: diff --git a/CODEOWNERS b/CODEOWNERS index 6426359812113..ac8f98a11b032 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -28,7 +28,7 @@ homeassistant/components/panel_iframe/* @home-assistant/core homeassistant/components/onboarding/* @home-assistant/core homeassistant/components/persistent_notification/* @home-assistant/core homeassistant/components/scene/__init__.py @home-assistant/core -homeassistant/components/scene/hass.py @home-assistant/core +homeassistant/components/scene/homeassistant.py @home-assistant/core homeassistant/components/script/* @home-assistant/core homeassistant/components/shell_command/* @home-assistant/core homeassistant/components/sun/* @home-assistant/core @@ -47,7 +47,6 @@ homeassistant/components/*/zwave.py @home-assistant/z-wave homeassistant/components/hassio/* @home-assistant/hassio # Individual platforms -homeassistant/components/alarm_control_panel/egardia.py @jeroenterheerdt homeassistant/components/alarm_control_panel/manual_mqtt.py @colinodell homeassistant/components/binary_sensor/hikvision.py @mezz64 homeassistant/components/binary_sensor/threshold.py @fabaff @@ -68,10 +67,8 @@ homeassistant/components/device_tracker/quantum_gateway.py @cisasteelersfan homeassistant/components/device_tracker/tile.py @bachya homeassistant/components/device_tracker/traccar.py @ludeeus homeassistant/components/device_tracker/bt_smarthub.py @jxwolstenholme -homeassistant/components/history_graph/* @andrey-git -homeassistant/components/influx/* @fabaff +homeassistant/components/device_tracker/synology_srm.py @aerialls homeassistant/components/light/lifx_legacy.py @amelchio -homeassistant/components/light/tplink.py @rytilahti homeassistant/components/light/yeelight.py @rytilahti homeassistant/components/light/yeelightsunflower.py @lindsaymarkward homeassistant/components/lock/nello.py @pschmitt @@ -82,20 +79,15 @@ homeassistant/components/media_player/liveboxplaytv.py @pschmitt homeassistant/components/media_player/mediaroom.py @dgomes homeassistant/components/media_player/monoprice.py @etsinko homeassistant/components/media_player/mpd.py @fabaff -homeassistant/components/media_player/sonos.py @amelchio homeassistant/components/media_player/xiaomi_tv.py @fattdev homeassistant/components/media_player/yamaha_musiccast.py @jalmeroth -homeassistant/components/no_ip/* @fabaff homeassistant/components/notify/file.py @fabaff homeassistant/components/notify/flock.py @fabaff -homeassistant/components/notify/instapush.py @fabaff homeassistant/components/notify/mastodon.py @fabaff homeassistant/components/notify/smtp.py @fabaff homeassistant/components/notify/syslog.py @fabaff homeassistant/components/notify/xmpp.py @fabaff homeassistant/components/notify/yessssms.py @flowolf -homeassistant/components/plant/* @ChristianKuehnel -homeassistant/components/remote/harmony.py @ehendrix23 homeassistant/components/scene/lifx_cloud.py @amelchio homeassistant/components/sensor/airvisual.py @bachya homeassistant/components/sensor/alpha_vantage.py @fabaff @@ -106,11 +98,12 @@ homeassistant/components/sensor/darksky.py @fabaff homeassistant/components/sensor/file.py @fabaff homeassistant/components/sensor/filter.py @dgomes homeassistant/components/sensor/fixer.py @fabaff -homeassistant/components/sensor/flunearyou.py.py @bachya +homeassistant/components/sensor/flunearyou.py @bachya homeassistant/components/sensor/gearbest.py @HerrHofrat homeassistant/components/sensor/gitter.py @fabaff homeassistant/components/sensor/glances.py @fabaff homeassistant/components/sensor/gpsd.py @fabaff +homeassistant/components/sensor/integration.py @dgomes homeassistant/components/sensor/irish_rail_transport.py @ttroy50 homeassistant/components/sensor/jewish_calendar.py @tsvi homeassistant/components/sensor/launch_library.py @ludeeus @@ -135,35 +128,28 @@ homeassistant/components/sensor/statistics.py @fabaff homeassistant/components/sensor/swiss*.py @fabaff homeassistant/components/sensor/sytadin.py @gautric homeassistant/components/sensor/tautulli.py @ludeeus -homeassistant/components/sensor/time_data.py @fabaff +homeassistant/components/sensor/time_date.py @fabaff homeassistant/components/sensor/version.py @fabaff homeassistant/components/sensor/waqi.py @andrey-git homeassistant/components/sensor/worldclock.py @fabaff -homeassistant/components/shiftr/* @fabaff -homeassistant/components/spaceapi/* @fabaff homeassistant/components/switch/switchbot.py @danielhiversen homeassistant/components/switch/switchmate.py @danielhiversen -homeassistant/components/switch/tplink.py @rytilahti homeassistant/components/vacuum/roomba.py @pschmitt homeassistant/components/weather/__init__.py @fabaff homeassistant/components/weather/darksky.py @fabaff homeassistant/components/weather/demo.py @fabaff homeassistant/components/weather/met.py @danielhiversen homeassistant/components/weather/openweathermap.py @fabaff -homeassistant/components/xiaomi_aqara/* @danielhiversen @syssi # A homeassistant/components/ambient_station/* @bachya homeassistant/components/arduino/* @fabaff -homeassistant/components/*/arduino.py @fabaff +homeassistant/components/axis/* @kane610 homeassistant/components/*/arest.py @fabaff -homeassistant/components/*/axis.py @kane610 # B homeassistant/components/blink/* @fronzbot -homeassistant/components/*/blink.py @fronzbot homeassistant/components/bmw_connected_drive/* @ChristianKuehnel -homeassistant/components/*/bmw_connected_drive.py @ChristianKuehnel homeassistant/components/*/broadlink.py @danielhiversen # C @@ -172,51 +158,44 @@ homeassistant/components/counter/* @fabaff # D homeassistant/components/daikin/* @fredrike @rofrantz -homeassistant/components/*/daikin.py @fredrike @rofrantz -homeassistant/components/*/deconz.py @kane610 +homeassistant/components/deconz/* @kane610 homeassistant/components/digital_ocean/* @fabaff -homeassistant/components/*/digital_ocean.py @fabaff homeassistant/components/dweet/* @fabaff -homeassistant/components/*/dweet.py @fabaff # E homeassistant/components/ecovacs/* @OverloadUT -homeassistant/components/*/ecovacs.py @OverloadUT -homeassistant/components/*/edp_redy.py @abmantis homeassistant/components/edp_redy/* @abmantis homeassistant/components/eight_sleep/* @mezz64 -homeassistant/components/*/eight_sleep.py @mezz64 +homeassistant/components/egardia/* @jeroenterheerdt homeassistant/components/esphome/*.py @OttoWinter +# F +homeassistant/components/freebox/*.py @snoof85 + # G homeassistant/components/googlehome/* @ludeeus -homeassistant/components/*/googlehome.py @ludeeus # H +homeassistant/components/harmony/* @ehendrix23 +homeassistant/components/history_graph/* @andrey-git homeassistant/components/hive/* @Rendili @KJonline -homeassistant/components/*/hive.py @Rendili @KJonline homeassistant/components/homekit/* @cdce8p homeassistant/components/huawei_lte/* @scop -homeassistant/components/*/huawei_lte.py @scop # I +homeassistant/components/influx/* @fabaff homeassistant/components/ipma/* @dgomes # K homeassistant/components/knx/* @Julius2342 -homeassistant/components/*/knx.py @Julius2342 homeassistant/components/konnected/* @heythisisnate -homeassistant/components/*/konnected.py @heythisisnate # L homeassistant/components/lifx/* @amelchio -homeassistant/components/*/lifx.py @amelchio homeassistant/components/luftdaten/* @fabaff -homeassistant/components/*/luftdaten.py @fabaff # M homeassistant/components/matrix/* @tinloaf -homeassistant/components/*/matrix.py @tinloaf homeassistant/components/melissa/* @kennedyshead homeassistant/components/*/melissa.py @kennedyshead homeassistant/components/*/mystrom.py @fabaff @@ -224,59 +203,56 @@ homeassistant/components/*/mystrom.py @fabaff # N homeassistant/components/ness_alarm/* @nickw444 homeassistant/components/*/ness_alarm.py @nickw444 +homeassistant/components/nissan_leaf/* @filcole +homeassistant/components/no_ip/* @fabaff # O homeassistant/components/openuv/* @bachya # P +homeassistant/components/plant/* @ChristianKuehnel homeassistant/components/point/* @fredrike -homeassistant/components/*/point.py @fredrike # Q homeassistant/components/qwikswitch/* @kellerza -homeassistant/components/*/qwikswitch.py @kellerza # R homeassistant/components/rainmachine/* @bachya +homeassistant/components/rfxtrx/* @danielhiversen homeassistant/components/*/random.py @fabaff -homeassistant/components/*/rfxtrx.py @danielhiversen # S +homeassistant/components/shiftr/* @fabaff homeassistant/components/simplisafe/* @bachya homeassistant/components/smartthings/* @andrewsayre +homeassistant/components/sonos/* @amelchio +homeassistant/components/spaceapi/* @fabaff homeassistant/components/spider/* @peternijssen # T homeassistant/components/tahoma/* @philklei -homeassistant/components/*/tahoma.py @philklei homeassistant/components/tellduslive/*.py @fredrike -homeassistant/components/*/tellduslive.py @fredrike homeassistant/components/tesla/* @zabuldon -homeassistant/components/*/tesla.py @zabuldon homeassistant/components/thethingsnetwork/* @fabaff -homeassistant/components/*/thethingsnetwork.py @fabaff homeassistant/components/tibber/* @danielhiversen -homeassistant/components/*/tibber.py @danielhiversen +homeassistant/components/tplink/* @rytilahti homeassistant/components/tradfri/* @ggravlingen -homeassistant/components/*/tradfri.py @ggravlingen +homeassistant/components/toon/* @frenck # U homeassistant/components/unifi/* @kane610 -homeassistant/components/switch/unifi.py @kane610 homeassistant/components/upcloud/* @scop -homeassistant/components/*/upcloud.py @scop +homeassistant/components/utility_meter/* @dgomes # V homeassistant/components/velux/* @Julius2342 -homeassistant/components/*/velux.py @Julius2342 # W homeassistant/components/wemo/* @sqldiablo -homeassistant/components/*/wemo.py @sqldiablo # X -homeassistant/components/*/xiaomi_aqara.py @danielhiversen @syssi -homeassistant/components/*/xiaomi_miio.py @rytilahti @syssi +homeassistant/components/xiaomi_aqara/* @danielhiversen @syssi +homeassistant/components/xiaomi_miio/* @rytilahti @syssi # Z homeassistant/components/zoneminder/* @rohankapoorcom diff --git a/homeassistant/auth/__init__.py b/homeassistant/auth/__init__.py index 3377bb2a6aa0d..bb90f2964687b 100644 --- a/homeassistant/auth/__init__.py +++ b/homeassistant/auth/__init__.py @@ -170,8 +170,7 @@ async def async_get_or_create_user(self, credentials: models.Credentials) \ user = await self.async_get_user_by_credentials(credentials) if user is None: raise ValueError('Unable to find the user.') - else: - return user + return user auth_provider = self._async_get_auth_provider(credentials) diff --git a/homeassistant/auth/mfa_modules/notify.py b/homeassistant/auth/mfa_modules/notify.py index 3c26f8b4bde44..310abff94842b 100644 --- a/homeassistant/auth/mfa_modules/notify.py +++ b/homeassistant/auth/mfa_modules/notify.py @@ -2,6 +2,7 @@ Sending HOTP through notify service """ +import asyncio import logging from collections import OrderedDict from typing import Any, Dict, Optional, List @@ -90,6 +91,7 @@ def __init__(self, hass: HomeAssistant, config: Dict[str, Any]) -> None: self._include = config.get(CONF_INCLUDE, []) self._exclude = config.get(CONF_EXCLUDE, []) self._message_template = config[CONF_MESSAGE] + self._init_lock = asyncio.Lock() @property def input_schema(self) -> vol.Schema: @@ -98,15 +100,19 @@ def input_schema(self) -> vol.Schema: async def _async_load(self) -> None: """Load stored data.""" - data = await self._user_store.async_load() + async with self._init_lock: + if self._user_settings is not None: + return - if data is None: - data = {STORAGE_USERS: {}} + data = await self._user_store.async_load() - self._user_settings = { - user_id: NotifySetting(**setting) - for user_id, setting in data.get(STORAGE_USERS, {}).items() - } + if data is None: + data = {STORAGE_USERS: {}} + + self._user_settings = { + user_id: NotifySetting(**setting) + for user_id, setting in data.get(STORAGE_USERS, {}).items() + } async def _async_save(self) -> None: """Save data.""" diff --git a/homeassistant/auth/mfa_modules/totp.py b/homeassistant/auth/mfa_modules/totp.py index 68f4e1d05968f..dc51152f565c7 100644 --- a/homeassistant/auth/mfa_modules/totp.py +++ b/homeassistant/auth/mfa_modules/totp.py @@ -1,4 +1,5 @@ """Time-based One Time Password auth module.""" +import asyncio import logging from io import BytesIO from typing import Any, Dict, Optional, Tuple # noqa: F401 @@ -68,6 +69,7 @@ def __init__(self, hass: HomeAssistant, config: Dict[str, Any]) -> None: self._users = None # type: Optional[Dict[str, str]] self._user_store = hass.helpers.storage.Store( STORAGE_VERSION, STORAGE_KEY, private=True) + self._init_lock = asyncio.Lock() @property def input_schema(self) -> vol.Schema: @@ -76,12 +78,16 @@ def input_schema(self) -> vol.Schema: async def _async_load(self) -> None: """Load stored data.""" - data = await self._user_store.async_load() + async with self._init_lock: + if self._users is not None: + return - if data is None: - data = {STORAGE_USERS: {}} + data = await self._user_store.async_load() - self._users = data.get(STORAGE_USERS, {}) + if data is None: + data = {STORAGE_USERS: {}} + + self._users = data.get(STORAGE_USERS, {}) async def _async_save(self) -> None: """Save data.""" diff --git a/homeassistant/auth/providers/homeassistant.py b/homeassistant/auth/providers/homeassistant.py index b22f93f11f135..2187d2728004d 100644 --- a/homeassistant/auth/providers/homeassistant.py +++ b/homeassistant/auth/providers/homeassistant.py @@ -1,4 +1,5 @@ """Home Assistant auth provider.""" +import asyncio import base64 from collections import OrderedDict import logging @@ -204,15 +205,21 @@ class HassAuthProvider(AuthProvider): DEFAULT_TITLE = 'Home Assistant Local' - data = None + def __init__(self, *args: Any, **kwargs: Any) -> None: + """Initialize an Home Assistant auth provider.""" + super().__init__(*args, **kwargs) + self.data = None # type: Optional[Data] + self._init_lock = asyncio.Lock() async def async_initialize(self) -> None: """Initialize the auth provider.""" - if self.data is not None: - return + async with self._init_lock: + if self.data is not None: + return - self.data = Data(self.hass) - await self.data.async_load() + data = Data(self.hass) + await data.async_load() + self.data = data async def async_login_flow( self, context: Optional[Dict]) -> LoginFlow: diff --git a/homeassistant/auth/providers/trusted_networks.py b/homeassistant/auth/providers/trusted_networks.py index 8a7e1d67c6d25..d0bc45c326a1a 100644 --- a/homeassistant/auth/providers/trusted_networks.py +++ b/homeassistant/auth/providers/trusted_networks.py @@ -3,18 +3,23 @@ It shows list of users if access from trusted network. Abort login flow if not access from trusted network. """ -from typing import Any, Dict, Optional, cast +from ipaddress import ip_network, IPv4Address, IPv6Address, IPv4Network,\ + IPv6Network +from typing import Any, Dict, List, Optional, Union, cast import voluptuous as vol -from homeassistant.components.http import HomeAssistantHTTP # noqa: F401 +import homeassistant.helpers.config_validation as cv from homeassistant.core import callback from homeassistant.exceptions import HomeAssistantError - from . import AuthProvider, AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS, LoginFlow from ..models import Credentials, UserMeta +IPAddress = Union[IPv4Address, IPv6Address] +IPNetwork = Union[IPv4Network, IPv6Network] + CONFIG_SCHEMA = AUTH_PROVIDER_SCHEMA.extend({ + vol.Required('trusted_networks'): vol.All(cv.ensure_list, [ip_network]) }, extra=vol.PREVENT_EXTRA) @@ -35,6 +40,11 @@ class TrustedNetworksAuthProvider(AuthProvider): DEFAULT_TITLE = 'Trusted Networks' + @property + def trusted_networks(self) -> List[IPNetwork]: + """Return trusted networks.""" + return cast(List[IPNetwork], self.config['trusted_networks']) + @property def support_mfa(self) -> bool: """Trusted Networks auth provider does not support MFA.""" @@ -49,7 +59,7 @@ async def async_login_flow(self, context: Optional[Dict]) -> LoginFlow: if not user.system_generated and user.is_active} return TrustedNetworksLoginFlow( - self, cast(str, context.get('ip_address')), available_users) + self, cast(IPAddress, context.get('ip_address')), available_users) async def async_get_or_create_credentials( self, flow_result: Dict[str, str]) -> Credentials: @@ -80,19 +90,17 @@ async def async_user_meta_for_credentials( raise NotImplementedError @callback - def async_validate_access(self, ip_address: str) -> None: + def async_validate_access(self, ip_addr: IPAddress) -> None: """Make sure the access from trusted networks. Raise InvalidAuthError if not. Raise InvalidAuthError if trusted_networks is not configured. """ - hass_http = getattr(self.hass, 'http', None) # type: HomeAssistantHTTP - - if not hass_http or not hass_http.trusted_networks: + if not self.trusted_networks: raise InvalidAuthError('trusted_networks is not configured') - if not any(ip_address in trusted_network for trusted_network - in hass_http.trusted_networks): + if not any(ip_addr in trusted_network for trusted_network + in self.trusted_networks): raise InvalidAuthError('Not in trusted_networks') @@ -100,12 +108,12 @@ class TrustedNetworksLoginFlow(LoginFlow): """Handler for the login flow.""" def __init__(self, auth_provider: TrustedNetworksAuthProvider, - ip_address: str, available_users: Dict[str, Optional[str]]) \ - -> None: + ip_addr: IPAddress, + available_users: Dict[str, Optional[str]]) -> None: """Initialize the login flow.""" super().__init__(auth_provider) self._available_users = available_users - self._ip_address = ip_address + self._ip_address = ip_addr async def async_step_init( self, user_input: Optional[Dict[str, str]] = None) \ diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index a018d5400338b..eef36b026e1e7 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -85,14 +85,18 @@ async def async_from_config_dict(config: Dict[str, Any], async_enable_logging(hass, verbose, log_rotate_days, log_file, log_no_color) + hass.config.skip_pip = skip_pip + if skip_pip: + _LOGGER.warning("Skipping pip installation of required modules. " + "This may cause issues") + core_config = config.get(core.DOMAIN, {}) - has_api_password = bool((config.get('http') or {}).get('api_password')) - has_trusted_networks = bool((config.get('http') or {}) - .get('trusted_networks')) + has_api_password = bool(config.get('http', {}).get('api_password')) + trusted_networks = config.get('http', {}).get('trusted_networks') try: await conf_util.async_process_ha_core_config( - hass, core_config, has_api_password, has_trusted_networks) + hass, core_config, has_api_password, trusted_networks) except vol.Invalid as config_err: conf_util.async_log_exception( config_err, 'homeassistant', core_config, hass) @@ -105,11 +109,6 @@ async def async_from_config_dict(config: Dict[str, Any], await hass.async_add_executor_job( conf_util.process_ha_config_upgrade, hass) - hass.config.skip_pip = skip_pip - if skip_pip: - _LOGGER.warning("Skipping pip installation of required modules. " - "This may cause issues") - # Make a copy because we are mutating it. config = OrderedDict(config) diff --git a/homeassistant/components/abode/__init__.py b/homeassistant/components/abode/__init__.py index 71a1dcdd590bb..591bae1a9cf66 100644 --- a/homeassistant/components/abode/__init__.py +++ b/homeassistant/components/abode/__init__.py @@ -17,7 +17,8 @@ _LOGGER = logging.getLogger(__name__) -CONF_ATTRIBUTION = "Data provided by goabode.com" +ATTRIBUTION = "Data provided by goabode.com" + CONF_POLLING = 'polling' DOMAIN = 'abode' @@ -280,7 +281,7 @@ def name(self): def device_state_attributes(self): """Return the state attributes.""" return { - ATTR_ATTRIBUTION: CONF_ATTRIBUTION, + ATTR_ATTRIBUTION: ATTRIBUTION, 'device_id': self._device.device_id, 'battery_low': self._device.battery_low, 'no_response': self._device.no_response, @@ -327,7 +328,7 @@ def name(self): def device_state_attributes(self): """Return the state attributes.""" return { - ATTR_ATTRIBUTION: CONF_ATTRIBUTION, + ATTR_ATTRIBUTION: ATTRIBUTION, 'automation_id': self._automation.automation_id, 'type': self._automation.type, 'sub_type': self._automation.sub_type diff --git a/homeassistant/components/abode/alarm_control_panel.py b/homeassistant/components/abode/alarm_control_panel.py index ec5038a7a8440..838d09b73af3e 100644 --- a/homeassistant/components/abode/alarm_control_panel.py +++ b/homeassistant/components/abode/alarm_control_panel.py @@ -2,7 +2,7 @@ import logging import homeassistant.components.alarm_control_panel as alarm -from homeassistant.components.abode import CONF_ATTRIBUTION, AbodeDevice +from homeassistant.components.abode import ATTRIBUTION, AbodeDevice from homeassistant.components.abode import DOMAIN as ABODE_DOMAIN from homeassistant.const import ( ATTR_ATTRIBUTION, STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, @@ -73,7 +73,7 @@ def name(self): def device_state_attributes(self): """Return the state attributes.""" return { - ATTR_ATTRIBUTION: CONF_ATTRIBUTION, + ATTR_ATTRIBUTION: ATTRIBUTION, 'device_id': self._device.device_id, 'battery_backup': self._device.battery, 'cellular_backup': self._device.is_cellular, diff --git a/homeassistant/components/ads/__init__.py b/homeassistant/components/ads/__init__.py index cfd0f37caa0d6..1b90e645af4dc 100644 --- a/homeassistant/components/ads/__init__.py +++ b/homeassistant/components/ads/__init__.py @@ -4,9 +4,11 @@ import logging import ctypes from collections import namedtuple + import voluptuous as vol -from homeassistant.const import CONF_DEVICE, CONF_PORT, CONF_IP_ADDRESS, \ - EVENT_HOMEASSISTANT_STOP + +from homeassistant.const import ( + CONF_DEVICE, CONF_IP_ADDRESS, CONF_PORT, EVENT_HOMEASSISTANT_STOP) import homeassistant.helpers.config_validation as cv REQUIREMENTS = ['pyads==3.0.7'] @@ -16,18 +18,20 @@ DATA_ADS = 'data_ads' # Supported Types +ADSTYPE_BOOL = 'bool' +ADSTYPE_BYTE = 'byte' +ADSTYPE_DINT = 'dint' ADSTYPE_INT = 'int' +ADSTYPE_UDINT = 'udint' ADSTYPE_UINT = 'uint' -ADSTYPE_BYTE = 'byte' -ADSTYPE_BOOL = 'bool' - -DOMAIN = 'ads' -CONF_ADS_VAR = 'adsvar' -CONF_ADS_VAR_BRIGHTNESS = 'adsvar_brightness' -CONF_ADS_TYPE = 'adstype' CONF_ADS_FACTOR = 'factor' +CONF_ADS_TYPE = 'adstype' CONF_ADS_VALUE = 'value' +CONF_ADS_VAR = 'adsvar' +CONF_ADS_VAR_BRIGHTNESS = 'adsvar_brightness' + +DOMAIN = 'ads' SERVICE_WRITE_DATA_BY_NAME = 'write_data_by_name' @@ -41,7 +45,8 @@ SCHEMA_SERVICE_WRITE_DATA_BY_NAME = vol.Schema({ vol.Required(CONF_ADS_TYPE): - vol.In([ADSTYPE_INT, ADSTYPE_UINT, ADSTYPE_BYTE, ADSTYPE_BOOL]), + vol.In([ADSTYPE_INT, ADSTYPE_UINT, ADSTYPE_BYTE, ADSTYPE_BOOL, + ADSTYPE_DINT, ADSTYPE_UDINT]), vol.Required(CONF_ADS_VALUE): vol.Coerce(int), vol.Required(CONF_ADS_VAR): cv.string, }) @@ -61,15 +66,19 @@ def setup(hass, config): AdsHub.ADS_TYPEMAP = { ADSTYPE_BOOL: pyads.PLCTYPE_BOOL, ADSTYPE_BYTE: pyads.PLCTYPE_BYTE, + ADSTYPE_DINT: pyads.PLCTYPE_DINT, ADSTYPE_INT: pyads.PLCTYPE_INT, + ADSTYPE_UDINT: pyads.PLCTYPE_UDINT, ADSTYPE_UINT: pyads.PLCTYPE_UINT, } + AdsHub.ADSError = pyads.ADSError AdsHub.PLCTYPE_BOOL = pyads.PLCTYPE_BOOL AdsHub.PLCTYPE_BYTE = pyads.PLCTYPE_BYTE + AdsHub.PLCTYPE_DINT = pyads.PLCTYPE_DINT AdsHub.PLCTYPE_INT = pyads.PLCTYPE_INT + AdsHub.PLCTYPE_UDINT = pyads.PLCTYPE_UDINT AdsHub.PLCTYPE_UINT = pyads.PLCTYPE_UINT - AdsHub.ADSError = pyads.ADSError try: ads = AdsHub(client) @@ -162,13 +171,12 @@ def add_device_notification(self, name, plc_datatype, callback): hnotify, huser = self._client.add_device_notification( name, attr, self._device_notification_callback) hnotify = int(hnotify) + self._notification_items[hnotify] = NotificationItem( + hnotify, huser, name, plc_datatype, callback) _LOGGER.debug( "Added device notification %d for variable %s", hnotify, name) - self._notification_items[hnotify] = NotificationItem( - hnotify, huser, name, plc_datatype, callback) - def _device_notification_callback(self, notification, name): """Handle device notifications.""" contents = notification.contents @@ -178,9 +186,10 @@ def _device_notification_callback(self, notification, name): data = contents.data try: - notification_item = self._notification_items[hnotify] + with self._lock: + notification_item = self._notification_items[hnotify] except KeyError: - _LOGGER.debug("Unknown device notification handle: %d", hnotify) + _LOGGER.error("Unknown device notification handle: %d", hnotify) return # Parse data to desired datatype @@ -192,6 +201,10 @@ def _device_notification_callback(self, notification, name): value = struct.unpack(' str: @property def device_state_attributes(self) -> dict: """Return other details about the sensor state.""" - return {'level': self._api.data.get('level')} + return {'level': self._api.data.get('level'), + 'location': self._api.data.get('location'), + } @property def name(self) -> str: diff --git a/homeassistant/components/air_quality/opensensemap.py b/homeassistant/components/air_quality/opensensemap.py index d77c0c9bfe20a..8462e40be5b54 100644 --- a/homeassistant/components/air_quality/opensensemap.py +++ b/homeassistant/components/air_quality/opensensemap.py @@ -1,9 +1,4 @@ -""" -Support for openSenseMap Air Quality data. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/air_quality.opensensemap/ -""" +"""Support for openSenseMap Air Quality data.""" from datetime import timedelta import logging @@ -16,7 +11,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.util import Throttle -REQUIREMENTS = ['opensensemap-api==0.1.3'] +REQUIREMENTS = ['opensensemap-api==0.1.4'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/alexa/smart_home.py b/homeassistant/components/alexa/smart_home.py index 4e2383bb43d01..a856a3d8e8252 100644 --- a/homeassistant/components/alexa/smart_home.py +++ b/homeassistant/components/alexa/smart_home.py @@ -11,8 +11,9 @@ import async_timeout from homeassistant.components import ( - alert, automation, binary_sensor, climate, cover, fan, group, http, + alert, automation, binary_sensor, cover, fan, group, http, input_boolean, light, lock, media_player, scene, script, sensor, switch) +from homeassistant.components.climate import const as climate from homeassistant.helpers import aiohttp_client from homeassistant.helpers.event import async_track_state_change from homeassistant.const import ( @@ -22,7 +23,7 @@ SERVICE_MEDIA_PLAY, SERVICE_MEDIA_PREVIOUS_TRACK, SERVICE_MEDIA_STOP, SERVICE_SET_COVER_POSITION, SERVICE_TURN_OFF, SERVICE_TURN_ON, SERVICE_UNLOCK, SERVICE_VOLUME_DOWN, SERVICE_VOLUME_UP, SERVICE_VOLUME_SET, - SERVICE_VOLUME_MUTE, STATE_LOCKED, STATE_ON, STATE_UNAVAILABLE, + SERVICE_VOLUME_MUTE, STATE_LOCKED, STATE_ON, STATE_OFF, STATE_UNAVAILABLE, STATE_UNLOCKED, TEMP_CELSIUS, TEMP_FAHRENHEIT, MATCH_ALL) import homeassistant.core as ha import homeassistant.util.color as color_util @@ -58,7 +59,7 @@ (climate.STATE_AUTO, 'AUTO'), (climate.STATE_ECO, 'ECO'), (climate.STATE_MANUAL, 'AUTO'), - (climate.STATE_OFF, 'OFF'), + (STATE_OFF, 'OFF'), (climate.STATE_IDLE, 'OFF'), (climate.STATE_FAN_ONLY, 'OFF'), (climate.STATE_DRY, 'OFF'), @@ -765,7 +766,7 @@ def get_property(self, name): unit = self.hass.config.units.temperature_unit if name == 'targetSetpoint': - temp = self.entity.attributes.get(climate.ATTR_TEMPERATURE) + temp = self.entity.attributes.get(ATTR_TEMPERATURE) elif name == 'lowerSetpoint': temp = self.entity.attributes.get(climate.ATTR_TARGET_TEMP_LOW) elif name == 'upperSetpoint': diff --git a/homeassistant/components/ambient_station/.translations/de.json b/homeassistant/components/ambient_station/.translations/de.json new file mode 100644 index 0000000000000..1431efbf167b2 --- /dev/null +++ b/homeassistant/components/ambient_station/.translations/de.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "identifier_exists": "Anwendungsschl\u00fcssel und / oder API-Schl\u00fcssel bereits registriert", + "invalid_key": "Ung\u00fcltiger API Key und / oder Anwendungsschl\u00fcssel", + "no_devices": "Keine Ger\u00e4te im Konto gefunden" + }, + "step": { + "user": { + "data": { + "api_key": "API Key", + "app_key": "Anwendungsschl\u00fcssel" + }, + "title": "Gib deine Informationen ein" + } + }, + "title": "Ambient PWS" + } +} \ No newline at end of file diff --git a/homeassistant/components/ambient_station/.translations/es-419.json b/homeassistant/components/ambient_station/.translations/es-419.json new file mode 100644 index 0000000000000..268a6ba001e45 --- /dev/null +++ b/homeassistant/components/ambient_station/.translations/es-419.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "identifier_exists": "Clave de aplicaci\u00f3n y/o clave de API ya registrada", + "invalid_key": "Clave de API y/o clave de aplicaci\u00f3n no v\u00e1lida", + "no_devices": "No se han encontrado dispositivos en la cuenta." + }, + "step": { + "user": { + "data": { + "api_key": "Clave API", + "app_key": "Clave de aplicaci\u00f3n" + }, + "title": "Completa tu informaci\u00f3n" + } + }, + "title": "Ambient PWS" + } +} \ No newline at end of file diff --git a/homeassistant/components/ambient_station/.translations/es.json b/homeassistant/components/ambient_station/.translations/es.json new file mode 100644 index 0000000000000..d6732423a7ec6 --- /dev/null +++ b/homeassistant/components/ambient_station/.translations/es.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "user": { + "title": "Completa tu informaci\u00f3n" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ambient_station/.translations/he.json b/homeassistant/components/ambient_station/.translations/he.json new file mode 100644 index 0000000000000..f5afbca71c0f2 --- /dev/null +++ b/homeassistant/components/ambient_station/.translations/he.json @@ -0,0 +1,15 @@ +{ + "config": { + "error": { + "no_devices": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05d4\u05ea\u05e7\u05df \u05d1\u05d7\u05e9\u05d1\u05d5\u05df" + }, + "step": { + "user": { + "data": { + "api_key": "\u05de\u05e4\u05ea\u05d7 API" + }, + "title": "\u05de\u05dc\u05d0 \u05d0\u05ea \u05d4\u05e4\u05e8\u05d8\u05d9\u05dd \u05e9\u05dc\u05da" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ambient_station/.translations/hu.json b/homeassistant/components/ambient_station/.translations/hu.json new file mode 100644 index 0000000000000..222b512c39f82 --- /dev/null +++ b/homeassistant/components/ambient_station/.translations/hu.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "identifier_exists": "Alkalmaz\u00e1s kulcsot \u00e9s/vagy az API kulcsot m\u00e1r regisztr\u00e1lt\u00e1k", + "invalid_key": "\u00c9rv\u00e9nytelen API kulcs \u00e9s / vagy alkalmaz\u00e1skulcs", + "no_devices": "Nincs a fi\u00f3kodban tal\u00e1lhat\u00f3 eszk\u00f6z" + }, + "step": { + "user": { + "data": { + "api_key": "API kulcs", + "app_key": "Alkalmaz\u00e1skulcs" + }, + "title": "T\u00f6ltsd ki az adataid" + } + }, + "title": "Ambient PWS" + } +} \ No newline at end of file diff --git a/homeassistant/components/ambient_station/.translations/it.json b/homeassistant/components/ambient_station/.translations/it.json new file mode 100644 index 0000000000000..f87c987a79fba --- /dev/null +++ b/homeassistant/components/ambient_station/.translations/it.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "identifier_exists": "API Key e/o Application Key gi\u00e0 registrata", + "invalid_key": "API Key e/o Application Key non valida", + "no_devices": "Nessun dispositivo trovato nell'account" + }, + "step": { + "user": { + "data": { + "api_key": "API Key", + "app_key": "Application Key" + }, + "title": "Inserisci i tuoi dati" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ambient_station/.translations/pt.json b/homeassistant/components/ambient_station/.translations/pt.json index 01078bbddfea9..92746b29f3d6a 100644 --- a/homeassistant/components/ambient_station/.translations/pt.json +++ b/homeassistant/components/ambient_station/.translations/pt.json @@ -1,11 +1,19 @@ { "config": { + "error": { + "identifier_exists": "Chave de aplica\u00e7\u00e3o e/ou chave de API j\u00e1 registradas.", + "invalid_key": "Chave de API e/ou chave de aplica\u00e7\u00e3o inv\u00e1lidas", + "no_devices": "Nenhum dispositivo encontrado na conta" + }, "step": { "user": { "data": { - "api_key": "Chave de API" - } + "api_key": "Chave de API", + "app_key": "Chave de aplica\u00e7\u00e3o" + }, + "title": "Preencha as suas informa\u00e7\u00f5es" } - } + }, + "title": "Ambient PWS" } } \ No newline at end of file diff --git a/homeassistant/components/ambient_station/.translations/sl.json b/homeassistant/components/ambient_station/.translations/sl.json new file mode 100644 index 0000000000000..906a6b404c463 --- /dev/null +++ b/homeassistant/components/ambient_station/.translations/sl.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "identifier_exists": "Aplikacijski klju\u010d in / ali klju\u010d API je \u017ee registriran", + "invalid_key": "Neveljaven klju\u010d API in / ali klju\u010d aplikacije", + "no_devices": "V ra\u010dunu ni najdene nobene naprave" + }, + "step": { + "user": { + "data": { + "api_key": "API Klju\u010d", + "app_key": "Klju\u010d aplikacije" + }, + "title": "Izpolnite svoje podatke" + } + }, + "title": "Ambient PWS" + } +} \ No newline at end of file diff --git a/homeassistant/components/ambient_station/.translations/sv.json b/homeassistant/components/ambient_station/.translations/sv.json new file mode 100644 index 0000000000000..c429d4395030f --- /dev/null +++ b/homeassistant/components/ambient_station/.translations/sv.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "identifier_exists": "Applikationsnyckel och/eller API-nyckel \u00e4r redan registrerade", + "invalid_key": "Ogiltigt API-nyckel och/eller applikationsnyckel", + "no_devices": "Inga enheter hittades i kontot" + }, + "step": { + "user": { + "data": { + "api_key": "API-nyckel", + "app_key": "Applikationsnyckel" + }, + "title": "Fyll i dina uppgifter" + } + }, + "title": "Ambient Weather PWS (Personal Weather Station)" + } +} \ No newline at end of file diff --git a/homeassistant/components/ambient_station/__init__.py b/homeassistant/components/ambient_station/__init__.py index 16b86a0e29878..70f6ce9fbba0a 100644 --- a/homeassistant/components/ambient_station/__init__.py +++ b/homeassistant/components/ambient_station/__init__.py @@ -20,13 +20,14 @@ ATTR_LAST_DATA, CONF_APP_KEY, DATA_CLIENT, DOMAIN, TOPIC_UPDATE, TYPE_BINARY_SENSOR, TYPE_SENSOR) -REQUIREMENTS = ['aioambient==0.1.2'] +REQUIREMENTS = ['aioambient==0.1.3'] _LOGGER = logging.getLogger(__name__) DATA_CONFIG = 'config' DEFAULT_SOCKET_MIN_RETRY = 15 +DEFAULT_WATCHDOG_SECONDS = 5 * 60 TYPE_24HOURRAININ = '24hourrainin' TYPE_BAROMABSIN = 'baromabsin' @@ -296,6 +297,7 @@ def __init__(self, hass, config_entry, client, monitored_conditions): """Initialize.""" self._config_entry = config_entry self._hass = hass + self._watchdog_listener = None self._ws_reconnect_delay = DEFAULT_SOCKET_MIN_RETRY self.client = client self.monitored_conditions = monitored_conditions @@ -305,9 +307,18 @@ async def ws_connect(self): """Register handlers and connect to the websocket.""" from aioambient.errors import WebsocketError + async def _ws_reconnect(event_time): + """Forcibly disconnect from and reconnect to the websocket.""" + _LOGGER.debug('Watchdog expired; forcing socket reconnection') + await self.client.websocket.disconnect() + await self.client.websocket.connect() + def on_connect(): """Define a handler to fire when the websocket is connected.""" _LOGGER.info('Connected to websocket') + _LOGGER.debug('Watchdog starting') + self._watchdog_listener = async_call_later( + self._hass, DEFAULT_WATCHDOG_SECONDS, _ws_reconnect) def on_data(data): """Define a handler to fire when the data is received.""" @@ -317,6 +328,11 @@ def on_data(data): self.stations[mac_address][ATTR_LAST_DATA] = data async_dispatcher_send(self._hass, TOPIC_UPDATE) + _LOGGER.debug('Resetting watchdog') + self._watchdog_listener() + self._watchdog_listener = async_call_later( + self._hass, DEFAULT_WATCHDOG_SECONDS, _ws_reconnect) + def on_disconnect(): """Define a handler to fire when the websocket is disconnected.""" _LOGGER.info('Disconnected from websocket') diff --git a/homeassistant/components/arlo/__init__.py b/homeassistant/components/arlo/__init__.py index 7e81836e522af..cbb720778e5e7 100644 --- a/homeassistant/components/arlo/__init__.py +++ b/homeassistant/components/arlo/__init__.py @@ -15,7 +15,7 @@ _LOGGER = logging.getLogger(__name__) -CONF_ATTRIBUTION = "Data provided by arlo.netgear.com" +ATTRIBUTION = "Data provided by arlo.netgear.com" DATA_ARLO = 'data_arlo' DEFAULT_BRAND = 'Netgear Arlo' diff --git a/homeassistant/components/arlo/alarm_control_panel.py b/homeassistant/components/arlo/alarm_control_panel.py index 8c21a448a23cb..931dfa1b15d99 100644 --- a/homeassistant/components/arlo/alarm_control_panel.py +++ b/homeassistant/components/arlo/alarm_control_panel.py @@ -9,7 +9,7 @@ from homeassistant.components.alarm_control_panel import ( AlarmControlPanel, PLATFORM_SCHEMA) from homeassistant.components.arlo import ( - DATA_ARLO, CONF_ATTRIBUTION, SIGNAL_UPDATE_ARLO) + DATA_ARLO, ATTRIBUTION, SIGNAL_UPDATE_ARLO) from homeassistant.const import ( ATTR_ATTRIBUTION, STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED, STATE_ALARM_ARMED_NIGHT) @@ -117,7 +117,7 @@ def name(self): def device_state_attributes(self): """Return the state attributes.""" return { - ATTR_ATTRIBUTION: CONF_ATTRIBUTION, + ATTR_ATTRIBUTION: ATTRIBUTION, 'device_id': self._base_station.device_id } diff --git a/homeassistant/components/arlo/sensor.py b/homeassistant/components/arlo/sensor.py index 3ad7b70a9479a..1c3cc9334380d 100644 --- a/homeassistant/components/arlo/sensor.py +++ b/homeassistant/components/arlo/sensor.py @@ -6,7 +6,7 @@ from homeassistant.core import callback import homeassistant.helpers.config_validation as cv from homeassistant.components.arlo import ( - CONF_ATTRIBUTION, DEFAULT_BRAND, DATA_ARLO, SIGNAL_UPDATE_ARLO) + ATTRIBUTION, DEFAULT_BRAND, DATA_ARLO, SIGNAL_UPDATE_ARLO) from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( ATTR_ATTRIBUTION, CONF_MONITORED_CONDITIONS, TEMP_CELSIUS, @@ -177,7 +177,7 @@ def device_state_attributes(self): """Return the device state attributes.""" attrs = {} - attrs[ATTR_ATTRIBUTION] = CONF_ATTRIBUTION + attrs[ATTR_ATTRIBUTION] = ATTRIBUTION attrs['brand'] = DEFAULT_BRAND if self._sensor_type != 'total_cameras': diff --git a/homeassistant/components/auth/.translations/es-419.json b/homeassistant/components/auth/.translations/es-419.json index 6caa9d49993c5..852965596e073 100644 --- a/homeassistant/components/auth/.translations/es-419.json +++ b/homeassistant/components/auth/.translations/es-419.json @@ -1,8 +1,27 @@ { "mfa_setup": { + "notify": { + "abort": { + "no_available_service": "No hay servicios de notificaci\u00f3n disponibles." + }, + "error": { + "invalid_code": "C\u00f3digo inv\u00e1lido, por favor int\u00e9ntelo de nuevo." + }, + "step": { + "init": { + "description": "Por favor seleccione uno de los servicios de notificaci\u00f3n:", + "title": "Configure la contrase\u00f1a de un solo uso entregada por el componente de notificaci\u00f3n" + }, + "setup": { + "description": "Se ha enviado una contrase\u00f1a \u00fanica a trav\u00e9s de **notify.{notify_service}**. Por favor ingr\u00e9selo a continuaci\u00f3n:", + "title": "Verificar la configuracion" + } + } + }, "totp": { "step": { "init": { + "description": "Para activar la autenticaci\u00f3n de dos factores utilizando contrase\u00f1as de un solo uso basadas en el tiempo, escanee el c\u00f3digo QR con su aplicaci\u00f3n de autenticaci\u00f3n. Si no tiene uno, le recomendamos [Autenticador de Google] (https://support.google.com/accounts/answer/1066447) o [Authy] (https://authy.com/). \n\n {qr_code} \n \n Despu\u00e9s de escanear el c\u00f3digo, ingrese el c\u00f3digo de seis d\u00edgitos de su aplicaci\u00f3n para verificar la configuraci\u00f3n. Si tiene problemas para escanear el c\u00f3digo QR, realice una configuraci\u00f3n manual con el c\u00f3digo ** ` {code} ` **.", "title": "Configurar la autenticaci\u00f3n de dos factores mediante TOTP" } }, diff --git a/homeassistant/components/auth/.translations/hu.json b/homeassistant/components/auth/.translations/hu.json index 0a3a3c5882066..a2d132d907331 100644 --- a/homeassistant/components/auth/.translations/hu.json +++ b/homeassistant/components/auth/.translations/hu.json @@ -9,7 +9,8 @@ }, "step": { "init": { - "description": "V\u00e1lassz \u00e9rtes\u00edt\u00e9si szolg\u00e1ltat\u00e1st:" + "description": "V\u00e1lassz \u00e9rtes\u00edt\u00e9si szolg\u00e1ltat\u00e1st:", + "title": "\u00c1ll\u00edtsa be az \u00e9rtes\u00edt\u00e9si \u00f6sszetev\u0151 \u00e1ltal megadott egyszeri jelsz\u00f3t" }, "setup": { "title": "Be\u00e1ll\u00edt\u00e1s ellen\u0151rz\u00e9se" diff --git a/homeassistant/components/auth/.translations/it.json b/homeassistant/components/auth/.translations/it.json index 25dad4c1aeb83..be06f0209c409 100644 --- a/homeassistant/components/auth/.translations/it.json +++ b/homeassistant/components/auth/.translations/it.json @@ -9,7 +9,8 @@ }, "step": { "init": { - "description": "Selezionare uno dei servizi di notifica:" + "description": "Selezionare uno dei servizi di notifica:", + "title": "Imposta la password one-time fornita dal componente di notifica" }, "setup": { "description": "\u00c8 stata inviata una password monouso tramite **notify.{notify_service}**. Per favore, inseriscila qui sotto:", diff --git a/homeassistant/components/auth/.translations/ru.json b/homeassistant/components/auth/.translations/ru.json index edf136bd7f353..5092e0792504c 100644 --- a/homeassistant/components/auth/.translations/ru.json +++ b/homeassistant/components/auth/.translations/ru.json @@ -21,11 +21,11 @@ }, "totp": { "error": { - "invalid_code": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043a\u043e\u0434. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0441\u043d\u043e\u0432\u0430. \u0415\u0441\u043b\u0438 \u0432\u044b \u043f\u043e\u0441\u0442\u043e\u044f\u043d\u043d\u043e \u043f\u043e\u043b\u0443\u0447\u0430\u0435\u0442\u0435 \u044d\u0442\u0443 \u043e\u0448\u0438\u0431\u043a\u0443, \u043f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u0443\u0431\u0435\u0434\u0438\u0442\u0435\u0441\u044c, \u0447\u0442\u043e \u0447\u0430\u0441\u044b \u0432 \u0432\u0430\u0448\u0435\u0439 \u0441\u0438\u0441\u0442\u0435\u043c\u0435 Home Assistant \u043f\u043e\u043a\u0430\u0437\u044b\u0432\u0430\u044e\u0442 \u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u043e\u0435 \u0432\u0440\u0435\u043c\u044f." + "invalid_code": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043a\u043e\u0434. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0441\u043d\u043e\u0432\u0430. \u0415\u0441\u043b\u0438 \u0412\u044b \u043f\u043e\u0441\u0442\u043e\u044f\u043d\u043d\u043e \u043f\u043e\u043b\u0443\u0447\u0430\u0435\u0442\u0435 \u044d\u0442\u0443 \u043e\u0448\u0438\u0431\u043a\u0443, \u043f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u0443\u0431\u0435\u0434\u0438\u0442\u0435\u0441\u044c, \u0447\u0442\u043e \u0447\u0430\u0441\u044b \u0432 \u0412\u0430\u0448\u0435\u0439 \u0441\u0438\u0441\u0442\u0435\u043c\u0435 Home Assistant \u043f\u043e\u043a\u0430\u0437\u044b\u0432\u0430\u044e\u0442 \u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u043e\u0435 \u0432\u0440\u0435\u043c\u044f." }, "step": { "init": { - "description": "\u0427\u0442\u043e\u0431\u044b \u0430\u043a\u0442\u0438\u0432\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u0434\u0432\u0443\u0445\u0444\u0430\u043a\u0442\u043e\u0440\u043d\u0443\u044e \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044e \u0441 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u0435\u043c \u043e\u0434\u043d\u043e\u0440\u0430\u0437\u043e\u0432\u044b\u0445 \u043f\u0430\u0440\u043e\u043b\u0435\u0439, \u043e\u0441\u043d\u043e\u0432\u0430\u043d\u043d\u044b\u0445 \u043d\u0430 \u0432\u0440\u0435\u043c\u0435\u043d\u0438, \u043e\u0442\u0441\u043a\u0430\u043d\u0438\u0440\u0443\u0439\u0442\u0435 QR-\u043a\u043e\u0434 \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u0434\u043b\u044f \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0438 \u043f\u043e\u0434\u043b\u0438\u043d\u043d\u043e\u0441\u0442\u0438. \u0415\u0441\u043b\u0438 \u0443 \u0432\u0430\u0441 \u0435\u0433\u043e \u043d\u0435\u0442, \u043c\u044b \u0440\u0435\u043a\u043e\u043c\u0435\u043d\u0434\u0443\u0435\u043c \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u043b\u0438\u0431\u043e [Google Authenticator](https://support.google.com/accounts/answer/1066447), \u043b\u0438\u0431\u043e [Authy](https://authy.com/). \n\n {qr_code} \n \n\u041f\u043e\u0441\u043b\u0435 \u0441\u043a\u0430\u043d\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f QR-\u043a\u043e\u0434\u0430 \u0432\u0432\u0435\u0434\u0438\u0442\u0435 \u0448\u0435\u0441\u0442\u0438\u0437\u043d\u0430\u0447\u043d\u044b\u0439 \u043a\u043e\u0434 \u0438\u0437 \u0432\u0430\u0448\u0435\u0433\u043e \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f, \u0447\u0442\u043e\u0431\u044b \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0434\u0438\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443. \u0415\u0441\u043b\u0438 \u0443 \u0432\u0430\u0441 \u0435\u0441\u0442\u044c \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u044b \u0441\u043e \u0441\u043a\u0430\u043d\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435\u043c QR-\u043a\u043e\u0434\u0430, \u0432\u044b\u043f\u043e\u043b\u043d\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443 \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e \u043a\u043e\u0434\u0430 **`{code}`**.", + "description": "\u0427\u0442\u043e\u0431\u044b \u0430\u043a\u0442\u0438\u0432\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u0434\u0432\u0443\u0445\u0444\u0430\u043a\u0442\u043e\u0440\u043d\u0443\u044e \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044e \u0441 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u0435\u043c \u043e\u0434\u043d\u043e\u0440\u0430\u0437\u043e\u0432\u044b\u0445 \u043f\u0430\u0440\u043e\u043b\u0435\u0439, \u043e\u0441\u043d\u043e\u0432\u0430\u043d\u043d\u044b\u0445 \u043d\u0430 \u0432\u0440\u0435\u043c\u0435\u043d\u0438, \u043e\u0442\u0441\u043a\u0430\u043d\u0438\u0440\u0443\u0439\u0442\u0435 QR-\u043a\u043e\u0434 \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u0434\u043b\u044f \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0438 \u043f\u043e\u0434\u043b\u0438\u043d\u043d\u043e\u0441\u0442\u0438. \u0415\u0441\u043b\u0438 \u0443 \u0412\u0430\u0441 \u0435\u0433\u043e \u043d\u0435\u0442, \u043c\u044b \u0440\u0435\u043a\u043e\u043c\u0435\u043d\u0434\u0443\u0435\u043c \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u043b\u0438\u0431\u043e [Google Authenticator](https://support.google.com/accounts/answer/1066447), \u043b\u0438\u0431\u043e [Authy](https://authy.com/). \n\n {qr_code} \n \n\u041f\u043e\u0441\u043b\u0435 \u0441\u043a\u0430\u043d\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f QR-\u043a\u043e\u0434\u0430 \u0432\u0432\u0435\u0434\u0438\u0442\u0435 \u0448\u0435\u0441\u0442\u0438\u0437\u043d\u0430\u0447\u043d\u044b\u0439 \u043a\u043e\u0434 \u0438\u0437 \u0412\u0430\u0448\u0435\u0433\u043e \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f, \u0447\u0442\u043e\u0431\u044b \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0434\u0438\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443. \u0415\u0441\u043b\u0438 \u0443 \u0412\u0430\u0441 \u0435\u0441\u0442\u044c \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u044b \u0441\u043e \u0441\u043a\u0430\u043d\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435\u043c QR-\u043a\u043e\u0434\u0430, \u0432\u044b\u043f\u043e\u043b\u043d\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443 \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e \u043a\u043e\u0434\u0430 **`{code}`**.", "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0434\u0432\u0443\u0445\u0444\u0430\u043a\u0442\u043e\u0440\u043d\u043e\u0439 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438 \u0441 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u0435\u043c TOTP" } }, diff --git a/homeassistant/components/binary_sensor/aurora.py b/homeassistant/components/binary_sensor/aurora.py index 04b402722b218..cfd683346ff7e 100644 --- a/homeassistant/components/binary_sensor/aurora.py +++ b/homeassistant/components/binary_sensor/aurora.py @@ -19,8 +19,8 @@ _LOGGER = logging.getLogger(__name__) -CONF_ATTRIBUTION = "Data provided by the National Oceanic and Atmospheric" \ - "Administration" +ATTRIBUTION = "Data provided by the National Oceanic and Atmospheric " \ + "Administration" CONF_THRESHOLD = 'forecast_threshold' DEFAULT_DEVICE_CLASS = 'visible' @@ -91,7 +91,7 @@ def device_state_attributes(self): if self.aurora_data: attrs['visibility_level'] = self.aurora_data.visibility_level attrs['message'] = self.aurora_data.is_visible_text - attrs[ATTR_ATTRIBUTION] = CONF_ATTRIBUTION + attrs[ATTR_ATTRIBUTION] = ATTRIBUTION return attrs def update(self): diff --git a/homeassistant/components/binary_sensor/hikvision.py b/homeassistant/components/binary_sensor/hikvision.py index 57618ca2652a8..fdefc40d8fdf0 100644 --- a/homeassistant/components/binary_sensor/hikvision.py +++ b/homeassistant/components/binary_sensor/hikvision.py @@ -18,7 +18,7 @@ CONF_SSL, EVENT_HOMEASSISTANT_STOP, EVENT_HOMEASSISTANT_START, ATTR_LAST_TRIP_TIME, CONF_CUSTOMIZE) -REQUIREMENTS = ['pyhik==0.1.9'] +REQUIREMENTS = ['pyhik==0.2.2'] _LOGGER = logging.getLogger(__name__) CONF_IGNORED = 'ignored' @@ -51,6 +51,8 @@ 'Unattended Baggage': 'motion', 'Attended Baggage': 'motion', 'Recording Failure': None, + 'Exiting Region': 'motion', + 'Entering Region': 'motion', } CUSTOMIZE_SCHEMA = vol.Schema({ diff --git a/homeassistant/components/binary_sensor/rest.py b/homeassistant/components/binary_sensor/rest.py index ac82ab126fd40..304ed701148c8 100644 --- a/homeassistant/components/binary_sensor/rest.py +++ b/homeassistant/components/binary_sensor/rest.py @@ -14,7 +14,7 @@ from homeassistant.components.sensor.rest import RestData from homeassistant.const import ( CONF_PAYLOAD, CONF_NAME, CONF_VALUE_TEMPLATE, CONF_METHOD, CONF_RESOURCE, - CONF_VERIFY_SSL, CONF_USERNAME, CONF_PASSWORD, + CONF_VERIFY_SSL, CONF_USERNAME, CONF_PASSWORD, CONF_TIMEOUT, CONF_HEADERS, CONF_AUTHENTICATION, HTTP_BASIC_AUTHENTICATION, HTTP_DIGEST_AUTHENTICATION, CONF_DEVICE_CLASS) import homeassistant.helpers.config_validation as cv @@ -25,6 +25,7 @@ DEFAULT_METHOD = 'GET' DEFAULT_NAME = 'REST Binary Sensor' DEFAULT_VERIFY_SSL = True +DEFAULT_TIMEOUT = 10 PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_RESOURCE): cv.url, @@ -39,6 +40,7 @@ vol.Optional(CONF_USERNAME): cv.string, vol.Optional(CONF_VALUE_TEMPLATE): cv.template, vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): cv.boolean, + vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int, }) @@ -49,6 +51,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): method = config.get(CONF_METHOD) payload = config.get(CONF_PAYLOAD) verify_ssl = config.get(CONF_VERIFY_SSL) + timeout = config.get(CONF_TIMEOUT) username = config.get(CONF_USERNAME) password = config.get(CONF_PASSWORD) headers = config.get(CONF_HEADERS) @@ -65,7 +68,8 @@ def setup_platform(hass, config, add_entities, discovery_info=None): else: auth = None - rest = RestData(method, resource, auth, headers, payload, verify_ssl) + rest = RestData(method, resource, auth, headers, payload, verify_ssl, + timeout) rest.update() if rest.data is None: raise PlatformNotReady diff --git a/homeassistant/components/binary_sensor/ring.py b/homeassistant/components/binary_sensor/ring.py index 5a65917f40b73..79fc61a62d47b 100644 --- a/homeassistant/components/binary_sensor/ring.py +++ b/homeassistant/components/binary_sensor/ring.py @@ -11,7 +11,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.components.ring import ( - CONF_ATTRIBUTION, DEFAULT_ENTITY_NAMESPACE, DATA_RING) + ATTRIBUTION, DEFAULT_ENTITY_NAMESPACE, DATA_RING) from homeassistant.const import ( ATTR_ATTRIBUTION, CONF_ENTITY_NAMESPACE, CONF_MONITORED_CONDITIONS) @@ -47,18 +47,14 @@ def setup_platform(hass, config, add_entities, discovery_info=None): for device in ring.doorbells: # ring.doorbells is doing I/O for sensor_type in config[CONF_MONITORED_CONDITIONS]: if 'doorbell' in SENSOR_TYPES[sensor_type][1]: - sensors.append(RingBinarySensor(hass, - device, - sensor_type)) + sensors.append(RingBinarySensor(hass, device, sensor_type)) for device in ring.stickup_cams: # ring.stickup_cams is doing I/O for sensor_type in config[CONF_MONITORED_CONDITIONS]: if 'stickup_cams' in SENSOR_TYPES[sensor_type][1]: - sensors.append(RingBinarySensor(hass, - device, - sensor_type)) + sensors.append(RingBinarySensor(hass, device, sensor_type)) + add_entities(sensors, True) - return True class RingBinarySensor(BinarySensorDevice): @@ -69,8 +65,8 @@ def __init__(self, hass, data, sensor_type): super(RingBinarySensor, self).__init__() self._sensor_type = sensor_type self._data = data - self._name = "{0} {1}".format(self._data.name, - SENSOR_TYPES.get(self._sensor_type)[0]) + self._name = "{0} {1}".format( + self._data.name, SENSOR_TYPES.get(self._sensor_type)[0]) self._device_class = SENSOR_TYPES.get(self._sensor_type)[2] self._state = None self._unique_id = '{}-{}'.format(self._data.id, self._sensor_type) @@ -99,7 +95,7 @@ def unique_id(self): def device_state_attributes(self): """Return the state attributes.""" attrs = {} - attrs[ATTR_ATTRIBUTION] = CONF_ATTRIBUTION + attrs[ATTR_ATTRIBUTION] = ATTRIBUTION attrs['device_id'] = self._data.id attrs['firmware'] = self._data.firmware diff --git a/homeassistant/components/binary_sensor/tod.py b/homeassistant/components/binary_sensor/tod.py new file mode 100644 index 0000000000000..7dc6e5ebe811c --- /dev/null +++ b/homeassistant/components/binary_sensor/tod.py @@ -0,0 +1,217 @@ +"""Support for representing current time of the day as binary sensors.""" +from datetime import datetime, timedelta +import logging + +import pytz +import voluptuous as vol + +from homeassistant.components.binary_sensor import ( + PLATFORM_SCHEMA, BinarySensorDevice) +from homeassistant.const import ( + CONF_AFTER, CONF_BEFORE, CONF_NAME, SUN_EVENT_SUNRISE, SUN_EVENT_SUNSET) +from homeassistant.core import callback +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.event import async_track_point_in_utc_time +from homeassistant.helpers.sun import ( + get_astral_event_date, get_astral_event_next) +from homeassistant.util import dt as dt_util + +_LOGGER = logging.getLogger(__name__) + +ATTR_AFTER = 'after' +ATTR_BEFORE = 'before' +ATTR_NEXT_UPDATE = 'next_update' + +CONF_AFTER_OFFSET = 'after_offset' +CONF_BEFORE_OFFSET = 'before_offset' + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_AFTER): + vol.Any(cv.time, vol.All(vol.Lower, cv.sun_event)), + vol.Required(CONF_BEFORE): + vol.Any(cv.time, vol.All(vol.Lower, cv.sun_event)), + vol.Required(CONF_NAME): cv.string, + vol.Optional(CONF_AFTER_OFFSET, default=timedelta(0)): cv.time_period, + vol.Optional(CONF_BEFORE_OFFSET, default=timedelta(0)): cv.time_period, +}) + + +async def async_setup_platform( + hass, config, async_add_entities, discovery_info=None): + """Set up the ToD sensors.""" + if hass.config.time_zone is None: + _LOGGER.error("Timezone is not set in Home Assistant configuration") + return + + after = config[CONF_AFTER] + after_offset = config[CONF_AFTER_OFFSET] + before = config[CONF_BEFORE] + before_offset = config[CONF_BEFORE_OFFSET] + name = config[CONF_NAME] + sensor = TodSensor(name, after, after_offset, before, before_offset) + + async_add_entities([sensor]) + + +def is_sun_event(event): + """Return true if event is sun event not time.""" + return event in (SUN_EVENT_SUNRISE, SUN_EVENT_SUNSET) + + +class TodSensor(BinarySensorDevice): + """Time of the Day Sensor.""" + + def __init__(self, name, after, after_offset, before, before_offset): + """Init the ToD Sensor...""" + self._name = name + self._time_before = self._time_after = self._next_update = None + self._after_offset = after_offset + self._before_offset = before_offset + self._before = before + self._after = after + + @property + def should_poll(self): + """Sensor does not need to be polled.""" + return False + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @property + def after(self): + """Return the timestamp for the begining of the period.""" + return self._time_after + + @property + def before(self): + """Return the timestamp for the end of the period.""" + return self._time_before + + @property + def is_on(self): + """Return True is sensor is on.""" + if self.after < self.before: + return self.after <= self.current_datetime < self.before + return False + + @property + def current_datetime(self): + """Return local current datetime according to hass configuration.""" + return dt_util.utcnow() + + @property + def next_update(self): + """Return the next update point in the UTC time.""" + return self._next_update + + @property + def device_state_attributes(self): + """Return the state attributes of the sensor.""" + return { + ATTR_AFTER: self.after.astimezone( + self.hass.config.time_zone).isoformat(), + ATTR_BEFORE: self.before.astimezone( + self.hass.config.time_zone).isoformat(), + ATTR_NEXT_UPDATE: self.next_update.astimezone( + self.hass.config.time_zone).isoformat(), + } + + def _calculate_initial_boudary_time(self): + """Calculate internal absolute time boudaries.""" + nowutc = self.current_datetime + # If after value is a sun event instead of absolute time + if is_sun_event(self._after): + # Calculate the today's event utc time or + # if not available take next + after_event_date = \ + get_astral_event_date(self.hass, self._after, nowutc) or \ + get_astral_event_next(self.hass, self._after, nowutc) + else: + # Convert local time provided to UTC today + # datetime.combine(date, time, tzinfo) is not supported + # in python 3.5. The self._after is provided + # with hass configured TZ not system wide + after_event_date = datetime.combine( + nowutc, self._after.replace( + tzinfo=self.hass.config.time_zone)).astimezone(tz=pytz.UTC) + + self._time_after = after_event_date + + # If before value is a sun event instead of absolute time + if is_sun_event(self._before): + # Calculate the today's event utc time or if not available take + # next + before_event_date = \ + get_astral_event_date(self.hass, self._before, nowutc) or \ + get_astral_event_next(self.hass, self._before, nowutc) + # Before is earlier than after + if before_event_date < after_event_date: + # Take next day for before + before_event_date = get_astral_event_next( + self.hass, self._before, after_event_date) + else: + # Convert local time provided to UTC today, see above + before_event_date = datetime.combine( + nowutc, self._before.replace( + tzinfo=self.hass.config.time_zone)).astimezone(tz=pytz.UTC) + + # It is safe to add timedelta days=1 to UTC as there is no DST + if before_event_date < after_event_date + self._after_offset: + before_event_date += timedelta(days=1) + + self._time_before = before_event_date + + # Add offset to utc boundaries according to the configuration + self._time_after += self._after_offset + self._time_before += self._before_offset + + def _turn_to_next_day(self): + """Turn to to the next day.""" + if is_sun_event(self._after): + self._time_after = get_astral_event_next( + self.hass, self._after, + self._time_after - self._after_offset) + self._time_after += self._after_offset + else: + # Offset is already there + self._time_after += timedelta(days=1) + + if is_sun_event(self._before): + self._time_before = get_astral_event_next( + self.hass, self._before, + self._time_before - self._before_offset) + self._time_before += self._before_offset + else: + # Offset is already there + self._time_before += timedelta(days=1) + + async def async_added_to_hass(self): + """Call when entity about to be added to Home Assistant.""" + await super().async_added_to_hass() + self._calculate_initial_boudary_time() + self._calculate_next_update() + self._point_in_time_listener(dt_util.now()) + + def _calculate_next_update(self): + """Datetime when the next update to the state.""" + now = self.current_datetime + if now < self.after: + self._next_update = self.after + return + if now < self.before: + self._next_update = self.before + return + self._turn_to_next_day() + self._next_update = self.after + + @callback + def _point_in_time_listener(self, now): + """Run when the state of the sensor should be updated.""" + self._calculate_next_update() + self.async_schedule_update_ha_state() + + async_track_point_in_utc_time( + self.hass, self._point_in_time_listener, self.next_update) diff --git a/homeassistant/components/binary_sensor/trend.py b/homeassistant/components/binary_sensor/trend.py index 494c3154b848d..0d4e96316500a 100644 --- a/homeassistant/components/binary_sensor/trend.py +++ b/homeassistant/components/binary_sensor/trend.py @@ -1,9 +1,4 @@ -""" -A sensor that monitors trends in other components. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/sensor.trend/ -""" +"""A sensor that monitors trends in other components.""" from collections import deque import logging import math @@ -22,7 +17,7 @@ from homeassistant.helpers.event import async_track_state_change from homeassistant.util import utcnow -REQUIREMENTS = ['numpy==1.16.0'] +REQUIREMENTS = ['numpy==1.16.1'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/binary_sensor/uptimerobot.py b/homeassistant/components/binary_sensor/uptimerobot.py index dbb83e53e9fd1..e48ac3039aeb4 100644 --- a/homeassistant/components/binary_sensor/uptimerobot.py +++ b/homeassistant/components/binary_sensor/uptimerobot.py @@ -19,7 +19,7 @@ ATTR_TARGET = 'target' -CONF_ATTRIBUTION = "Data provided by Uptime Robot" +ATTRIBUTION = "Data provided by Uptime Robot" PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_API_KEY): cv.string, @@ -78,7 +78,7 @@ def device_class(self): def device_state_attributes(self): """Return the state attributes of the binary sensor.""" return { - ATTR_ATTRIBUTION: CONF_ATTRIBUTION, + ATTR_ATTRIBUTION: ATTRIBUTION, ATTR_TARGET: self._target, } diff --git a/homeassistant/components/bloomsky/__init__.py b/homeassistant/components/bloomsky/__init__.py index a42eb34004be9..7f9249296626f 100644 --- a/homeassistant/components/bloomsky/__init__.py +++ b/homeassistant/components/bloomsky/__init__.py @@ -66,7 +66,7 @@ def refresh_devices(self): self.API_URL, headers={AUTHORIZATION: self._api_key}, timeout=10) if response.status_code == 401: raise RuntimeError("Invalid API_KEY") - elif response.status_code != 200: + if response.status_code != 200: _LOGGER.error("Invalid HTTP response: %s", response.status_code) return # Create dictionary keyed off of the device unique id diff --git a/homeassistant/components/camera/ring.py b/homeassistant/components/camera/ring.py index da1119281b344..ce9ceb7b76fc2 100644 --- a/homeassistant/components/camera/ring.py +++ b/homeassistant/components/camera/ring.py @@ -13,7 +13,7 @@ from homeassistant.helpers import config_validation as cv from homeassistant.components.ring import ( - DATA_RING, CONF_ATTRIBUTION, NOTIFICATION_ID) + DATA_RING, ATTRIBUTION, NOTIFICATION_ID) from homeassistant.components.camera import Camera, PLATFORM_SCHEMA from homeassistant.components.ffmpeg import DATA_FFMPEG from homeassistant.const import ATTR_ATTRIBUTION, CONF_SCAN_INTERVAL @@ -34,8 +34,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_FFMPEG_ARGUMENTS): cv.string, - vol.Optional(CONF_SCAN_INTERVAL, default=SCAN_INTERVAL): - cv.time_period, + vol.Optional(CONF_SCAN_INTERVAL, default=SCAN_INTERVAL): cv.time_period, }) @@ -106,7 +105,7 @@ def unique_id(self): def device_state_attributes(self): """Return the state attributes.""" return { - ATTR_ATTRIBUTION: CONF_ATTRIBUTION, + ATTR_ATTRIBUTION: ATTRIBUTION, 'device_id': self._camera.id, 'firmware': self._camera.firmware, 'kind': self._camera.kind, diff --git a/homeassistant/components/climate/__init__.py b/homeassistant/components/climate/__init__.py index e1d3093995c4c..0283359b1f234 100644 --- a/homeassistant/components/climate/__init__.py +++ b/homeassistant/components/climate/__init__.py @@ -51,6 +51,17 @@ SERVICE_SET_OPERATION_MODE, SERVICE_SET_SWING_MODE, SERVICE_SET_TEMPERATURE, + SUPPORT_TARGET_TEMPERATURE_HIGH, + SUPPORT_TARGET_TEMPERATURE_LOW, + SUPPORT_TARGET_HUMIDITY, + SUPPORT_TARGET_HUMIDITY_HIGH, + SUPPORT_TARGET_HUMIDITY_LOW, + SUPPORT_FAN_MODE, + SUPPORT_OPERATION_MODE, + SUPPORT_HOLD_MODE, + SUPPORT_SWING_MODE, + SUPPORT_AWAY_MODE, + SUPPORT_AUX_HEAT, ) from .reproduce_state import async_reproduce_states # noqa @@ -62,29 +73,6 @@ ENTITY_ID_FORMAT = DOMAIN + '.{}' SCAN_INTERVAL = timedelta(seconds=60) -STATE_HEAT = 'heat' -STATE_COOL = 'cool' -STATE_IDLE = 'idle' -STATE_AUTO = 'auto' -STATE_MANUAL = 'manual' -STATE_DRY = 'dry' -STATE_FAN_ONLY = 'fan_only' -STATE_ECO = 'eco' - -SUPPORT_TARGET_TEMPERATURE = 1 -SUPPORT_TARGET_TEMPERATURE_HIGH = 2 -SUPPORT_TARGET_TEMPERATURE_LOW = 4 -SUPPORT_TARGET_HUMIDITY = 8 -SUPPORT_TARGET_HUMIDITY_HIGH = 16 -SUPPORT_TARGET_HUMIDITY_LOW = 32 -SUPPORT_FAN_MODE = 64 -SUPPORT_OPERATION_MODE = 128 -SUPPORT_HOLD_MODE = 256 -SUPPORT_SWING_MODE = 512 -SUPPORT_AWAY_MODE = 1024 -SUPPORT_AUX_HEAT = 2048 -SUPPORT_ON_OFF = 4096 - CONVERTIBLE_ATTRIBUTE = [ ATTR_TEMPERATURE, ATTR_TARGET_TEMP_LOW, diff --git a/homeassistant/components/climate/const.py b/homeassistant/components/climate/const.py index 2f84ee27bbdcc..e213ae09de6ac 100644 --- a/homeassistant/components/climate/const.py +++ b/homeassistant/components/climate/const.py @@ -20,6 +20,11 @@ ATTR_TARGET_TEMP_LOW = 'target_temp_low' ATTR_TARGET_TEMP_STEP = 'target_temp_step' +DEFAULT_MIN_TEMP = 7 +DEFAULT_MAX_TEMP = 35 +DEFAULT_MIN_HUMITIDY = 30 +DEFAULT_MAX_HUMIDITY = 99 + DOMAIN = 'climate' SERVICE_SET_AUX_HEAT = 'set_aux_heat' @@ -30,3 +35,26 @@ SERVICE_SET_OPERATION_MODE = 'set_operation_mode' SERVICE_SET_SWING_MODE = 'set_swing_mode' SERVICE_SET_TEMPERATURE = 'set_temperature' + +STATE_HEAT = 'heat' +STATE_COOL = 'cool' +STATE_IDLE = 'idle' +STATE_AUTO = 'auto' +STATE_MANUAL = 'manual' +STATE_DRY = 'dry' +STATE_FAN_ONLY = 'fan_only' +STATE_ECO = 'eco' + +SUPPORT_TARGET_TEMPERATURE = 1 +SUPPORT_TARGET_TEMPERATURE_HIGH = 2 +SUPPORT_TARGET_TEMPERATURE_LOW = 4 +SUPPORT_TARGET_HUMIDITY = 8 +SUPPORT_TARGET_HUMIDITY_HIGH = 16 +SUPPORT_TARGET_HUMIDITY_LOW = 32 +SUPPORT_FAN_MODE = 64 +SUPPORT_OPERATION_MODE = 128 +SUPPORT_HOLD_MODE = 256 +SUPPORT_SWING_MODE = 512 +SUPPORT_AWAY_MODE = 1024 +SUPPORT_AUX_HEAT = 2048 +SUPPORT_ON_OFF = 4096 diff --git a/homeassistant/components/climate/coolmaster.py b/homeassistant/components/climate/coolmaster.py index 32c77b93eeabf..fd00c9f22c468 100644 --- a/homeassistant/components/climate/coolmaster.py +++ b/homeassistant/components/climate/coolmaster.py @@ -9,10 +9,11 @@ import voluptuous as vol -from homeassistant.components.climate import ( - PLATFORM_SCHEMA, STATE_AUTO, STATE_COOL, STATE_DRY, STATE_FAN_ONLY, +from homeassistant.components.climate import ClimateDevice, PLATFORM_SCHEMA +from homeassistant.components.climate.const import ( + STATE_AUTO, STATE_COOL, STATE_DRY, STATE_FAN_ONLY, STATE_HEAT, SUPPORT_FAN_MODE, SUPPORT_ON_OFF, SUPPORT_OPERATION_MODE, - SUPPORT_TARGET_TEMPERATURE, ClimateDevice) + SUPPORT_TARGET_TEMPERATURE) from homeassistant.const import ( ATTR_TEMPERATURE, CONF_HOST, CONF_PORT, TEMP_CELSIUS, TEMP_FAHRENHEIT) import homeassistant.helpers.config_validation as cv diff --git a/homeassistant/components/climate/demo.py b/homeassistant/components/climate/demo.py index 14c22cefbe902..5b4775982a6b3 100644 --- a/homeassistant/components/climate/demo.py +++ b/homeassistant/components/climate/demo.py @@ -4,8 +4,9 @@ For more details about this platform, please refer to the documentation https://home-assistant.io/components/demo/ """ -from homeassistant.components.climate import ( - ClimateDevice, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, +from homeassistant.components.climate import ClimateDevice +from homeassistant.components.climate.const import ( + ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, SUPPORT_TARGET_TEMPERATURE, SUPPORT_TARGET_HUMIDITY, SUPPORT_TARGET_HUMIDITY_LOW, SUPPORT_TARGET_HUMIDITY_HIGH, SUPPORT_AWAY_MODE, SUPPORT_HOLD_MODE, SUPPORT_FAN_MODE, diff --git a/homeassistant/components/climate/dyson.py b/homeassistant/components/climate/dyson.py index 0b09ec7f0b4c8..09196a82bed8b 100644 --- a/homeassistant/components/climate/dyson.py +++ b/homeassistant/components/climate/dyson.py @@ -7,8 +7,9 @@ import logging from homeassistant.components.dyson import DYSON_DEVICES -from homeassistant.components.climate import ( - ClimateDevice, STATE_HEAT, STATE_COOL, STATE_IDLE, +from homeassistant.components.climate import ClimateDevice +from homeassistant.components.climate.const import ( + STATE_HEAT, STATE_COOL, STATE_IDLE, SUPPORT_TARGET_TEMPERATURE, SUPPORT_FAN_MODE, SUPPORT_OPERATION_MODE) from homeassistant.const import TEMP_CELSIUS, ATTR_TEMPERATURE diff --git a/homeassistant/components/climate/ephember.py b/homeassistant/components/climate/ephember.py index cd410cf3be455..9884d81a19924 100644 --- a/homeassistant/components/climate/ephember.py +++ b/homeassistant/components/climate/ephember.py @@ -8,12 +8,12 @@ from datetime import timedelta import voluptuous as vol -from homeassistant.components.climate import ( - ClimateDevice, PLATFORM_SCHEMA, STATE_HEAT, STATE_OFF, - STATE_AUTO, SUPPORT_AUX_HEAT, SUPPORT_OPERATION_MODE, +from homeassistant.components.climate import ClimateDevice, PLATFORM_SCHEMA +from homeassistant.components.climate.const import ( + STATE_HEAT, STATE_AUTO, SUPPORT_AUX_HEAT, SUPPORT_OPERATION_MODE, SUPPORT_TARGET_TEMPERATURE) from homeassistant.const import ( - TEMP_CELSIUS, CONF_USERNAME, CONF_PASSWORD, ATTR_TEMPERATURE) + ATTR_TEMPERATURE, TEMP_CELSIUS, CONF_USERNAME, CONF_PASSWORD, STATE_OFF) import homeassistant.helpers.config_validation as cv REQUIREMENTS = ['pyephember==0.2.0'] diff --git a/homeassistant/components/climate/eq3btsmart.py b/homeassistant/components/climate/eq3btsmart.py index 1eaaaa9d34e81..c7c5973fb8653 100644 --- a/homeassistant/components/climate/eq3btsmart.py +++ b/homeassistant/components/climate/eq3btsmart.py @@ -8,13 +8,14 @@ import voluptuous as vol -from homeassistant.components.climate import ( - STATE_ON, STATE_OFF, STATE_HEAT, STATE_MANUAL, STATE_ECO, PLATFORM_SCHEMA, - ClimateDevice, +from homeassistant.components.climate import ClimateDevice, PLATFORM_SCHEMA +from homeassistant.components.climate.const import ( + STATE_HEAT, STATE_MANUAL, STATE_ECO, SUPPORT_TARGET_TEMPERATURE, SUPPORT_OPERATION_MODE, SUPPORT_AWAY_MODE, SUPPORT_ON_OFF) from homeassistant.const import ( - CONF_MAC, CONF_DEVICES, TEMP_CELSIUS, ATTR_TEMPERATURE, PRECISION_HALVES) + ATTR_TEMPERATURE, CONF_MAC, CONF_DEVICES, STATE_ON, STATE_OFF, + TEMP_CELSIUS, PRECISION_HALVES) import homeassistant.helpers.config_validation as cv REQUIREMENTS = ['python-eq3bt==0.1.9', 'construct==2.9.45'] diff --git a/homeassistant/components/climate/flexit.py b/homeassistant/components/climate/flexit.py index e0453b8bf90fa..fe7b5ff8e7cd2 100644 --- a/homeassistant/components/climate/flexit.py +++ b/homeassistant/components/climate/flexit.py @@ -17,8 +17,9 @@ from homeassistant.const import ( CONF_NAME, CONF_SLAVE, TEMP_CELSIUS, ATTR_TEMPERATURE, DEVICE_DEFAULT_NAME) -from homeassistant.components.climate import ( - ClimateDevice, PLATFORM_SCHEMA, SUPPORT_TARGET_TEMPERATURE, +from homeassistant.components.climate import ClimateDevice, PLATFORM_SCHEMA +from homeassistant.components.climate.const import ( + SUPPORT_TARGET_TEMPERATURE, SUPPORT_FAN_MODE) from homeassistant.components.modbus import ( CONF_HUB, DEFAULT_HUB, DOMAIN as MODBUS_DOMAIN) diff --git a/homeassistant/components/climate/generic_thermostat.py b/homeassistant/components/climate/generic_thermostat.py index ffab50c989d70..da4f79ec1e6b4 100644 --- a/homeassistant/components/climate/generic_thermostat.py +++ b/homeassistant/components/climate/generic_thermostat.py @@ -11,10 +11,11 @@ from homeassistant.core import callback from homeassistant.core import DOMAIN as HA_DOMAIN -from homeassistant.components.climate import ( - STATE_HEAT, STATE_COOL, STATE_IDLE, STATE_AUTO, ClimateDevice, +from homeassistant.components.climate import ClimateDevice, PLATFORM_SCHEMA +from homeassistant.components.climate.const import ( + STATE_HEAT, STATE_COOL, STATE_IDLE, STATE_AUTO, ATTR_OPERATION_MODE, ATTR_AWAY_MODE, SUPPORT_OPERATION_MODE, - SUPPORT_AWAY_MODE, SUPPORT_TARGET_TEMPERATURE, PLATFORM_SCHEMA) + SUPPORT_AWAY_MODE, SUPPORT_TARGET_TEMPERATURE) from homeassistant.const import ( STATE_ON, STATE_OFF, ATTR_TEMPERATURE, CONF_NAME, ATTR_ENTITY_ID, SERVICE_TURN_ON, SERVICE_TURN_OFF, STATE_UNKNOWN, PRECISION_HALVES, diff --git a/homeassistant/components/climate/heatmiser.py b/homeassistant/components/climate/heatmiser.py index a03d1567e01aa..ff495706be77c 100644 --- a/homeassistant/components/climate/heatmiser.py +++ b/homeassistant/components/climate/heatmiser.py @@ -8,8 +8,9 @@ import voluptuous as vol -from homeassistant.components.climate import ( - ClimateDevice, PLATFORM_SCHEMA, SUPPORT_TARGET_TEMPERATURE) +from homeassistant.components.climate import ClimateDevice, PLATFORM_SCHEMA +from homeassistant.components.climate.const import ( + SUPPORT_TARGET_TEMPERATURE) from homeassistant.const import ( TEMP_CELSIUS, ATTR_TEMPERATURE, CONF_PORT, CONF_NAME, CONF_ID) import homeassistant.helpers.config_validation as cv diff --git a/homeassistant/components/climate/honeywell.py b/homeassistant/components/climate/honeywell.py index e0f104a84b195..dbcbebff56656 100644 --- a/homeassistant/components/climate/honeywell.py +++ b/homeassistant/components/climate/honeywell.py @@ -12,8 +12,9 @@ import voluptuous as vol import homeassistant.helpers.config_validation as cv -from homeassistant.components.climate import ( - ClimateDevice, PLATFORM_SCHEMA, ATTR_FAN_MODE, ATTR_FAN_LIST, +from homeassistant.components.climate import ClimateDevice, PLATFORM_SCHEMA +from homeassistant.components.climate.const import ( + ATTR_FAN_MODE, ATTR_FAN_LIST, ATTR_OPERATION_MODE, ATTR_OPERATION_LIST, SUPPORT_TARGET_TEMPERATURE, SUPPORT_AWAY_MODE, SUPPORT_OPERATION_MODE) from homeassistant.const import ( diff --git a/homeassistant/components/climate/melissa.py b/homeassistant/components/climate/melissa.py index 25beedfe0dd29..b9eb28a61d75f 100644 --- a/homeassistant/components/climate/melissa.py +++ b/homeassistant/components/climate/melissa.py @@ -6,8 +6,9 @@ """ import logging -from homeassistant.components.climate import ( - ClimateDevice, SUPPORT_OPERATION_MODE, SUPPORT_TARGET_TEMPERATURE, +from homeassistant.components.climate import ClimateDevice +from homeassistant.components.climate.const import ( + SUPPORT_OPERATION_MODE, SUPPORT_TARGET_TEMPERATURE, SUPPORT_ON_OFF, STATE_AUTO, STATE_HEAT, STATE_COOL, STATE_DRY, STATE_FAN_ONLY, SUPPORT_FAN_MODE ) diff --git a/homeassistant/components/climate/mill.py b/homeassistant/components/climate/mill.py index b735927cb8085..6867f57ee485b 100644 --- a/homeassistant/components/climate/mill.py +++ b/homeassistant/components/climate/mill.py @@ -9,8 +9,9 @@ import voluptuous as vol -from homeassistant.components.climate import ( - ClimateDevice, DOMAIN, PLATFORM_SCHEMA, STATE_HEAT, +from homeassistant.components.climate import ClimateDevice, PLATFORM_SCHEMA +from homeassistant.components.climate.const import ( + DOMAIN, STATE_HEAT, SUPPORT_TARGET_TEMPERATURE, SUPPORT_FAN_MODE, SUPPORT_ON_OFF, SUPPORT_OPERATION_MODE) from homeassistant.const import ( diff --git a/homeassistant/components/climate/nuheat.py b/homeassistant/components/climate/nuheat.py index d0bfe5add5815..f52d2c7b5017d 100644 --- a/homeassistant/components/climate/nuheat.py +++ b/homeassistant/components/climate/nuheat.py @@ -9,8 +9,8 @@ import voluptuous as vol -from homeassistant.components.climate import ( - ClimateDevice, +from homeassistant.components.climate import ClimateDevice +from homeassistant.components.climate.const import ( DOMAIN, SUPPORT_HOLD_MODE, SUPPORT_OPERATION_MODE, diff --git a/homeassistant/components/climate/oem.py b/homeassistant/components/climate/oem.py index e006242331c1b..f1e03396b0500 100644 --- a/homeassistant/components/climate/oem.py +++ b/homeassistant/components/climate/oem.py @@ -13,11 +13,12 @@ import voluptuous as vol # Import the device class from the component that you want to support -from homeassistant.components.climate import ( - ClimateDevice, PLATFORM_SCHEMA, STATE_HEAT, STATE_IDLE, ATTR_TEMPERATURE, - SUPPORT_TARGET_TEMPERATURE, SUPPORT_AWAY_MODE) -from homeassistant.const import (CONF_HOST, CONF_USERNAME, CONF_PASSWORD, - CONF_PORT, TEMP_CELSIUS, CONF_NAME) +from homeassistant.components.climate import ClimateDevice, PLATFORM_SCHEMA +from homeassistant.components.climate.const import ( + STATE_HEAT, STATE_IDLE, SUPPORT_TARGET_TEMPERATURE, SUPPORT_AWAY_MODE) +from homeassistant.const import ( + ATTR_TEMPERATURE, CONF_HOST, CONF_USERNAME, CONF_PASSWORD, + CONF_PORT, TEMP_CELSIUS, CONF_NAME) import homeassistant.helpers.config_validation as cv REQUIREMENTS = ['oemthermostat==1.1'] diff --git a/homeassistant/components/climate/proliphix.py b/homeassistant/components/climate/proliphix.py index 76160a28c6e51..c88ece033df43 100644 --- a/homeassistant/components/climate/proliphix.py +++ b/homeassistant/components/climate/proliphix.py @@ -6,11 +6,12 @@ """ import voluptuous as vol -from homeassistant.components.climate import ( - PRECISION_TENTHS, STATE_COOL, STATE_HEAT, STATE_IDLE, - ClimateDevice, PLATFORM_SCHEMA, SUPPORT_TARGET_TEMPERATURE) +from homeassistant.components.climate import ClimateDevice, PLATFORM_SCHEMA +from homeassistant.components.climate.const import ( + STATE_COOL, STATE_HEAT, STATE_IDLE, SUPPORT_TARGET_TEMPERATURE) from homeassistant.const import ( - CONF_HOST, CONF_PASSWORD, CONF_USERNAME, TEMP_FAHRENHEIT, ATTR_TEMPERATURE) + CONF_HOST, CONF_PASSWORD, CONF_USERNAME, PRECISION_TENTHS, TEMP_FAHRENHEIT, + ATTR_TEMPERATURE) import homeassistant.helpers.config_validation as cv REQUIREMENTS = ['proliphix==0.4.1'] diff --git a/homeassistant/components/climate/radiotherm.py b/homeassistant/components/climate/radiotherm.py index a72bf711242b3..bad20884536f4 100644 --- a/homeassistant/components/climate/radiotherm.py +++ b/homeassistant/components/climate/radiotherm.py @@ -9,12 +9,14 @@ import voluptuous as vol -from homeassistant.components.climate import ( - STATE_AUTO, STATE_COOL, STATE_HEAT, STATE_IDLE, STATE_ON, STATE_OFF, - ClimateDevice, PLATFORM_SCHEMA, SUPPORT_TARGET_TEMPERATURE, +from homeassistant.components.climate import ClimateDevice, PLATFORM_SCHEMA +from homeassistant.components.climate.const import ( + STATE_AUTO, STATE_COOL, STATE_HEAT, STATE_IDLE, + SUPPORT_TARGET_TEMPERATURE, SUPPORT_OPERATION_MODE, SUPPORT_FAN_MODE, SUPPORT_AWAY_MODE) from homeassistant.const import ( - CONF_HOST, TEMP_FAHRENHEIT, ATTR_TEMPERATURE, PRECISION_HALVES) + ATTR_TEMPERATURE, CONF_HOST, PRECISION_HALVES, TEMP_FAHRENHEIT, STATE_ON, + STATE_OFF) import homeassistant.helpers.config_validation as cv REQUIREMENTS = ['radiotherm==2.0.0'] diff --git a/homeassistant/components/climate/sensibo.py b/homeassistant/components/climate/sensibo.py index bf1cf5bf345e0..7850b08fd6b78 100644 --- a/homeassistant/components/climate/sensibo.py +++ b/homeassistant/components/climate/sensibo.py @@ -15,8 +15,9 @@ from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_STATE, ATTR_TEMPERATURE, CONF_API_KEY, CONF_ID, STATE_ON, STATE_OFF, TEMP_CELSIUS, TEMP_FAHRENHEIT) -from homeassistant.components.climate import ( - ATTR_CURRENT_HUMIDITY, ClimateDevice, DOMAIN, PLATFORM_SCHEMA, +from homeassistant.components.climate import ClimateDevice, PLATFORM_SCHEMA +from homeassistant.components.climate.const import ( + ATTR_CURRENT_HUMIDITY, DOMAIN, SUPPORT_TARGET_TEMPERATURE, SUPPORT_OPERATION_MODE, SUPPORT_FAN_MODE, SUPPORT_SWING_MODE, SUPPORT_ON_OFF, STATE_HEAT, STATE_COOL, STATE_FAN_ONLY, STATE_DRY, diff --git a/homeassistant/components/climate/touchline.py b/homeassistant/components/climate/touchline.py index 641f6e9a1d8b7..fa38bd37c8f7e 100644 --- a/homeassistant/components/climate/touchline.py +++ b/homeassistant/components/climate/touchline.py @@ -8,8 +8,9 @@ import voluptuous as vol -from homeassistant.components.climate import ( - ClimateDevice, PLATFORM_SCHEMA, SUPPORT_TARGET_TEMPERATURE) +from homeassistant.components.climate import ClimateDevice, PLATFORM_SCHEMA +from homeassistant.components.climate.const import ( + SUPPORT_TARGET_TEMPERATURE) from homeassistant.const import CONF_HOST, TEMP_CELSIUS, ATTR_TEMPERATURE import homeassistant.helpers.config_validation as cv diff --git a/homeassistant/components/climate/venstar.py b/homeassistant/components/climate/venstar.py index 16c0b2061546e..820443ee186de 100644 --- a/homeassistant/components/climate/venstar.py +++ b/homeassistant/components/climate/venstar.py @@ -8,14 +8,14 @@ import voluptuous as vol -from homeassistant.components.climate import ( +from homeassistant.components.climate import ClimateDevice, PLATFORM_SCHEMA +from homeassistant.components.climate.const import ( ATTR_OPERATION_MODE, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, - PLATFORM_SCHEMA, STATE_AUTO, STATE_COOL, STATE_HEAT, SUPPORT_FAN_MODE, + STATE_AUTO, STATE_COOL, STATE_HEAT, SUPPORT_FAN_MODE, SUPPORT_OPERATION_MODE, SUPPORT_TARGET_HUMIDITY, SUPPORT_AWAY_MODE, SUPPORT_TARGET_HUMIDITY_HIGH, SUPPORT_TARGET_HUMIDITY_LOW, SUPPORT_HOLD_MODE, SUPPORT_TARGET_TEMPERATURE, - SUPPORT_TARGET_TEMPERATURE_HIGH, SUPPORT_TARGET_TEMPERATURE_LOW, - ClimateDevice) + SUPPORT_TARGET_TEMPERATURE_HIGH, SUPPORT_TARGET_TEMPERATURE_LOW) from homeassistant.const import ( ATTR_TEMPERATURE, CONF_HOST, CONF_PASSWORD, CONF_SSL, CONF_TIMEOUT, CONF_USERNAME, PRECISION_WHOLE, STATE_OFF, STATE_ON, TEMP_CELSIUS, diff --git a/homeassistant/components/climate/zhong_hong.py b/homeassistant/components/climate/zhong_hong.py index b564e9d1fa4a3..78cd7d16c483d 100644 --- a/homeassistant/components/climate/zhong_hong.py +++ b/homeassistant/components/climate/zhong_hong.py @@ -8,10 +8,11 @@ import voluptuous as vol -from homeassistant.components.climate import ( - ATTR_OPERATION_MODE, PLATFORM_SCHEMA, STATE_COOL, STATE_DRY, +from homeassistant.components.climate import ClimateDevice, PLATFORM_SCHEMA +from homeassistant.components.climate.const import ( + ATTR_OPERATION_MODE, STATE_COOL, STATE_DRY, STATE_FAN_ONLY, STATE_HEAT, SUPPORT_FAN_MODE, SUPPORT_ON_OFF, - SUPPORT_OPERATION_MODE, SUPPORT_TARGET_TEMPERATURE, ClimateDevice) + SUPPORT_OPERATION_MODE, SUPPORT_TARGET_TEMPERATURE) from homeassistant.const import (ATTR_TEMPERATURE, CONF_HOST, CONF_PORT, EVENT_HOMEASSISTANT_STOP, TEMP_CELSIUS) import homeassistant.helpers.config_validation as cv diff --git a/homeassistant/components/config/config_entries.py b/homeassistant/components/config/config_entries.py index 68890a79ca653..65f65cbcec553 100644 --- a/homeassistant/components/config/config_entries.py +++ b/homeassistant/components/config/config_entries.py @@ -17,6 +17,10 @@ async def async_setup(hass): hass.http.register_view( ConfigManagerFlowResourceView(hass.config_entries.flow)) hass.http.register_view(ConfigManagerAvailableFlowView) + hass.http.register_view( + OptionManagerFlowIndexView(hass.config_entries.options.flow)) + hass.http.register_view( + OptionManagerFlowResourceView(hass.config_entries.options.flow)) return True @@ -45,8 +49,9 @@ class ConfigManagerEntryIndexView(HomeAssistantView): name = 'api:config:config_entries:entry' async def get(self, request): - """List flows in progress.""" + """List available config entries.""" hass = request.app['hass'] + return self.json([{ 'entry_id': entry.entry_id, 'domain': entry.domain, @@ -54,6 +59,9 @@ async def get(self, request): 'source': entry.source, 'state': entry.state, 'connection_class': entry.connection_class, + 'supports_options': hasattr( + config_entries.HANDLERS[entry.domain], + 'async_get_options_flow'), } for entry in hass.config_entries.async_entries()]) @@ -145,3 +153,48 @@ class ConfigManagerAvailableFlowView(HomeAssistantView): async def get(self, request): """List available flow handlers.""" return self.json(config_entries.FLOWS) + + +class OptionManagerFlowIndexView(FlowManagerIndexView): + """View to create option flows.""" + + url = '/api/config/config_entries/entry/option/flow' + name = 'api:config:config_entries:entry:resource:option:flow' + + # pylint: disable=arguments-differ + async def post(self, request): + """Handle a POST request. + + handler in request is entry_id. + """ + if not request['hass_user'].is_admin: + raise Unauthorized( + perm_category=CAT_CONFIG_ENTRIES, permission='edit') + + # pylint: disable=no-value-for-parameter + return await super().post(request) + + +class OptionManagerFlowResourceView(ConfigManagerFlowResourceView): + """View to interact with the option flow manager.""" + + url = '/api/config/config_entries/options/flow/{flow_id}' + name = 'api:config:config_entries:options:flow:resource' + + async def get(self, request, flow_id): + """Get the current state of a data_entry_flow.""" + if not request['hass_user'].is_admin: + raise Unauthorized( + perm_category=CAT_CONFIG_ENTRIES, permission='edit') + + return await super().get(request, flow_id) + + # pylint: disable=arguments-differ + async def post(self, request, flow_id): + """Handle a POST request.""" + if not request['hass_user'].is_admin: + raise Unauthorized( + perm_category=CAT_CONFIG_ENTRIES, permission='edit') + + # pylint: disable=no-value-for-parameter + return await super().post(request, flow_id) diff --git a/homeassistant/components/config/device_registry.py b/homeassistant/components/config/device_registry.py index 0677531242a98..9554f6aeee671 100644 --- a/homeassistant/components/config/device_registry.py +++ b/homeassistant/components/config/device_registry.py @@ -19,6 +19,7 @@ vol.Required('type'): WS_TYPE_UPDATE, vol.Required('device_id'): str, vol.Optional('area_id'): vol.Any(str, None), + vol.Optional('name_by_user'): vol.Any(str, None), }) @@ -49,11 +50,13 @@ async def websocket_update_device(hass, connection, msg): """Handle update area websocket command.""" registry = await async_get_registry(hass) - entry = registry.async_update_device( - msg['device_id'], area_id=msg['area_id']) + msg.pop('type') + msg_id = msg.pop('id') + + entry = registry.async_update_device(**msg) connection.send_message(websocket_api.result_message( - msg['id'], _entry_dict(entry) + msg_id, _entry_dict(entry) )) @@ -70,4 +73,5 @@ def _entry_dict(entry): 'id': entry.id, 'hub_device_id': entry.hub_device_id, 'area_id': entry.area_id, + 'name_by_user': entry.name_by_user, } diff --git a/homeassistant/components/cover/__init__.py b/homeassistant/components/cover/__init__.py index bd003f1ad6703..8b4031f09edd3 100644 --- a/homeassistant/components/cover/__init__.py +++ b/homeassistant/components/cover/__init__.py @@ -35,12 +35,27 @@ ENTITY_ID_FORMAT = DOMAIN + '.{}' +# Refer to the cover dev docs for device class descriptions +DEVICE_CLASS_AWNING = 'awning' +DEVICE_CLASS_BLIND = 'blind' +DEVICE_CLASS_CURTAIN = 'curtain' +DEVICE_CLASS_DAMPER = 'damper' +DEVICE_CLASS_DOOR = 'door' +DEVICE_CLASS_GARAGE = 'garage' +DEVICE_CLASS_SHADE = 'shade' +DEVICE_CLASS_SHUTTER = 'shutter' +DEVICE_CLASS_WINDOW = 'window' DEVICE_CLASSES = [ - 'damper', - 'garage', # Garage door control - 'window', # Window control + DEVICE_CLASS_AWNING, + DEVICE_CLASS_BLIND, + DEVICE_CLASS_CURTAIN, + DEVICE_CLASS_DAMPER, + DEVICE_CLASS_DOOR, + DEVICE_CLASS_GARAGE, + DEVICE_CLASS_SHADE, + DEVICE_CLASS_SHUTTER, + DEVICE_CLASS_WINDOW ] - DEVICE_CLASSES_SCHEMA = vol.All(vol.Lower, vol.In(DEVICE_CLASSES)) SUPPORT_OPEN = 1 diff --git a/homeassistant/components/daikin/.translations/es-419.json b/homeassistant/components/daikin/.translations/es-419.json new file mode 100644 index 0000000000000..dae3afdba6fab --- /dev/null +++ b/homeassistant/components/daikin/.translations/es-419.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "device_timeout": "Tiempo de espera de conexi\u00f3n al dispositivo." + }, + "step": { + "user": { + "data": { + "host": "Host" + }, + "description": "Introduzca la direcci\u00f3n IP de su Daikin AC.", + "title": "Configurar Daikin AC" + } + }, + "title": "Daikin AC" + } +} \ No newline at end of file diff --git a/homeassistant/components/daikin/.translations/hu.json b/homeassistant/components/daikin/.translations/hu.json index 623fab6828a02..cbca935f5517c 100644 --- a/homeassistant/components/daikin/.translations/hu.json +++ b/homeassistant/components/daikin/.translations/hu.json @@ -1,11 +1,19 @@ { "config": { + "abort": { + "already_configured": "Az eszk\u00f6zt m\u00e1r konfigur\u00e1ltuk", + "device_fail": "Az eszk\u00f6z l\u00e9trehoz\u00e1sakor v\u00e1ratlan hiba l\u00e9pett fel.", + "device_timeout": "Id\u0151t\u00fall\u00e9p\u00e9s a k\u00e9sz\u00fcl\u00e9k csatlakoz\u00e1sakor." + }, "step": { "user": { "data": { "host": "Kiszolg\u00e1l\u00f3" - } + }, + "description": "Add meg a Daikin l\u00e9gkond\u00edcion\u00e1l\u00f3 IP-c\u00edm\u00e9t.", + "title": "A Daikin l\u00e9gkond\u00edcion\u00e1l\u00f3 konfigur\u00e1l\u00e1sa" } - } + }, + "title": "Daikin L\u00e9gkond\u00edci\u00f3n\u00e1l\u00f3" } } \ No newline at end of file diff --git a/homeassistant/components/daikin/.translations/it.json b/homeassistant/components/daikin/.translations/it.json new file mode 100644 index 0000000000000..0b8151d23f658 --- /dev/null +++ b/homeassistant/components/daikin/.translations/it.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", + "device_fail": "Errore inatteso durante la creazione del dispositivo.", + "device_timeout": "Tempo scaduto per la connessione al dispositivo." + }, + "step": { + "user": { + "data": { + "host": "Host" + }, + "description": "Inserisci l'indirizzo IP del tuo Daikin AC.", + "title": "Configura Daikin AC" + } + }, + "title": "Daikin AC" + } +} \ No newline at end of file diff --git a/homeassistant/components/daikin/.translations/ru.json b/homeassistant/components/daikin/.translations/ru.json index 83c42f4280c83..0549aa3b160b7 100644 --- a/homeassistant/components/daikin/.translations/ru.json +++ b/homeassistant/components/daikin/.translations/ru.json @@ -10,7 +10,7 @@ "data": { "host": "\u0425\u043e\u0441\u0442" }, - "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 IP-\u0430\u0434\u0440\u0435\u0441 \u0432\u0430\u0448\u0435\u0433\u043e Daikin AC.", + "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 IP-\u0430\u0434\u0440\u0435\u0441 \u0412\u0430\u0448\u0435\u0433\u043e Daikin AC.", "title": "Daikin AC" } }, diff --git a/homeassistant/components/daikin/.translations/sv.json b/homeassistant/components/daikin/.translations/sv.json new file mode 100644 index 0000000000000..0f1247197aa93 --- /dev/null +++ b/homeassistant/components/daikin/.translations/sv.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad", + "device_fail": "Ov\u00e4ntat fel vid skapande av enhet.", + "device_timeout": "Timeout f\u00f6r anslutning till enheten." + }, + "step": { + "user": { + "data": { + "host": "V\u00e4rddatorn" + }, + "description": "Ange IP-adressen f\u00f6r din Daikin AC.", + "title": "Konfigurera Daikin AC" + } + }, + "title": "Daikin AC" + } +} \ No newline at end of file diff --git a/homeassistant/components/daikin/climate.py b/homeassistant/components/daikin/climate.py index d97b506e2732c..775e4a216e5cb 100644 --- a/homeassistant/components/daikin/climate.py +++ b/homeassistant/components/daikin/climate.py @@ -4,17 +4,17 @@ import voluptuous as vol -from homeassistant.components.climate import ( +from homeassistant.components.climate import ClimateDevice, PLATFORM_SCHEMA +from homeassistant.components.climate.const import ( ATTR_CURRENT_TEMPERATURE, ATTR_FAN_MODE, ATTR_OPERATION_MODE, - ATTR_SWING_MODE, PLATFORM_SCHEMA, STATE_AUTO, STATE_COOL, STATE_DRY, - STATE_FAN_ONLY, STATE_HEAT, STATE_OFF, SUPPORT_FAN_MODE, - SUPPORT_OPERATION_MODE, SUPPORT_SWING_MODE, SUPPORT_TARGET_TEMPERATURE, - ClimateDevice) + ATTR_SWING_MODE, STATE_AUTO, STATE_COOL, STATE_DRY, + STATE_FAN_ONLY, STATE_HEAT, SUPPORT_FAN_MODE, + SUPPORT_OPERATION_MODE, SUPPORT_SWING_MODE, SUPPORT_TARGET_TEMPERATURE) from homeassistant.components.daikin import DOMAIN as DAIKIN_DOMAIN from homeassistant.components.daikin.const import ( ATTR_INSIDE_TEMPERATURE, ATTR_OUTSIDE_TEMPERATURE, ATTR_TARGET_TEMPERATURE) from homeassistant.const import ( - ATTR_TEMPERATURE, CONF_HOST, CONF_NAME, TEMP_CELSIUS) + ATTR_TEMPERATURE, CONF_HOST, CONF_NAME, STATE_OFF, TEMP_CELSIUS) import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/danfoss_air/__init__.py b/homeassistant/components/danfoss_air/__init__.py index d6123a25f235f..f4a7b92c17cbb 100644 --- a/homeassistant/components/danfoss_air/__init__.py +++ b/homeassistant/components/danfoss_air/__init__.py @@ -9,11 +9,11 @@ import homeassistant.helpers.config_validation as cv from homeassistant.util import Throttle -REQUIREMENTS = ['pydanfossair==0.0.6'] +REQUIREMENTS = ['pydanfossair==0.0.7'] _LOGGER = logging.getLogger(__name__) -DANFOSS_AIR_PLATFORMS = ['sensor', 'binary_sensor'] +DANFOSS_AIR_PLATFORMS = ['sensor', 'binary_sensor', 'switch'] DOMAIN = 'danfoss_air' MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60) @@ -52,6 +52,10 @@ def get_value(self, item): """Get value for sensor.""" return self._data.get(item) + def update_state(self, command, state_command): + """Send update command to Danfoss Air CCM.""" + self._data[state_command] = self._client.command(command) + @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): """Use the data from Danfoss Air API.""" @@ -71,5 +75,17 @@ def update(self): = round(self._client.command(ReadCommand.filterPercent), 2) self._data[ReadCommand.bypass] \ = self._client.command(ReadCommand.bypass) + self._data[ReadCommand.fan_step] \ + = self._client.command(ReadCommand.fan_step) + self._data[ReadCommand.supply_fan_speed] \ + = self._client.command(ReadCommand.supply_fan_speed) + self._data[ReadCommand.exhaust_fan_speed] \ + = self._client.command(ReadCommand.exhaust_fan_speed) + self._data[ReadCommand.away_mode] \ + = self._client.command(ReadCommand.away_mode) + self._data[ReadCommand.boost] \ + = self._client.command(ReadCommand.boost) + self._data[ReadCommand.battery_percent] \ + = self._client.command(ReadCommand.battery_percent) _LOGGER.debug("Done fetching data from Danfoss Air CCM module") diff --git a/homeassistant/components/danfoss_air/binary_sensor.py b/homeassistant/components/danfoss_air/binary_sensor.py index bf8fe952993d4..4052a100540cd 100644 --- a/homeassistant/components/danfoss_air/binary_sensor.py +++ b/homeassistant/components/danfoss_air/binary_sensor.py @@ -1,9 +1,4 @@ -""" -Support for the for Danfoss Air HRV binary sensor platform. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/binary_sensor.danfoss_air/ -""" +"""Support for the for Danfoss Air HRV binary sensors.""" from homeassistant.components.binary_sensor import BinarySensorDevice from homeassistant.components.danfoss_air import DOMAIN \ as DANFOSS_AIR_DOMAIN @@ -14,12 +9,16 @@ def setup_platform(hass, config, add_entities, discovery_info=None): from pydanfossair.commands import ReadCommand data = hass.data[DANFOSS_AIR_DOMAIN] - sensors = [["Danfoss Air Bypass Active", ReadCommand.bypass]] + sensors = [ + ["Danfoss Air Bypass Active", ReadCommand.bypass, "opening"], + ["Danfoss Air Away Mode Active", ReadCommand.away_mode, None], + ] dev = [] for sensor in sensors: - dev.append(DanfossAirBinarySensor(data, sensor[0], sensor[1])) + dev.append(DanfossAirBinarySensor( + data, sensor[0], sensor[1], sensor[2])) add_entities(dev, True) @@ -27,12 +26,13 @@ def setup_platform(hass, config, add_entities, discovery_info=None): class DanfossAirBinarySensor(BinarySensorDevice): """Representation of a Danfoss Air binary sensor.""" - def __init__(self, data, name, sensor_type): + def __init__(self, data, name, sensor_type, device_class): """Initialize the Danfoss Air binary sensor.""" self._data = data self._name = name self._state = None self._type = sensor_type + self._device_class = device_class @property def name(self): @@ -47,7 +47,7 @@ def is_on(self): @property def device_class(self): """Type of device class.""" - return "opening" + return self._device_class def update(self): """Fetch new state data for the sensor.""" diff --git a/homeassistant/components/danfoss_air/sensor.py b/homeassistant/components/danfoss_air/sensor.py index 2f3807c499994..9902184e62410 100644 --- a/homeassistant/components/danfoss_air/sensor.py +++ b/homeassistant/components/danfoss_air/sensor.py @@ -1,14 +1,15 @@ -""" -Support for the for Danfoss Air HRV sensor platform. +"""Support for the for Danfoss Air HRV sensors.""" +import logging -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/sensor.danfoss_air/ -""" from homeassistant.components.danfoss_air import DOMAIN \ as DANFOSS_AIR_DOMAIN -from homeassistant.const import TEMP_CELSIUS +from homeassistant.const import ( + TEMP_CELSIUS, DEVICE_CLASS_BATTERY, + DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_HUMIDITY) from homeassistant.helpers.entity import Entity +_LOGGER = logging.getLogger(__name__) + def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the available Danfoss Air sensors etc.""" @@ -18,23 +19,32 @@ def setup_platform(hass, config, add_entities, discovery_info=None): sensors = [ ["Danfoss Air Exhaust Temperature", TEMP_CELSIUS, - ReadCommand.exhaustTemperature], + ReadCommand.exhaustTemperature, DEVICE_CLASS_TEMPERATURE], ["Danfoss Air Outdoor Temperature", TEMP_CELSIUS, - ReadCommand.outdoorTemperature], + ReadCommand.outdoorTemperature, DEVICE_CLASS_TEMPERATURE], ["Danfoss Air Supply Temperature", TEMP_CELSIUS, - ReadCommand.supplyTemperature], + ReadCommand.supplyTemperature, DEVICE_CLASS_TEMPERATURE], ["Danfoss Air Extract Temperature", TEMP_CELSIUS, - ReadCommand.extractTemperature], + ReadCommand.extractTemperature, DEVICE_CLASS_TEMPERATURE], ["Danfoss Air Remaining Filter", '%', - ReadCommand.filterPercent], + ReadCommand.filterPercent, None], ["Danfoss Air Humidity", '%', - ReadCommand.humidity] + ReadCommand.humidity, DEVICE_CLASS_HUMIDITY], + ["Danfoss Air Fan Step", '%', + ReadCommand.fan_step, None], + ["Dandoss Air Exhaust Fan Speed", 'RPM', + ReadCommand.exhaust_fan_speed, None], + ["Dandoss Air Supply Fan Speed", 'RPM', + ReadCommand.supply_fan_speed, None], + ["Dandoss Air Dial Battery", '%', + ReadCommand.battery_percent, DEVICE_CLASS_BATTERY] ] dev = [] for sensor in sensors: - dev.append(DanfossAir(data, sensor[0], sensor[1], sensor[2])) + dev.append(DanfossAir( + data, sensor[0], sensor[1], sensor[2], sensor[3])) add_entities(dev, True) @@ -42,19 +52,25 @@ def setup_platform(hass, config, add_entities, discovery_info=None): class DanfossAir(Entity): """Representation of a Sensor.""" - def __init__(self, data, name, sensor_unit, sensor_type): + def __init__(self, data, name, sensor_unit, sensor_type, device_class): """Initialize the sensor.""" self._data = data self._name = name self._state = None self._type = sensor_type self._unit = sensor_unit + self._device_class = device_class @property def name(self): """Return the name of the sensor.""" return self._name + @property + def device_class(self): + """Return the device class of the sensor.""" + return self._device_class + @property def state(self): """Return the state of the sensor.""" @@ -74,3 +90,5 @@ def update(self): self._data.update() self._state = self._data.get_value(self._type) + if self._state is None: + _LOGGER.debug("Could not get data for %s", self._type) diff --git a/homeassistant/components/danfoss_air/switch.py b/homeassistant/components/danfoss_air/switch.py new file mode 100644 index 0000000000000..ec85757be59dd --- /dev/null +++ b/homeassistant/components/danfoss_air/switch.py @@ -0,0 +1,72 @@ +"""Support for the for Danfoss Air HRV sswitches.""" +import logging + +from homeassistant.components.switch import ( + SwitchDevice) +from homeassistant.components.danfoss_air import DOMAIN \ + as DANFOSS_AIR_DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the Danfoss Air HRV switch platform.""" + from pydanfossair.commands import ReadCommand, UpdateCommand + + data = hass.data[DANFOSS_AIR_DOMAIN] + + switches = [ + ["Danfoss Air Boost", + ReadCommand.boost, + UpdateCommand.boost_activate, + UpdateCommand.boost_deactivate], + ] + + dev = [] + + for switch in switches: + dev.append(DanfossAir( + data, switch[0], switch[1], switch[2], switch[3])) + + add_entities(dev) + + +class DanfossAir(SwitchDevice): + """Representation of a Danfoss Air HRV Switch.""" + + def __init__(self, data, name, state_command, on_command, off_command): + """Initialize the switch.""" + self._data = data + self._name = name + self._state_command = state_command + self._on_command = on_command + self._off_command = off_command + self._state = None + + @property + def name(self): + """Return the name of the switch.""" + return self._name + + @property + def is_on(self): + """Return true if switch is on.""" + return self._state + + def turn_on(self, **kwargs): + """Turn the switch on.""" + _LOGGER.debug("Turning on switch with command %s", self._on_command) + self._data.update_state(self._on_command, self._state_command) + + def turn_off(self, **kwargs): + """Turn the switch off.""" + _LOGGER.debug("Turning of switch with command %s", self._off_command) + self._data.update_state(self._off_command, self._state_command) + + def update(self): + """Update the switch's state.""" + self._data.update() + + self._state = self._data.get_value(self._state_command) + if self._state is None: + _LOGGER.debug("Could not get data for %s", self._state_command) diff --git a/homeassistant/components/deconz/.translations/es-419.json b/homeassistant/components/deconz/.translations/es-419.json index ab47a5b43c824..c2298a5fcc241 100644 --- a/homeassistant/components/deconz/.translations/es-419.json +++ b/homeassistant/components/deconz/.translations/es-419.json @@ -12,7 +12,7 @@ "init": { "data": { "host": "Host", - "port": "Puerto (valor predeterminado: '80')" + "port": "Puerto" }, "title": "Definir el gateway deCONZ" }, @@ -23,7 +23,8 @@ "data": { "allow_clip_sensor": "Permitir la importaci\u00f3n de sensores virtuales", "allow_deconz_groups": "Permitir la importaci\u00f3n de grupos deCONZ" - } + }, + "title": "Opciones de configuraci\u00f3n adicionales para deCONZ" } }, "title": "deCONZ Zigbee gateway" diff --git a/homeassistant/components/deconz/.translations/hu.json b/homeassistant/components/deconz/.translations/hu.json index fbb5c26ba04aa..06211f61bf218 100644 --- a/homeassistant/components/deconz/.translations/hu.json +++ b/homeassistant/components/deconz/.translations/hu.json @@ -12,7 +12,7 @@ "init": { "data": { "host": "H\u00e1zigazda (Host)", - "port": "Port (alap\u00e9rtelmezett \u00e9rt\u00e9k: '80')" + "port": "Port" }, "title": "deCONZ \u00e1tj\u00e1r\u00f3 megad\u00e1sa" }, diff --git a/homeassistant/components/deconz/.translations/it.json b/homeassistant/components/deconz/.translations/it.json index 87dcd0610f2cc..c0a23d47be366 100644 --- a/homeassistant/components/deconz/.translations/it.json +++ b/homeassistant/components/deconz/.translations/it.json @@ -28,6 +28,6 @@ "title": "Opzioni di configurazione extra per deCONZ" } }, - "title": "deCONZ" + "title": "Gateway Zigbee deCONZ" } } \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/sv.json b/homeassistant/components/deconz/.translations/sv.json index 88cf8742acde8..a1157cbfb9c59 100644 --- a/homeassistant/components/deconz/.translations/sv.json +++ b/homeassistant/components/deconz/.translations/sv.json @@ -28,6 +28,6 @@ "title": "Extra konfigurationsalternativ f\u00f6r deCONZ" } }, - "title": "deCONZ" + "title": "deCONZ Zigbee Gateway" } } \ No newline at end of file diff --git a/homeassistant/components/deconz/__init__.py b/homeassistant/components/deconz/__init__.py index 8015324be13fd..d107cba8f7b71 100644 --- a/homeassistant/components/deconz/__init__.py +++ b/homeassistant/components/deconz/__init__.py @@ -12,10 +12,7 @@ from .const import DEFAULT_PORT, DOMAIN, _LOGGER from .gateway import DeconzGateway -REQUIREMENTS = ['pydeconz==47'] - -SUPPORTED_PLATFORMS = ['binary_sensor', 'cover', - 'light', 'scene', 'sensor', 'switch'] +REQUIREMENTS = ['pydeconz==52'] CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.Schema({ @@ -71,11 +68,11 @@ async def async_setup_entry(hass, config_entry): gateway = DeconzGateway(hass, config_entry) - hass.data[DOMAIN] = gateway - if not await gateway.async_setup(): return False + hass.data[DOMAIN] = gateway + device_registry = await \ hass.helpers.device_registry.async_get_registry() device_registry.async_get_or_create( diff --git a/homeassistant/components/deconz/binary_sensor.py b/homeassistant/components/deconz/binary_sensor.py index 77d01c5c40be9..cb68b842f4af1 100644 --- a/homeassistant/components/deconz/binary_sensor.py +++ b/homeassistant/components/deconz/binary_sensor.py @@ -5,7 +5,8 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect from .const import ( - ATTR_DARK, ATTR_ON, CONF_ALLOW_CLIP_SENSOR, DOMAIN as DECONZ_DOMAIN) + ATTR_DARK, ATTR_ON, CONF_ALLOW_CLIP_SENSOR, DOMAIN as DECONZ_DOMAIN, + NEW_SENSOR) from .deconz_device import DeconzDevice DEPENDENCIES = ['deconz'] @@ -34,7 +35,7 @@ def async_add_sensor(sensors): async_add_entities(entities, True) gateway.listeners.append( - async_dispatcher_connect(hass, 'deconz_new_sensor', async_add_sensor)) + async_dispatcher_connect(hass, NEW_SENSOR, async_add_sensor)) async_add_sensor(gateway.api.sensors.values()) diff --git a/homeassistant/components/deconz/climate.py b/homeassistant/components/deconz/climate.py new file mode 100644 index 0000000000000..1f39b8705c723 --- /dev/null +++ b/homeassistant/components/deconz/climate.py @@ -0,0 +1,111 @@ +"""Support for deCONZ climate devices.""" +from homeassistant.components.climate import ClimateDevice +from homeassistant.components.climate.const import ( + SUPPORT_ON_OFF, SUPPORT_TARGET_TEMPERATURE) +from homeassistant.const import ( + ATTR_BATTERY_LEVEL, ATTR_TEMPERATURE, TEMP_CELSIUS) +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect + +from .const import ( + ATTR_OFFSET, ATTR_VALVE, CONF_ALLOW_CLIP_SENSOR, + DOMAIN as DECONZ_DOMAIN, NEW_SENSOR) +from .deconz_device import DeconzDevice + +DEPENDENCIES = ['deconz'] + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the deCONZ climate devices. + + Thermostats are based on the same device class as sensors in deCONZ. + """ + gateway = hass.data[DECONZ_DOMAIN] + + @callback + def async_add_climate(sensors): + """Add climate devices from deCONZ.""" + from pydeconz.sensor import THERMOSTAT + entities = [] + allow_clip_sensor = config_entry.data.get(CONF_ALLOW_CLIP_SENSOR, True) + for sensor in sensors: + if sensor.type in THERMOSTAT and \ + not (not allow_clip_sensor and sensor.type.startswith('CLIP')): + entities.append(DeconzThermostat(sensor, gateway)) + async_add_entities(entities, True) + + gateway.listeners.append( + async_dispatcher_connect(hass, NEW_SENSOR, async_add_climate)) + + async_add_climate(gateway.api.sensors.values()) + + +class DeconzThermostat(DeconzDevice, ClimateDevice): + """Representation of a deCONZ thermostat.""" + + def __init__(self, device, gateway): + """Set up thermostat device.""" + super().__init__(device, gateway) + + self._features = SUPPORT_ON_OFF + self._features |= SUPPORT_TARGET_TEMPERATURE + + @property + def supported_features(self): + """Return the list of supported features.""" + return self._features + + @property + def is_on(self): + """Return true if on.""" + return self._device.on + + async def async_turn_on(self): + """Turn on switch.""" + data = {'mode': 'auto'} + await self._device.async_set_config(data) + + async def async_turn_off(self): + """Turn off switch.""" + data = {'mode': 'off'} + await self._device.async_set_config(data) + + @property + def current_temperature(self): + """Return the current temperature.""" + return self._device.temperature + + @property + def target_temperature(self): + """Return the target temperature.""" + return self._device.heatsetpoint + + async def async_set_temperature(self, **kwargs): + """Set new target temperature.""" + data = {} + + if ATTR_TEMPERATURE in kwargs: + data['heatsetpoint'] = kwargs[ATTR_TEMPERATURE] * 100 + + await self._device.async_set_config(data) + + @property + def temperature_unit(self): + """Return the unit of measurement.""" + return TEMP_CELSIUS + + @property + def device_state_attributes(self): + """Return the state attributes of the thermostat.""" + attr = {} + + if self._device.battery: + attr[ATTR_BATTERY_LEVEL] = self._device.battery + + if self._device.offset: + attr[ATTR_OFFSET] = self._device.offset + + if self._device.valve is not None: + attr[ATTR_VALVE] = self._device.valve + + return attr diff --git a/homeassistant/components/deconz/const.py b/homeassistant/components/deconz/const.py index b08f3d7182428..bf0799d1fa26f 100644 --- a/homeassistant/components/deconz/const.py +++ b/homeassistant/components/deconz/const.py @@ -10,13 +10,27 @@ CONF_ALLOW_CLIP_SENSOR = 'allow_clip_sensor' CONF_ALLOW_DECONZ_GROUPS = 'allow_deconz_groups' -SUPPORTED_PLATFORMS = ['binary_sensor', 'cover', +SUPPORTED_PLATFORMS = ['binary_sensor', 'climate', 'cover', 'light', 'scene', 'sensor', 'switch'] DECONZ_REACHABLE = 'deconz_reachable' +NEW_GROUP = 'deconz_new_group' +NEW_LIGHT = 'deconz_new_light' +NEW_SCENE = 'deconz_new_scene' +NEW_SENSOR = 'deconz_new_sensor' + +NEW_DEVICE = { + 'group': NEW_GROUP, + 'light': NEW_LIGHT, + 'scene': NEW_SCENE, + 'sensor': NEW_SENSOR +} + ATTR_DARK = 'dark' +ATTR_OFFSET = 'offset' ATTR_ON = 'on' +ATTR_VALVE = 'valve' DAMPERS = ["Level controllable output"] WINDOW_COVERS = ["Window covering device"] diff --git a/homeassistant/components/deconz/cover.py b/homeassistant/components/deconz/cover.py index 48f06a894bb0c..fda4fe4309c37 100644 --- a/homeassistant/components/deconz/cover.py +++ b/homeassistant/components/deconz/cover.py @@ -5,7 +5,8 @@ from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect -from .const import COVER_TYPES, DAMPERS, DOMAIN as DECONZ_DOMAIN, WINDOW_COVERS +from .const import ( + COVER_TYPES, DAMPERS, DOMAIN as DECONZ_DOMAIN, NEW_LIGHT, WINDOW_COVERS) from .deconz_device import DeconzDevice DEPENDENCIES = ['deconz'] @@ -39,7 +40,7 @@ def async_add_cover(lights): async_add_entities(entities, True) gateway.listeners.append( - async_dispatcher_connect(hass, 'deconz_new_light', async_add_cover)) + async_dispatcher_connect(hass, NEW_LIGHT, async_add_cover)) async_add_cover(gateway.api.lights.values()) @@ -48,7 +49,7 @@ class DeconzCover(DeconzDevice, CoverDevice): """Representation of a deCONZ cover.""" def __init__(self, device, gateway): - """Set up cover and add update callback to get data from websocket.""" + """Set up cover device.""" super().__init__(device, gateway) self._features = SUPPORT_OPEN diff --git a/homeassistant/components/deconz/gateway.py b/homeassistant/components/deconz/gateway.py index fe9fc4b77523e..829485e1e9245 100644 --- a/homeassistant/components/deconz/gateway.py +++ b/homeassistant/components/deconz/gateway.py @@ -8,7 +8,8 @@ from homeassistant.util import slugify from .const import ( - DECONZ_REACHABLE, CONF_ALLOW_CLIP_SENSOR, SUPPORTED_PLATFORMS) + _LOGGER, DECONZ_REACHABLE, CONF_ALLOW_CLIP_SENSOR, NEW_DEVICE, NEW_SENSOR, + SUPPORTED_PLATFORMS) class DeconzGateway: @@ -44,7 +45,7 @@ async def async_setup(self, tries=0): self.listeners.append( async_dispatcher_connect( - hass, 'deconz_new_sensor', self.async_add_remote)) + hass, NEW_SENSOR, self.async_add_remote)) self.async_add_remote(self.api.sensors.values()) @@ -64,8 +65,7 @@ def async_add_device_callback(self, device_type, device): """Handle event of new device creation in deCONZ.""" if not isinstance(device, list): device = [device] - async_dispatcher_send( - self.hass, 'deconz_new_{}'.format(device_type), device) + async_dispatcher_send(self.hass, NEW_DEVICE[device_type], device) @callback def async_add_remote(self, sensors): @@ -140,6 +140,7 @@ def __init__(self, hass, device): self._device.register_async_callback(self.async_update_callback) self._event = 'deconz_{}'.format(CONF_EVENT) self._id = slugify(self._device.name) + _LOGGER.debug("deCONZ event created: %s", self._id) @callback def async_will_remove_from_hass(self) -> None: diff --git a/homeassistant/components/deconz/light.py b/homeassistant/components/deconz/light.py index 50e22c84d6fe3..3b63da8d9f8a1 100644 --- a/homeassistant/components/deconz/light.py +++ b/homeassistant/components/deconz/light.py @@ -9,8 +9,8 @@ import homeassistant.util.color as color_util from .const import ( - CONF_ALLOW_DECONZ_GROUPS, DOMAIN as DECONZ_DOMAIN, COVER_TYPES, - SWITCH_TYPES) + CONF_ALLOW_DECONZ_GROUPS, DOMAIN as DECONZ_DOMAIN, COVER_TYPES, NEW_GROUP, + NEW_LIGHT, SWITCH_TYPES) from .deconz_device import DeconzDevice DEPENDENCIES = ['deconz'] @@ -36,7 +36,7 @@ def async_add_light(lights): async_add_entities(entities, True) gateway.listeners.append( - async_dispatcher_connect(hass, 'deconz_new_light', async_add_light)) + async_dispatcher_connect(hass, NEW_LIGHT, async_add_light)) @callback def async_add_group(groups): @@ -49,7 +49,7 @@ def async_add_group(groups): async_add_entities(entities, True) gateway.listeners.append( - async_dispatcher_connect(hass, 'deconz_new_group', async_add_group)) + async_dispatcher_connect(hass, NEW_GROUP, async_add_group)) async_add_light(gateway.api.lights.values()) async_add_group(gateway.api.groups.values()) diff --git a/homeassistant/components/deconz/scene.py b/homeassistant/components/deconz/scene.py index d3a6df810bae8..22b4c47f2ab8b 100644 --- a/homeassistant/components/deconz/scene.py +++ b/homeassistant/components/deconz/scene.py @@ -1,9 +1,10 @@ """Support for deCONZ scenes.""" -from homeassistant.components.deconz import DOMAIN as DECONZ_DOMAIN from homeassistant.components.scene import Scene from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect +from .const import DOMAIN as DECONZ_DOMAIN, NEW_SCENE + DEPENDENCIES = ['deconz'] @@ -25,7 +26,7 @@ def async_add_scene(scenes): entities.append(DeconzScene(scene, gateway)) async_add_entities(entities) gateway.listeners.append( - async_dispatcher_connect(hass, 'deconz_new_scene', async_add_scene)) + async_dispatcher_connect(hass, NEW_SCENE, async_add_scene)) async_add_scene(gateway.api.scenes.values()) diff --git a/homeassistant/components/deconz/sensor.py b/homeassistant/components/deconz/sensor.py index 3083f0c673256..e6b033906e76a 100644 --- a/homeassistant/components/deconz/sensor.py +++ b/homeassistant/components/deconz/sensor.py @@ -6,7 +6,8 @@ from homeassistant.util import slugify from .const import ( - ATTR_DARK, ATTR_ON, CONF_ALLOW_CLIP_SENSOR, DOMAIN as DECONZ_DOMAIN) + ATTR_DARK, ATTR_ON, CONF_ALLOW_CLIP_SENSOR, DOMAIN as DECONZ_DOMAIN, + NEW_SENSOR) from .deconz_device import DeconzDevice DEPENDENCIES = ['deconz'] @@ -29,7 +30,8 @@ async def async_setup_entry(hass, config_entry, async_add_entities): @callback def async_add_sensor(sensors): """Add sensors from deCONZ.""" - from pydeconz.sensor import DECONZ_SENSOR, SWITCH as DECONZ_REMOTE + from pydeconz.sensor import ( + DECONZ_SENSOR, SWITCH as DECONZ_REMOTE) entities = [] allow_clip_sensor = config_entry.data.get(CONF_ALLOW_CLIP_SENSOR, True) for sensor in sensors: @@ -43,7 +45,7 @@ def async_add_sensor(sensors): async_add_entities(entities, True) gateway.listeners.append( - async_dispatcher_connect(hass, 'deconz_new_sensor', async_add_sensor)) + async_dispatcher_connect(hass, NEW_SENSOR, async_add_sensor)) async_add_sensor(gateway.api.sensors.values()) diff --git a/homeassistant/components/deconz/switch.py b/homeassistant/components/deconz/switch.py index c48c7205e01d1..56d37d504cbe8 100644 --- a/homeassistant/components/deconz/switch.py +++ b/homeassistant/components/deconz/switch.py @@ -3,7 +3,7 @@ from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect -from .const import DOMAIN as DECONZ_DOMAIN, POWER_PLUGS, SIRENS +from .const import DOMAIN as DECONZ_DOMAIN, NEW_LIGHT, POWER_PLUGS, SIRENS from .deconz_device import DeconzDevice DEPENDENCIES = ['deconz'] @@ -34,7 +34,7 @@ def async_add_switch(lights): async_add_entities(entities, True) gateway.listeners.append( - async_dispatcher_connect(hass, 'deconz_new_light', async_add_switch)) + async_dispatcher_connect(hass, NEW_LIGHT, async_add_switch)) async_add_switch(gateway.api.lights.values()) diff --git a/homeassistant/components/default_config/__init__.py b/homeassistant/components/default_config/__init__.py index d56cf9a4ee896..badc403c7c835 100644 --- a/homeassistant/components/default_config/__init__.py +++ b/homeassistant/components/default_config/__init__.py @@ -11,6 +11,7 @@ 'history', 'logbook', 'map', + 'mobile_app', 'person', 'script', 'sun', diff --git a/homeassistant/components/device_sun_light_trigger/__init__.py b/homeassistant/components/device_sun_light_trigger/__init__.py index 2b047e92c1e6a..00adefc6b5c31 100644 --- a/homeassistant/components/device_sun_light_trigger/__init__.py +++ b/homeassistant/components/device_sun_light_trigger/__init__.py @@ -1,9 +1,4 @@ -""" -Provides functionality to turn on lights based on the states. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/device_sun_light_trigger/ -""" +"""Support to turn on lights based on the states.""" import logging from datetime import timedelta diff --git a/homeassistant/components/device_tracker/__init__.py b/homeassistant/components/device_tracker/__init__.py index af33453c9d55f..1263811aae76d 100644 --- a/homeassistant/components/device_tracker/__init__.py +++ b/homeassistant/components/device_tracker/__init__.py @@ -291,7 +291,7 @@ async def async_see( """ if mac is None and dev_id is None: raise HomeAssistantError('Neither mac or device id passed in') - elif mac is not None: + if mac is not None: mac = str(mac).upper() device = self.mac_to_dev.get(mac) if not device: @@ -580,6 +580,7 @@ async def async_added_to_hass(self): return self._state = state.state self.last_update_home = (state.state == STATE_HOME) + self.last_seen = dt_util.utcnow() for attr, var in ( (ATTR_SOURCE_TYPE, 'source_type'), diff --git a/homeassistant/components/device_tracker/google_maps.py b/homeassistant/components/device_tracker/google_maps.py index c324f3c2757df..3de60d6cb3876 100644 --- a/homeassistant/components/device_tracker/google_maps.py +++ b/homeassistant/components/device_tracker/google_maps.py @@ -61,8 +61,9 @@ def __init__(self, hass, config: ConfigType, see) -> None: self.max_gps_accuracy = config[CONF_MAX_GPS_ACCURACY] try: - self.service = Service(self.username, self.password, - hass.config.path(CREDENTIALS_FILE)) + credfile = "{}.{}".format(hass.config.path(CREDENTIALS_FILE), + slugify(self.username)) + self.service = Service(self.username, self.password, credfile) self._update_info() track_time_interval( diff --git a/homeassistant/components/device_tracker/luci.py b/homeassistant/components/device_tracker/luci.py index 30b09834b68ab..f60e8edd8c4a5 100644 --- a/homeassistant/components/device_tracker/luci.py +++ b/homeassistant/components/device_tracker/luci.py @@ -4,21 +4,17 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/device_tracker.luci/ """ -import json import logging -import re -from collections import namedtuple - -import requests import voluptuous as vol import homeassistant.helpers.config_validation as cv -from homeassistant.exceptions import HomeAssistantError from homeassistant.components.device_tracker import ( DOMAIN, PLATFORM_SCHEMA, DeviceScanner) from homeassistant.const import ( CONF_HOST, CONF_USERNAME, CONF_PASSWORD, CONF_SSL) +REQUIREMENTS = ['openwrt-luci-rpc==1.0.5'] + _LOGGER = logging.getLogger(__name__) DEFAULT_SSL = False @@ -31,12 +27,6 @@ }) -class InvalidLuciTokenError(HomeAssistantError): - """When an invalid token is detected.""" - - pass - - def get_scanner(hass, config): """Validate the configuration and return a Luci scanner.""" scanner = LuciDeviceScanner(config[DOMAIN]) @@ -44,138 +34,58 @@ def get_scanner(hass, config): return scanner if scanner.success_init else None -Device = namedtuple('Device', ['mac', 'ip', 'flags', 'device', 'host']) - - class LuciDeviceScanner(DeviceScanner): - """This class queries a wireless router running OpenWrt firmware.""" + """This class scans for devices connected to an OpenWrt router.""" def __init__(self, config): """Initialize the scanner.""" - self.host = config[CONF_HOST] - protocol = 'http' if not config[CONF_SSL] else 'https' - self.origin = '{}://{}'.format(protocol, self.host) - self.username = config[CONF_USERNAME] - self.password = config[CONF_PASSWORD] + from openwrt_luci_rpc import OpenWrtRpc - self.parse_api_pattern = re.compile(r"(?P\w*) = (?P.*);") + self.router = OpenWrtRpc(config[CONF_HOST], + config[CONF_USERNAME], + config[CONF_PASSWORD], + config[CONF_SSL]) self.last_results = {} - self.refresh_token() - self.mac2name = None - self.success_init = self.token is not None - - def refresh_token(self): - """Get a new token.""" - self.token = _get_token(self.origin, self.username, self.password) + self.success_init = self.router.is_logged_in() def scan_devices(self): """Scan for new devices and return a list with found device IDs.""" self._update_info() + return [device.mac for device in self.last_results] def get_device_name(self, device): """Return the name of the given device or None if we don't know.""" - if self.mac2name is None: - url = '{}/cgi-bin/luci/rpc/uci'.format(self.origin) - result = _req_json_rpc( - url, 'get_all', 'dhcp', params={'auth': self.token}) - if result: - hosts = [x for x in result.values() - if x['.type'] == 'host' and - 'mac' in x and 'name' in x] - mac2name_list = [ - (x['mac'].upper(), x['name']) for x in hosts] - self.mac2name = dict(mac2name_list) - else: - # Error, handled in the _req_json_rpc - return - return self.mac2name.get(device.upper(), None) + name = next(( + result.hostname for result in self.last_results + if result.mac == device), None) + return name def get_extra_attributes(self, device): - """Return the IP of the given device.""" - filter_att = next(( - { - 'ip': result.ip, - 'flags': result.flags, - 'device': result.device, - 'host': result.host - } for result in self.last_results + """ + Get extra attributes of a device. + + Some known extra attributes that may be returned in the device tuple + include Mac Address (mac), Network Device (dev), Ip Address + (ip), reachable status (reachable), Associated router + (host), Hostname if known (hostname) among others. + """ + device = next(( + result for result in self.last_results if result.mac == device), None) - return filter_att + return device._asdict() def _update_info(self): - """Ensure the information from the Luci router is up to date. + """Check the Luci router for devices.""" + result = self.router.get_all_connected_devices( + only_reachable=True) - Returns boolean if scanning successful. - """ - if not self.success_init: - return False - - _LOGGER.info("Checking ARP") - - url = '{}/cgi-bin/luci/rpc/sys'.format(self.origin) - - try: - result = _req_json_rpc( - url, 'net.arptable', params={'auth': self.token}) - except InvalidLuciTokenError: - _LOGGER.info("Refreshing token") - self.refresh_token() - return False - - if result: - self.last_results = [] - for device_entry in result: - # Check if the Flags for each device contain - # NUD_REACHABLE and if so, add it to last_results - if int(device_entry['Flags'], 16) & 0x2: - self.last_results.append(Device(device_entry['HW address'], - device_entry['IP address'], - device_entry['Flags'], - device_entry['Device'], - self.host)) - - return True - - return False - - -def _req_json_rpc(url, method, *args, **kwargs): - """Perform one JSON RPC operation.""" - data = json.dumps({'method': method, 'params': args}) - - try: - res = requests.post(url, data=data, timeout=5, **kwargs) - except requests.exceptions.Timeout: - _LOGGER.exception("Connection to the router timed out") - return - if res.status_code == 200: - try: - result = res.json() - except ValueError: - # If json decoder could not parse the response - _LOGGER.exception("Failed to parse response from luci") - return - try: - return result['result'] - except KeyError: - _LOGGER.exception("No result in response from luci") - return - elif res.status_code == 401: - # Authentication error - _LOGGER.exception( - "Failed to authenticate, check your username and password") - return - elif res.status_code == 403: - _LOGGER.error("Luci responded with a 403 Invalid token") - raise InvalidLuciTokenError - - else: - _LOGGER.error("Invalid response from luci: %s", res) - - -def _get_token(origin, username, password): - """Get authentication token for the given configuration.""" - url = '{}/cgi-bin/luci/rpc/auth'.format(origin) - return _req_json_rpc(url, 'login', username, password) + _LOGGER.debug("Luci get_all_connected_devices returned:" + " %s", result) + + last_results = [] + for device in result: + last_results.append(device) + + self.last_results = last_results diff --git a/homeassistant/components/device_tracker/synology_srm.py b/homeassistant/components/device_tracker/synology_srm.py index 5c7ac9a5d00df..bf5653d681bbb 100644 --- a/homeassistant/components/device_tracker/synology_srm.py +++ b/homeassistant/components/device_tracker/synology_srm.py @@ -13,7 +13,7 @@ CONF_HOST, CONF_USERNAME, CONF_PASSWORD, CONF_PORT, CONF_SSL, CONF_VERIFY_SSL) -REQUIREMENTS = ['synology-srm==0.0.4'] +REQUIREMENTS = ['synology-srm==0.0.6'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/device_tracker/traccar.py b/homeassistant/components/device_tracker/traccar.py index b288b8633d108..1447f7c896c76 100644 --- a/homeassistant/components/device_tracker/traccar.py +++ b/homeassistant/components/device_tracker/traccar.py @@ -12,14 +12,15 @@ from homeassistant.components.device_tracker import PLATFORM_SCHEMA from homeassistant.const import ( CONF_HOST, CONF_PORT, CONF_SSL, CONF_VERIFY_SSL, - CONF_PASSWORD, CONF_USERNAME, ATTR_BATTERY_LEVEL) + CONF_PASSWORD, CONF_USERNAME, ATTR_BATTERY_LEVEL, + CONF_SCAN_INTERVAL, CONF_MONITORED_CONDITIONS) 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 -REQUIREMENTS = ['pytraccar==0.2.1'] +REQUIREMENTS = ['pytraccar==0.3.0'] _LOGGER = logging.getLogger(__name__) @@ -31,6 +32,7 @@ ATTR_TRACKER = 'tracker' DEFAULT_SCAN_INTERVAL = timedelta(seconds=30) +SCAN_INTERVAL = DEFAULT_SCAN_INTERVAL PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_PASSWORD): cv.string, @@ -39,6 +41,8 @@ vol.Optional(CONF_PORT, default=8082): cv.port, vol.Optional(CONF_SSL, default=False): cv.boolean, vol.Optional(CONF_VERIFY_SSL, default=True): cv.boolean, + vol.Optional(CONF_MONITORED_CONDITIONS, + default=[]): vol.All(cv.ensure_list, [cv.string]), }) @@ -50,15 +54,22 @@ async def async_setup_scanner(hass, config, async_see, discovery_info=None): api = API(hass.loop, session, config[CONF_USERNAME], config[CONF_PASSWORD], config[CONF_HOST], config[CONF_PORT], config[CONF_SSL]) - scanner = TraccarScanner(api, hass, async_see) + + scanner = TraccarScanner( + api, hass, async_see, + config.get(CONF_SCAN_INTERVAL, SCAN_INTERVAL), + config[CONF_MONITORED_CONDITIONS]) + return await scanner.async_init() class TraccarScanner: """Define an object to retrieve Traccar data.""" - def __init__(self, api, hass, async_see): + def __init__(self, api, hass, async_see, scan_interval, custom_attributes): """Initialize.""" + self._custom_attributes = custom_attributes + self._scan_interval = scan_interval self._async_see = async_see self._api = api self._hass = hass @@ -70,14 +81,14 @@ async def async_init(self): await self._async_update() async_track_time_interval(self._hass, self._async_update, - DEFAULT_SCAN_INTERVAL) + self._scan_interval) return self._api.authenticated async def _async_update(self, now=None): """Update info from Traccar.""" _LOGGER.debug('Updating device data.') - await self._api.get_device_info() + await self._api.get_device_info(self._custom_attributes) for devicename in self._api.device_info: device = self._api.device_info[devicename] attr = {} @@ -94,6 +105,9 @@ async def _async_update(self, now=None): attr[ATTR_BATTERY_LEVEL] = device['battery'] if device.get('motion') is not None: attr[ATTR_MOTION] = device['motion'] + for custom_attr in self._custom_attributes: + if device.get(custom_attr) is not None: + attr[custom_attr] = device[custom_attr] await self._async_see( dev_id=slugify(device['device_id']), gps=(device.get('latitude'), device.get('longitude')), diff --git a/homeassistant/components/device_tracker/ubee.py b/homeassistant/components/device_tracker/ubee.py new file mode 100644 index 0000000000000..f4ecc7d485543 --- /dev/null +++ b/homeassistant/components/device_tracker/ubee.py @@ -0,0 +1,92 @@ +""" +Support for Ubee router. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/device_tracker.ubee/ +""" + +import logging +import voluptuous as vol + +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 + +REQUIREMENTS = ['pyubee==0.2'] + +_LOGGER = logging.getLogger(__name__) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_HOST): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Required(CONF_USERNAME): cv.string, +}) + + +def get_scanner(hass, config): + """Validate the configuration and return a Ubee scanner.""" + try: + return UbeeDeviceScanner(config[DOMAIN]) + except ConnectionError: + return None + + +class UbeeDeviceScanner(DeviceScanner): + """This class queries a wireless Ubee router.""" + + def __init__(self, config): + """Initialize the Ubee scanner.""" + from pyubee import Ubee + + self.host = config[CONF_HOST] + self.username = config[CONF_USERNAME] + self.password = config[CONF_PASSWORD] + + self.last_results = {} + self.mac2name = {} + + self.ubee = Ubee(self.host, self.username, self.password) + _LOGGER.info("Logging in") + results = self.get_connected_devices() + self.success_init = results is not None + + if self.success_init: + self.last_results = results + else: + _LOGGER.error("Login failed") + + def scan_devices(self): + """Scan for new devices and return a list with found device IDs.""" + self._update_info() + + return self.last_results + + def get_device_name(self, device): + """Return the name of the given device or None if we don't know.""" + if device in self.mac2name: + return self.mac2name.get(device) + + return None + + def _update_info(self): + """Retrieve latest information from the Ubee router.""" + if not self.success_init: + return + + _LOGGER.debug("Scanning") + results = self.get_connected_devices() + + if results is None: + _LOGGER.warning("Error scanning devices") + return + + self.last_results = results or [] + + def get_connected_devices(self): + """List connected devices with pyubee.""" + if not self.ubee.session_active(): + self.ubee.login() + + return self.ubee.get_connected_devices() diff --git a/homeassistant/components/device_tracker/ubus.py b/homeassistant/components/device_tracker/ubus.py index 94e3b407d13ab..96f2f60c1e524 100644 --- a/homeassistant/components/device_tracker/ubus.py +++ b/homeassistant/components/device_tracker/ubus.py @@ -216,8 +216,7 @@ def _req_json_rpc(url, session_id, rpcmethod, subsystem, method, **params): if 'message' in response['error'] and \ response['error']['message'] == "Access denied": raise PermissionError(response['error']['message']) - else: - raise HomeAssistantError(response['error']['message']) + raise HomeAssistantError(response['error']['message']) if rpcmethod == "call": try: diff --git a/homeassistant/components/dialogflow/.translations/es-419.json b/homeassistant/components/dialogflow/.translations/es-419.json new file mode 100644 index 0000000000000..41a66b038f5a2 --- /dev/null +++ b/homeassistant/components/dialogflow/.translations/es-419.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "Su instancia de Home Assistant debe ser accesible desde Internet para recibir mensajes de Dialogflow.", + "one_instance_allowed": "Solo una instancia es necesaria." + }, + "create_entry": { + "default": "Para enviar eventos a Home Assistant, deber\u00e1 configurar [integraci\u00f3n de webhook de Dialogflow] ( {dialogflow_url} ). \n\n Complete la siguiente informaci\u00f3n: \n\n - URL: ` {webhook_url} ` \n - M\u00e9todo: POST \n - Tipo de contenido: aplicaci\u00f3n / json \n\n Vea [la documentaci\u00f3n] ( {docs_url} ) para m\u00e1s detalles." + }, + "step": { + "user": { + "description": "\u00bfEst\u00e1 seguro de que desea configurar Dialogflow?", + "title": "Configurar el Webhook de Dialogflow" + } + }, + "title": "Dialogflow" + } +} \ No newline at end of file diff --git a/homeassistant/components/dialogflow/.translations/it.json b/homeassistant/components/dialogflow/.translations/it.json new file mode 100644 index 0000000000000..cc1a7ac851074 --- /dev/null +++ b/homeassistant/components/dialogflow/.translations/it.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "La tua istanza di Home Assistant deve essere accessibile da Internet per ricevere messaggi da Dialogflow.", + "one_instance_allowed": "\u00c8 necessaria una sola istanza." + }, + "create_entry": { + "default": "Per inviare eventi a Home Assistant, dovrai configurare [l'integrazione webhook di Dialogflow]({dialogflow_url})\n\n Compila le seguenti informazioni: \n\n - URL: ` {webhook_url} ` \n - Method: POST \n - Content Type: application/json \n\n Vedi [la documentazione]({docs_url}) for ulteriori dettagli." + }, + "step": { + "user": { + "description": "Sei sicuro di voler configurare Dialogflow?", + "title": "Configura il webhook di Dialogflow" + } + }, + "title": "Dialogflow" + } +} \ No newline at end of file diff --git a/homeassistant/components/dialogflow/.translations/ru.json b/homeassistant/components/dialogflow/.translations/ru.json index 8625780e65c05..899f776c095fd 100644 --- a/homeassistant/components/dialogflow/.translations/ru.json +++ b/homeassistant/components/dialogflow/.translations/ru.json @@ -5,7 +5,7 @@ "one_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430." }, "create_entry": { - "default": "\u0414\u043b\u044f \u043e\u0442\u043f\u0440\u0430\u0432\u043a\u0438 \u0441\u043e\u0431\u044b\u0442\u0438\u0439 \u0432 Home Assistant \u0432\u044b \u0434\u043e\u043b\u0436\u043d\u044b \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c [webhooks \u0434\u043b\u044f Dialogflow]({dialogflow_url}).\n\n\u0414\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0443\u044e \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e:\n\n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/json\n\n\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0435\u0439]({docs_url}) \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0431\u043e\u043b\u0435\u0435 \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u043e\u0439 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438." + "default": "\u0414\u043b\u044f \u043e\u0442\u043f\u0440\u0430\u0432\u043a\u0438 \u0441\u043e\u0431\u044b\u0442\u0438\u0439 \u0432 Home Assistant \u0412\u044b \u0434\u043e\u043b\u0436\u043d\u044b \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c [webhooks \u0434\u043b\u044f Dialogflow]({dialogflow_url}).\n\n\u0414\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0443\u044e \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e:\n\n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/json\n\n\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0435\u0439]({docs_url}) \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0431\u043e\u043b\u0435\u0435 \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u043e\u0439 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438." }, "step": { "user": { diff --git a/homeassistant/components/dialogflow/.translations/sv.json b/homeassistant/components/dialogflow/.translations/sv.json new file mode 100644 index 0000000000000..07fe5e112172c --- /dev/null +++ b/homeassistant/components/dialogflow/.translations/sv.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "Din Home Assistant instans m\u00e5ste vara tillg\u00e4nglig fr\u00e5n internet f\u00f6r att ta emot Dialogflow meddelanden.", + "one_instance_allowed": "Endast en enda instans \u00e4r n\u00f6dv\u00e4ndig." + }, + "create_entry": { + "default": "F\u00f6r att skicka h\u00e4ndelser till Home Assistant m\u00e5ste du konfigurera [webhook funktionen i Dialogflow]({dialogflow_url}).\n\n Fyll i f\u00f6ljande information:\n \n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/json\n\nSe [dokumentation]({docs_url}) om hur du konfigurerar detta f\u00f6r mer information." + }, + "step": { + "user": { + "description": "\u00c4r du s\u00e4ker p\u00e5 att du vill konfigurera Dialogflow?", + "title": "Konfigurera Dialogflow Webhook" + } + }, + "title": "Dialogflow" + } +} \ No newline at end of file diff --git a/homeassistant/components/digital_ocean/__init__.py b/homeassistant/components/digital_ocean/__init__.py index d061dad67261c..7975a6eea0d69 100644 --- a/homeassistant/components/digital_ocean/__init__.py +++ b/homeassistant/components/digital_ocean/__init__.py @@ -22,7 +22,8 @@ ATTR_REGION = 'region' ATTR_VCPUS = 'vcpus' -CONF_ATTRIBUTION = 'Data provided by Digital Ocean' +ATTRIBUTION = 'Data provided by Digital Ocean' + CONF_DROPLETS = 'droplets' DATA_DIGITAL_OCEAN = 'data_do' diff --git a/homeassistant/components/digital_ocean/binary_sensor.py b/homeassistant/components/digital_ocean/binary_sensor.py index 255f43b67bab3..88df56cc629d0 100644 --- a/homeassistant/components/digital_ocean/binary_sensor.py +++ b/homeassistant/components/digital_ocean/binary_sensor.py @@ -9,7 +9,7 @@ from homeassistant.components.digital_ocean import ( CONF_DROPLETS, ATTR_CREATED_AT, ATTR_DROPLET_ID, ATTR_DROPLET_NAME, ATTR_FEATURES, ATTR_IPV4_ADDRESS, ATTR_IPV6_ADDRESS, ATTR_MEMORY, - ATTR_REGION, ATTR_VCPUS, CONF_ATTRIBUTION, DATA_DIGITAL_OCEAN) + ATTR_REGION, ATTR_VCPUS, ATTRIBUTION, DATA_DIGITAL_OCEAN) from homeassistant.const import ATTR_ATTRIBUTION _LOGGER = logging.getLogger(__name__) @@ -71,7 +71,7 @@ def device_class(self): def device_state_attributes(self): """Return the state attributes of the Digital Ocean droplet.""" return { - ATTR_ATTRIBUTION: CONF_ATTRIBUTION, + ATTR_ATTRIBUTION: ATTRIBUTION, ATTR_CREATED_AT: self.data.created_at, ATTR_DROPLET_ID: self.data.id, ATTR_DROPLET_NAME: self.data.name, diff --git a/homeassistant/components/digital_ocean/switch.py b/homeassistant/components/digital_ocean/switch.py index a10c961b8e43a..9b5ddda340806 100644 --- a/homeassistant/components/digital_ocean/switch.py +++ b/homeassistant/components/digital_ocean/switch.py @@ -8,7 +8,7 @@ from homeassistant.components.digital_ocean import ( CONF_DROPLETS, ATTR_CREATED_AT, ATTR_DROPLET_ID, ATTR_DROPLET_NAME, ATTR_FEATURES, ATTR_IPV4_ADDRESS, ATTR_IPV6_ADDRESS, ATTR_MEMORY, - ATTR_REGION, ATTR_VCPUS, CONF_ATTRIBUTION, DATA_DIGITAL_OCEAN) + ATTR_REGION, ATTR_VCPUS, ATTRIBUTION, DATA_DIGITAL_OCEAN) from homeassistant.const import ATTR_ATTRIBUTION _LOGGER = logging.getLogger(__name__) @@ -65,7 +65,7 @@ def is_on(self): def device_state_attributes(self): """Return the state attributes of the Digital Ocean droplet.""" return { - ATTR_ATTRIBUTION: CONF_ATTRIBUTION, + ATTR_ATTRIBUTION: ATTRIBUTION, ATTR_CREATED_AT: self.data.created_at, ATTR_DROPLET_ID: self.data.id, ATTR_DROPLET_NAME: self.data.name, diff --git a/homeassistant/components/ebusd/.translations/es-419.json b/homeassistant/components/ebusd/.translations/es-419.json new file mode 100644 index 0000000000000..7a6291e3f17e2 --- /dev/null +++ b/homeassistant/components/ebusd/.translations/es-419.json @@ -0,0 +1,6 @@ +{ + "state": { + "day": "D\u00eda", + "night": "Noche" + } +} \ No newline at end of file diff --git a/homeassistant/components/ebusd/.translations/es.json b/homeassistant/components/ebusd/.translations/es.json new file mode 100644 index 0000000000000..7a6291e3f17e2 --- /dev/null +++ b/homeassistant/components/ebusd/.translations/es.json @@ -0,0 +1,6 @@ +{ + "state": { + "day": "D\u00eda", + "night": "Noche" + } +} \ No newline at end of file diff --git a/homeassistant/components/ebusd/.translations/he.json b/homeassistant/components/ebusd/.translations/he.json new file mode 100644 index 0000000000000..0232fc3044d37 --- /dev/null +++ b/homeassistant/components/ebusd/.translations/he.json @@ -0,0 +1,6 @@ +{ + "state": { + "day": "\u05d9\u05d5\u05dd", + "night": "\u05dc\u05d9\u05dc\u05d4" + } +} \ No newline at end of file diff --git a/homeassistant/components/ebusd/.translations/hu.json b/homeassistant/components/ebusd/.translations/hu.json new file mode 100644 index 0000000000000..a5ab8f0d194e1 --- /dev/null +++ b/homeassistant/components/ebusd/.translations/hu.json @@ -0,0 +1,6 @@ +{ + "state": { + "day": "Nappal", + "night": "\u00c9jszaka" + } +} \ No newline at end of file diff --git a/homeassistant/components/ebusd/.translations/no.json b/homeassistant/components/ebusd/.translations/no.json new file mode 100644 index 0000000000000..92f4355066dd9 --- /dev/null +++ b/homeassistant/components/ebusd/.translations/no.json @@ -0,0 +1,6 @@ +{ + "state": { + "day": "Dag", + "night": "Natt" + } +} \ No newline at end of file diff --git a/homeassistant/components/ebusd/.translations/pt.json b/homeassistant/components/ebusd/.translations/pt.json new file mode 100644 index 0000000000000..9925fdfab9cc3 --- /dev/null +++ b/homeassistant/components/ebusd/.translations/pt.json @@ -0,0 +1,6 @@ +{ + "state": { + "day": "Dia", + "night": "Noite" + } +} \ No newline at end of file diff --git a/homeassistant/components/ebusd/.translations/sl.json b/homeassistant/components/ebusd/.translations/sl.json new file mode 100644 index 0000000000000..de2ca81f8a8e3 --- /dev/null +++ b/homeassistant/components/ebusd/.translations/sl.json @@ -0,0 +1,6 @@ +{ + "state": { + "day": "Dan", + "night": "No\u010d" + } +} \ No newline at end of file diff --git a/homeassistant/components/ebusd/.translations/sv.json b/homeassistant/components/ebusd/.translations/sv.json new file mode 100644 index 0000000000000..92f4355066dd9 --- /dev/null +++ b/homeassistant/components/ebusd/.translations/sv.json @@ -0,0 +1,6 @@ +{ + "state": { + "day": "Dag", + "night": "Natt" + } +} \ No newline at end of file diff --git a/homeassistant/components/ebusd/.translations/uk.json b/homeassistant/components/ebusd/.translations/uk.json new file mode 100644 index 0000000000000..2e7a22e49a379 --- /dev/null +++ b/homeassistant/components/ebusd/.translations/uk.json @@ -0,0 +1,6 @@ +{ + "state": { + "day": "\u0414\u0435\u043d\u044c", + "night": "\u041d\u0456\u0447" + } +} \ No newline at end of file diff --git a/homeassistant/components/ecobee/climate.py b/homeassistant/components/ecobee/climate.py index aa6440894e1e7..bfc67e7cfafc2 100644 --- a/homeassistant/components/ecobee/climate.py +++ b/homeassistant/components/ecobee/climate.py @@ -4,15 +4,16 @@ import voluptuous as vol from homeassistant.components import ecobee -from homeassistant.components.climate import ( - DOMAIN, STATE_COOL, STATE_HEAT, STATE_AUTO, STATE_IDLE, ClimateDevice, +from homeassistant.components.climate import ClimateDevice +from homeassistant.components.climate.const import ( + DOMAIN, STATE_COOL, STATE_HEAT, STATE_AUTO, STATE_IDLE, ATTR_TARGET_TEMP_LOW, ATTR_TARGET_TEMP_HIGH, SUPPORT_TARGET_TEMPERATURE, SUPPORT_AWAY_MODE, SUPPORT_HOLD_MODE, SUPPORT_OPERATION_MODE, SUPPORT_TARGET_HUMIDITY_LOW, SUPPORT_TARGET_HUMIDITY_HIGH, SUPPORT_AUX_HEAT, SUPPORT_TARGET_TEMPERATURE_HIGH, SUPPORT_FAN_MODE, - SUPPORT_TARGET_TEMPERATURE_LOW, STATE_OFF) + SUPPORT_TARGET_TEMPERATURE_LOW) from homeassistant.const import ( - ATTR_ENTITY_ID, STATE_ON, ATTR_TEMPERATURE, TEMP_FAHRENHEIT) + ATTR_ENTITY_ID, STATE_ON, STATE_OFF, ATTR_TEMPERATURE, TEMP_FAHRENHEIT) import homeassistant.helpers.config_validation as cv _CONFIGURING = {} diff --git a/homeassistant/components/elkm1/climate.py b/homeassistant/components/elkm1/climate.py index 467d542ee6d49..72f93b5419c50 100644 --- a/homeassistant/components/elkm1/climate.py +++ b/homeassistant/components/elkm1/climate.py @@ -1,12 +1,14 @@ """Support for control of Elk-M1 connected thermostats.""" -from homeassistant.components.climate import ( - ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, PRECISION_WHOLE, STATE_AUTO, +from homeassistant.components.climate import ClimateDevice +from homeassistant.components.climate.const import ( + ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, STATE_AUTO, STATE_COOL, STATE_FAN_ONLY, STATE_HEAT, STATE_IDLE, SUPPORT_AUX_HEAT, SUPPORT_FAN_MODE, SUPPORT_OPERATION_MODE, SUPPORT_TARGET_TEMPERATURE_HIGH, - SUPPORT_TARGET_TEMPERATURE_LOW, ClimateDevice) + SUPPORT_TARGET_TEMPERATURE_LOW) from homeassistant.components.elkm1 import ( DOMAIN as ELK_DOMAIN, ElkEntity, create_elk_entities) -from homeassistant.const import STATE_ON +from homeassistant.const import ( + STATE_ON, PRECISION_WHOLE) DEPENDENCIES = [ELK_DOMAIN] diff --git a/homeassistant/components/emulated_roku/.translations/de.json b/homeassistant/components/emulated_roku/.translations/de.json new file mode 100644 index 0000000000000..f9c8a21240a50 --- /dev/null +++ b/homeassistant/components/emulated_roku/.translations/de.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "name_exists": "Name existiert bereits" + }, + "step": { + "user": { + "data": { + "advertise_ip": "IP Adresse annoncieren", + "advertise_port": "Port annoncieren", + "host_ip": "Host-IP", + "listen_port": "Listen-Port", + "name": "Name", + "upnp_bind_multicast": "Multicast binden (True/False)" + }, + "title": "Serverkonfiguration definieren" + } + }, + "title": "EmulatedRoku" + } +} \ No newline at end of file diff --git a/homeassistant/components/emulated_roku/.translations/es-419.json b/homeassistant/components/emulated_roku/.translations/es-419.json new file mode 100644 index 0000000000000..51c18c764db4c --- /dev/null +++ b/homeassistant/components/emulated_roku/.translations/es-419.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "name_exists": "El nombre ya existe" + }, + "step": { + "user": { + "data": { + "host_ip": "IP del host", + "name": "Nombre" + }, + "title": "Definir la configuraci\u00f3n del servidor." + } + }, + "title": "EmulatedRoku" + } +} \ No newline at end of file diff --git a/homeassistant/components/emulated_roku/.translations/hu.json b/homeassistant/components/emulated_roku/.translations/hu.json new file mode 100644 index 0000000000000..c38e6890d8a3c --- /dev/null +++ b/homeassistant/components/emulated_roku/.translations/hu.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "name_exists": "A n\u00e9v m\u00e1r l\u00e9tezik" + }, + "step": { + "user": { + "data": { + "host_ip": "H\u00e1zigazda IP", + "listen_port": "Port figyel\u00e9se", + "name": "N\u00e9v" + }, + "title": "A kiszolg\u00e1l\u00f3 szerver konfigur\u00e1l\u00e1sa" + } + }, + "title": "EmulatedRoku" + } +} \ No newline at end of file diff --git a/homeassistant/components/emulated_roku/.translations/it.json b/homeassistant/components/emulated_roku/.translations/it.json new file mode 100644 index 0000000000000..cba89add79948 --- /dev/null +++ b/homeassistant/components/emulated_roku/.translations/it.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "name_exists": "Il nome \u00e8 gi\u00e0 esistente" + }, + "step": { + "user": { + "data": { + "host_ip": "Indirizzo IP dell'host", + "name": "Nome" + }, + "title": "Definisci la configurazione del server" + } + }, + "title": "EmulatedRoku" + } +} \ No newline at end of file diff --git a/homeassistant/components/emulated_roku/.translations/pt.json b/homeassistant/components/emulated_roku/.translations/pt.json index 286cd58dd8966..138e077d4a46d 100644 --- a/homeassistant/components/emulated_roku/.translations/pt.json +++ b/homeassistant/components/emulated_roku/.translations/pt.json @@ -1,11 +1,21 @@ { "config": { + "abort": { + "name_exists": "Nome j\u00e1 existe" + }, "step": { "user": { "data": { - "name": "Nome" - } + "advertise_ip": "Anuncie o IP", + "advertise_port": "Anuncie porto", + "host_ip": "IP do host", + "listen_port": "Porta \u00e0 escuta", + "name": "Nome", + "upnp_bind_multicast": "Liga\u00e7\u00e3o multicast (Verdadeiro/Falso)" + }, + "title": "Definir configura\u00e7\u00e3o do servidor" } - } + }, + "title": "EmulatedRoku" } } \ No newline at end of file diff --git a/homeassistant/components/emulated_roku/.translations/sv.json b/homeassistant/components/emulated_roku/.translations/sv.json new file mode 100644 index 0000000000000..4ae7a356c4c0b --- /dev/null +++ b/homeassistant/components/emulated_roku/.translations/sv.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "name_exists": "Namnet finns redan" + }, + "step": { + "user": { + "data": { + "advertise_ip": "Annonsera med IP", + "advertise_port": "Annonsera p\u00e5 port", + "host_ip": "IP p\u00e5 v\u00e4rddatorn", + "listen_port": "Lyssna p\u00e5 port", + "name": "Namn", + "upnp_bind_multicast": "Bind multicast (True/False)" + }, + "title": "Definiera serverkonfiguration" + } + }, + "title": "EmulatedRoku" + } +} \ No newline at end of file diff --git a/homeassistant/components/esphome/.translations/ca.json b/homeassistant/components/esphome/.translations/ca.json index 9fe0bb5e9d017..74d2c1d4b9a4c 100644 --- a/homeassistant/components/esphome/.translations/ca.json +++ b/homeassistant/components/esphome/.translations/ca.json @@ -16,6 +16,10 @@ "description": "Introdueix la contrasenya que has posat en la teva configuraci\u00f3.", "title": "Introdueix la contrasenya" }, + "discovery_confirm": { + "description": "Vols afegir el node `{name}` d'ESPHome a Home Assistant?", + "title": "Node d'ESPHome descobert" + }, "user": { "data": { "host": "Amfitri\u00f3", diff --git a/homeassistant/components/esphome/.translations/en.json b/homeassistant/components/esphome/.translations/en.json index 53331ebc0a970..3a73e54c34558 100644 --- a/homeassistant/components/esphome/.translations/en.json +++ b/homeassistant/components/esphome/.translations/en.json @@ -13,9 +13,13 @@ "data": { "password": "Password" }, - "description": "Please enter the password you set in your configuration.", + "description": "Please enter the password you set in your configuration for {name}.", "title": "Enter Password" }, + "discovery_confirm": { + "description": "Do you want to add the ESPHome node `{name}` to Home Assistant?", + "title": "Discovered ESPHome node" + }, "user": { "data": { "host": "Host", diff --git a/homeassistant/components/esphome/.translations/es-419.json b/homeassistant/components/esphome/.translations/es-419.json new file mode 100644 index 0000000000000..84000783435f1 --- /dev/null +++ b/homeassistant/components/esphome/.translations/es-419.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "already_configured": "ESP ya est\u00e1 configurado" + }, + "error": { + "connection_error": "No se puede conectar a ESP. Aseg\u00farese de que su archivo YAML contenga una l\u00ednea 'api:'.", + "invalid_password": "\u00a1Contrase\u00f1a invalida!", + "resolve_error": "No se puede resolver la direcci\u00f3n de la ESP. Si este error persiste, configure una direcci\u00f3n IP est\u00e1tica: https://esphomelib.com/esphomeyaml/components/wifi.html#manual-ips" + }, + "step": { + "authenticate": { + "data": { + "password": "Contrase\u00f1a" + }, + "description": "Por favor ingrese la contrase\u00f1a que estableci\u00f3 en su configuraci\u00f3n para {name} .", + "title": "Escriba la contrase\u00f1a" + }, + "user": { + "data": { + "host": "Host", + "port": "Puerto" + }, + "description": "Por favor Ingrese la configuraci\u00f3n de conexi\u00f3n de su nodo [ESPHome] (https://esphomelib.com/).", + "title": "ESPHome" + } + }, + "title": "ESPHome" + } +} \ No newline at end of file diff --git a/homeassistant/components/esphome/.translations/es.json b/homeassistant/components/esphome/.translations/es.json index 8010b330b88fa..c4b18899eafbf 100644 --- a/homeassistant/components/esphome/.translations/es.json +++ b/homeassistant/components/esphome/.translations/es.json @@ -18,8 +18,10 @@ "data": { "host": "Host", "port": "Puerto" - } + }, + "title": "ESPHome" } - } + }, + "title": "ESPHome" } } \ No newline at end of file diff --git a/homeassistant/components/esphome/.translations/hu.json b/homeassistant/components/esphome/.translations/hu.json index 7fe5da59de692..1e72bd8030cd5 100644 --- a/homeassistant/components/esphome/.translations/hu.json +++ b/homeassistant/components/esphome/.translations/hu.json @@ -1,6 +1,10 @@ { "config": { + "abort": { + "already_configured": "Az ESP-t m\u00e1r konfigur\u00e1ltad." + }, "error": { + "connection_error": "Nem tud csatlakozni az ESP-hez. K\u00e9rlek gy\u0151z\u0151dj meg r\u00f3la, hogy a YAML f\u00e1jl tartalmaz egy \"api:\" sort.", "invalid_password": "\u00c9rv\u00e9nytelen jelsz\u00f3!" }, "step": { @@ -8,6 +12,7 @@ "data": { "password": "Jelsz\u00f3" }, + "description": "K\u00e9rj\u00fck, add meg a konfigur\u00e1ci\u00f3ban be\u00e1ll\u00edtott jelsz\u00f3t.", "title": "Adja meg a jelsz\u00f3t" }, "user": { diff --git a/homeassistant/components/esphome/.translations/it.json b/homeassistant/components/esphome/.translations/it.json new file mode 100644 index 0000000000000..d3c51f0497f2d --- /dev/null +++ b/homeassistant/components/esphome/.translations/it.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "already_configured": "ESP \u00e8 gi\u00e0 configurato" + }, + "error": { + "connection_error": "Impossibile connettersi ad ESP. Assicurati che il tuo file YAML contenga una riga \"api:\".", + "invalid_password": "Password non valida!", + "resolve_error": "Impossibile risolvere l'indirizzo dell'ESP. Se questo errore persiste, impostare un indirizzo IP statico: https://esphomelib.com/esphomeyaml/components/wifi.html#manual-ips" + }, + "step": { + "authenticate": { + "data": { + "password": "Password" + }, + "description": "Inserisci la password che hai impostato nella tua configurazione.", + "title": "Inserisci la password" + }, + "user": { + "data": { + "host": "Host", + "port": "Porta" + }, + "description": "Inserisci le impostazioni di connessione del tuo nodo [ESPHome] (https://esphomelib.com/).", + "title": "ESPHome" + } + }, + "title": "ESPHome" + } +} \ No newline at end of file diff --git a/homeassistant/components/esphome/.translations/ko.json b/homeassistant/components/esphome/.translations/ko.json index 24f84851254cf..f58d43f9df9ae 100644 --- a/homeassistant/components/esphome/.translations/ko.json +++ b/homeassistant/components/esphome/.translations/ko.json @@ -6,16 +6,20 @@ "error": { "connection_error": "ESP \uc5d0 \uc5f0\uacb0\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4. YAML \ud30c\uc77c\uc5d0 'api:' \ub97c \uad6c\uc131\ud588\ub294\uc9c0 \ud655\uc778\ud574\uc8fc\uc138\uc694.", "invalid_password": "\uc798\ubabb\ub41c \ube44\ubc00\ubc88\ud638", - "resolve_error": "ESP \uc758 \uc8fc\uc18c\ub97c \ud655\uc778\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4. \uc774 \uc624\ub958\uac00 \uacc4\uc18d \ubc1c\uc0dd\ud558\uba74 \uace0\uc815 IP \uc8fc\uc18c (https://esphomelib.com/esphomeyaml/components/wifi.html#manual-ips)\ub97c \uc124\uc815\ud574\uc8fc\uc138\uc694" + "resolve_error": "ESP \uc758 \uc8fc\uc18c\ub97c \ud655\uc778\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4. \uc774 \uc624\ub958\uac00 \uacc4\uc18d \ubc1c\uc0dd\ud558\uba74 \uace0\uc815 IP \uc8fc\uc18c\ub97c \uc124\uc815\ud574\uc8fc\uc138\uc694: https://esphomelib.com/esphomeyaml/components/wifi.html#manual-ips" }, "step": { "authenticate": { "data": { "password": "\ube44\ubc00\ubc88\ud638" }, - "description": "ESP \uc5d0\uc11c \uc124\uc815\ud55c \ube44\ubc00\ubc88\ud638\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694.", + "description": "{name} \uc758 \uad6c\uc131\uc5d0 \uc124\uc815\ud55c \ube44\ubc00\ubc88\ud638\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694.", "title": "\ube44\ubc00\ubc88\ud638 \uc785\ub825" }, + "discovery_confirm": { + "description": "Home Assistant \uc5d0 ESPHome node `{name}` \uc744(\ub97c) \ucd94\uac00 \ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", + "title": "\ubc1c\uacac \ub41c ESPHome node" + }, "user": { "data": { "host": "\ud638\uc2a4\ud2b8", diff --git a/homeassistant/components/esphome/.translations/lb.json b/homeassistant/components/esphome/.translations/lb.json index 13dddd0beca22..a240debfaf5af 100644 --- a/homeassistant/components/esphome/.translations/lb.json +++ b/homeassistant/components/esphome/.translations/lb.json @@ -16,6 +16,10 @@ "description": "Gitt d'Passwuert vun \u00e4rer Konfiguratioun an.", "title": "Passwuert aginn" }, + "discovery_confirm": { + "description": "W\u00ebllt dir den ESPHome Provider `{name}` am 'Home Assistant dob\u00e4isetzen?", + "title": "Entdeckten ESPHome Provider" + }, "user": { "data": { "host": "Apparat", diff --git a/homeassistant/components/esphome/.translations/ru.json b/homeassistant/components/esphome/.translations/ru.json index fc74825e188df..2b631ea219c4b 100644 --- a/homeassistant/components/esphome/.translations/ru.json +++ b/homeassistant/components/esphome/.translations/ru.json @@ -4,7 +4,7 @@ "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430." }, "error": { - "connection_error": "\u041d\u0435 \u0443\u0434\u0430\u0435\u0442\u0441\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a ESP. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u0443\u0431\u0435\u0434\u0438\u0442\u0435\u0441\u044c, \u0447\u0442\u043e \u0432\u0430\u0448 YAML-\u0444\u0430\u0439\u043b \u0441\u043e\u0434\u0435\u0440\u0436\u0438\u0442 \u0441\u0442\u0440\u043e\u043a\u0443 'api:'.", + "connection_error": "\u041d\u0435 \u0443\u0434\u0430\u0435\u0442\u0441\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a ESP. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u0443\u0431\u0435\u0434\u0438\u0442\u0435\u0441\u044c, \u0447\u0442\u043e \u0412\u0430\u0448 YAML-\u0444\u0430\u0439\u043b \u0441\u043e\u0434\u0435\u0440\u0436\u0438\u0442 \u0441\u0442\u0440\u043e\u043a\u0443 'api:'.", "invalid_password": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043f\u0430\u0440\u043e\u043b\u044c!", "resolve_error": "\u041d\u0435 \u0443\u0434\u0430\u0435\u0442\u0441\u044f \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0438\u0442\u044c \u0430\u0434\u0440\u0435\u0441 ESP. \u0415\u0441\u043b\u0438 \u044d\u0442\u0430 \u043e\u0448\u0438\u0431\u043a\u0430 \u043f\u043e\u0432\u0442\u043e\u0440\u044f\u0435\u0442\u0441\u044f, \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 \u0441\u0442\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438\u0439 IP-\u0430\u0434\u0440\u0435\u0441: https://esphomelib.com/esphomeyaml/components/wifi.html#manual-ips" }, @@ -13,9 +13,13 @@ "data": { "password": "\u041f\u0430\u0440\u043e\u043b\u044c" }, - "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u043f\u0430\u0440\u043e\u043b\u044c, \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d\u043d\u044b\u0439 \u0432 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438.", + "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u043f\u0430\u0440\u043e\u043b\u044c, \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d\u043d\u044b\u0439 \u0432 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 {name}.", "title": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u043f\u0430\u0440\u043e\u043b\u044c" }, + "discovery_confirm": { + "description": "\u0412\u044b \u0443\u0432\u0435\u0440\u0435\u043d\u044b, \u0447\u0442\u043e \u0445\u043e\u0442\u0438\u0442\u0435 \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c ESPHome `{name}`?", + "title": "ESPHome" + }, "user": { "data": { "host": "\u0425\u043e\u0441\u0442", diff --git a/homeassistant/components/esphome/.translations/sv.json b/homeassistant/components/esphome/.translations/sv.json new file mode 100644 index 0000000000000..6eadcb4e18edf --- /dev/null +++ b/homeassistant/components/esphome/.translations/sv.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "already_configured": "ESP \u00e4r redan konfigurerad" + }, + "error": { + "connection_error": "Kan inte ansluta till ESP. Se till att din YAML-fil inneh\u00e5ller en 'api:' line.", + "invalid_password": "Ogiltigt l\u00f6senord!", + "resolve_error": "Det g\u00e5r inte att hitta IP-adressen f\u00f6r ESP med DNS-namnet. Om det h\u00e4r felet kvarst\u00e5r anger du en statisk IP-adress: https://esphomelib.com/esphomeyaml/components/wifi.html#manual-ips" + }, + "step": { + "authenticate": { + "data": { + "password": "L\u00f6senord" + }, + "description": "Ange det l\u00f6senord du angav i din konfiguration.", + "title": "Ange l\u00f6senord" + }, + "user": { + "data": { + "host": "V\u00e4rddatorn", + "port": "Port" + }, + "description": "Ange anslutningsinst\u00e4llningarna f\u00f6r noden [ESPHome](https://esphomelib.com/).", + "title": "ESPHome" + } + }, + "title": "ESPHome" + } +} \ No newline at end of file diff --git a/homeassistant/components/esphome/.translations/zh-Hant.json b/homeassistant/components/esphome/.translations/zh-Hant.json index 055100cf07738..65817470860a1 100644 --- a/homeassistant/components/esphome/.translations/zh-Hant.json +++ b/homeassistant/components/esphome/.translations/zh-Hant.json @@ -16,6 +16,10 @@ "description": "\u8acb\u8f38\u5165\u8a2d\u5b9a\u5167\u6240\u8a2d\u5b9a\u4e4b\u5bc6\u78bc\u3002", "title": "\u8f38\u5165\u5bc6\u78bc" }, + "discovery_confirm": { + "description": "\u662f\u5426\u8981\u5c07 ESPHome node\u300c{name}\u300d\u65b0\u589e\u81f3 Home Assistant\uff1f", + "title": "\u81ea\u52d5\u63a2\u7d22\u5230\u7684 ESPHome node" + }, "user": { "data": { "host": "\u4e3b\u6a5f\u7aef", diff --git a/homeassistant/components/esphome/__init__.py b/homeassistant/components/esphome/__init__.py index 004162341b110..51f565a0980f8 100644 --- a/homeassistant/components/esphome/__init__.py +++ b/homeassistant/components/esphome/__init__.py @@ -1,7 +1,7 @@ """Support for esphome devices.""" import asyncio import logging -from typing import Any, Dict, List, Optional, TYPE_CHECKING, Callable +from typing import Any, Dict, List, Optional, TYPE_CHECKING, Callable, Tuple import attr import voluptuous as vol @@ -13,6 +13,7 @@ from homeassistant.core import callback, Event, State import homeassistant.helpers.device_registry as dr from homeassistant.exceptions import TemplateError +import homeassistant.helpers.config_validation as cv from homeassistant.helpers import template from homeassistant.helpers.dispatcher import async_dispatcher_connect, \ async_dispatcher_send @@ -28,9 +29,10 @@ if TYPE_CHECKING: from aioesphomeapi import APIClient, EntityInfo, EntityState, DeviceInfo, \ - ServiceCall + ServiceCall, UserService -REQUIREMENTS = ['aioesphomeapi==1.5.0'] +DOMAIN = 'esphome' +REQUIREMENTS = ['aioesphomeapi==1.6.0'] _LOGGER = logging.getLogger(__name__) @@ -69,6 +71,7 @@ class RuntimeEntryData: reconnect_task = attr.ib(type=Optional[asyncio.Task], default=None) state = attr.ib(type=Dict[str, Dict[str, Any]], factory=dict) info = attr.ib(type=Dict[str, Dict[str, Any]], factory=dict) + services = attr.ib(type=Dict[int, 'UserService'], factory=dict) available = attr.ib(type=bool, default=False) device_info = attr.ib(type='DeviceInfo', default=None) cleanup_callbacks = attr.ib(type=List[Callable[[], None]], factory=list) @@ -105,14 +108,16 @@ def async_update_device_state(self, hass: HomeAssistantType) -> None: signal = DISPATCHER_ON_DEVICE_UPDATE.format(entry_id=self.entry_id) async_dispatcher_send(hass, signal) - async def async_load_from_store(self) -> List['EntityInfo']: + async def async_load_from_store(self) -> Tuple[List['EntityInfo'], + List['UserService']]: """Load the retained data from store and return de-serialized data.""" # pylint: disable= redefined-outer-name - from aioesphomeapi import COMPONENT_TYPE_TO_INFO, DeviceInfo + from aioesphomeapi import COMPONENT_TYPE_TO_INFO, DeviceInfo, \ + UserService restored = await self.store.async_load() if restored is None: - return [] + return [], [] self.device_info = _attr_obj_from_dict(DeviceInfo, **restored.pop('device_info')) @@ -123,17 +128,23 @@ async def async_load_from_store(self) -> List['EntityInfo']: for info in restored_infos: cls = COMPONENT_TYPE_TO_INFO[comp_type] infos.append(_attr_obj_from_dict(cls, **info)) - return infos + services = [] + for service in restored.get('services', []): + services.append(UserService.from_dict(service)) + return infos, services async def async_save_to_store(self) -> None: """Generate dynamic data to store and save it to the filesystem.""" store_data = { - 'device_info': attr.asdict(self.device_info) + 'device_info': attr.asdict(self.device_info), + 'services': [] } for comp_type, infos in self.info.items(): store_data[comp_type] = [attr.asdict(info) for info in infos.values()] + for service in self.services.values(): + store_data['services'].append(service.to_dict()) await self.store.async_save(store_data) @@ -233,8 +244,9 @@ async def on_login() -> None: entry_data.device_info) entry_data.async_update_device_state(hass) - entity_infos = await cli.list_entities() + entity_infos, services = await cli.list_entities_services() entry_data.async_update_static_infos(hass, entity_infos) + await _setup_services(hass, entry_data, services) await cli.subscribe_states(async_on_state) await cli.subscribe_service_calls(async_on_service_call) await cli.subscribe_home_assistant_states( @@ -277,8 +289,9 @@ async def complete_setup() -> None: entry, component)) await asyncio.wait(tasks) - infos = await entry_data.async_load_from_store() + infos, services = await entry_data.async_load_from_store() entry_data.async_update_static_infos(hass, infos) + await _setup_services(hass, entry_data, services) # If first connect fails, the next re-connect will be scheduled # outside of _pending_task, in order not to delay HA startup @@ -366,6 +379,60 @@ async def _async_setup_device_registry(hass: HomeAssistantType, ) +async def _register_service(hass: HomeAssistantType, + entry_data: RuntimeEntryData, + service: 'UserService'): + from aioesphomeapi import USER_SERVICE_ARG_BOOL, USER_SERVICE_ARG_INT, \ + USER_SERVICE_ARG_FLOAT, USER_SERVICE_ARG_STRING + service_name = '{}_{}'.format(entry_data.device_info.name, service.name) + schema = {} + for arg in service.args: + schema[vol.Required(arg.name)] = { + USER_SERVICE_ARG_BOOL: cv.boolean, + USER_SERVICE_ARG_INT: vol.Coerce(int), + USER_SERVICE_ARG_FLOAT: vol.Coerce(float), + USER_SERVICE_ARG_STRING: cv.string, + }[arg.type_] + + async def execute_service(call): + await entry_data.client.execute_service(service, call.data) + + hass.services.async_register(DOMAIN, service_name, execute_service, + vol.Schema(schema)) + + +async def _setup_services(hass: HomeAssistantType, + entry_data: RuntimeEntryData, + services: List['UserService']): + old_services = entry_data.services.copy() + to_unregister = [] + to_register = [] + for service in services: + if service.key in old_services: + # Already exists + matching = old_services.pop(service.key) + if matching != service: + # Need to re-register + to_unregister.append(matching) + to_register.append(service) + else: + # New service + to_register.append(service) + + for service in old_services.values(): + to_unregister.append(service) + + entry_data.services = {serv.key: serv for serv in services} + + for service in to_unregister: + service_name = '{}_{}'.format(entry_data.device_info.name, + service.name) + hass.services.async_remove(DOMAIN, service_name) + + for service in to_register: + await _register_service(hass, entry_data, service) + + async def _cleanup_instance(hass: HomeAssistantType, entry: ConfigEntry) -> None: """Cleanup the esphome client if it exists.""" diff --git a/homeassistant/components/esphome/config_flow.py b/homeassistant/components/esphome/config_flow.py index e509455c12e43..f6b8bb9abd7f1 100644 --- a/homeassistant/components/esphome/config_flow.py +++ b/homeassistant/components/esphome/config_flow.py @@ -26,18 +26,7 @@ async def async_step_user(self, user_input: Optional[ConfigType] = None, error: Optional[str] = None): """Handle a flow initialized by the user.""" if user_input is not None: - self._host = user_input['host'] - self._port = user_input['port'] - error, device_info = await self.fetch_device_info() - if error is not None: - return await self.async_step_user(error=error) - self._name = device_info.name - - # Only show authentication step if device uses password - if device_info.uses_password: - return await self.async_step_authenticate() - - return self._async_get_entry() + return await self._async_authenticate_or_add(user_input) fields = OrderedDict() fields[vol.Required('host', default=self._host or vol.UNDEFINED)] = str @@ -53,6 +42,33 @@ async def async_step_user(self, user_input: Optional[ConfigType] = None, errors=errors ) + async def _async_authenticate_or_add(self, user_input, + from_discovery=False): + self._host = user_input['host'] + self._port = user_input['port'] + error, device_info = await self.fetch_device_info() + if error is not None: + return await self.async_step_user(error=error) + self._name = device_info.name + # Only show authentication step if device uses password + if device_info.uses_password: + return await self.async_step_authenticate() + + if from_discovery: + # If from discovery, do not create entry immediately, + # First present user with message + return await self.async_step_discovery_confirm() + return self._async_get_entry() + + async def async_step_discovery_confirm(self, user_input=None): + """Handle user-confirmation of discovered node.""" + if user_input is not None: + return self._async_get_entry() + return self.async_show_form( + step_id='discovery_confirm', + description_placeholders={'name': self._name}, + ) + async def async_step_discovery(self, user_input: ConfigType): """Handle discovery.""" address = user_input['properties'].get( @@ -63,12 +79,10 @@ async def async_step_discovery(self, user_input: ConfigType): reason='already_configured' ) - # Prefer .local addresses (mDNS is available after all, otherwise - # we wouldn't have received the discovery message) - return await self.async_step_user(user_input={ + return await self._async_authenticate_or_add(user_input={ 'host': address, 'port': user_input['port'], - }) + }, from_discovery=True) def _async_get_entry(self): return self.async_create_entry( @@ -99,6 +113,7 @@ async def async_step_authenticate(self, user_input=None, error=None): data_schema=vol.Schema({ vol.Required('password'): str }), + description_placeholders={'name': self._name}, errors=errors ) diff --git a/homeassistant/components/esphome/strings.json b/homeassistant/components/esphome/strings.json index 56eeed8ea4101..8f691d9cb00bf 100644 --- a/homeassistant/components/esphome/strings.json +++ b/homeassistant/components/esphome/strings.json @@ -21,8 +21,12 @@ "data": { "password": "Password" }, - "description": "Please enter the password you set in your configuration.", + "description": "Please enter the password you set in your configuration for {name}.", "title": "Enter Password" + }, + "discovery_confirm": { + "description": "Do you want to add the ESPHome node `{name}` to Home Assistant?", + "title": "Discovered ESPHome node" } }, "title": "ESPHome" diff --git a/homeassistant/components/eufy/__init__.py b/homeassistant/components/eufy/__init__.py index d5a0938bf66f3..b0bd9109363e3 100644 --- a/homeassistant/components/eufy/__init__.py +++ b/homeassistant/components/eufy/__init__.py @@ -9,7 +9,7 @@ from homeassistant.helpers import discovery import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['lakeside==0.11'] +REQUIREMENTS = ['lakeside==0.12'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/evohome/climate.py b/homeassistant/components/evohome/climate.py index ef82a3dc81cf1..955b82e37e3c7 100644 --- a/homeassistant/components/evohome/climate.py +++ b/homeassistant/components/evohome/climate.py @@ -4,13 +4,13 @@ from requests.exceptions import HTTPError -from homeassistant.components.climate import ( - STATE_AUTO, STATE_ECO, STATE_MANUAL, STATE_OFF, +from homeassistant.components.climate import ClimateDevice +from homeassistant.components.climate.const import ( + STATE_AUTO, STATE_ECO, STATE_MANUAL, SUPPORT_AWAY_MODE, SUPPORT_ON_OFF, SUPPORT_OPERATION_MODE, SUPPORT_TARGET_TEMPERATURE, - ClimateDevice ) from homeassistant.components.evohome import ( DATA_EVOHOME, DISPATCHER_EVOHOME, @@ -22,6 +22,7 @@ CONF_SCAN_INTERVAL, HTTP_TOO_MANY_REQUESTS, PRECISION_HALVES, + STATE_OFF, TEMP_CELSIUS ) from homeassistant.core import callback diff --git a/homeassistant/components/fritzbox/climate.py b/homeassistant/components/fritzbox/climate.py index 64d99ebf133fd..e8c20061b4e23 100644 --- a/homeassistant/components/fritzbox/climate.py +++ b/homeassistant/components/fritzbox/climate.py @@ -8,13 +8,14 @@ ATTR_STATE_DEVICE_LOCKED, ATTR_STATE_BATTERY_LOW, ATTR_STATE_HOLIDAY_MODE, ATTR_STATE_LOCKED, ATTR_STATE_SUMMER_MODE, ATTR_STATE_WINDOW_OPEN) -from homeassistant.components.climate import ( - ATTR_OPERATION_MODE, ClimateDevice, STATE_ECO, STATE_HEAT, STATE_MANUAL, - STATE_OFF, STATE_ON, SUPPORT_OPERATION_MODE, +from homeassistant.components.climate import ClimateDevice +from homeassistant.components.climate.const import ( + ATTR_OPERATION_MODE, STATE_ECO, STATE_HEAT, STATE_MANUAL, + SUPPORT_OPERATION_MODE, SUPPORT_TARGET_TEMPERATURE) from homeassistant.const import ( - ATTR_BATTERY_LEVEL, ATTR_TEMPERATURE, PRECISION_HALVES, TEMP_CELSIUS) - + ATTR_BATTERY_LEVEL, ATTR_TEMPERATURE, PRECISION_HALVES, TEMP_CELSIUS, + STATE_OFF, STATE_ON) DEPENDENCIES = ['fritzbox'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index caf6bbccb5c3f..d7c1aabdb49a3 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -21,7 +21,7 @@ from .storage import async_setup_frontend_storage -REQUIREMENTS = ['home-assistant-frontend==20190220.0'] +REQUIREMENTS = ['home-assistant-frontend==20190305.0'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log', diff --git a/homeassistant/components/frontend/storage.py b/homeassistant/components/frontend/storage.py index f01abc79e8e98..17aae14c820c8 100644 --- a/homeassistant/components/frontend/storage.py +++ b/homeassistant/components/frontend/storage.py @@ -11,7 +11,7 @@ async def async_setup_frontend_storage(hass): """Set up frontend storage.""" - hass.data[DATA_STORAGE] = {} + hass.data[DATA_STORAGE] = ({}, {}) hass.components.websocket_api.async_register_command( websocket_set_user_data ) @@ -25,12 +25,16 @@ def with_store(orig_func): @wraps(orig_func) async def with_store_func(hass, connection, msg): """Provide user specific data and store to function.""" - store = hass.helpers.storage.Store( - STORAGE_VERSION_USER_DATA, - STORAGE_KEY_USER_DATA.format(connection.user.id) - ) - data = hass.data[DATA_STORAGE] + stores, data = hass.data[DATA_STORAGE] user_id = connection.user.id + store = stores.get(user_id) + + if store is None: + store = stores[user_id] = hass.helpers.storage.Store( + STORAGE_VERSION_USER_DATA, + STORAGE_KEY_USER_DATA.format(connection.user.id) + ) + if user_id not in data: data[user_id] = await store.async_load() or {} diff --git a/homeassistant/components/geo_location/__init__.py b/homeassistant/components/geo_location/__init__.py index 9095ce617aab1..75c99ecc74c87 100644 --- a/homeassistant/components/geo_location/__init__.py +++ b/homeassistant/components/geo_location/__init__.py @@ -1,9 +1,4 @@ -""" -Geolocation component. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/geo_location/ -""" +"""Support for Geolocation.""" from datetime import timedelta import logging from typing import Optional diff --git a/homeassistant/components/geo_location/demo.py b/homeassistant/components/geo_location/demo.py index 0e7274e7a0a6f..523e125a737b8 100644 --- a/homeassistant/components/geo_location/demo.py +++ b/homeassistant/components/geo_location/demo.py @@ -1,9 +1,4 @@ -""" -Demo platform for the geolocation component. - -For more details about this platform, please refer to the documentation -https://home-assistant.io/components/demo/ -""" +"""Demo platform for the geolocation component.""" from datetime import timedelta import logging from math import cos, pi, radians, sin diff --git a/homeassistant/components/geo_location/geo_json_events.py b/homeassistant/components/geo_location/geo_json_events.py index cbfe605e7223c..e89616126d5f2 100644 --- a/homeassistant/components/geo_location/geo_json_events.py +++ b/homeassistant/components/geo_location/geo_json_events.py @@ -1,9 +1,4 @@ -""" -Generic GeoJSON events platform. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/geo_location/geo_json_events/ -""" +"""Support for generic GeoJSON events.""" from datetime import timedelta import logging from typing import Optional @@ -13,8 +8,8 @@ from homeassistant.components.geo_location import ( PLATFORM_SCHEMA, GeolocationEvent) from homeassistant.const import ( - CONF_RADIUS, CONF_SCAN_INTERVAL, CONF_URL, EVENT_HOMEASSISTANT_START, - CONF_LATITUDE, CONF_LONGITUDE) + CONF_LATITUDE, CONF_LONGITUDE, CONF_RADIUS, CONF_SCAN_INTERVAL, CONF_URL, + EVENT_HOMEASSISTANT_START) from homeassistant.core import callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import ( diff --git a/homeassistant/components/geo_location/nsw_rural_fire_service_feed.py b/homeassistant/components/geo_location/nsw_rural_fire_service_feed.py index e0974ed415d08..38491feb32f40 100644 --- a/homeassistant/components/geo_location/nsw_rural_fire_service_feed.py +++ b/homeassistant/components/geo_location/nsw_rural_fire_service_feed.py @@ -1,9 +1,4 @@ -""" -NSW Rural Fire Service Feed platform. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/geo_location/nsw_rural_fire_service_feed/ -""" +"""Support for NSW Rural Fire Service Feeds.""" from datetime import timedelta import logging from typing import Optional @@ -13,8 +8,8 @@ from homeassistant.components.geo_location import ( PLATFORM_SCHEMA, GeolocationEvent) from homeassistant.const import ( - ATTR_ATTRIBUTION, ATTR_LOCATION, CONF_RADIUS, CONF_SCAN_INTERVAL, - EVENT_HOMEASSISTANT_START, CONF_LATITUDE, CONF_LONGITUDE) + ATTR_ATTRIBUTION, ATTR_LOCATION, CONF_LATITUDE, CONF_LONGITUDE, + CONF_RADIUS, CONF_SCAN_INTERVAL, EVENT_HOMEASSISTANT_START) from homeassistant.core import callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import ( diff --git a/homeassistant/components/geo_location/usgs_earthquakes_feed.py b/homeassistant/components/geo_location/usgs_earthquakes_feed.py index 6a7bbba44648d..1d11b1971ccaa 100644 --- a/homeassistant/components/geo_location/usgs_earthquakes_feed.py +++ b/homeassistant/components/geo_location/usgs_earthquakes_feed.py @@ -1,9 +1,4 @@ -""" -U.S. Geological Survey Earthquake Hazards Program Feed platform. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/geo_location/usgs_earthquakes_feed/ -""" +"""Support for U.S. Geological Survey Earthquake Hazards Program Feeds.""" from datetime import timedelta import logging from typing import Optional @@ -13,8 +8,8 @@ from homeassistant.components.geo_location import ( PLATFORM_SCHEMA, GeolocationEvent) from homeassistant.const import ( - ATTR_ATTRIBUTION, CONF_RADIUS, CONF_SCAN_INTERVAL, - EVENT_HOMEASSISTANT_START, CONF_LATITUDE, CONF_LONGITUDE) + ATTR_ATTRIBUTION, CONF_LATITUDE, CONF_LONGITUDE, CONF_RADIUS, + CONF_SCAN_INTERVAL, EVENT_HOMEASSISTANT_START) from homeassistant.core import callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import ( diff --git a/homeassistant/components/geofency/.translations/de.json b/homeassistant/components/geofency/.translations/de.json new file mode 100644 index 0000000000000..ad4722fa9fc70 --- /dev/null +++ b/homeassistant/components/geofency/.translations/de.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "Deine Home-Assistant-Instanz muss aus dem internet erreichbar sein, um Nachrichten von Geofency zu erhalten.", + "one_instance_allowed": "Es ist nur eine einzige Instanz erforderlich." + }, + "create_entry": { + "default": "Um Ereignisse an den Home Assistant zu senden, musst das Webhook Feature in Geofency konfiguriert werden.\n\n F\u00fcge die folgenden Informationen ein: \n\n - URL: ` {webhook_url} ` \n - Methode: POST \n \n Weitere Informationen finden sich in der [Dokumentation]({docs_url})." + }, + "step": { + "user": { + "description": "M\u00f6chtest du den Geofency Webhook wirklich einrichten?", + "title": "Richten Sie den Geofency Webhook ein" + } + }, + "title": "Geofency Webhook" + } +} \ No newline at end of file diff --git a/homeassistant/components/geofency/.translations/es-419.json b/homeassistant/components/geofency/.translations/es-419.json new file mode 100644 index 0000000000000..637a430a1f8a4 --- /dev/null +++ b/homeassistant/components/geofency/.translations/es-419.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "Su instancia de Home Assistant debe ser accesible desde Internet para recibir mensajes de Geofency.", + "one_instance_allowed": "Solo una instancia es necesaria." + }, + "create_entry": { + "default": "Para enviar eventos a Home Assistant, deber\u00e1 configurar la funci\u00f3n de webhook en Geofency. \n\n Complete la siguiente informaci\u00f3n: \n\n - URL: ` {webhook_url} ` \n - M\u00e9todo: POST \n\n Vea [la documentaci\u00f3n] ( {docs_url} ) para m\u00e1s detalles." + }, + "step": { + "user": { + "description": "\u00bfEst\u00e1s seguro de que quieres montar el Webhook de Geofency?", + "title": "Configurar el Webhook de Geofency" + } + }, + "title": "Geofency Webhook" + } +} \ No newline at end of file diff --git a/homeassistant/components/geofency/.translations/es.json b/homeassistant/components/geofency/.translations/es.json index cd14e21db1060..a81fc927b6b80 100644 --- a/homeassistant/components/geofency/.translations/es.json +++ b/homeassistant/components/geofency/.translations/es.json @@ -3,6 +3,9 @@ "abort": { "not_internet_accessible": "Tu Home Assistant debe ser accesible desde Internet para recibir mensajes de GPSLogger.", "one_instance_allowed": "Solo se necesita una instancia." + }, + "create_entry": { + "default": "Para enviar eventos a Home Assistant, necesitar\u00e1s configurar la funci\u00f3n de webhook en Geofency.\n\nRellene la siguiente informaci\u00f3n:\n\n- URL: ``{webhook_url}``\n- M\u00e9todo: POST\n\nVer[la documentaci\u00f3n]({docs_url}) para m\u00e1s detalles." } } } \ No newline at end of file diff --git a/homeassistant/components/geofency/.translations/hu.json b/homeassistant/components/geofency/.translations/hu.json new file mode 100644 index 0000000000000..85f71d74434cd --- /dev/null +++ b/homeassistant/components/geofency/.translations/hu.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "Az Home Assistantnek el\u00e9rhet\u0151nek kell lennie az internetr\u0151l, hogy megkapja a Geofency \u00fczeneteit.", + "one_instance_allowed": "Csak egy p\u00e9ld\u00e1ny sz\u00fcks\u00e9ges." + }, + "create_entry": { + "default": "Az esem\u00e9ny Home Assistantnek val\u00f3 k\u00fcld\u00e9s\u00e9hez be kell \u00e1ll\u00edtanod a webhook funkci\u00f3t a Geofencyben. \n\n Az al\u00e1bbi inform\u00e1ci\u00f3kat haszn\u00e1ld: \n\n - URL: ` {webhook_url} ` \n - Method: POST \n\n Tov\u00e1bbi r\u00e9szletek a [dokument\u00e1ci\u00f3] ( {docs_url} ) linken tal\u00e1lhat\u00f3k." + }, + "step": { + "user": { + "description": "Biztosan be szeretn\u00e9d \u00e1ll\u00edtani a Geofency Webhookot?", + "title": "A Geofency Webhook be\u00e1ll\u00edt\u00e1sa" + } + }, + "title": "Geofency Webhook" + } +} \ No newline at end of file diff --git a/homeassistant/components/geofency/.translations/it.json b/homeassistant/components/geofency/.translations/it.json new file mode 100644 index 0000000000000..1adad3825a302 --- /dev/null +++ b/homeassistant/components/geofency/.translations/it.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "La tua istanza di Home Assistant deve essere accessibile da Internet per ricevere messaggi da Geofency.", + "one_instance_allowed": "\u00c8 necessaria una sola istanza." + }, + "create_entry": { + "default": "Per inviare eventi a Home Assistant, dovrai configurare la funzionalit\u00e0 webhook in Geofency.\n\n Compila le seguenti informazioni: \n\n - URL: ` {webhook_url} ` \n - Method: POST \n\n Vedi [la documentazione]({docs_url}) for ulteriori dettagli." + }, + "step": { + "user": { + "description": "Sei sicuro di voler configurare il webhook di Geofency?", + "title": "Configura il webhook di Geofency" + } + }, + "title": "Webhook di Geofency" + } +} \ No newline at end of file diff --git a/homeassistant/components/geofency/.translations/pt.json b/homeassistant/components/geofency/.translations/pt.json new file mode 100644 index 0000000000000..bc68c3ec8223c --- /dev/null +++ b/homeassistant/components/geofency/.translations/pt.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "A sua inst\u00e2ncia Home Assistent precisa de ser acess\u00edvel a partir da internet para receber mensagens IFTTT.", + "one_instance_allowed": "Apenas uma inst\u00e2ncia \u00e9 necess\u00e1ria." + }, + "create_entry": { + "default": "Para enviar eventos para o Home Assistant, \u00e9 necess\u00e1rio configurar um webhook no Geofency. \n\n Preencha as seguintes informa\u00e7\u00f5es: \n\n - URL: `{webhook_url}`\n - M\u00e9todo: POST \n\n Veja [the documentation]({docs_url}) para obter mais detalhes." + }, + "step": { + "user": { + "description": "Tem certeza de que deseja configurar o Geofency Webhook?", + "title": "Configurar o Geofency Webhook" + } + }, + "title": "Geofency Webhook" + } +} \ No newline at end of file diff --git a/homeassistant/components/geofency/.translations/ru.json b/homeassistant/components/geofency/.translations/ru.json index 2460e28393ac5..6c699d21ce67e 100644 --- a/homeassistant/components/geofency/.translations/ru.json +++ b/homeassistant/components/geofency/.translations/ru.json @@ -5,7 +5,7 @@ "one_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430." }, "create_entry": { - "default": "\u0414\u043b\u044f \u043e\u0442\u043f\u0440\u0430\u0432\u043a\u0438 \u0441\u043e\u0431\u044b\u0442\u0438\u0439 \u0432 Home Assistant \u0432\u044b \u0434\u043e\u043b\u0436\u043d\u044b \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c webhooks \u0434\u043b\u044f Geofency.\n\n\u0414\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0443\u044e \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e:\n\n- URL: `{webhook_url}`\n- Method: POST\n\n\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0435\u0439]({docs_url}) \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0431\u043e\u043b\u0435\u0435 \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u043e\u0439 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438." + "default": "\u0414\u043b\u044f \u043e\u0442\u043f\u0440\u0430\u0432\u043a\u0438 \u0441\u043e\u0431\u044b\u0442\u0438\u0439 \u0432 Home Assistant \u0412\u044b \u0434\u043e\u043b\u0436\u043d\u044b \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c webhooks \u0434\u043b\u044f Geofency.\n\n\u0414\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0443\u044e \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e:\n\n- URL: `{webhook_url}`\n- Method: POST\n\n\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0435\u0439]({docs_url}) \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0431\u043e\u043b\u0435\u0435 \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u043e\u0439 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438." }, "step": { "user": { diff --git a/homeassistant/components/geofency/.translations/sv.json b/homeassistant/components/geofency/.translations/sv.json new file mode 100644 index 0000000000000..88c9709147fcc --- /dev/null +++ b/homeassistant/components/geofency/.translations/sv.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "Din Home Assistant instans m\u00e5ste vara \u00e5tkomlig ifr\u00e5n internet f\u00f6r att ta emot meddelanden ifr\u00e5n Geofency.", + "one_instance_allowed": "Endast en enda instans \u00e4r n\u00f6dv\u00e4ndig." + }, + "create_entry": { + "default": "F\u00f6r att skicka h\u00e4ndelser till Home Assistant m\u00e5ste du konfigurera webhook funktionen i Geofency.\n\n Fyll i f\u00f6ljande information:\n \n- URL: `{webhook_url}`\n- Method: POST\n\nSe [dokumentation]({docs_url}) om hur du konfigurerar detta f\u00f6r mer information." + }, + "step": { + "user": { + "description": "\u00c4r du s\u00e4ker p\u00e5 att du vill konfigurera Geofency Webhook?", + "title": "Konfigurera Geofency Webhook" + } + }, + "title": "Geofency Webhook" + } +} \ No newline at end of file diff --git a/homeassistant/components/geofency/__init__.py b/homeassistant/components/geofency/__init__.py index f265bd3492a1d..f27798e9e0d74 100644 --- a/homeassistant/components/geofency/__init__.py +++ b/homeassistant/components/geofency/__init__.py @@ -1,19 +1,15 @@ -""" -Support for Geofency. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/geofency/ -""" +"""Support for Geofency.""" import logging -import voluptuous as vol from aiohttp import web +import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.device_tracker import DOMAIN as DEVICE_TRACKER -from homeassistant.const import HTTP_UNPROCESSABLE_ENTITY, STATE_NOT_HOME, \ - ATTR_LATITUDE, ATTR_LONGITUDE, CONF_WEBHOOK_ID, HTTP_OK, ATTR_NAME +from homeassistant.const import ( + ATTR_LATITUDE, ATTR_LONGITUDE, ATTR_NAME, CONF_WEBHOOK_ID, HTTP_OK, + HTTP_UNPROCESSABLE_ENTITY, STATE_NOT_HOME) from homeassistant.helpers import config_entry_flow +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.util import slugify @@ -27,9 +23,7 @@ CONFIG_SCHEMA = vol.Schema({ vol.Optional(DOMAIN): vol.Schema({ vol.Optional(CONF_MOBILE_BEACONS, default=[]): vol.All( - cv.ensure_list, - [cv.string] - ), + cv.ensure_list, [cv.string]), }), }, extra=vol.ALLOW_EXTRA) @@ -62,7 +56,7 @@ def _address(value: str) -> str: vol.Required(ATTR_NAME): vol.All(cv.string, slugify), vol.Optional(ATTR_CURRENT_LATITUDE): cv.latitude, vol.Optional(ATTR_CURRENT_LONGITUDE): cv.longitude, - vol.Optional(ATTR_BEACON_ID): cv.string + vol.Optional(ATTR_BEACON_ID): cv.string, }, extra=vol.ALLOW_EXTRA) @@ -114,18 +108,11 @@ def _set_location(hass, data, location_name): device = _device_name(data) async_dispatcher_send( - hass, - TRACKER_UPDATE, - device, - (data[ATTR_LATITUDE], data[ATTR_LONGITUDE]), - location_name, - data - ) + hass, TRACKER_UPDATE, device, + (data[ATTR_LATITUDE], data[ATTR_LONGITUDE]), location_name, data) return web.Response( - text="Setting location for {}".format(device), - status=HTTP_OK - ) + text="Setting location for {}".format(device), status=HTTP_OK) async def async_setup_entry(hass, entry): diff --git a/homeassistant/components/geofency/device_tracker.py b/homeassistant/components/geofency/device_tracker.py index eea0960ec1157..51201240c1c4e 100644 --- a/homeassistant/components/geofency/device_tracker.py +++ b/homeassistant/components/geofency/device_tracker.py @@ -6,10 +6,10 @@ """ import logging -from homeassistant.components.device_tracker import DOMAIN as \ - DEVICE_TRACKER_DOMAIN -from homeassistant.components.geofency import TRACKER_UPDATE, \ - DOMAIN as GEOFENCY_DOMAIN +from homeassistant.components.device_tracker import ( + DOMAIN as DEVICE_TRACKER_DOMAIN) +from homeassistant.components.geofency import ( + DOMAIN as GEOFENCY_DOMAIN, TRACKER_UPDATE) from homeassistant.helpers.dispatcher import async_dispatcher_connect _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/goalfeed/__init__.py b/homeassistant/components/goalfeed/__init__.py index c16390302d69a..6f0149f657a90 100644 --- a/homeassistant/components/goalfeed/__init__.py +++ b/homeassistant/components/goalfeed/__init__.py @@ -1,9 +1,4 @@ -""" -Component for the Goalfeed service. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/goalfeed/ -""" +"""Component for the Goalfeed service.""" import json import requests @@ -48,8 +43,8 @@ def connect_handler(data): 'username': username, 'password': password, 'connection_info': data} - resp = requests.post(GOALFEED_AUTH_ENDPOINT, post_data, - timeout=30).json() + resp = requests.post( + GOALFEED_AUTH_ENDPOINT, post_data, timeout=30).json() channel = pusher.subscribe('private-goals', resp['auth']) channel.bind('goal', goal_handler) diff --git a/homeassistant/components/google/__init__.py b/homeassistant/components/google/__init__.py index 49cb195d6c919..8fba016df57a4 100644 --- a/homeassistant/components/google/__init__.py +++ b/homeassistant/components/google/__init__.py @@ -1,14 +1,4 @@ -""" -Support for Google - Calendar Event Devices. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/google/ - -NOTE TO OTHER DEVELOPERS: IF YOU ADD MORE SCOPES TO THE OAUTH THAN JUST -CALENDAR THEN USERS WILL NEED TO DELETE THEIR TOKEN_FILE. THEY WILL LOSE THEIR -REFRESH_TOKEN PIECE WHEN RE-AUTHENTICATING TO ADD MORE API ACCESS -IT'S BEST TO JUST HAVE SEPARATE OAUTH FOR DIFFERENT PIECES OF GOOGLE -""" +"""Support for Google - Calendar Event Devices.""" import logging import os import yaml @@ -75,10 +65,10 @@ _SINGLE_CALSEARCH_CONFIG = vol.Schema({ vol.Required(CONF_NAME): cv.string, vol.Required(CONF_DEVICE_ID): cv.string, - vol.Optional(CONF_TRACK): cv.boolean, - vol.Optional(CONF_SEARCH): cv.string, - vol.Optional(CONF_OFFSET): cv.string, vol.Optional(CONF_IGNORE_AVAILABILITY, default=True): cv.boolean, + vol.Optional(CONF_OFFSET): cv.string, + vol.Optional(CONF_SEARCH): cv.string, + vol.Optional(CONF_TRACK): cv.boolean, }) DEVICE_SCHEMA = vol.Schema({ @@ -95,10 +85,7 @@ def do_authentication(hass, hass_config, config): until we have an access token. """ from oauth2client.client import ( - OAuth2WebServerFlow, - OAuth2DeviceCodeError, - FlowExchangeError - ) + OAuth2WebServerFlow, OAuth2DeviceCodeError, FlowExchangeError) from oauth2client.file import Storage oauth = OAuth2WebServerFlow( @@ -152,8 +139,8 @@ def step2_exchange(now): 'been found'.format(YAML_DEVICES), title=NOTIFICATION_TITLE, notification_id=NOTIFICATION_ID) - listener = track_time_change(hass, step2_exchange, - second=range(0, 60, dev_flow.interval)) + listener = track_time_change( + hass, step2_exchange, second=range(0, 60, dev_flow.interval)) return True diff --git a/homeassistant/components/google/calendar.py b/homeassistant/components/google/calendar.py index abb4fd28dd471..cc65c6d655d3e 100644 --- a/homeassistant/components/google/calendar.py +++ b/homeassistant/components/google/calendar.py @@ -1,9 +1,4 @@ -""" -Support for Google Calendar Search binary sensors. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/calendar.google/ -""" +"""Support for Google Calendar Search binary sensors.""" import logging from datetime import timedelta diff --git a/homeassistant/components/google/tts.py b/homeassistant/components/google/tts.py index 0d449083f7218..49a945cbbfd24 100644 --- a/homeassistant/components/google/tts.py +++ b/homeassistant/components/google/tts.py @@ -1,9 +1,4 @@ -""" -Support for the google speech service. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/tts.google/ -""" +"""Support for the Google speech service.""" import asyncio import logging import re @@ -101,16 +96,16 @@ async def async_get_tts_audio(self, message, language, options=None): ) if request.status != 200: - _LOGGER.error("Error %d on load url %s", + _LOGGER.error("Error %d on load URL %s", request.status, request.url) - return (None, None) + return None, None data += await request.read() except (asyncio.TimeoutError, aiohttp.ClientError): - _LOGGER.error("Timeout for google speech.") - return (None, None) + _LOGGER.error("Timeout for google speech") + return None, None - return ("mp3", data) + return 'mp3', data @staticmethod def _split_message_to_parts(message): diff --git a/homeassistant/components/google_assistant/__init__.py b/homeassistant/components/google_assistant/__init__.py index c0dff15d888c2..0fd167c272943 100644 --- a/homeassistant/components/google_assistant/__init__.py +++ b/homeassistant/components/google_assistant/__init__.py @@ -1,9 +1,4 @@ -""" -Support for Actions on Google Assistant Smart Home Control. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/google_assistant/ -""" +"""Support for Actions on Google Assistant Smart Home Control.""" import asyncio import logging from typing import Dict, Any @@ -27,6 +22,8 @@ CONF_EXPOSE, CONF_ALIASES, CONF_ROOM_HINT, CONF_ALLOW_UNLOCK, DEFAULT_ALLOW_UNLOCK ) +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 _LOGGER = logging.getLogger(__name__) @@ -37,7 +34,7 @@ vol.Optional(CONF_NAME): cv.string, vol.Optional(CONF_EXPOSE): cv.boolean, vol.Optional(CONF_ALIASES): vol.All(cv.ensure_list, [cv.string]), - vol.Optional(CONF_ROOM_HINT): cv.string + vol.Optional(CONF_ROOM_HINT): cv.string, }) GOOGLE_ASSISTANT_SCHEMA = vol.Schema({ @@ -49,7 +46,7 @@ vol.Optional(CONF_API_KEY): cv.string, vol.Optional(CONF_ENTITY_CONFIG): {cv.entity_id: ENTITY_SCHEMA}, vol.Optional(CONF_ALLOW_UNLOCK, - default=DEFAULT_ALLOW_UNLOCK): cv.boolean + default=DEFAULT_ALLOW_UNLOCK): cv.boolean, }, extra=vol.PREVENT_EXTRA) CONFIG_SCHEMA = vol.Schema({ diff --git a/homeassistant/components/google_assistant/const.py b/homeassistant/components/google_assistant/const.py index bfeb0fcadf57e..220ed6dd58c84 100644 --- a/homeassistant/components/google_assistant/const.py +++ b/homeassistant/components/google_assistant/const.py @@ -42,3 +42,8 @@ ERR_PROTOCOL_ERROR = 'protocolError' ERR_UNKNOWN_ERROR = 'unknownError' ERR_FUNCTION_NOT_SUPPORTED = 'functionNotSupported' + +# Event types +EVENT_COMMAND_RECEIVED = 'google_assistant_command' +EVENT_QUERY_RECEIVED = 'google_assistant_query' +EVENT_SYNC_RECEIVED = 'google_assistant_sync' diff --git a/homeassistant/components/google_assistant/smart_home.py b/homeassistant/components/google_assistant/smart_home.py index bab63bdb7ae54..21316c6208551 100644 --- a/homeassistant/components/google_assistant/smart_home.py +++ b/homeassistant/components/google_assistant/smart_home.py @@ -8,7 +8,7 @@ from homeassistant.core import callback from homeassistant.const import ( CLOUD_NEVER_EXPOSED_ENTITIES, CONF_NAME, STATE_UNAVAILABLE, - ATTR_SUPPORTED_FEATURES + ATTR_SUPPORTED_FEATURES, ATTR_ENTITY_ID, ) from homeassistant.components import ( climate, @@ -32,7 +32,8 @@ TYPE_THERMOSTAT, TYPE_FAN, CONF_ALIASES, CONF_ROOM_HINT, ERR_FUNCTION_NOT_SUPPORTED, ERR_PROTOCOL_ERROR, ERR_DEVICE_OFFLINE, - ERR_UNKNOWN_ERROR + ERR_UNKNOWN_ERROR, + EVENT_COMMAND_RECEIVED, EVENT_SYNC_RECEIVED, EVENT_QUERY_RECEIVED ) from .helpers import SmartHomeError @@ -187,7 +188,7 @@ async def async_handle_message(hass, config, message): """Handle incoming API messages.""" response = await _process(hass, config, message) - if 'errorCode' in response['payload']: + if response and 'errorCode' in response['payload']: _LOGGER.error('Error handling message %s: %s', message, response['payload']) @@ -214,8 +215,8 @@ async def _process(hass, config, message): } try: - result = await handler(hass, config, inputs[0].get('payload')) - return {'requestId': request_id, 'payload': result} + result = await handler(hass, config, request_id, + inputs[0].get('payload')) except SmartHomeError as err: return { 'requestId': request_id, @@ -228,13 +229,21 @@ async def _process(hass, config, message): 'payload': {'errorCode': ERR_UNKNOWN_ERROR} } + if result is None: + return None + return {'requestId': request_id, 'payload': result} + @HANDLERS.register('action.devices.SYNC') -async def async_devices_sync(hass, config, payload): +async def async_devices_sync(hass, config, request_id, payload): """Handle action.devices.SYNC request. https://developers.google.com/actions/smarthome/create-app#actiondevicessync """ + hass.bus.async_fire(EVENT_SYNC_RECEIVED, { + 'request_id': request_id + }) + devices = [] for state in hass.states.async_all(): if state.entity_id in CLOUD_NEVER_EXPOSED_ENTITIES: @@ -252,14 +261,16 @@ async def async_devices_sync(hass, config, payload): devices.append(serialized) - return { + response = { 'agentUserId': config.agent_user_id, 'devices': devices, } + return response + @HANDLERS.register('action.devices.QUERY') -async def async_devices_query(hass, config, payload): +async def async_devices_query(hass, config, request_id, payload): """Handle action.devices.QUERY request. https://developers.google.com/actions/smarthome/create-app#actiondevicesquery @@ -269,6 +280,11 @@ async def async_devices_query(hass, config, payload): devid = device['id'] state = hass.states.get(devid) + hass.bus.async_fire(EVENT_QUERY_RECEIVED, { + 'request_id': request_id, + ATTR_ENTITY_ID: devid, + }) + if not state: # If we can't find a state, the device is offline devices[devid] = {'online': False} @@ -280,7 +296,7 @@ async def async_devices_query(hass, config, payload): @HANDLERS.register('action.devices.EXECUTE') -async def handle_devices_execute(hass, config, payload): +async def handle_devices_execute(hass, config, request_id, payload): """Handle action.devices.EXECUTE request. https://developers.google.com/actions/smarthome/create-app#actiondevicesexecute @@ -293,6 +309,12 @@ async def handle_devices_execute(hass, config, payload): command['execution']): entity_id = device['id'] + hass.bus.async_fire(EVENT_COMMAND_RECEIVED, { + 'request_id': request_id, + ATTR_ENTITY_ID: entity_id, + 'execution': execution + }) + # Happens if error occurred. Skip entity for further processing if entity_id in results: continue @@ -337,6 +359,15 @@ async def handle_devices_execute(hass, config, payload): return {'commands': final_results} +@HANDLERS.register('action.devices.DISCONNECT') +async def async_devices_disconnect(hass, config, request_id, payload): + """Handle action.devices.DISCONNECT request. + + https://developers.google.com/actions/smarthome/create#actiondevicesdisconnect + """ + return None + + def turned_off_response(message): """Return a device turned off response.""" return { diff --git a/homeassistant/components/google_assistant/trait.py b/homeassistant/components/google_assistant/trait.py index 7153115e3ef19..d0368ee077596 100644 --- a/homeassistant/components/google_assistant/trait.py +++ b/homeassistant/components/google_assistant/trait.py @@ -1,8 +1,7 @@ -"""Implement the Smart Home traits.""" +"""Implement the Google Smart Home traits.""" import logging from homeassistant.components import ( - climate, cover, group, fan, @@ -15,6 +14,7 @@ switch, vacuum, ) +from homeassistant.components.climate import const as climate from homeassistant.const import ( ATTR_ENTITY_ID, SERVICE_TURN_OFF, @@ -24,6 +24,7 @@ TEMP_CELSIUS, TEMP_FAHRENHEIT, ATTR_SUPPORTED_FEATURES, + ATTR_TEMPERATURE, ) from homeassistant.core import DOMAIN as HA_DOMAIN from homeassistant.util import color as color_util, temperature as temp_util @@ -516,7 +517,7 @@ class TemperatureSettingTrait(_Trait): hass_to_google = { climate.STATE_HEAT: 'heat', climate.STATE_COOL: 'cool', - climate.STATE_OFF: 'off', + STATE_OFF: 'off', climate.STATE_AUTO: 'heatcool', climate.STATE_FAN_ONLY: 'fan-only', climate.STATE_DRY: 'dry', @@ -576,7 +577,7 @@ def query_attributes(self): round(temp_util.convert(attrs[climate.ATTR_TARGET_TEMP_LOW], unit, TEMP_CELSIUS), 1) else: - target_temp = attrs.get(climate.ATTR_TEMPERATURE) + target_temp = attrs.get(ATTR_TEMPERATURE) if target_temp is not None: response['thermostatTemperatureSetpoint'] = round( temp_util.convert(target_temp, unit, TEMP_CELSIUS), 1) @@ -606,7 +607,7 @@ async def execute(self, command, params): await self.hass.services.async_call( climate.DOMAIN, climate.SERVICE_SET_TEMPERATURE, { ATTR_ENTITY_ID: self.state.entity_id, - climate.ATTR_TEMPERATURE: temp + ATTR_TEMPERATURE: temp }, blocking=True) elif command == COMMAND_THERMOSTAT_TEMPERATURE_SET_RANGE: diff --git a/homeassistant/components/google_domains/__init__.py b/homeassistant/components/google_domains/__init__.py index 32bdb79557a98..f884e46cc4c11 100644 --- a/homeassistant/components/google_domains/__init__.py +++ b/homeassistant/components/google_domains/__init__.py @@ -1,9 +1,4 @@ -""" -Integrate with Google Domains. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/google_domains/ -""" +"""Support for Google Domains.""" import asyncio from datetime import timedelta import logging @@ -62,8 +57,8 @@ async def update_domain_interval(now): return True -async def _update_google_domains(hass, session, domain, user, password, - timeout): +async def _update_google_domains( + hass, session, domain, user, password, timeout): """Update Google Domains.""" url = UPDATE_URL.format(user, password) diff --git a/homeassistant/components/google_pubsub/__init__.py b/homeassistant/components/google_pubsub/__init__.py index af8bb60f8b1dc..18c068ea454bc 100644 --- a/homeassistant/components/google_pubsub/__init__.py +++ b/homeassistant/components/google_pubsub/__init__.py @@ -1,9 +1,4 @@ -""" -Support for Google Cloud Pub/Sub. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/google_pubsub/ -""" +"""Support for Google Cloud Pub/Sub.""" import datetime import json import logging @@ -34,7 +29,7 @@ vol.Required(CONF_PROJECT_ID): cv.string, vol.Required(CONF_TOPIC_NAME): cv.string, vol.Required(CONF_SERVICE_PRINCIPAL): cv.string, - vol.Required(CONF_FILTER): FILTER_SCHEMA + vol.Required(CONF_FILTER): FILTER_SCHEMA, }), }, extra=vol.ALLOW_EXTRA) @@ -46,8 +41,8 @@ def setup(hass: HomeAssistant, yaml_config: Dict[str, Any]): config = yaml_config[DOMAIN] project_id = config[CONF_PROJECT_ID] topic_name = config[CONF_TOPIC_NAME] - service_principal_path = os.path.join(hass.config.config_dir, - config[CONF_SERVICE_PRINCIPAL]) + service_principal_path = os.path.join( + hass.config.config_dir, config[CONF_SERVICE_PRINCIPAL]) if not os.path.isfile(service_principal_path): _LOGGER.error("Path to credentials file cannot be found") diff --git a/homeassistant/components/googlehome/__init__.py b/homeassistant/components/googlehome/__init__.py index f2d5ad09350f3..6ebc2f512b1fe 100644 --- a/homeassistant/components/googlehome/__init__.py +++ b/homeassistant/components/googlehome/__init__.py @@ -1,9 +1,4 @@ -""" -Support Google Home units. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/googlehome/ -""" +"""Support Google Home units.""" import logging import asyncio @@ -25,18 +20,19 @@ CONF_DEVICE_TYPES = 'device_types' CONF_RSSI_THRESHOLD = 'rssi_threshold' CONF_TRACK_ALARMS = 'track_alarms' +CONF_TRACK_DEVICES = 'track_devices' DEVICE_TYPES = [1, 2, 3] DEFAULT_RSSI_THRESHOLD = -70 DEVICE_CONFIG = vol.Schema({ vol.Required(CONF_HOST): cv.string, - vol.Optional(CONF_DEVICE_TYPES, - default=DEVICE_TYPES): vol.All(cv.ensure_list, - [vol.In(DEVICE_TYPES)]), - vol.Optional(CONF_RSSI_THRESHOLD, - default=DEFAULT_RSSI_THRESHOLD): vol.Coerce(int), + vol.Optional(CONF_DEVICE_TYPES, default=DEVICE_TYPES): + vol.All(cv.ensure_list, [vol.In(DEVICE_TYPES)]), + vol.Optional(CONF_RSSI_THRESHOLD, default=DEFAULT_RSSI_THRESHOLD): + vol.Coerce(int), vol.Optional(CONF_TRACK_ALARMS, default=False): cv.boolean, + vol.Optional(CONF_TRACK_DEVICES, default=True): cv.boolean, }) @@ -54,9 +50,10 @@ async def async_setup(hass, config): for device in config[DOMAIN][CONF_DEVICES]: hass.data[DOMAIN][device['host']] = {} - hass.async_create_task( - discovery.async_load_platform( - hass, 'device_tracker', DOMAIN, device, config)) + if device[CONF_TRACK_DEVICES]: + hass.async_create_task( + discovery.async_load_platform( + hass, 'device_tracker', DOMAIN, device, config)) if device[CONF_TRACK_ALARMS]: hass.async_create_task( diff --git a/homeassistant/components/googlehome/device_tracker.py b/homeassistant/components/googlehome/device_tracker.py index c4b490ab316b2..462f5db3b9b77 100644 --- a/homeassistant/components/googlehome/device_tracker.py +++ b/homeassistant/components/googlehome/device_tracker.py @@ -1,9 +1,4 @@ -""" -Support for Google Home bluetooth tacker. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/device_tracker.googlehome/ -""" +"""Support for Google Home Bluetooth tacker.""" import logging from datetime import timedelta @@ -13,12 +8,12 @@ from homeassistant.helpers.event import async_track_time_interval from homeassistant.util import slugify +_LOGGER = logging.getLogger(__name__) + DEPENDENCIES = ['googlehome'] DEFAULT_SCAN_INTERVAL = timedelta(seconds=10) -_LOGGER = logging.getLogger(__name__) - async def async_setup_scanner(hass, config, async_see, discovery_info=None): """Validate the configuration and return a Google Home scanner.""" diff --git a/homeassistant/components/googlehome/sensor.py b/homeassistant/components/googlehome/sensor.py index 90b9cda80bbfa..7577ee0b01782 100644 --- a/homeassistant/components/googlehome/sensor.py +++ b/homeassistant/components/googlehome/sensor.py @@ -1,9 +1,4 @@ -""" -Support for Google Home alarm sensor. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/sensor.googlehome/ -""" +"""Support for Google Home alarm sensor.""" import logging from datetime import timedelta @@ -13,7 +8,6 @@ from homeassistant.helpers.entity import Entity import homeassistant.util.dt as dt_util - DEPENDENCIES = ['googlehome'] SCAN_INTERVAL = timedelta(seconds=10) @@ -23,13 +17,13 @@ ICON = 'mdi:alarm' SENSOR_TYPES = { - 'timer': "Timer", - 'alarm': "Alarm", + 'timer': 'Timer', + 'alarm': 'Alarm', } -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): """Set up the googlehome sensor platform.""" if discovery_info is None: _LOGGER.warning( diff --git a/homeassistant/components/gpslogger/.translations/de.json b/homeassistant/components/gpslogger/.translations/de.json new file mode 100644 index 0000000000000..82c1dfa3e53b5 --- /dev/null +++ b/homeassistant/components/gpslogger/.translations/de.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "Deine Home-Assistant-Instanz muss aus dem internet erreichbar sein, um Nachrichten von GPSLogger zu erhalten.", + "one_instance_allowed": "Nur eine einzige Instanz ist notwendig." + }, + "create_entry": { + "default": "Um Ereignisse an Home Assistant zu senden, muss das Webhook Feature in der GPSLogger konfiguriert werden.\n\n F\u00fcge die folgenden Informationen ein: \n\n - URL: ` {webhook_url} ` \n - Methode: POST \n \n Weitere Informationen finden sich in der [Dokumentation]({docs_url})." + }, + "step": { + "user": { + "description": "M\u00f6chten Sie den GPSLogger Webhook wirklich einrichten?", + "title": "GPSLogger Webhook einrichten" + } + }, + "title": "GPSLogger Webhook" + } +} \ No newline at end of file diff --git a/homeassistant/components/gpslogger/.translations/es-419.json b/homeassistant/components/gpslogger/.translations/es-419.json new file mode 100644 index 0000000000000..960198eb04ef5 --- /dev/null +++ b/homeassistant/components/gpslogger/.translations/es-419.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "Su instancia de Home Assistant debe ser accesible desde Internet para recibir mensajes de GPSLogger.", + "one_instance_allowed": "Solo una instancia es necesaria." + }, + "create_entry": { + "default": "Para enviar eventos a Home Assistant, deber\u00e1 configurar la funci\u00f3n de webhook en GPSLogger. \n\n Complete la siguiente informaci\u00f3n: \n\n - URL: ` {webhook_url} ` \n - M\u00e9todo: POST \n\n Vea [la documentaci\u00f3n] ( {docs_url} ) para m\u00e1s detalles." + }, + "step": { + "user": { + "description": "\u00bfEst\u00e1 seguro de que desea configurar el Webhook de GPSLogger?", + "title": "Configurar el Webhook de GPSLogger" + } + }, + "title": "GPSLogger Webhook" + } +} \ No newline at end of file diff --git a/homeassistant/components/gpslogger/.translations/hu.json b/homeassistant/components/gpslogger/.translations/hu.json new file mode 100644 index 0000000000000..2d1dcad217417 --- /dev/null +++ b/homeassistant/components/gpslogger/.translations/hu.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "Az Home Assistantnek el\u00e9rhet\u0151nek kell lennie az internetr\u0151l, hogy megkapja a GPSLogger \u00fczeneteit.", + "one_instance_allowed": "Csak egy p\u00e9ld\u00e1ny sz\u00fcks\u00e9ges." + }, + "create_entry": { + "default": "Az esem\u00e9ny Home Assistantnek val\u00f3 k\u00fcld\u00e9s\u00e9hez be kell \u00e1ll\u00edtanod a webhook funkci\u00f3t a GPSLoggerben. \n\n Az al\u00e1bbi inform\u00e1ci\u00f3kat haszn\u00e1ld: \n\n - URL: ` {webhook_url} ` \n - Method: POST \n\n Tov\u00e1bbi r\u00e9szletek a [dokument\u00e1ci\u00f3] ( {docs_url} ) linken tal\u00e1lhat\u00f3k." + }, + "step": { + "user": { + "description": "Biztosan be szeretn\u00e9d \u00e1ll\u00edtani a GPSLogger Webhookot?", + "title": "GPSLogger Webhook be\u00e1ll\u00edt\u00e1sa" + } + }, + "title": "GPSLogger Webhook" + } +} \ No newline at end of file diff --git a/homeassistant/components/gpslogger/.translations/it.json b/homeassistant/components/gpslogger/.translations/it.json new file mode 100644 index 0000000000000..aab8edbe44a0a --- /dev/null +++ b/homeassistant/components/gpslogger/.translations/it.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "La tua istanza di Home Assistant deve essere accessibile da Internet per ricevere messaggi da GPSLogger.", + "one_instance_allowed": "\u00c8 necessaria una sola istanza." + }, + "create_entry": { + "default": "Per inviare eventi a Home Assistant, dovrai configurare la funzionalit\u00e0 webhook in GPSLogger.\n\n Compila le seguenti informazioni: \n\n - URL: ` {webhook_url} ` \n - Method: POST \n\n Vedi [la documentazione]({docs_url}) for ulteriori dettagli." + }, + "step": { + "user": { + "description": "Sei sicuro di voler configurare il webhook di GPSLogger?", + "title": "Configura il webhook di GPSLogger" + } + }, + "title": "Webhook di GPSLogger" + } +} \ No newline at end of file diff --git a/homeassistant/components/gpslogger/.translations/pt.json b/homeassistant/components/gpslogger/.translations/pt.json new file mode 100644 index 0000000000000..4dcfda527534a --- /dev/null +++ b/homeassistant/components/gpslogger/.translations/pt.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "A sua inst\u00e2ncia Home Assistent precisa de ser acess\u00edvel a partir da internet para receber mensagens GPSlogger.", + "one_instance_allowed": "Apenas uma inst\u00e2ncia \u00e9 necess\u00e1ria." + }, + "create_entry": { + "default": "Para enviar eventos para o Home Assistant, \u00e9 necess\u00e1rio configurar um webhook no GPslogger. \n\n Preencha as seguintes informa\u00e7\u00f5es: \n\n - URL: `{webhook_url}`\n - M\u00e9todo: POST \n\n Veja [the documentation]({docs_url}) para obter mais detalhes." + }, + "step": { + "user": { + "description": "Tem certeza de que deseja configurar o GPSLogger Webhook?", + "title": "Configurar o Geofency Webhook" + } + }, + "title": "GPSLogger Webhook" + } +} \ No newline at end of file diff --git a/homeassistant/components/gpslogger/.translations/ru.json b/homeassistant/components/gpslogger/.translations/ru.json index ac9c1c2d43ebe..366cb1735d59c 100644 --- a/homeassistant/components/gpslogger/.translations/ru.json +++ b/homeassistant/components/gpslogger/.translations/ru.json @@ -5,7 +5,7 @@ "one_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430." }, "create_entry": { - "default": "\u0414\u043b\u044f \u043e\u0442\u043f\u0440\u0430\u0432\u043a\u0438 \u0441\u043e\u0431\u044b\u0442\u0438\u0439 \u0432 Home Assistant \u0432\u044b \u0434\u043e\u043b\u0436\u043d\u044b \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c webhooks \u0434\u043b\u044f GPSLogger.\n\n\u0414\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0443\u044e \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e:\n\n- URL: `{webhook_url}`\n- Method: POST\n\n\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0435\u0439]({docs_url}) \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0431\u043e\u043b\u0435\u0435 \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u043e\u0439 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438." + "default": "\u0414\u043b\u044f \u043e\u0442\u043f\u0440\u0430\u0432\u043a\u0438 \u0441\u043e\u0431\u044b\u0442\u0438\u0439 \u0432 Home Assistant \u0412\u044b \u0434\u043e\u043b\u0436\u043d\u044b \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c webhooks \u0434\u043b\u044f GPSLogger.\n\n\u0414\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0443\u044e \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e:\n\n- URL: `{webhook_url}`\n- Method: POST\n\n\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0435\u0439]({docs_url}) \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0431\u043e\u043b\u0435\u0435 \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u043e\u0439 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438." }, "step": { "user": { diff --git a/homeassistant/components/gpslogger/.translations/sv.json b/homeassistant/components/gpslogger/.translations/sv.json new file mode 100644 index 0000000000000..3a927a70e61e9 --- /dev/null +++ b/homeassistant/components/gpslogger/.translations/sv.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "Din Home Assistant instans m\u00e5ste vara \u00e5tkomlig ifr\u00e5n internet f\u00f6r att ta emot meddelanden ifr\u00e5n GPSLogger.", + "one_instance_allowed": "Endast en enda instans \u00e4r n\u00f6dv\u00e4ndig." + }, + "create_entry": { + "default": "F\u00f6r att skicka h\u00e4ndelser till Home Assistant m\u00e5ste du konfigurera webhook funktionen i GPSLogger.\n\n Fyll i f\u00f6ljande information:\n \n- URL: `{webhook_url}`\n- Method: POST\n\nSe [dokumentation]({docs_url}) om hur du konfigurerar detta f\u00f6r mer information." + }, + "step": { + "user": { + "description": "\u00c4r du s\u00e4ker p\u00e5 att du vill konfigurera GPSLogger Webhook?", + "title": "Konfigurera GPSLogger Webhook" + } + }, + "title": "GPSLogger Webhook" + } +} \ No newline at end of file diff --git a/homeassistant/components/gpslogger/__init__.py b/homeassistant/components/gpslogger/__init__.py index 39d795dcd2515..2e956bd7fc593 100644 --- a/homeassistant/components/gpslogger/__init__.py +++ b/homeassistant/components/gpslogger/__init__.py @@ -1,9 +1,4 @@ -""" -Support for GPSLogger. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/gpslogger/ -""" +"""Support for GPSLogger.""" import logging import voluptuous as vol @@ -42,16 +37,16 @@ def _id(value: str) -> str: WEBHOOK_SCHEMA = vol.Schema({ + vol.Required(ATTR_DEVICE): _id, vol.Required(ATTR_LATITUDE): cv.latitude, vol.Required(ATTR_LONGITUDE): cv.longitude, - vol.Required(ATTR_DEVICE): _id, vol.Optional(ATTR_ACCURACY, default=DEFAULT_ACCURACY): vol.Coerce(float), + vol.Optional(ATTR_ACTIVITY): cv.string, + vol.Optional(ATTR_ALTITUDE): vol.Coerce(float), vol.Optional(ATTR_BATTERY, default=DEFAULT_BATTERY): vol.Coerce(float), - vol.Optional(ATTR_SPEED): vol.Coerce(float), vol.Optional(ATTR_DIRECTION): vol.Coerce(float), - vol.Optional(ATTR_ALTITUDE): vol.Coerce(float), vol.Optional(ATTR_PROVIDER): cv.string, - vol.Optional(ATTR_ACTIVITY): cv.string + vol.Optional(ATTR_SPEED): vol.Coerce(float), }) @@ -81,14 +76,9 @@ async def handle_webhook(hass, webhook_id, request): device = data[ATTR_DEVICE] async_dispatcher_send( - hass, - TRACKER_UPDATE, - device, + hass, TRACKER_UPDATE, device, (data[ATTR_LATITUDE], data[ATTR_LONGITUDE]), - data[ATTR_BATTERY], - data[ATTR_ACCURACY], - attrs - ) + data[ATTR_BATTERY], data[ATTR_ACCURACY], attrs) return web.Response( text='Setting location for {}'.format(device), diff --git a/homeassistant/components/gpslogger/device_tracker.py b/homeassistant/components/gpslogger/device_tracker.py index 8a312afa02481..90d2dc04f8912 100644 --- a/homeassistant/components/gpslogger/device_tracker.py +++ b/homeassistant/components/gpslogger/device_tracker.py @@ -1,9 +1,4 @@ -""" -Support for the GPSLogger platform. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/device_tracker.gpslogger/ -""" +"""Support for the GPSLogger device tracking.""" import logging from homeassistant.components.device_tracker import DOMAIN as \ diff --git a/homeassistant/components/graphite/__init__.py b/homeassistant/components/graphite/__init__.py index 26cd80d8da29b..e3f9e359f5a32 100644 --- a/homeassistant/components/graphite/__init__.py +++ b/homeassistant/components/graphite/__init__.py @@ -1,9 +1,4 @@ -""" -Component that sends data to a Graphite installation. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/graphite/ -""" +"""Support for sending data to a Graphite installation.""" import logging import queue import socket @@ -69,10 +64,8 @@ def __init__(self, hass, host, port, prefix): self._quit_object = object() self._we_started = False - hass.bus.listen_once(EVENT_HOMEASSISTANT_START, - self.start_listen) - hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, - self.shutdown) + hass.bus.listen_once(EVENT_HOMEASSISTANT_START, self.start_listen) + hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, self.shutdown) hass.bus.listen(EVENT_STATE_CHANGED, self.event_listener) _LOGGER.debug("Graphite feeding to %s:%i initialized", self._host, self._port) @@ -95,7 +88,7 @@ def event_listener(self, event): self._queue.put(event) else: _LOGGER.error( - "Graphite feeder thread has died, not queuing event!") + "Graphite feeder thread has died, not queuing event") def _send_to_graphite(self, data): """Send data to Graphite.""" diff --git a/homeassistant/components/greeneye_monitor/__init__.py b/homeassistant/components/greeneye_monitor/__init__.py index c1e2f285772aa..aedc98aac314e 100644 --- a/homeassistant/components/greeneye_monitor/__init__.py +++ b/homeassistant/components/greeneye_monitor/__init__.py @@ -1,9 +1,4 @@ -""" -Support for monitoring a GreenEye Monitor energy monitor. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/greeneye_monitor/ -""" +"""Support for monitoring a GreenEye Monitor energy monitor.""" import logging import voluptuous as vol diff --git a/homeassistant/components/group/__init__.py b/homeassistant/components/group/__init__.py index d1cd88a8438d6..e0315209ba1d4 100644 --- a/homeassistant/components/group/__init__.py +++ b/homeassistant/components/group/__init__.py @@ -1,9 +1,4 @@ -""" -Provide the functionality to group entities. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/group/ -""" +"""Provide the functionality to group entities.""" import asyncio import logging diff --git a/homeassistant/components/habitica/__init__.py b/homeassistant/components/habitica/__init__.py index 8e77c6bf50b32..23113a1388b4f 100644 --- a/homeassistant/components/habitica/__init__.py +++ b/homeassistant/components/habitica/__init__.py @@ -1,9 +1,4 @@ -""" -The Habitica API component. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/habitica/ -""" +"""Support for Habitica devices.""" from collections import namedtuple import logging diff --git a/homeassistant/components/habitica/sensor.py b/homeassistant/components/habitica/sensor.py index d2f13eb30e694..fb3a5670c2b60 100644 --- a/homeassistant/components/habitica/sensor.py +++ b/homeassistant/components/habitica/sensor.py @@ -1,18 +1,13 @@ -""" -The Habitica sensor. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/sensor.habitica/ -""" - -import logging +"""Support for Habitica sensors.""" from datetime import timedelta +import logging +from homeassistant.components import habitica from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle -from homeassistant.components import habitica _LOGGER = logging.getLogger(__name__) + MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=15) @@ -36,11 +31,7 @@ class HabitipyData: """Habitica API user data cache.""" def __init__(self, api): - """ - Habitica API user data cache. - - api - HAHabitipyAsync object - """ + """Habitica API user data cache.""" self.api = api self.data = None @@ -54,12 +45,7 @@ class HabitipySensor(Entity): """A generic Habitica sensor.""" def __init__(self, name, sensor_name, updater): - """ - Init a generic Habitica sensor. - - name - Habitica platform name - sensor_name - one of the names from ALL_SENSOR_TYPES - """ + """Initialize a generic Habitica sensor.""" self._name = name self._sensor_name = sensor_name self._sensor_type = habitica.SENSORS_TYPES[sensor_name] diff --git a/homeassistant/components/hangouts/.translations/es-419.json b/homeassistant/components/hangouts/.translations/es-419.json index a3699db08aeb5..ab78213b53a1d 100644 --- a/homeassistant/components/hangouts/.translations/es-419.json +++ b/homeassistant/components/hangouts/.translations/es-419.json @@ -4,7 +4,13 @@ "already_configured": "Google Hangouts ya est\u00e1 configurado", "unknown": "Se produjo un error desconocido." }, + "error": { + "invalid_login": "Inicio de sesi\u00f3n no v\u00e1lido, por favor, int\u00e9ntalo de nuevo." + }, "step": { + "2fa": { + "title": "Autenticaci\u00f3n de 2 factores" + }, "user": { "data": { "email": "Direcci\u00f3n de correo electr\u00f3nico", diff --git a/homeassistant/components/hangouts/__init__.py b/homeassistant/components/hangouts/__init__.py index 01d81cc466c3b..4796744c170b1 100644 --- a/homeassistant/components/hangouts/__init__.py +++ b/homeassistant/components/hangouts/__init__.py @@ -1,9 +1,4 @@ -""" -The hangouts bot component. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/hangouts/ -""" +"""Support for Hangouts.""" import logging import voluptuous as vol @@ -11,21 +6,18 @@ from homeassistant import config_entries from homeassistant.components.hangouts.intents import HelpIntent from homeassistant.const import EVENT_HOMEASSISTANT_STOP -from homeassistant.helpers import intent -from homeassistant.helpers import dispatcher +from homeassistant.helpers import dispatcher, intent import homeassistant.helpers.config_validation as cv -from .const import ( - CONF_BOT, CONF_INTENTS, CONF_REFRESH_TOKEN, DOMAIN, - EVENT_HANGOUTS_CONNECTED, EVENT_HANGOUTS_CONVERSATIONS_CHANGED, - MESSAGE_SCHEMA, SERVICE_SEND_MESSAGE, - SERVICE_UPDATE, CONF_SENTENCES, CONF_MATCHERS, - CONF_ERROR_SUPPRESSED_CONVERSATIONS, INTENT_SCHEMA, TARGETS_SCHEMA, - CONF_DEFAULT_CONVERSATIONS, EVENT_HANGOUTS_CONVERSATIONS_RESOLVED, - INTENT_HELP, SERVICE_RECONNECT) - # We need an import from .config_flow, without it .config_flow is never loaded. from .config_flow import HangoutsFlowHandler # noqa: F401 +from .const import ( + CONF_BOT, CONF_DEFAULT_CONVERSATIONS, CONF_ERROR_SUPPRESSED_CONVERSATIONS, + CONF_INTENTS, CONF_MATCHERS, CONF_REFRESH_TOKEN, CONF_SENTENCES, DOMAIN, + EVENT_HANGOUTS_CONNECTED, EVENT_HANGOUTS_CONVERSATIONS_CHANGED, + EVENT_HANGOUTS_CONVERSATIONS_RESOLVED, INTENT_HELP, INTENT_SCHEMA, + MESSAGE_SCHEMA, SERVICE_RECONNECT, SERVICE_SEND_MESSAGE, SERVICE_UPDATE, + TARGETS_SCHEMA) REQUIREMENTS = ['hangups==0.4.6'] @@ -39,7 +31,7 @@ vol.Optional(CONF_DEFAULT_CONVERSATIONS, default=[]): [TARGETS_SCHEMA], vol.Optional(CONF_ERROR_SUPPRESSED_CONVERSATIONS, default=[]): - [TARGETS_SCHEMA] + [TARGETS_SCHEMA], }) }, extra=vol.ALLOW_EXTRA) diff --git a/homeassistant/components/hangouts/config_flow.py b/homeassistant/components/hangouts/config_flow.py index 9d66338dff0cb..5eecc24d45e96 100644 --- a/homeassistant/components/hangouts/config_flow.py +++ b/homeassistant/components/hangouts/config_flow.py @@ -5,8 +5,7 @@ from homeassistant.const import CONF_EMAIL, CONF_PASSWORD from homeassistant.core import callback -from .const import CONF_2FA, CONF_REFRESH_TOKEN -from .const import DOMAIN as HANGOUTS_DOMAIN +from .const import CONF_2FA, CONF_REFRESH_TOKEN, DOMAIN as HANGOUTS_DOMAIN @callback diff --git a/homeassistant/components/hangouts/const.py b/homeassistant/components/hangouts/const.py index cf5374c317e01..ca0fdf986eeca 100644 --- a/homeassistant/components/hangouts/const.py +++ b/homeassistant/components/hangouts/const.py @@ -3,8 +3,8 @@ import voluptuous as vol -from homeassistant.components.notify \ - import ATTR_MESSAGE, ATTR_TARGET, ATTR_DATA +from homeassistant.components.notify import ( + ATTR_DATA, ATTR_MESSAGE, ATTR_TARGET) import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger('homeassistant.components.hangouts') diff --git a/homeassistant/components/hangouts/hangouts_bot.py b/homeassistant/components/hangouts/hangouts_bot.py index 748079452d8b6..fe72c50de778f 100644 --- a/homeassistant/components/hangouts/hangouts_bot.py +++ b/homeassistant/components/hangouts/hangouts_bot.py @@ -1,17 +1,19 @@ """The Hangouts Bot.""" +import asyncio import io import logging -import asyncio + import aiohttp -from homeassistant.helpers.aiohttp_client import async_get_clientsession + from homeassistant.helpers import dispatcher, intent +from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import ( - ATTR_MESSAGE, ATTR_TARGET, ATTR_DATA, CONF_CONVERSATIONS, DOMAIN, + ATTR_DATA, ATTR_MESSAGE, ATTR_TARGET, CONF_CONVERSATION_ID, + CONF_CONVERSATION_NAME, CONF_CONVERSATIONS, CONF_MATCHERS, DOMAIN, EVENT_HANGOUTS_CONNECTED, EVENT_HANGOUTS_CONVERSATIONS_CHANGED, - EVENT_HANGOUTS_DISCONNECTED, EVENT_HANGOUTS_MESSAGE_RECEIVED, - CONF_MATCHERS, CONF_CONVERSATION_ID, - CONF_CONVERSATION_NAME, EVENT_HANGOUTS_CONVERSATIONS_RESOLVED, INTENT_HELP) + EVENT_HANGOUTS_CONVERSATIONS_RESOLVED, EVENT_HANGOUTS_DISCONNECTED, + EVENT_HANGOUTS_MESSAGE_RECEIVED, INTENT_HELP) _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/hangouts/intents.py b/homeassistant/components/hangouts/intents.py index be52f059139f5..3887a644700ba 100644 --- a/homeassistant/components/hangouts/intents.py +++ b/homeassistant/components/hangouts/intents.py @@ -1,8 +1,8 @@ -"""Intents for the hangouts component.""" +"""Intents for the Hangouts component.""" from homeassistant.helpers import intent import homeassistant.helpers.config_validation as cv -from .const import INTENT_HELP, DOMAIN, CONF_BOT +from .const import CONF_BOT, DOMAIN, INTENT_HELP class HelpIntent(intent.IntentHandler): diff --git a/homeassistant/components/hangouts/notify.py b/homeassistant/components/hangouts/notify.py index 7261663b99fc7..c3b5450be0574 100644 --- a/homeassistant/components/hangouts/notify.py +++ b/homeassistant/components/hangouts/notify.py @@ -1,20 +1,13 @@ -""" -Hangouts notification service. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/notify.hangouts/ -""" +"""Support for Hangouts notifications.""" import logging import voluptuous as vol -from homeassistant.components.notify import (ATTR_TARGET, PLATFORM_SCHEMA, - BaseNotificationService, - ATTR_MESSAGE, ATTR_DATA) - -from homeassistant.components.hangouts.const \ - import (DOMAIN, SERVICE_SEND_MESSAGE, TARGETS_SCHEMA, - CONF_DEFAULT_CONVERSATIONS) +from homeassistant.components.hangouts.const import ( + CONF_DEFAULT_CONVERSATIONS, DOMAIN, SERVICE_SEND_MESSAGE, TARGETS_SCHEMA) +from homeassistant.components.notify import ( + ATTR_DATA, ATTR_MESSAGE, ATTR_TARGET, PLATFORM_SCHEMA, + BaseNotificationService) _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/harmony/__init__.py b/homeassistant/components/harmony/__init__.py index 25a33929c1a80..12ccc78077e93 100644 --- a/homeassistant/components/harmony/__init__.py +++ b/homeassistant/components/harmony/__init__.py @@ -1,5 +1 @@ -"""The harmony component. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/harmony/ -""" +"""Support for Harmony devices.""" diff --git a/homeassistant/components/harmony/remote.py b/homeassistant/components/harmony/remote.py index 489fe9144f2b5..4ea199bdcd1f2 100644 --- a/homeassistant/components/harmony/remote.py +++ b/homeassistant/components/harmony/remote.py @@ -1,9 +1,4 @@ -""" -Support for Harmony Hub devices. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/remote.harmony/ -""" +"""Support for Harmony Hub devices.""" import asyncio import json import logging @@ -51,12 +46,12 @@ HARMONY_CHANGE_CHANNEL_SCHEMA = vol.Schema({ vol.Required(ATTR_ENTITY_ID): cv.entity_ids, - vol.Required(ATTR_CHANNEL): cv.positive_int + vol.Required(ATTR_CHANNEL): cv.positive_int, }) -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): """Set up the Harmony platform.""" activity = None diff --git a/homeassistant/components/hassio/__init__.py b/homeassistant/components/hassio/__init__.py index 3c058281b0a78..e070c889f3168 100644 --- a/homeassistant/components/hassio/__init__.py +++ b/homeassistant/components/hassio/__init__.py @@ -1,9 +1,4 @@ -""" -Exposes regular REST commands as services. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/hassio/ -""" +"""Support for Hass.io.""" from datetime import timedelta import logging import os @@ -14,16 +9,15 @@ from homeassistant.components import SERVICE_CHECK_CONFIG from homeassistant.const import ( ATTR_NAME, SERVICE_HOMEASSISTANT_RESTART, SERVICE_HOMEASSISTANT_STOP) -from homeassistant.core import DOMAIN as HASS_DOMAIN -from homeassistant.core import callback +from homeassistant.core import DOMAIN as HASS_DOMAIN, callback +from homeassistant.exceptions import HomeAssistantError import homeassistant.helpers.config_validation as cv from homeassistant.loader import bind_hass from homeassistant.util.dt import utcnow -from homeassistant.exceptions import HomeAssistantError from .auth import async_setup_auth -from .handler import HassIO, HassioAPIError from .discovery import async_setup_discovery +from .handler import HassIO, HassioAPIError from .http import HassIOView _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/hassio/auth.py b/homeassistant/components/hassio/auth.py index 4be3ba9956ce1..b104d53aff95f 100644 --- a/homeassistant/components/hassio/auth.py +++ b/homeassistant/components/hassio/auth.py @@ -1,20 +1,20 @@ """Implement the auth feature from Hass.io for Add-ons.""" -import logging from ipaddress import ip_address +import logging import os from aiohttp import web from aiohttp.web_exceptions import HTTPForbidden, HTTPNotFound import voluptuous as vol -from homeassistant.core import callback -import homeassistant.helpers.config_validation as cv -from homeassistant.exceptions import HomeAssistantError from homeassistant.components.http import HomeAssistantView from homeassistant.components.http.const import KEY_REAL_IP from homeassistant.components.http.data_validator import RequestDataValidator +from homeassistant.core import callback +from homeassistant.exceptions import HomeAssistantError +import homeassistant.helpers.config_validation as cv -from .const import ATTR_USERNAME, ATTR_PASSWORD, ATTR_ADDON +from .const import ATTR_ADDON, ATTR_PASSWORD, ATTR_USERNAME _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/hassio/discovery.py b/homeassistant/components/hassio/discovery.py index a5f62b9e1a1b4..804247d2407d8 100644 --- a/homeassistant/components/hassio/discovery.py +++ b/homeassistant/components/hassio/discovery.py @@ -1,18 +1,18 @@ -"""Implement the serivces discovery feature from Hass.io for Add-ons.""" +"""Implement the services discovery feature from Hass.io for Add-ons.""" import asyncio import logging from aiohttp import web from aiohttp.web_exceptions import HTTPServiceUnavailable -from homeassistant.core import callback, CoreState -from homeassistant.const import EVENT_HOMEASSISTANT_START from homeassistant.components.http import HomeAssistantView +from homeassistant.const import EVENT_HOMEASSISTANT_START +from homeassistant.core import CoreState, callback -from .handler import HassioAPIError from .const import ( - ATTR_DISCOVERY, ATTR_ADDON, ATTR_NAME, ATTR_SERVICE, ATTR_CONFIG, + ATTR_ADDON, ATTR_CONFIG, ATTR_DISCOVERY, ATTR_NAME, ATTR_SERVICE, ATTR_UUID) +from .handler import HassioAPIError _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/hassio/handler.py b/homeassistant/components/hassio/handler.py index c33125d840e2e..640ed29e578aa 100644 --- a/homeassistant/components/hassio/handler.py +++ b/homeassistant/components/hassio/handler.py @@ -1,9 +1,4 @@ -""" -Exposes regular REST commands as services. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/hassio/ -""" +"""Handler for Hass.io.""" import asyncio import logging import os diff --git a/homeassistant/components/hassio/http.py b/homeassistant/components/hassio/http.py index 6b8004f76644e..01ded9ca11dc7 100644 --- a/homeassistant/components/hassio/http.py +++ b/homeassistant/components/hassio/http.py @@ -1,23 +1,18 @@ -""" -Exposes regular REST commands as services. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/hassio/ -""" +"""HTTP Support for Hass.io.""" import asyncio import logging import os import re -import async_timeout import aiohttp from aiohttp import web from aiohttp.hdrs import CONTENT_TYPE from aiohttp.web_exceptions import HTTPBadGateway +import async_timeout from homeassistant.components.http import KEY_AUTHENTICATED, HomeAssistantView -from .const import X_HASSIO, X_HASS_USER_ID, X_HASS_IS_ADMIN +from .const import X_HASS_IS_ADMIN, X_HASS_USER_ID, X_HASSIO _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/hdmi_cec/__init__.py b/homeassistant/components/hdmi_cec/__init__.py index 5fb2e19edcfbc..8eb13c5ab213a 100644 --- a/homeassistant/components/hdmi_cec/__init__.py +++ b/homeassistant/components/hdmi_cec/__init__.py @@ -1,26 +1,20 @@ -""" -HDMI CEC component. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/hdmi_cec/ -""" -import logging -import multiprocessing +"""Support for HDMI CEC.""" from collections import defaultdict from functools import reduce +import logging +import multiprocessing import voluptuous as vol -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers import discovery from homeassistant.components.media_player import DOMAIN as MEDIA_PLAYER from homeassistant.components.switch import DOMAIN as SWITCH -from homeassistant.const import (EVENT_HOMEASSISTANT_START, - EVENT_HOMEASSISTANT_STOP, STATE_ON, - STATE_OFF, CONF_DEVICES, CONF_PLATFORM, - STATE_PLAYING, STATE_IDLE, - STATE_PAUSED, CONF_HOST) +from homeassistant.const import ( + CONF_DEVICES, CONF_HOST, CONF_PLATFORM, EVENT_HOMEASSISTANT_START, + EVENT_HOMEASSISTANT_STOP, STATE_IDLE, STATE_OFF, STATE_ON, STATE_PAUSED, + STATE_PLAYING) from homeassistant.core import HomeAssistant +from homeassistant.helpers import discovery +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity REQUIREMENTS = ['pyCEC==0.4.13'] @@ -43,7 +37,7 @@ 1: ICON_RECORDER, 3: ICON_TUNER, 4: ICON_PLAYER, - 5: ICON_AUDIO + 5: ICON_AUDIO, } CEC_DEVICES = defaultdict(list) @@ -87,7 +81,7 @@ vol.Optional(ATTR_SRC): _VOL_HEX, vol.Optional(ATTR_DST): _VOL_HEX, vol.Optional(ATTR_ATT): _VOL_HEX, - vol.Optional(ATTR_RAW): vol.Coerce(str) + vol.Optional(ATTR_RAW): vol.Coerce(str), }, extra=vol.PREVENT_EXTRA) SERVICE_VOLUME = 'volume' diff --git a/homeassistant/components/hdmi_cec/media_player.py b/homeassistant/components/hdmi_cec/media_player.py index 6e691cad94fc7..553983a1f03fe 100644 --- a/homeassistant/components/hdmi_cec/media_player.py +++ b/homeassistant/components/hdmi_cec/media_player.py @@ -1,9 +1,4 @@ -""" -Support for HDMI CEC devices as media players. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/hdmi_cec/ -""" +"""Support for HDMI CEC devices as media players.""" import logging from homeassistant.components.hdmi_cec import ATTR_NEW, CecDevice @@ -25,7 +20,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Find and return HDMI devices as +switches.""" if ATTR_NEW in discovery_info: - _LOGGER.info("Setting up HDMI devices %s", discovery_info[ATTR_NEW]) + _LOGGER.debug("Setting up HDMI devices %s", discovery_info[ATTR_NEW]) entities = [] for device in discovery_info[ATTR_NEW]: hdmi_device = hass.data.get(device) diff --git a/homeassistant/components/hdmi_cec/switch.py b/homeassistant/components/hdmi_cec/switch.py index 1016e91d8d28d..ff423890ba584 100644 --- a/homeassistant/components/hdmi_cec/switch.py +++ b/homeassistant/components/hdmi_cec/switch.py @@ -1,14 +1,9 @@ -""" -Support for HDMI CEC devices as switches. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/hdmi_cec/ -""" +"""Support for HDMI CEC devices as switches.""" import logging -from homeassistant.components.hdmi_cec import CecDevice, ATTR_NEW -from homeassistant.components.switch import SwitchDevice, DOMAIN -from homeassistant.const import STATE_OFF, STATE_STANDBY, STATE_ON +from homeassistant.components.hdmi_cec import ATTR_NEW, CecDevice +from homeassistant.components.switch import DOMAIN, SwitchDevice +from homeassistant.const import STATE_OFF, STATE_ON, STATE_STANDBY DEPENDENCIES = ['hdmi_cec'] diff --git a/homeassistant/components/history/__init__.py b/homeassistant/components/history/__init__.py index 1773a55b3f1bb..7b07fac19a692 100644 --- a/homeassistant/components/history/__init__.py +++ b/homeassistant/components/history/__init__.py @@ -1,9 +1,4 @@ -""" -Provide pre-made queries on top of the recorder component. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/history/ -""" +"""Provide pre-made queries on top of the recorder component.""" from collections import defaultdict from datetime import timedelta from itertools import groupby @@ -34,7 +29,7 @@ }) }, extra=vol.ALLOW_EXTRA) -SIGNIFICANT_DOMAINS = ('thermostat', 'climate') +SIGNIFICANT_DOMAINS = ('thermostat', 'climate', 'water_heater') IGNORE_DOMAINS = ('zone', 'scene',) diff --git a/homeassistant/components/history_graph/__init__.py b/homeassistant/components/history_graph/__init__.py index 7d9db379705a3..893f3514d77f9 100644 --- a/homeassistant/components/history_graph/__init__.py +++ b/homeassistant/components/history_graph/__init__.py @@ -1,9 +1,4 @@ -""" -Support to graphs card in the UI. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/history_graph/ -""" +"""Support to graphs card in the UI.""" import logging import voluptuous as vol @@ -34,7 +29,7 @@ CONFIG_SCHEMA = vol.Schema({ - DOMAIN: cv.schema_with_slug_keys(GRAPH_SCHEMA) + DOMAIN: cv.schema_with_slug_keys(GRAPH_SCHEMA), }, extra=vol.ALLOW_EXTRA) diff --git a/homeassistant/components/hive/__init__.py b/homeassistant/components/hive/__init__.py index 093198499336d..934c44028ac27 100644 --- a/homeassistant/components/hive/__init__.py +++ b/homeassistant/components/hive/__init__.py @@ -1,20 +1,17 @@ -""" -Support for the Hive devices. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/hive/ -""" +"""Support for the Hive devices.""" import logging + import voluptuous as vol -from homeassistant.const import (CONF_PASSWORD, CONF_SCAN_INTERVAL, - CONF_USERNAME) +from homeassistant.const import ( + CONF_PASSWORD, CONF_SCAN_INTERVAL, CONF_USERNAME) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.discovery import load_platform REQUIREMENTS = ['pyhiveapi==0.2.17'] _LOGGER = logging.getLogger(__name__) + DOMAIN = 'hive' DATA_HIVE = 'data_hive' DEVICETYPES = { @@ -23,7 +20,7 @@ 'light': 'device_list_light', 'switch': 'device_list_plug', 'sensor': 'device_list_sensor', - } +} CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.Schema({ @@ -59,9 +56,8 @@ def setup(hass, config): password = config[DOMAIN][CONF_PASSWORD] update_interval = config[DOMAIN][CONF_SCAN_INTERVAL] - devicelist = session.core.initialise_api(username, - password, - update_interval) + devicelist = session.core.initialise_api( + username, password, update_interval) if devicelist is None: _LOGGER.error("Hive API initialization failed") diff --git a/homeassistant/components/hive/binary_sensor.py b/homeassistant/components/hive/binary_sensor.py index e114e67f90f19..dee27c5c7104d 100644 --- a/homeassistant/components/hive/binary_sensor.py +++ b/homeassistant/components/hive/binary_sensor.py @@ -1,16 +1,13 @@ -""" -Support for the Hive devices. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/binary_sensor.hive/ -""" +"""Support for the Hive binary sensors.""" from homeassistant.components.binary_sensor import BinarySensorDevice from homeassistant.components.hive import DATA_HIVE, DOMAIN DEPENDENCIES = ['hive'] -DEVICETYPE_DEVICE_CLASS = {'motionsensor': 'motion', - 'contactsensor': 'opening'} +DEVICETYPE_DEVICE_CLASS = { + 'motionsensor': 'motion', + 'contactsensor': 'opening', +} def setup_platform(hass, config, add_entities, discovery_info=None): @@ -76,8 +73,8 @@ def device_state_attributes(self): @property def is_on(self): """Return true if the binary sensor is on.""" - return self.session.sensor.get_state(self.node_id, - self.node_device_type) + return self.session.sensor.get_state( + self.node_id, self.node_device_type) def update(self): """Update all Node data from Hive.""" diff --git a/homeassistant/components/hive/climate.py b/homeassistant/components/hive/climate.py index 87d426d6f0579..45829cda08732 100644 --- a/homeassistant/components/hive/climate.py +++ b/homeassistant/components/hive/climate.py @@ -1,20 +1,27 @@ -""" -Support for the Hive devices. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/climate.hive/ -""" -from homeassistant.components.climate import ( - ClimateDevice, STATE_AUTO, STATE_HEAT, STATE_OFF, STATE_ON, +"""Support for the Hive climate devices.""" +from homeassistant.components.climate import ClimateDevice +from homeassistant.components.climate.const import ( + STATE_AUTO, STATE_HEAT, SUPPORT_AUX_HEAT, SUPPORT_TARGET_TEMPERATURE, SUPPORT_OPERATION_MODE) -from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS from homeassistant.components.hive import DATA_HIVE, DOMAIN +from homeassistant.const import ( + ATTR_TEMPERATURE, STATE_OFF, STATE_ON, TEMP_CELSIUS) DEPENDENCIES = ['hive'] -HIVE_TO_HASS_STATE = {'SCHEDULE': STATE_AUTO, 'MANUAL': STATE_HEAT, - 'ON': STATE_ON, 'OFF': STATE_OFF} -HASS_TO_HIVE_STATE = {STATE_AUTO: 'SCHEDULE', STATE_HEAT: 'MANUAL', - STATE_ON: 'ON', STATE_OFF: 'OFF'} + +HIVE_TO_HASS_STATE = { + 'SCHEDULE': STATE_AUTO, + 'MANUAL': STATE_HEAT, + 'ON': STATE_ON, + 'OFF': STATE_OFF, +} + +HASS_TO_HIVE_STATE = { + STATE_AUTO: 'SCHEDULE', + STATE_HEAT: 'MANUAL', + STATE_ON: 'ON', + STATE_OFF: 'OFF', +} SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_OPERATION_MODE | @@ -42,8 +49,8 @@ def __init__(self, hivesession, hivedevice): self.thermostat_node_id = hivedevice["Thermostat_NodeID"] self.session = hivesession self.attributes = {} - self.data_updatesource = '{}.{}'.format(self.device_type, - self.node_id) + self.data_updatesource = '{}.{}'.format( + self.device_type, self.node_id) self._unique_id = '{}-{}'.format(self.node_id, self.device_type) if self.device_type == "Heating": diff --git a/homeassistant/components/hive/light.py b/homeassistant/components/hive/light.py index c2bb95f40da38..2bec60f0ee422 100644 --- a/homeassistant/components/hive/light.py +++ b/homeassistant/components/hive/light.py @@ -1,15 +1,8 @@ -""" -Support for the Hive devices. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/light.hive/ -""" +"""Support for the Hive lights.""" from homeassistant.components.hive import DATA_HIVE, DOMAIN -from homeassistant.components.light import (ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, - ATTR_HS_COLOR, - SUPPORT_BRIGHTNESS, - SUPPORT_COLOR_TEMP, - SUPPORT_COLOR, Light) +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_HS_COLOR, SUPPORT_BRIGHTNESS, + SUPPORT_COLOR, SUPPORT_COLOR_TEMP, Light) import homeassistant.util.color as color_util DEPENDENCIES = ['hive'] @@ -35,8 +28,8 @@ def __init__(self, hivesession, hivedevice): self.light_device_type = hivedevice["Hive_Light_DeviceType"] self.session = hivesession self.attributes = {} - self.data_updatesource = '{}.{}'.format(self.device_type, - self.node_id) + self.data_updatesource = '{}.{}'.format( + self.device_type, self.node_id) self._unique_id = '{}-{}'.format(self.node_id, self.device_type) self.session.entities.append(self) diff --git a/homeassistant/components/hive/sensor.py b/homeassistant/components/hive/sensor.py index e989074fb4b3a..142c8c7ee94f7 100644 --- a/homeassistant/components/hive/sensor.py +++ b/homeassistant/components/hive/sensor.py @@ -1,19 +1,19 @@ -""" -Support for the Hive devices. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/sensor.hive/ -""" -from homeassistant.const import TEMP_CELSIUS +"""Support for the Hive sensors.""" from homeassistant.components.hive import DATA_HIVE, DOMAIN +from homeassistant.const import TEMP_CELSIUS from homeassistant.helpers.entity import Entity DEPENDENCIES = ['hive'] -FRIENDLY_NAMES = {'Hub_OnlineStatus': 'Hive Hub Status', - 'Hive_OutsideTemperature': 'Outside Temperature'} -DEVICETYPE_ICONS = {'Hub_OnlineStatus': 'mdi:switch', - 'Hive_OutsideTemperature': 'mdi:thermometer'} +FRIENDLY_NAMES = { + 'Hub_OnlineStatus': 'Hive Hub Status', + 'Hive_OutsideTemperature': 'Outside Temperature', +} + +DEVICETYPE_ICONS = { + 'Hub_OnlineStatus': 'mdi:switch', + 'Hive_OutsideTemperature': 'mdi:thermometer', +} def setup_platform(hass, config, add_entities, discovery_info=None): @@ -36,8 +36,8 @@ def __init__(self, hivesession, hivedevice): self.device_type = hivedevice["HA_DeviceType"] self.node_device_type = hivedevice["Hive_DeviceType"] self.session = hivesession - self.data_updatesource = '{}.{}'.format(self.device_type, - self.node_id) + self.data_updatesource = '{}.{}'.format( + self.device_type, self.node_id) self._unique_id = '{}-{}'.format(self.node_id, self.device_type) self.session.entities.append(self) diff --git a/homeassistant/components/hive/switch.py b/homeassistant/components/hive/switch.py index a50323f0a4e67..c897e37f34bca 100644 --- a/homeassistant/components/hive/switch.py +++ b/homeassistant/components/hive/switch.py @@ -1,11 +1,6 @@ -""" -Support for the Hive devices. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/switch.hive/ -""" -from homeassistant.components.switch import SwitchDevice +"""Support for the Hive switches.""" from homeassistant.components.hive import DATA_HIVE, DOMAIN +from homeassistant.components.switch import SwitchDevice DEPENDENCIES = ['hive'] @@ -29,8 +24,8 @@ def __init__(self, hivesession, hivedevice): self.device_type = hivedevice["HA_DeviceType"] self.session = hivesession self.attributes = {} - self.data_updatesource = '{}.{}'.format(self.device_type, - self.node_id) + self.data_updatesource = '{}.{}'.format( + self.device_type, self.node_id) self._unique_id = '{}-{}'.format(self.node_id, self.device_type) self.session.entities.append(self) diff --git a/homeassistant/components/hlk_sw16/__init__.py b/homeassistant/components/hlk_sw16/__init__.py index cfbb8ac010c7d..aab3f79b8b2dc 100644 --- a/homeassistant/components/hlk_sw16/__init__.py +++ b/homeassistant/components/hlk_sw16/__init__.py @@ -1,9 +1,4 @@ -""" -Support for HLK-SW16 relay switch. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/hlk_sw16/ -""" +"""Support for HLK-SW16 relay switches.""" import logging import voluptuous as vol diff --git a/homeassistant/components/hlk_sw16/switch.py b/homeassistant/components/hlk_sw16/switch.py index d76528c56f06a..b1bfc5ce23d00 100644 --- a/homeassistant/components/hlk_sw16/switch.py +++ b/homeassistant/components/hlk_sw16/switch.py @@ -1,9 +1,4 @@ -""" -Support for HLK-SW16 switches. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/switch.hlk_sw16/ -""" +"""Support for HLK-SW16 switches.""" import logging from homeassistant.components.hlk_sw16 import ( @@ -31,8 +26,8 @@ def devices_from_config(hass, domain_config): return devices -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): """Set up the HLK-SW16 platform.""" async_add_entities(devices_from_config(hass, discovery_info)) diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index c34b527252fdc..01979f03b9a5f 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -1,8 +1,4 @@ -"""Support for Apple HomeKit. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/homekit/ -""" +"""Support for Apple HomeKit.""" import ipaddress import logging from zlib import adler32 @@ -14,19 +10,19 @@ ATTR_DEVICE_CLASS, ATTR_SUPPORTED_FEATURES, ATTR_UNIT_OF_MEASUREMENT, CONF_IP_ADDRESS, CONF_NAME, CONF_PORT, CONF_TYPE, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_ILLUMINANCE, DEVICE_CLASS_TEMPERATURE, - EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, - TEMP_CELSIUS, TEMP_FAHRENHEIT) + EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, TEMP_CELSIUS, + TEMP_FAHRENHEIT) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entityfilter import FILTER_SCHEMA from homeassistant.util import get_local_ip from homeassistant.util.decorator import Registry + from .const import ( BRIDGE_NAME, CONF_AUTO_START, CONF_ENTITY_CONFIG, CONF_FEATURE_LIST, CONF_FILTER, CONF_SAFE_MODE, DEFAULT_AUTO_START, DEFAULT_PORT, - DEFAULT_SAFE_MODE, DEVICE_CLASS_CO, - DEVICE_CLASS_CO2, DEVICE_CLASS_PM25, DOMAIN, HOMEKIT_FILE, - SERVICE_HOMEKIT_START, TYPE_FAUCET, TYPE_OUTLET, TYPE_SHOWER, - TYPE_SPRINKLER, TYPE_SWITCH, TYPE_VALVE) + DEFAULT_SAFE_MODE, DEVICE_CLASS_CO, DEVICE_CLASS_CO2, DEVICE_CLASS_PM25, + DOMAIN, HOMEKIT_FILE, SERVICE_HOMEKIT_START, TYPE_FAUCET, TYPE_OUTLET, + TYPE_SHOWER, TYPE_SPRINKLER, TYPE_SWITCH, TYPE_VALVE) from .util import ( show_setup_message, validate_entity_config, validate_media_player_features) diff --git a/homeassistant/components/homekit/accessories.py b/homeassistant/components/homekit/accessories.py index ca1b560e336c4..2738fafbfdb1f 100644 --- a/homeassistant/components/homekit/accessories.py +++ b/homeassistant/components/homekit/accessories.py @@ -9,10 +9,9 @@ from pyhap.const import CATEGORY_OTHER from homeassistant.const import ( - __version__, ATTR_BATTERY_CHARGING, ATTR_BATTERY_LEVEL, ATTR_ENTITY_ID, - ATTR_SERVICE) -from homeassistant.core import callback as ha_callback -from homeassistant.core import split_entity_id + ATTR_BATTERY_CHARGING, ATTR_BATTERY_LEVEL, ATTR_ENTITY_ID, ATTR_SERVICE, + __version__) +from homeassistant.core import callback as ha_callback, split_entity_id from homeassistant.helpers.event import ( async_track_state_change, track_point_in_utc_time) from homeassistant.util import dt as dt_util @@ -22,8 +21,7 @@ CHAR_BATTERY_LEVEL, CHAR_CHARGING_STATE, CHAR_STATUS_LOW_BATTERY, DEBOUNCE_TIMEOUT, EVENT_HOMEKIT_CHANGED, MANUFACTURER, SERV_BATTERY_SERVICE) -from .util import ( - convert_to_float, show_setup_message, dismiss_setup_message) +from .util import convert_to_float, dismiss_setup_message, show_setup_message _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/homekit/type_covers.py b/homeassistant/components/homekit/type_covers.py index b3beb11c8b6d3..5273480b6cef0 100644 --- a/homeassistant/components/homekit/type_covers.py +++ b/homeassistant/components/homekit/type_covers.py @@ -11,11 +11,11 @@ STATE_CLOSED, STATE_OPEN) from . import TYPES -from .accessories import debounce, HomeAccessory +from .accessories import HomeAccessory, debounce from .const import ( CHAR_CURRENT_DOOR_STATE, CHAR_CURRENT_POSITION, CHAR_POSITION_STATE, - CHAR_TARGET_DOOR_STATE, CHAR_TARGET_POSITION, - SERV_GARAGE_DOOR_OPENER, SERV_WINDOW_COVERING) + CHAR_TARGET_DOOR_STATE, CHAR_TARGET_POSITION, SERV_GARAGE_DOOR_OPENER, + SERV_WINDOW_COVERING) _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/homekit/type_fans.py b/homeassistant/components/homekit/type_fans.py index dcc93b7cf9e49..d2777a296dcbf 100644 --- a/homeassistant/components/homekit/type_fans.py +++ b/homeassistant/components/homekit/type_fans.py @@ -13,7 +13,7 @@ STATE_OFF, STATE_ON) from . import TYPES -from .accessories import debounce, HomeAccessory +from .accessories import HomeAccessory, debounce from .const import ( CHAR_ACTIVE, CHAR_ROTATION_DIRECTION, CHAR_ROTATION_SPEED, CHAR_SWING_MODE, SERV_FANV2) diff --git a/homeassistant/components/homekit/type_lights.py b/homeassistant/components/homekit/type_lights.py index a9007ace35b28..f549958f755a3 100644 --- a/homeassistant/components/homekit/type_lights.py +++ b/homeassistant/components/homekit/type_lights.py @@ -5,17 +5,17 @@ from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_BRIGHTNESS_PCT, ATTR_COLOR_TEMP, ATTR_HS_COLOR, - ATTR_MAX_MIREDS, ATTR_MIN_MIREDS, DOMAIN, - SUPPORT_BRIGHTNESS, SUPPORT_COLOR, SUPPORT_COLOR_TEMP) + ATTR_MAX_MIREDS, ATTR_MIN_MIREDS, DOMAIN, SUPPORT_BRIGHTNESS, + SUPPORT_COLOR, SUPPORT_COLOR_TEMP) from homeassistant.const import ( - ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, SERVICE_TURN_ON, - SERVICE_TURN_OFF, STATE_OFF, STATE_ON) + ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, SERVICE_TURN_OFF, SERVICE_TURN_ON, + STATE_OFF, STATE_ON) from . import TYPES -from .accessories import debounce, HomeAccessory +from .accessories import HomeAccessory, debounce from .const import ( CHAR_BRIGHTNESS, CHAR_COLOR_TEMPERATURE, CHAR_HUE, CHAR_ON, - CHAR_SATURATION, SERV_LIGHTBULB, PROP_MAX_VALUE, PROP_MIN_VALUE) + CHAR_SATURATION, PROP_MAX_VALUE, PROP_MIN_VALUE, SERV_LIGHTBULB) _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/homekit/type_locks.py b/homeassistant/components/homekit/type_locks.py index 22c47d59c62cd..4ed1cebd20774 100644 --- a/homeassistant/components/homekit/type_locks.py +++ b/homeassistant/components/homekit/type_locks.py @@ -13,13 +13,19 @@ _LOGGER = logging.getLogger(__name__) -HASS_TO_HOMEKIT = {STATE_UNLOCKED: 0, - STATE_LOCKED: 1, - # value 2 is Jammed which hass doesn't have a state for - STATE_UNKNOWN: 3} +HASS_TO_HOMEKIT = { + STATE_UNLOCKED: 0, + STATE_LOCKED: 1, + # Value 2 is Jammed which hass doesn't have a state for + STATE_UNKNOWN: 3, +} + HOMEKIT_TO_HASS = {c: s for s, c in HASS_TO_HOMEKIT.items()} -STATE_TO_SERVICE = {STATE_LOCKED: 'lock', - STATE_UNLOCKED: 'unlock'} + +STATE_TO_SERVICE = { + STATE_LOCKED: 'lock', + STATE_UNLOCKED: 'unlock', +} @TYPES.register('Lock') @@ -45,7 +51,7 @@ def __init__(self, *args): def set_state(self, value): """Set lock state to value if call came from HomeKit.""" - _LOGGER.debug('%s: Set state to %d', self.entity_id, value) + _LOGGER.debug("%s: Set state to %d", self.entity_id, value) self._flag_state = True hass_value = HOMEKIT_TO_HASS.get(value) @@ -62,7 +68,7 @@ def update_state(self, new_state): if hass_state in HASS_TO_HOMEKIT: current_lock_state = HASS_TO_HOMEKIT[hass_state] self.char_current_state.set_value(current_lock_state) - _LOGGER.debug('%s: Updated current state to %s (%d)', + _LOGGER.debug("%s: Updated current state to %s (%d)", self.entity_id, hass_state, current_lock_state) # LockTargetState only supports locked and unlocked diff --git a/homeassistant/components/homekit/type_media_players.py b/homeassistant/components/homekit/type_media_players.py index 09088871fd273..f8f4ef9699247 100644 --- a/homeassistant/components/homekit/type_media_players.py +++ b/homeassistant/components/homekit/type_media_players.py @@ -3,12 +3,12 @@ from pyhap.const import CATEGORY_SWITCH +from homeassistant.components.media_player import ( + ATTR_MEDIA_VOLUME_MUTED, DOMAIN) from homeassistant.const import ( ATTR_ENTITY_ID, SERVICE_MEDIA_PAUSE, SERVICE_MEDIA_PLAY, SERVICE_MEDIA_STOP, SERVICE_TURN_OFF, SERVICE_TURN_ON, SERVICE_VOLUME_MUTE, STATE_OFF, STATE_PLAYING, STATE_UNKNOWN) -from homeassistant.components.media_player import ( - ATTR_MEDIA_VOLUME_MUTED, DOMAIN) from . import TYPES from .accessories import HomeAccessory @@ -18,10 +18,12 @@ _LOGGER = logging.getLogger(__name__) -MODE_FRIENDLY_NAME = {FEATURE_ON_OFF: 'Power', - FEATURE_PLAY_PAUSE: 'Play/Pause', - FEATURE_PLAY_STOP: 'Play/Stop', - FEATURE_TOGGLE_MUTE: 'Mute'} +MODE_FRIENDLY_NAME = { + FEATURE_ON_OFF: 'Power', + FEATURE_PLAY_PAUSE: 'Play/Pause', + FEATURE_PLAY_STOP: 'Play/Stop', + FEATURE_TOGGLE_MUTE: 'Mute', +} @TYPES.register('MediaPlayer') diff --git a/homeassistant/components/homekit/type_security_systems.py b/homeassistant/components/homekit/type_security_systems.py index e210217df2ff0..10befb4af61e7 100644 --- a/homeassistant/components/homekit/type_security_systems.py +++ b/homeassistant/components/homekit/type_security_systems.py @@ -5,10 +5,10 @@ from homeassistant.components.alarm_control_panel import DOMAIN from homeassistant.const import ( - ATTR_ENTITY_ID, ATTR_CODE, SERVICE_ALARM_ARM_AWAY, SERVICE_ALARM_ARM_HOME, + ATTR_CODE, ATTR_ENTITY_ID, SERVICE_ALARM_ARM_AWAY, SERVICE_ALARM_ARM_HOME, SERVICE_ALARM_ARM_NIGHT, SERVICE_ALARM_DISARM, STATE_ALARM_ARMED_AWAY, - STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT, STATE_ALARM_TRIGGERED, - STATE_ALARM_DISARMED) + STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT, STATE_ALARM_DISARMED, + STATE_ALARM_TRIGGERED) from . import TYPES from .accessories import HomeAccessory @@ -18,17 +18,22 @@ _LOGGER = logging.getLogger(__name__) -HASS_TO_HOMEKIT = {STATE_ALARM_ARMED_HOME: 0, - STATE_ALARM_ARMED_AWAY: 1, - STATE_ALARM_ARMED_NIGHT: 2, - STATE_ALARM_DISARMED: 3, - STATE_ALARM_TRIGGERED: 4} +HASS_TO_HOMEKIT = { + STATE_ALARM_ARMED_HOME: 0, + STATE_ALARM_ARMED_AWAY: 1, + STATE_ALARM_ARMED_NIGHT: 2, + STATE_ALARM_DISARMED: 3, + STATE_ALARM_TRIGGERED: 4, +} + HOMEKIT_TO_HASS = {c: s for s, c in HASS_TO_HOMEKIT.items()} + STATE_TO_SERVICE = { STATE_ALARM_ARMED_AWAY: SERVICE_ALARM_ARM_AWAY, STATE_ALARM_ARMED_HOME: SERVICE_ALARM_ARM_HOME, STATE_ALARM_ARMED_NIGHT: SERVICE_ALARM_ARM_NIGHT, - STATE_ALARM_DISARMED: SERVICE_ALARM_DISARM} + STATE_ALARM_DISARMED: SERVICE_ALARM_DISARM, +} @TYPES.register('SecuritySystem') diff --git a/homeassistant/components/homekit/type_sensors.py b/homeassistant/components/homekit/type_sensors.py index 09da361ddb8d6..0d7dd94d01459 100644 --- a/homeassistant/components/homekit/type_sensors.py +++ b/homeassistant/components/homekit/type_sensors.py @@ -4,7 +4,7 @@ from pyhap.const import CATEGORY_SENSOR from homeassistant.const import ( - ATTR_DEVICE_CLASS, ATTR_UNIT_OF_MEASUREMENT, STATE_ON, STATE_HOME, + ATTR_DEVICE_CLASS, ATTR_UNIT_OF_MEASUREMENT, STATE_HOME, STATE_ON, TEMP_CELSIUS) from . import TYPES @@ -26,7 +26,7 @@ SERV_LIGHT_SENSOR, SERV_MOTION_SENSOR, SERV_OCCUPANCY_SENSOR, SERV_SMOKE_SENSOR, SERV_TEMPERATURE_SENSOR, THRESHOLD_CO, THRESHOLD_CO2) from .util import ( - convert_to_float, temperature_to_homekit, density_to_air_quality) + convert_to_float, density_to_air_quality, temperature_to_homekit) _LOGGER = logging.getLogger(__name__) @@ -42,7 +42,8 @@ DEVICE_CLASS_OCCUPANCY: (SERV_OCCUPANCY_SENSOR, CHAR_OCCUPANCY_DETECTED), DEVICE_CLASS_OPENING: (SERV_CONTACT_SENSOR, CHAR_CONTACT_SENSOR_STATE), DEVICE_CLASS_SMOKE: (SERV_SMOKE_SENSOR, CHAR_SMOKE_DETECTED), - DEVICE_CLASS_WINDOW: (SERV_CONTACT_SENSOR, CHAR_CONTACT_SENSOR_STATE)} + DEVICE_CLASS_WINDOW: (SERV_CONTACT_SENSOR, CHAR_CONTACT_SENSOR_STATE), +} @TYPES.register('TemperatureSensor') diff --git a/homeassistant/components/homekit/type_switches.py b/homeassistant/components/homekit/type_switches.py index b41e1a015432f..7629e33a4d71b 100644 --- a/homeassistant/components/homekit/type_switches.py +++ b/homeassistant/components/homekit/type_switches.py @@ -2,13 +2,13 @@ import logging from pyhap.const import ( - CATEGORY_FAUCET, CATEGORY_OUTLET, CATEGORY_SHOWER_HEAD, - CATEGORY_SPRINKLER, CATEGORY_SWITCH) + CATEGORY_FAUCET, CATEGORY_OUTLET, CATEGORY_SHOWER_HEAD, CATEGORY_SPRINKLER, + CATEGORY_SWITCH) from homeassistant.components.script import ATTR_CAN_CANCEL from homeassistant.components.switch import DOMAIN from homeassistant.const import ( - ATTR_ENTITY_ID, CONF_TYPE, SERVICE_TURN_ON, SERVICE_TURN_OFF, STATE_ON) + ATTR_ENTITY_ID, CONF_TYPE, SERVICE_TURN_OFF, SERVICE_TURN_ON, STATE_ON) from homeassistant.core import split_entity_id from homeassistant.helpers.event import call_later diff --git a/homeassistant/components/homekit/type_thermostats.py b/homeassistant/components/homekit/type_thermostats.py index f78a05b1a45b7..85cf7938fbde5 100644 --- a/homeassistant/components/homekit/type_thermostats.py +++ b/homeassistant/components/homekit/type_thermostats.py @@ -3,32 +3,32 @@ from pyhap.const import CATEGORY_THERMOSTAT -from homeassistant.components.climate import ( +from homeassistant.components.climate.const import ( ATTR_CURRENT_TEMPERATURE, ATTR_MAX_TEMP, ATTR_MIN_TEMP, - ATTR_OPERATION_LIST, ATTR_OPERATION_MODE, - ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, - DEFAULT_MAX_TEMP, DEFAULT_MIN_TEMP, DOMAIN as DOMAIN_CLIMATE, + ATTR_OPERATION_LIST, ATTR_OPERATION_MODE, ATTR_TARGET_TEMP_HIGH, + ATTR_TARGET_TEMP_LOW, DEFAULT_MAX_TEMP, DEFAULT_MIN_TEMP, + DOMAIN as DOMAIN_CLIMATE, SERVICE_SET_OPERATION_MODE as SERVICE_SET_OPERATION_MODE_THERMOSTAT, - SERVICE_SET_TEMPERATURE as SERVICE_SET_TEMPERATURE_THERMOSTAT, - STATE_AUTO, STATE_COOL, STATE_HEAT, SUPPORT_ON_OFF, - SUPPORT_TARGET_TEMPERATURE_HIGH, SUPPORT_TARGET_TEMPERATURE_LOW) + SERVICE_SET_TEMPERATURE as SERVICE_SET_TEMPERATURE_THERMOSTAT, STATE_AUTO, + STATE_COOL, STATE_HEAT, SUPPORT_ON_OFF, SUPPORT_TARGET_TEMPERATURE_HIGH, + SUPPORT_TARGET_TEMPERATURE_LOW) from homeassistant.components.water_heater import ( DOMAIN as DOMAIN_WATER_HEATER, SERVICE_SET_TEMPERATURE as SERVICE_SET_TEMPERATURE_WATER_HEATER) from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, ATTR_TEMPERATURE, - SERVICE_TURN_OFF, SERVICE_TURN_ON, - STATE_OFF, TEMP_CELSIUS, TEMP_FAHRENHEIT) + SERVICE_TURN_OFF, SERVICE_TURN_ON, STATE_OFF, TEMP_CELSIUS, + TEMP_FAHRENHEIT) from . import TYPES -from .accessories import debounce, HomeAccessory +from .accessories import HomeAccessory, debounce from .const import ( CHAR_COOLING_THRESHOLD_TEMPERATURE, CHAR_CURRENT_HEATING_COOLING, - CHAR_CURRENT_TEMPERATURE, CHAR_TARGET_HEATING_COOLING, - CHAR_HEATING_THRESHOLD_TEMPERATURE, CHAR_TARGET_TEMPERATURE, - CHAR_TEMP_DISPLAY_UNITS, - DEFAULT_MAX_TEMP_WATER_HEATER, DEFAULT_MIN_TEMP_WATER_HEATER, - PROP_MAX_VALUE, PROP_MIN_STEP, PROP_MIN_VALUE, SERV_THERMOSTAT) + CHAR_CURRENT_TEMPERATURE, CHAR_HEATING_THRESHOLD_TEMPERATURE, + CHAR_TARGET_HEATING_COOLING, CHAR_TARGET_TEMPERATURE, + CHAR_TEMP_DISPLAY_UNITS, DEFAULT_MAX_TEMP_WATER_HEATER, + DEFAULT_MIN_TEMP_WATER_HEATER, PROP_MAX_VALUE, PROP_MIN_STEP, + PROP_MIN_VALUE, SERV_THERMOSTAT) from .util import temperature_to_homekit, temperature_to_states _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/homekit/util.py b/homeassistant/components/homekit/util.py index f1327f8b5270f..2ba5819a202d4 100644 --- a/homeassistant/components/homekit/util.py +++ b/homeassistant/components/homekit/util.py @@ -1,5 +1,5 @@ """Collection of useful functions for the HomeKit component.""" -from collections import namedtuple, OrderedDict +from collections import OrderedDict, namedtuple import logging import voluptuous as vol diff --git a/homeassistant/components/homekit_controller/__init__.py b/homeassistant/components/homekit_controller/__init__.py index 77d0825ef0b4e..eb748a3d883eb 100644 --- a/homeassistant/components/homekit_controller/__init__.py +++ b/homeassistant/components/homekit_controller/__init__.py @@ -1,9 +1,4 @@ -""" -Support for Homekit device discovery. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/homekit_controller/ -""" +"""Support for Homekit device discovery.""" import json import logging import os @@ -35,7 +30,7 @@ HOMEKIT_IGNORE = [ 'BSB002', 'Home Assistant Bridge', - 'TRADFRI gateway' + 'TRADFRI gateway', ] KNOWN_ACCESSORIES = "{}-accessories".format(DOMAIN) @@ -348,9 +343,17 @@ def discovery_dispatch(service, discovery_info): # model, id host = discovery_info['host'] port = discovery_info['port'] - model = discovery_info['properties']['md'] - hkid = discovery_info['properties']['id'] - config_num = int(discovery_info['properties']['c#']) + + # Fold property keys to lower case, making them effectively + # case-insensitive. Some HomeKit devices capitalize them. + properties = { + key.lower(): value + for (key, value) in discovery_info['properties'].items() + } + + model = properties['md'] + hkid = properties['id'] + config_num = int(properties['c#']) if model in HOMEKIT_IGNORE: return diff --git a/homeassistant/components/homekit_controller/alarm_control_panel.py b/homeassistant/components/homekit_controller/alarm_control_panel.py index 3a2e5170453ec..5d366b6e27b5d 100644 --- a/homeassistant/components/homekit_controller/alarm_control_panel.py +++ b/homeassistant/components/homekit_controller/alarm_control_panel.py @@ -1,18 +1,12 @@ -""" -Support for Homekit Alarm Control Panel. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/alarm_control_panel.homekit_controller/ -""" +"""Support for Homekit Alarm Control Panel.""" import logging -from homeassistant.components.homekit_controller import (HomeKitEntity, - KNOWN_ACCESSORIES) from homeassistant.components.alarm_control_panel import AlarmControlPanel +from homeassistant.components.homekit_controller import ( + KNOWN_ACCESSORIES, HomeKitEntity) from homeassistant.const import ( - STATE_ALARM_DISARMED, STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, - STATE_ALARM_ARMED_NIGHT, STATE_ALARM_TRIGGERED) -from homeassistant.const import ATTR_BATTERY_LEVEL + ATTR_BATTERY_LEVEL, STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, + STATE_ALARM_ARMED_NIGHT, STATE_ALARM_DISARMED, STATE_ALARM_TRIGGERED) DEPENDENCIES = ['homekit_controller'] @@ -25,7 +19,7 @@ 1: STATE_ALARM_ARMED_AWAY, 2: STATE_ALARM_ARMED_NIGHT, 3: STATE_ALARM_DISARMED, - 4: STATE_ALARM_TRIGGERED + 4: STATE_ALARM_TRIGGERED, } TARGET_STATE_MAP = { diff --git a/homeassistant/components/homekit_controller/binary_sensor.py b/homeassistant/components/homekit_controller/binary_sensor.py index 531297dc911ec..5d83ce6d984ea 100644 --- a/homeassistant/components/homekit_controller/binary_sensor.py +++ b/homeassistant/components/homekit_controller/binary_sensor.py @@ -1,14 +1,9 @@ -""" -Support for Homekit motion sensors. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/binary_sensor.homekit_controller/ -""" +"""Support for Homekit motion sensors.""" import logging -from homeassistant.components.homekit_controller import (HomeKitEntity, - KNOWN_ACCESSORIES) from homeassistant.components.binary_sensor import BinarySensorDevice +from homeassistant.components.homekit_controller import ( + KNOWN_ACCESSORIES, HomeKitEntity) DEPENDENCIES = ['homekit_controller'] diff --git a/homeassistant/components/homekit_controller/climate.py b/homeassistant/components/homekit_controller/climate.py index 15378e2b0464f..ceadcd46b9d3a 100644 --- a/homeassistant/components/homekit_controller/climate.py +++ b/homeassistant/components/homekit_controller/climate.py @@ -1,16 +1,12 @@ -""" -Support for Homekit climate devices. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/climate.homekit_controller/ -""" +"""Support for Homekit climate devices.""" import logging +from homeassistant.components.climate import ClimateDevice +from homeassistant.components.climate.const import ( + STATE_HEAT, STATE_COOL, STATE_IDLE, + SUPPORT_TARGET_TEMPERATURE, SUPPORT_OPERATION_MODE) from homeassistant.components.homekit_controller import ( HomeKitEntity, KNOWN_ACCESSORIES) -from homeassistant.components.climate import ( - ClimateDevice, STATE_HEAT, STATE_COOL, STATE_IDLE, - SUPPORT_TARGET_TEMPERATURE, SUPPORT_OPERATION_MODE) from homeassistant.const import TEMP_CELSIUS, STATE_OFF, ATTR_TEMPERATURE DEPENDENCIES = ['homekit_controller'] diff --git a/homeassistant/components/homekit_controller/cover.py b/homeassistant/components/homekit_controller/cover.py index c8f087254bb23..3951cf577d416 100644 --- a/homeassistant/components/homekit_controller/cover.py +++ b/homeassistant/components/homekit_controller/cover.py @@ -1,17 +1,12 @@ -""" -Support for Homekit Cover. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/cover.homekit_controller/ -""" +"""Support for Homekit covers.""" import logging -from homeassistant.components.homekit_controller import (HomeKitEntity, - KNOWN_ACCESSORIES) from homeassistant.components.cover import ( - CoverDevice, SUPPORT_OPEN, SUPPORT_CLOSE, SUPPORT_SET_POSITION, - SUPPORT_OPEN_TILT, SUPPORT_CLOSE_TILT, SUPPORT_SET_TILT_POSITION, - ATTR_POSITION, ATTR_TILT_POSITION) + ATTR_POSITION, ATTR_TILT_POSITION, SUPPORT_CLOSE, SUPPORT_CLOSE_TILT, + SUPPORT_OPEN, SUPPORT_OPEN_TILT, SUPPORT_SET_POSITION, + SUPPORT_SET_TILT_POSITION, CoverDevice) +from homeassistant.components.homekit_controller import ( + KNOWN_ACCESSORIES, HomeKitEntity) from homeassistant.const import ( STATE_CLOSED, STATE_CLOSING, STATE_OPEN, STATE_OPENING) diff --git a/homeassistant/components/homekit_controller/light.py b/homeassistant/components/homekit_controller/light.py index 74ef8948f4544..f39e793c1848d 100644 --- a/homeassistant/components/homekit_controller/light.py +++ b/homeassistant/components/homekit_controller/light.py @@ -1,15 +1,10 @@ -""" -Support for Homekit lights. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/light.homekit_controller/ -""" +"""Support for Homekit lights.""" import logging from homeassistant.components.homekit_controller import ( - HomeKitEntity, KNOWN_ACCESSORIES) + KNOWN_ACCESSORIES, HomeKitEntity) from homeassistant.components.light import ( - ATTR_BRIGHTNESS, ATTR_HS_COLOR, ATTR_COLOR_TEMP, SUPPORT_BRIGHTNESS, + ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_HS_COLOR, SUPPORT_BRIGHTNESS, SUPPORT_COLOR, SUPPORT_COLOR_TEMP, Light) DEPENDENCIES = ['homekit_controller'] diff --git a/homeassistant/components/homekit_controller/lock.py b/homeassistant/components/homekit_controller/lock.py index e27ed44452864..635d457198a8c 100644 --- a/homeassistant/components/homekit_controller/lock.py +++ b/homeassistant/components/homekit_controller/lock.py @@ -1,17 +1,11 @@ -""" -Support for HomeKit Controller locks. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/lock.homekit_controller/ -""" - +"""Support for HomeKit Controller locks.""" import logging -from homeassistant.components.homekit_controller import (HomeKitEntity, - KNOWN_ACCESSORIES) +from homeassistant.components.homekit_controller import ( + KNOWN_ACCESSORIES, HomeKitEntity) from homeassistant.components.lock import LockDevice -from homeassistant.const import (STATE_LOCKED, STATE_UNLOCKED, - ATTR_BATTERY_LEVEL) +from homeassistant.const import ( + ATTR_BATTERY_LEVEL, STATE_LOCKED, STATE_UNLOCKED) DEPENDENCIES = ['homekit_controller'] @@ -28,7 +22,7 @@ TARGET_STATE_MAP = { STATE_UNLOCKED: 0, - STATE_LOCKED: 1 + STATE_LOCKED: 1, } @@ -37,8 +31,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): if discovery_info is None: return accessory = hass.data[KNOWN_ACCESSORIES][discovery_info['serial']] - add_entities([HomeKitLock(accessory, discovery_info)], - True) + add_entities([HomeKitLock(accessory, discovery_info)], True) class HomeKitLock(HomeKitEntity, LockDevice): diff --git a/homeassistant/components/homekit_controller/switch.py b/homeassistant/components/homekit_controller/switch.py index ba4a04022f03d..daa4ede68986e 100644 --- a/homeassistant/components/homekit_controller/switch.py +++ b/homeassistant/components/homekit_controller/switch.py @@ -1,13 +1,8 @@ -""" -Support for Homekit switches. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/switch.homekit_controller/ -""" +"""Support for Homekit switches.""" import logging -from homeassistant.components.homekit_controller import (HomeKitEntity, - KNOWN_ACCESSORIES) +from homeassistant.components.homekit_controller import ( + KNOWN_ACCESSORIES, HomeKitEntity) from homeassistant.components.switch import SwitchDevice DEPENDENCIES = ['homekit_controller'] diff --git a/homeassistant/components/homematic/__init__.py b/homeassistant/components/homematic/__init__.py index 3439a23adb3bc..dba4add216d46 100644 --- a/homeassistant/components/homematic/__init__.py +++ b/homeassistant/components/homematic/__init__.py @@ -1,9 +1,4 @@ -""" -Support for HomeMatic devices. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/homematic/ -""" +"""Support for HomeMatic devices.""" from datetime import timedelta from functools import partial import logging @@ -13,13 +8,13 @@ from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_NAME, CONF_HOST, CONF_HOSTS, CONF_PASSWORD, - CONF_PLATFORM, CONF_USERNAME, CONF_SSL, CONF_VERIFY_SSL, + CONF_PLATFORM, CONF_SSL, CONF_USERNAME, CONF_VERIFY_SSL, EVENT_HOMEASSISTANT_STOP, STATE_UNKNOWN) from homeassistant.helpers import discovery import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity -REQUIREMENTS = ['pyhomematic==0.1.55'] +REQUIREMENTS = ['pyhomematic==0.1.56'] _LOGGER = logging.getLogger(__name__) @@ -109,11 +104,11 @@ HM_ATTRIBUTE_SUPPORT = { 'LOWBAT': ['battery', {0: 'High', 1: 'Low'}], 'LOW_BAT': ['battery', {0: 'High', 1: 'Low'}], - 'ERROR': ['sabotage', {0: 'No', 1: 'Yes'}], + 'ERROR': ['error', {0: 'No'}], 'ERROR_SABOTAGE': ['sabotage', {0: 'No', 1: 'Yes'}], 'SABOTAGE': ['sabotage', {0: 'No', 1: 'Yes'}], - 'RSSI_PEER': ['rssi', {}], - 'RSSI_DEVICE': ['rssi', {}], + 'RSSI_PEER': ['rssi_peer', {}], + 'RSSI_DEVICE': ['rssi_device', {}], 'VALVE_STATE': ['valve', {}], 'LEVEL': ['level', {}], 'BATTERY_STATE': ['battery', {}], diff --git a/homeassistant/components/homematic/binary_sensor.py b/homeassistant/components/homematic/binary_sensor.py index 9cfe4bbd6a757..1704411c9cc6d 100644 --- a/homeassistant/components/homematic/binary_sensor.py +++ b/homeassistant/components/homematic/binary_sensor.py @@ -1,9 +1,4 @@ -""" -Support for HomeMatic binary sensors. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/binary_sensor.homematic/ -""" +"""Support for HomeMatic binary sensors.""" import logging from homeassistant.components.binary_sensor import BinarySensorDevice diff --git a/homeassistant/components/homematic/climate.py b/homeassistant/components/homematic/climate.py index 5233501ec307e..e5eb292b4ff9d 100644 --- a/homeassistant/components/homematic/climate.py +++ b/homeassistant/components/homematic/climate.py @@ -1,14 +1,10 @@ -""" -Support for Homematic thermostats. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/climate.homematic/ -""" +"""Support for Homematic thermostats.""" import logging -from homeassistant.components.climate import ( +from homeassistant.components.climate import ClimateDevice +from homeassistant.components.climate.const import ( STATE_AUTO, STATE_MANUAL, SUPPORT_OPERATION_MODE, - SUPPORT_TARGET_TEMPERATURE, ClimateDevice) + SUPPORT_TARGET_TEMPERATURE) from homeassistant.components.homematic import ( ATTR_DISCOVER_DEVICES, HM_ATTRIBUTE_SUPPORT, HMDevice) from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS diff --git a/homeassistant/components/homematic/cover.py b/homeassistant/components/homematic/cover.py index 935743212031d..79a1afe9a0e39 100644 --- a/homeassistant/components/homematic/cover.py +++ b/homeassistant/components/homematic/cover.py @@ -1,9 +1,4 @@ -""" -The HomeMatic cover platform. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/cover.homematic/ -""" +"""Support for HomeMatic covers.""" import logging from homeassistant.components.cover import ( diff --git a/homeassistant/components/homematic/light.py b/homeassistant/components/homematic/light.py index de11c96f8b760..21b875742c4f3 100644 --- a/homeassistant/components/homematic/light.py +++ b/homeassistant/components/homematic/light.py @@ -1,15 +1,10 @@ -""" -Support for Homematic lights. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/light.homematic/ -""" +"""Support for Homematic lights.""" import logging from homeassistant.components.homematic import ATTR_DISCOVER_DEVICES, HMDevice from homeassistant.components.light import ( - ATTR_BRIGHTNESS, SUPPORT_BRIGHTNESS, ATTR_HS_COLOR, SUPPORT_COLOR, - ATTR_EFFECT, SUPPORT_EFFECT, Light) + ATTR_BRIGHTNESS, ATTR_EFFECT, ATTR_HS_COLOR, SUPPORT_BRIGHTNESS, + SUPPORT_COLOR, SUPPORT_EFFECT, Light) _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/homematic/lock.py b/homeassistant/components/homematic/lock.py index 9d9f2a28b4f71..5d857617fdef6 100644 --- a/homeassistant/components/homematic/lock.py +++ b/homeassistant/components/homematic/lock.py @@ -1,9 +1,4 @@ -""" -Support for Homematic lock. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/lock.homematic/ -""" +"""Support for Homematic locks.""" import logging from homeassistant.components.homematic import ATTR_DISCOVER_DEVICES, HMDevice diff --git a/homeassistant/components/homematic/notify.py b/homeassistant/components/homematic/notify.py index 2897123c690e5..e6ef1a60e287c 100644 --- a/homeassistant/components/homematic/notify.py +++ b/homeassistant/components/homematic/notify.py @@ -8,12 +8,12 @@ import voluptuous as vol +from homeassistant.components.homematic import ( + ATTR_ADDRESS, ATTR_CHANNEL, ATTR_INTERFACE, ATTR_PARAM, ATTR_VALUE, DOMAIN, + SERVICE_SET_DEVICE_VALUE) from homeassistant.components.notify import ( - BaseNotificationService, PLATFORM_SCHEMA, ATTR_DATA) + ATTR_DATA, PLATFORM_SCHEMA, BaseNotificationService) import homeassistant.helpers.config_validation as cv -from homeassistant.components.homematic import ( - DOMAIN, SERVICE_SET_DEVICE_VALUE, ATTR_ADDRESS, ATTR_CHANNEL, ATTR_PARAM, - ATTR_VALUE, ATTR_INTERFACE) import homeassistant.helpers.template as template_helper _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/homematic/sensor.py b/homeassistant/components/homematic/sensor.py index 84cf19652a1d1..c4d97dca3feee 100644 --- a/homeassistant/components/homematic/sensor.py +++ b/homeassistant/components/homematic/sensor.py @@ -1,9 +1,4 @@ -""" -The HomeMatic sensor platform. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/sensor.homematic/ -""" +"""Support for HomeMatic sensors.""" import logging from homeassistant.components.homematic import ATTR_DISCOVER_DEVICES, HMDevice @@ -14,26 +9,14 @@ DEPENDENCIES = ['homematic'] HM_STATE_HA_CAST = { - 'RotaryHandleSensor': {0: 'closed', - 1: 'tilted', - 2: 'open'}, - 'RotaryHandleSensorIP': {0: 'closed', - 1: 'tilted', - 2: 'open'}, - 'WaterSensor': {0: 'dry', - 1: 'wet', - 2: 'water'}, - 'CO2Sensor': {0: 'normal', - 1: 'added', - 2: 'strong'}, - 'IPSmoke': {0: 'off', - 1: 'primary', - 2: 'intrusion', - 3: 'secondary'}, - 'RFSiren': {0: 'disarmed', - 1: 'extsens_armed', - 2: 'allsens_armed', - 3: 'alarm_blocked'}, + 'RotaryHandleSensor': {0: 'closed', 1: 'tilted', 2: 'open'}, + 'RotaryHandleSensorIP': {0: 'closed', 1: 'tilted', 2: 'open'}, + 'WaterSensor': {0: 'dry', 1: 'wet', 2: 'water'}, + 'CO2Sensor': {0: 'normal', 1: 'added', 2: 'strong'}, + 'IPSmoke': {0: 'off', 1: 'primary', 2: 'intrusion', 3: 'secondary'}, + 'RFSiren': { + 0: 'disarmed', 1: 'extsens_armed', 2: 'allsens_armed', + 3: 'alarm_blocked'}, } HM_UNIT_HA_CAST = { diff --git a/homeassistant/components/homematic/switch.py b/homeassistant/components/homematic/switch.py index b5921819ea4ff..cfcd26891e018 100644 --- a/homeassistant/components/homematic/switch.py +++ b/homeassistant/components/homematic/switch.py @@ -1,9 +1,4 @@ -""" -Support for HomeMatic switches. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/switch.homematic/ -""" +"""Support for HomeMatic switches.""" import logging from homeassistant.components.homematic import ATTR_DISCOVER_DEVICES, HMDevice diff --git a/homeassistant/components/homematicip_cloud/.translations/es-419.json b/homeassistant/components/homematicip_cloud/.translations/es-419.json index 8675d6e12b119..5102b25aaee92 100644 --- a/homeassistant/components/homematicip_cloud/.translations/es-419.json +++ b/homeassistant/components/homematicip_cloud/.translations/es-419.json @@ -17,6 +17,9 @@ "name": "Nombre (opcional, usado como prefijo de nombre para todos los dispositivos)", "pin": "C\u00f3digo PIN (opcional)" } + }, + "link": { + "description": "Presione el bot\u00f3n azul en el punto de acceso y el bot\u00f3n enviar para registrar HomematicIP con Home Assistant. \n\n ! [Ubicaci\u00f3n del bot\u00f3n en el puente] (/static/images/config_flows/config_homematicip_cloud.png)" } }, "title": "HomematicIP Cloud" diff --git a/homeassistant/components/homematicip_cloud/.translations/hu.json b/homeassistant/components/homematicip_cloud/.translations/hu.json index cfb4f5e87fd26..61ff5ac5fe255 100644 --- a/homeassistant/components/homematicip_cloud/.translations/hu.json +++ b/homeassistant/components/homematicip_cloud/.translations/hu.json @@ -1,19 +1,27 @@ { "config": { "abort": { + "already_configured": "A hozz\u00e1f\u00e9r\u00e9si pontot m\u00e1r konfigur\u00e1ltuk", "connection_aborted": "Nem siker\u00fclt csatlakozni a HMIP szerverhez", "unknown": "Unknown error occurred." }, "error": { "invalid_pin": "\u00c9rv\u00e9nytelen PIN, pr\u00f3b\u00e1lkozz \u00fajra.", "press_the_button": "Nyomd meg a k\u00e9k gombot.", - "register_failed": "Regisztr\u00e1ci\u00f3 nem siker\u00fclt, pr\u00f3b\u00e1ld \u00fajra." + "register_failed": "Regisztr\u00e1ci\u00f3 nem siker\u00fclt, pr\u00f3b\u00e1ld \u00fajra.", + "timeout_button": "K\u00e9k gomb megnyom\u00e1s\u00e1nak id\u0151t\u00fall\u00e9p\u00e9se, pr\u00f3b\u00e1lkozz \u00fajra." }, "step": { "init": { "data": { + "hapid": "Hozz\u00e1f\u00e9r\u00e9si pont azonos\u00edt\u00f3ja (SGTIN)", + "name": "N\u00e9v (opcion\u00e1lis, minden eszk\u00f6z n\u00e9vel\u0151tagjak\u00e9nt haszn\u00e1latos)", "pin": "Pin k\u00f3d (opcion\u00e1lis)" - } + }, + "title": "V\u00e1lassz HomematicIP hozz\u00e1f\u00e9r\u00e9si pontot" + }, + "link": { + "title": "Link Hozz\u00e1f\u00e9r\u00e9si pont" } }, "title": "HomematicIP Felh\u0151" diff --git a/homeassistant/components/homematicip_cloud/.translations/it.json b/homeassistant/components/homematicip_cloud/.translations/it.json index 9ef1abd500c92..6e6d7c8a59fe0 100644 --- a/homeassistant/components/homematicip_cloud/.translations/it.json +++ b/homeassistant/components/homematicip_cloud/.translations/it.json @@ -2,19 +2,29 @@ "config": { "abort": { "already_configured": "Il punto di accesso \u00e8 gi\u00e0 configurato", - "connection_aborted": "Impossibile connettersi al server HMIP" + "connection_aborted": "Impossibile connettersi al server HMIP", + "unknown": "Si \u00e8 verificato un errore sconosciuto." }, "error": { "invalid_pin": "PIN non valido, riprova.", "press_the_button": "Si prega di premere il pulsante blu.", - "register_failed": "Registrazione fallita, si prega di riprovare." + "register_failed": "Registrazione fallita, si prega di riprovare.", + "timeout_button": "Timeout della pressione del pulsante blu, riprovare." }, "step": { "init": { "data": { + "hapid": "ID del punto di accesso (SGTIN)", + "name": "Nome (facoltativo, utilizzato come prefisso del nome per tutti i dispositivi)", "pin": "Codice Pin (opzionale)" - } + }, + "title": "Scegli punto di accesso HomematicIP" + }, + "link": { + "description": "Premi il pulsante blu sull'access point ed il pulsante di invio per registrare HomematicIP con Home Assistant. \n\n ![Posizione del pulsante sul bridge](/static/images/config_flows/config_homematicip_cloud.png)", + "title": "Collegamento access point" } - } + }, + "title": "HomematicIP Cloud" } } \ No newline at end of file diff --git a/homeassistant/components/homematicip_cloud/.translations/sv.json b/homeassistant/components/homematicip_cloud/.translations/sv.json index da6bde77ae305..f155e8fd1c15a 100644 --- a/homeassistant/components/homematicip_cloud/.translations/sv.json +++ b/homeassistant/components/homematicip_cloud/.translations/sv.json @@ -21,7 +21,7 @@ "title": "V\u00e4lj HomematicIP Accesspunkt" }, "link": { - "description": "Tryck p\u00e5 den bl\u00e5 knappen p\u00e5 accesspunkten och p\u00e5 skickaknappen f\u00f6r att registrera HomematicIP med Home-Assistant. \n\n ![Placering av knapp p\u00e5 bryggan](/static/images/config_flows/config_homematicip_cloud.png)", + "description": "Tryck p\u00e5 den bl\u00e5 knappen p\u00e5 accesspunkten och p\u00e5 skicka-knappen f\u00f6r att registrera HomematicIP med Home Assistant. \n\n ![Placering av knappen p\u00e5 bryggan](/static/images/config_flows/config_homematicip_cloud.png)", "title": "L\u00e4nka Accesspunkt" } }, diff --git a/homeassistant/components/homematicip_cloud/__init__.py b/homeassistant/components/homematicip_cloud/__init__.py index f048a50d1d0e4..fd07356d7fbf5 100644 --- a/homeassistant/components/homematicip_cloud/__init__.py +++ b/homeassistant/components/homematicip_cloud/__init__.py @@ -1,15 +1,11 @@ -""" -Support for HomematicIP Cloud components. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/homematicip_cloud/ -""" +"""Support for HomematicIP Cloud devices.""" import logging import voluptuous as vol from homeassistant import config_entries from homeassistant.const import CONF_NAME +from homeassistant.helpers import device_registry as dr import homeassistant.helpers.config_validation as cv from .config_flow import configured_haps @@ -57,7 +53,22 @@ async def async_setup_entry(hass, entry): hap = HomematicipHAP(hass, entry) hapid = entry.data[HMIPC_HAPID].replace('-', '').upper() hass.data[DOMAIN][hapid] = hap - return await hap.async_setup() + + if not await hap.async_setup(): + return False + + # Register hap as device in registry. + device_registry = await dr.async_get_registry(hass) + home = hap.home + device_registry.async_get_or_create( + config_entry_id=home.id, + identifiers={(DOMAIN, home.id)}, + manufacturer='eQ-3', + name=home.label, + model=home.modelType, + sw_version=home.currentAPVersion, + ) + return True async def async_unload_entry(hass, entry): diff --git a/homeassistant/components/homematicip_cloud/alarm_control_panel.py b/homeassistant/components/homematicip_cloud/alarm_control_panel.py index 3fdfc768c5250..efa1ea1f46e2c 100644 --- a/homeassistant/components/homematicip_cloud/alarm_control_panel.py +++ b/homeassistant/components/homematicip_cloud/alarm_control_panel.py @@ -1,16 +1,9 @@ -""" -Support for HomematicIP Cloud alarm control panel. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/alarm_control_panel.homematicip_cloud/ -""" - +"""Support for HomematicIP Cloud alarm control panel.""" import logging from homeassistant.components.alarm_control_panel import AlarmControlPanel from homeassistant.components.homematicip_cloud import ( - HMIPC_HAPID, HomematicipGenericDevice) -from homeassistant.components.homematicip_cloud import DOMAIN as HMIPC_DOMAIN + DOMAIN as HMIPC_DOMAIN, HMIPC_HAPID, HomematicipGenericDevice) from homeassistant.const import ( STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED, STATE_ALARM_TRIGGERED) @@ -49,7 +42,7 @@ class HomematicipSecurityZone(HomematicipGenericDevice, AlarmControlPanel): def __init__(self, home, device): """Initialize the security zone group.""" device.modelType = 'Group-SecurityZone' - device.windowState = '' + device.windowState = None super().__init__(home, device) @property @@ -59,7 +52,8 @@ def state(self): if self._device.active: if (self._device.sabotage or self._device.motionDetected or - self._device.windowState == WindowState.OPEN): + self._device.windowState == WindowState.OPEN or + self._device.windowState == WindowState.TILTED): return STATE_ALARM_TRIGGERED active = self._home.get_security_zones_activation() diff --git a/homeassistant/components/homematicip_cloud/binary_sensor.py b/homeassistant/components/homematicip_cloud/binary_sensor.py index 910666f93cb80..4b82a500bdec5 100644 --- a/homeassistant/components/homematicip_cloud/binary_sensor.py +++ b/homeassistant/components/homematicip_cloud/binary_sensor.py @@ -1,20 +1,22 @@ -""" -Support for HomematicIP Cloud binary sensor. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/binary_sensor.homematicip_cloud/ -""" +"""Support for HomematicIP Cloud binary sensor.""" import logging from homeassistant.components.binary_sensor import BinarySensorDevice from homeassistant.components.homematicip_cloud import ( - HMIPC_HAPID, HomematicipGenericDevice) -from homeassistant.components.homematicip_cloud import DOMAIN as HMIPC_DOMAIN + DOMAIN as HMIPC_DOMAIN, HMIPC_HAPID, HomematicipGenericDevice) DEPENDENCIES = ['homematicip_cloud'] _LOGGER = logging.getLogger(__name__) +ATTR_MOTIONDETECTED = 'motion detected' +ATTR_PRESENCEDETECTED = 'presence detected' +ATTR_POWERMAINSFAILURE = 'power mains failure' +ATTR_WINDOWSTATE = 'window state' +ATTR_MOISTUREDETECTED = 'moisture detected' +ATTR_WATERLEVELDETECTED = 'water level detected' +ATTR_SMOKEDETECTORALARM = 'smoke detector alarm' + async def async_setup_platform( hass, config, async_add_entities, discovery_info=None): @@ -29,6 +31,9 @@ async def async_setup_entry(hass, config_entry, async_add_entities): AsyncWaterSensor, AsyncRotaryHandleSensor, AsyncMotionDetectorPushButton) + from homematicip.group import ( + SecurityGroup, SecurityZoneGroup) + home = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]].home devices = [] for device in home.devices: @@ -42,6 +47,12 @@ async def async_setup_entry(hass, config_entry, async_add_entities): elif isinstance(device, AsyncWaterSensor): devices.append(HomematicipWaterDetector(home, device)) + for group in home.groups: + if isinstance(group, SecurityGroup): + devices.append(HomematicipSecuritySensorGroup(home, group)) + elif isinstance(group, SecurityZoneGroup): + devices.append(HomematicipSecurityZoneSensorGroup(home, group)) + if devices: async_add_entities(devices) @@ -110,3 +121,91 @@ def device_class(self): def is_on(self): """Return true if moisture or waterlevel is detected.""" return self._device.moistureDetected or self._device.waterlevelDetected + + +class HomematicipSecurityZoneSensorGroup(HomematicipGenericDevice, + BinarySensorDevice): + """Representation of a HomematicIP Cloud security zone group.""" + + def __init__(self, home, device, post='SecurityZone'): + """Initialize security zone group.""" + device.modelType = 'HmIP-{}'.format(post) + super().__init__(home, device, post) + + @property + def device_class(self): + """Return the class of this sensor.""" + return 'safety' + + @property + def device_state_attributes(self): + """Return the state attributes of the security zone group.""" + attr = super().device_state_attributes + + if self._device.motionDetected: + attr.update({ATTR_MOTIONDETECTED: True}) + if self._device.presenceDetected: + attr.update({ATTR_PRESENCEDETECTED: True}) + from homematicip.base.enums import WindowState + if self._device.windowState is not None and \ + self._device.windowState != WindowState.CLOSED: + attr.update({ATTR_WINDOWSTATE: str(self._device.windowState)}) + + return attr + + @property + def is_on(self): + """Return true if security issue detected.""" + if self._device.motionDetected or \ + self._device.presenceDetected: + return True + from homematicip.base.enums import WindowState + if self._device.windowState is not None and \ + self._device.windowState != WindowState.CLOSED: + return True + return False + + +class HomematicipSecuritySensorGroup(HomematicipSecurityZoneSensorGroup, + BinarySensorDevice): + """Representation of a HomematicIP security group.""" + + def __init__(self, home, device): + """Initialize security group.""" + super().__init__(home, device, 'Sensors') + + @property + def device_state_attributes(self): + """Return the state attributes of the security group.""" + attr = super().device_state_attributes + + if self._device.powerMainsFailure: + attr.update({ATTR_POWERMAINSFAILURE: True}) + if self._device.moistureDetected: + attr.update({ATTR_MOISTUREDETECTED: True}) + if self._device.waterlevelDetected: + attr.update({ATTR_WATERLEVELDETECTED: True}) + from homematicip.base.enums import SmokeDetectorAlarmType + if self._device.smokeDetectorAlarmType is not None and \ + self._device.smokeDetectorAlarmType != \ + SmokeDetectorAlarmType.IDLE_OFF: + attr.update({ATTR_SMOKEDETECTORALARM: str( + self._device.smokeDetectorAlarmType)}) + + return attr + + @property + def is_on(self): + """Return true if security issue detected.""" + parent_is_on = super().is_on + from homematicip.base.enums import SmokeDetectorAlarmType + if parent_is_on or \ + self._device.powerMainsFailure or \ + self._device.moistureDetected or \ + self._device.waterlevelDetected: + return True + if self._device.smokeDetectorAlarmType is not None and \ + self._device.smokeDetectorAlarmType != \ + SmokeDetectorAlarmType.IDLE_OFF: + return True + return False diff --git a/homeassistant/components/homematicip_cloud/climate.py b/homeassistant/components/homematicip_cloud/climate.py index 966cd95ade187..08c88bbb796bc 100644 --- a/homeassistant/components/homematicip_cloud/climate.py +++ b/homeassistant/components/homematicip_cloud/climate.py @@ -1,18 +1,12 @@ -""" -Support for HomematicIP Cloud climate devices. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/climate.homematicip_cloud/ -""" +"""Support for HomematicIP Cloud climate devices.""" import logging -from homeassistant.components.climate import ( - ATTR_TEMPERATURE, STATE_AUTO, STATE_MANUAL, SUPPORT_TARGET_TEMPERATURE, - ClimateDevice) +from homeassistant.components.climate import ClimateDevice +from homeassistant.components.climate.const import ( + STATE_AUTO, STATE_MANUAL, SUPPORT_TARGET_TEMPERATURE) from homeassistant.components.homematicip_cloud import ( - HMIPC_HAPID, HomematicipGenericDevice) -from homeassistant.components.homematicip_cloud import DOMAIN as HMIPC_DOMAIN -from homeassistant.const import TEMP_CELSIUS + DOMAIN as HMIPC_DOMAIN, HMIPC_HAPID, HomematicipGenericDevice) +from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/homematicip_cloud/config_flow.py b/homeassistant/components/homematicip_cloud/config_flow.py index ea251a3bf8798..458186bcce1cc 100644 --- a/homeassistant/components/homematicip_cloud/config_flow.py +++ b/homeassistant/components/homematicip_cloud/config_flow.py @@ -4,9 +4,9 @@ from homeassistant import config_entries from homeassistant.core import callback -from .const import DOMAIN as HMIPC_DOMAIN -from .const import HMIPC_AUTHTOKEN, HMIPC_HAPID, HMIPC_NAME, HMIPC_PIN -from .const import _LOGGER +from .const import ( + _LOGGER, DOMAIN as HMIPC_DOMAIN, HMIPC_AUTHTOKEN, HMIPC_HAPID, HMIPC_NAME, + HMIPC_PIN) from .hap import HomematicipAuth diff --git a/homeassistant/components/homematicip_cloud/cover.py b/homeassistant/components/homematicip_cloud/cover.py index 80fc8f7b43065..86c11dab70dd3 100644 --- a/homeassistant/components/homematicip_cloud/cover.py +++ b/homeassistant/components/homematicip_cloud/cover.py @@ -1,15 +1,9 @@ -""" -Support for HomematicIP Cloud cover devices. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/cover.homematicip_cloud/ -""" +"""Support for HomematicIP Cloud cover devices.""" import logging -from homeassistant.components.cover import ( - ATTR_POSITION, CoverDevice) +from homeassistant.components.cover import ATTR_POSITION, CoverDevice from homeassistant.components.homematicip_cloud import ( - HMIPC_HAPID, HomematicipGenericDevice, DOMAIN as HMIPC_DOMAIN) + DOMAIN as HMIPC_DOMAIN, HMIPC_HAPID, HomematicipGenericDevice) DEPENDENCIES = ['homematicip_cloud'] @@ -45,7 +39,7 @@ class HomematicipCoverShutter(HomematicipGenericDevice, CoverDevice): @property def current_cover_position(self): """Return current position of cover.""" - return int(self._device.shutterLevel * 100) + return int((1 - self._device.shutterLevel) * 100) async def async_set_cover_position(self, **kwargs): """Move the cover to a specific position.""" diff --git a/homeassistant/components/homematicip_cloud/device.py b/homeassistant/components/homematicip_cloud/device.py index c43f0e24e2b6a..85cc3c0c77af9 100644 --- a/homeassistant/components/homematicip_cloud/device.py +++ b/homeassistant/components/homematicip_cloud/device.py @@ -1,6 +1,7 @@ """Generic device for the HomematicIP Cloud component.""" import logging +from homeassistant.components import homematicip_cloud from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -32,6 +33,25 @@ def __init__(self, home, device, post=None): self.post = post _LOGGER.info("Setting up %s (%s)", self.name, self._device.modelType) + @property + def device_info(self): + """Return device specific attributes.""" + from homematicip.aio.device import AsyncDevice + # Only physical devices should be HA devices. + if isinstance(self._device, AsyncDevice): + return { + 'identifiers': { + # Serial numbers of Homematic IP device + (homematicip_cloud.DOMAIN, self._device.id) + }, + 'name': self._device.label, + 'manufacturer': self._device.oem, + 'model': self._device.modelType, + 'sw_version': self._device.firmwareVersion, + 'via_hub': (homematicip_cloud.DOMAIN, self._device.homeId), + } + return None + async def async_added_to_hass(self): """Register callbacks.""" self._device.on_update(self._device_changed) diff --git a/homeassistant/components/homematicip_cloud/hap.py b/homeassistant/components/homematicip_cloud/hap.py index 9af6669652d9c..64721c0a96c5b 100644 --- a/homeassistant/components/homematicip_cloud/hap.py +++ b/homeassistant/components/homematicip_cloud/hap.py @@ -2,8 +2,8 @@ import asyncio import logging -from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.core import callback +from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import ( @@ -39,8 +39,7 @@ async def async_checkbutton(self): from homematicip.base.base_connection import HmipConnectionError try: - await self.auth.isRequestAcknowledged() - return True + return await self.auth.isRequestAcknowledged() except HmipConnectionError: return False diff --git a/homeassistant/components/homematicip_cloud/light.py b/homeassistant/components/homematicip_cloud/light.py index 5d604d2c66568..73c607683ba6b 100644 --- a/homeassistant/components/homematicip_cloud/light.py +++ b/homeassistant/components/homematicip_cloud/light.py @@ -1,9 +1,4 @@ -""" -Support for HomematicIP Cloud lights. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/light.homematicip_cloud/ -""" +"""Support for HomematicIP Cloud lights.""" import logging from homeassistant.components.homematicip_cloud import ( diff --git a/homeassistant/components/homematicip_cloud/sensor.py b/homeassistant/components/homematicip_cloud/sensor.py index 911c00e45bc13..d755735e0e04d 100644 --- a/homeassistant/components/homematicip_cloud/sensor.py +++ b/homeassistant/components/homematicip_cloud/sensor.py @@ -1,16 +1,11 @@ -""" -Support for HomematicIP Cloud sensors. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/sensor.homematicip_cloud/ -""" +"""Support for HomematicIP Cloud sensors.""" import logging from homeassistant.components.homematicip_cloud import ( DOMAIN as HMIPC_DOMAIN, HMIPC_HAPID, HomematicipGenericDevice) from homeassistant.const import ( - DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_ILLUMINANCE, - DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS) + DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_ILLUMINANCE, DEVICE_CLASS_TEMPERATURE, + TEMP_CELSIUS) _LOGGER = logging.getLogger(__name__) @@ -70,6 +65,17 @@ def __init__(self, home): """Initialize access point device.""" super().__init__(home, home) + @property + def device_info(self): + """Return device specific attributes.""" + # Adds a sensor to the existing HAP device + return { + 'identifiers': { + # Serial numbers of Homematic IP device + (HMIPC_DOMAIN, self._device.id) + } + } + @property def icon(self): """Return the icon of the access point device.""" diff --git a/homeassistant/components/homematicip_cloud/switch.py b/homeassistant/components/homematicip_cloud/switch.py index a1b3e1789bf10..f129febb5e7cb 100644 --- a/homeassistant/components/homematicip_cloud/switch.py +++ b/homeassistant/components/homematicip_cloud/switch.py @@ -1,14 +1,8 @@ -""" -Support for HomematicIP Cloud switch. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/switch.homematicip_cloud/ -""" +"""Support for HomematicIP Cloud switches.""" import logging from homeassistant.components.homematicip_cloud import ( - HMIPC_HAPID, HomematicipGenericDevice) -from homeassistant.components.homematicip_cloud import DOMAIN as HMIPC_DOMAIN + DOMAIN as HMIPC_DOMAIN, HMIPC_HAPID, HomematicipGenericDevice) from homeassistant.components.switch import SwitchDevice DEPENDENCIES = ['homematicip_cloud'] @@ -28,26 +22,37 @@ async def async_setup_platform( async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the HomematicIP switch from a config entry.""" - from homematicip.device import ( - PlugableSwitch, - PlugableSwitchMeasuring, - BrandSwitchMeasuring, - FullFlushSwitchMeasuring, + from homematicip.aio.device import ( + AsyncPlugableSwitch, + AsyncPlugableSwitchMeasuring, + AsyncBrandSwitchMeasuring, + AsyncFullFlushSwitchMeasuring, + AsyncOpenCollector8Module, ) + from homematicip.group import SwitchingGroup + home = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]].home devices = [] for device in home.devices: - if isinstance(device, BrandSwitchMeasuring): + if isinstance(device, AsyncBrandSwitchMeasuring): # BrandSwitchMeasuring inherits PlugableSwitchMeasuring # This device is implemented in the light platform and will # not be added in the switch platform pass - elif isinstance(device, (PlugableSwitchMeasuring, - FullFlushSwitchMeasuring)): + elif isinstance(device, (AsyncPlugableSwitchMeasuring, + AsyncFullFlushSwitchMeasuring)): devices.append(HomematicipSwitchMeasuring(home, device)) - elif isinstance(device, PlugableSwitch): + elif isinstance(device, AsyncPlugableSwitch): devices.append(HomematicipSwitch(home, device)) + elif isinstance(device, AsyncOpenCollector8Module): + for channel in range(1, 9): + devices.append(HomematicipMultiSwitch(home, device, channel)) + + for group in home.groups: + if isinstance(group, SwitchingGroup): + devices.append( + HomematicipGroupSwitch(home, group)) if devices: async_add_entities(devices) @@ -74,6 +79,28 @@ async def async_turn_off(self, **kwargs): await self._device.turn_off() +class HomematicipGroupSwitch(HomematicipGenericDevice, SwitchDevice): + """representation of a HomematicIP switching group.""" + + def __init__(self, home, device, post='Group'): + """Initialize switching group.""" + device.modelType = 'HmIP-{}'.format(post) + super().__init__(home, device, post) + + @property + def is_on(self): + """Return true if group is on.""" + return self._device.on + + async def async_turn_on(self, **kwargs): + """Turn the group on.""" + await self._device.turn_on() + + async def async_turn_off(self, **kwargs): + """Turn the group off.""" + await self._device.turn_off() + + class HomematicipSwitchMeasuring(HomematicipSwitch): """Representation of a HomematicIP measuring switch device.""" @@ -88,3 +115,31 @@ def today_energy_kwh(self): if self._device.energyCounter is None: return 0 return round(self._device.energyCounter) + + +class HomematicipMultiSwitch(HomematicipGenericDevice, SwitchDevice): + """Representation of a HomematicIP Cloud multi switch device.""" + + def __init__(self, home, device, channel): + """Initialize the multi switch device.""" + self.channel = channel + super().__init__(home, device, 'Channel{}'.format(channel)) + + @property + def unique_id(self): + """Return a unique ID.""" + return "{}_{}_{}".format(self.__class__.__name__, + self.post, self._device.id) + + @property + def is_on(self): + """Return true if device is on.""" + return self._device.functionalChannels[self.channel].on + + async def async_turn_on(self, **kwargs): + """Turn the device on.""" + await self._device.turn_on(self.channel) + + async def async_turn_off(self, **kwargs): + """Turn the device off.""" + await self._device.turn_off(self.channel) diff --git a/homeassistant/components/homeworks/__init__.py b/homeassistant/components/homeworks/__init__.py index b0510cfe9b570..d0769ed25e619 100644 --- a/homeassistant/components/homeworks/__init__.py +++ b/homeassistant/components/homeworks/__init__.py @@ -1,19 +1,15 @@ -"""Component for interfacing to Lutron Homeworks Series 4 and 8 systems. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/homeworks/ -""" +"""Support for Lutron Homeworks Series 4 and 8 systems.""" import logging import voluptuous as vol -from homeassistant.core import callback from homeassistant.const import ( CONF_HOST, CONF_ID, CONF_NAME, CONF_PORT, EVENT_HOMEASSISTANT_STOP) +from homeassistant.core import callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.discovery import load_platform from homeassistant.helpers.dispatcher import ( - dispatcher_send, async_dispatcher_connect) + async_dispatcher_connect, dispatcher_send) from homeassistant.util import slugify REQUIREMENTS = ['pyhomeworks==0.0.6'] @@ -39,7 +35,7 @@ DIMMER_SCHEMA = vol.Schema({ vol.Required(CONF_ADDR): cv.string, vol.Required(CONF_NAME): cv.string, - vol.Optional(CONF_RATE, default=FADE_RATE): CV_FADE_RATE + vol.Optional(CONF_RATE, default=FADE_RATE): CV_FADE_RATE, }) KEYPAD_SCHEMA = vol.Schema({ @@ -52,8 +48,8 @@ vol.Required(CONF_HOST): cv.string, vol.Required(CONF_PORT): cv.port, vol.Required(CONF_DIMMERS): vol.All(cv.ensure_list, [DIMMER_SCHEMA]), - vol.Optional(CONF_KEYPADS, default=[]): vol.All(cv.ensure_list, - [KEYPAD_SCHEMA]), + vol.Optional(CONF_KEYPADS, default=[]): + vol.All(cv.ensure_list, [KEYPAD_SCHEMA]), }), }, extra=vol.ALLOW_EXTRA) diff --git a/homeassistant/components/homeworks/light.py b/homeassistant/components/homeworks/light.py index 3ba5f805c5289..7f5d7f6aab7e0 100644 --- a/homeassistant/components/homeworks/light.py +++ b/homeassistant/components/homeworks/light.py @@ -1,19 +1,14 @@ -"""Component for interfacing to Lutron Homeworks lights. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/light.homeworks/ -""" +"""Support for Lutron Homeworks lights.""" import logging from homeassistant.components.homeworks import ( - HomeworksDevice, HOMEWORKS_CONTROLLER, ENTITY_SIGNAL, - CONF_DIMMERS, CONF_ADDR, CONF_RATE) + CONF_ADDR, CONF_DIMMERS, CONF_RATE, ENTITY_SIGNAL, HOMEWORKS_CONTROLLER, + HomeworksDevice) from homeassistant.components.light import ( ATTR_BRIGHTNESS, SUPPORT_BRIGHTNESS, Light) from homeassistant.const import CONF_NAME from homeassistant.core import callback -from homeassistant.helpers.dispatcher import ( - async_dispatcher_connect) +from homeassistant.helpers.dispatcher import async_dispatcher_connect DEPENDENCIES = ['homeworks'] diff --git a/homeassistant/components/http/__init__.py b/homeassistant/components/http/__init__.py index 02b9affefd417..4928ae2ab17e5 100644 --- a/homeassistant/components/http/__init__.py +++ b/homeassistant/components/http/__init__.py @@ -1,9 +1,4 @@ -""" -This module provides WSGI application to serve the Home Assistant API. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/http/ -""" +"""Support to serve the Home Assistant API as WSGI application.""" from ipaddress import ip_network import logging import os @@ -18,17 +13,15 @@ EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, SERVER_PORT) import homeassistant.helpers.config_validation as cv import homeassistant.util as hass_util -from homeassistant.util.logging import HideSensitiveDataFilter from homeassistant.util import ssl as ssl_util +from homeassistant.util.logging import HideSensitiveDataFilter from .auth import setup_auth from .ban import setup_bans +from .const import KEY_AUTHENTICATED, KEY_REAL_IP # noqa from .cors import setup_cors from .real_ip import setup_real_ip -from .static import CachingFileResponse, CachingStaticResource - -# Import as alias -from .const import KEY_AUTHENTICATED, KEY_REAL_IP # noqa +from .static import CACHE_HEADERS, CachingStaticResource from .view import HomeAssistantView # noqa REQUIREMENTS = ['aiohttp_cors==0.7.0'] @@ -59,6 +52,20 @@ DEFAULT_DEVELOPMENT = '0' NO_LOGIN_ATTEMPT_THRESHOLD = -1 + +def trusted_networks_deprecated(value): + """Warn user trusted_networks config is deprecated.""" + if not value: + return value + + _LOGGER.warning( + "Configuring trusted_networks via the http component has been" + " deprecated. Use the trusted networks auth provider instead." + " For instructions, see https://www.home-assistant.io/docs/" + "authentication/providers/#trusted-networks") + return value + + HTTP_SCHEMA = vol.Schema({ vol.Optional(CONF_API_PASSWORD): cv.string, vol.Optional(CONF_SERVER_HOST, default=DEFAULT_SERVER_HOST): cv.string, @@ -73,7 +80,7 @@ vol.Inclusive(CONF_TRUSTED_PROXIES, 'proxy'): vol.All(cv.ensure_list, [ip_network]), vol.Optional(CONF_TRUSTED_NETWORKS, default=[]): - vol.All(cv.ensure_list, [ip_network]), + vol.All(cv.ensure_list, [ip_network], trusted_networks_deprecated), vol.Optional(CONF_LOGIN_ATTEMPTS_THRESHOLD, default=NO_LOGIN_ATTEMPT_THRESHOLD): vol.Any(cv.positive_int, NO_LOGIN_ATTEMPT_THRESHOLD), @@ -278,25 +285,13 @@ def register_static_path(self, url_path, path, cache_headers=True): if cache_headers: async def serve_file(request): """Serve file from disk.""" - return CachingFileResponse(path) + return web.FileResponse(path, headers=CACHE_HEADERS) else: async def serve_file(request): """Serve file from disk.""" return web.FileResponse(path) - # aiohttp supports regex matching for variables. Using that as temp - # to work around cache busting MD5. - # Turns something like /static/dev-panel.html into - # /static/{filename:dev-panel(-[a-z0-9]{32}|)\.html} - base, ext = os.path.splitext(url_path) - if ext: - base, file = base.rsplit('/', 1) - regex = r"{}(-[a-z0-9]{{32}}|){}".format(file, ext) - url_pattern = "{}/{{filename:{}}}".format(base, regex) - else: - url_pattern = url_path - - self.app.router.add_route('GET', url_pattern, serve_file) + self.app.router.add_route('GET', url_path, serve_file) async def start(self): """Start the aiohttp server.""" diff --git a/homeassistant/components/http/auth.py b/homeassistant/components/http/auth.py index a515fcd198eaf..312fc2164c37e 100644 --- a/homeassistant/components/http/auth.py +++ b/homeassistant/components/http/auth.py @@ -1,5 +1,4 @@ """Authentication for HTTP component.""" - import base64 import hmac import logging @@ -8,20 +7,20 @@ from aiohttp.web import middleware import jwt -from homeassistant.core import callback -from homeassistant.const import HTTP_HEADER_HA_AUTH from homeassistant.auth.providers import legacy_api_password from homeassistant.auth.util import generate_secret +from homeassistant.const import HTTP_HEADER_HA_AUTH +from homeassistant.core import callback from homeassistant.util import dt as dt_util from .const import KEY_AUTHENTICATED, KEY_REAL_IP +_LOGGER = logging.getLogger(__name__) + DATA_API_PASSWORD = 'api_password' DATA_SIGN_SECRET = 'http.auth.sign_secret' SIGN_QUERY_PARAM = 'authSig' -_LOGGER = logging.getLogger(__name__) - @callback def async_sign_path(hass, refresh_token_id, path, expiration): diff --git a/homeassistant/components/http/ban.py b/homeassistant/components/http/ban.py index 0d748c91c6635..92c41157a3350 100644 --- a/homeassistant/components/http/ban.py +++ b/homeassistant/components/http/ban.py @@ -9,11 +9,12 @@ from aiohttp.web_exceptions import HTTPForbidden, HTTPUnauthorized import voluptuous as vol -from homeassistant.core import callback, HomeAssistant from homeassistant.config import load_yaml_config_file +from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError import homeassistant.helpers.config_validation as cv from homeassistant.util.yaml import dump + from .const import KEY_REAL_IP _LOGGER = logging.getLogger(__name__) @@ -26,7 +27,7 @@ NOTIFICATION_ID_LOGIN = 'http-login' IP_BANS_FILE = 'ip_bans.yaml' -ATTR_BANNED_AT = "banned_at" +ATTR_BANNED_AT = 'banned_at' SCHEMA_IP_BAN_ENTRY = vol.Schema({ vol.Optional('banned_at'): vol.Any(None, cv.datetime) @@ -52,7 +53,7 @@ async def ban_startup(app): async def ban_middleware(request, handler): """IP Ban middleware.""" if KEY_BANNED_IPS not in request.app: - _LOGGER.error('IP Ban middleware loaded but banned IPs not loaded') + _LOGGER.error("IP Ban middleware loaded but banned IPs not loaded") return await handler(request) # Verify if IP is not banned diff --git a/homeassistant/components/http/cors.py b/homeassistant/components/http/cors.py index 5698c6048e34d..6da3b0e51d7bd 100644 --- a/homeassistant/components/http/cors.py +++ b/homeassistant/components/http/cors.py @@ -1,15 +1,10 @@ """Provide CORS support for the HTTP component.""" - - -from aiohttp.hdrs import ACCEPT, ORIGIN, CONTENT_TYPE +from aiohttp.hdrs import ACCEPT, CONTENT_TYPE, ORIGIN from homeassistant.const import ( - HTTP_HEADER_X_REQUESTED_WITH, HTTP_HEADER_HA_AUTH) - - + HTTP_HEADER_HA_AUTH, HTTP_HEADER_X_REQUESTED_WITH) from homeassistant.core import callback - ALLOWED_CORS_HEADERS = [ ORIGIN, ACCEPT, HTTP_HEADER_X_REQUESTED_WITH, CONTENT_TYPE, HTTP_HEADER_HA_AUTH] diff --git a/homeassistant/components/http/data_validator.py b/homeassistant/components/http/data_validator.py index 8fc7cd8e658db..98686e5cabd10 100644 --- a/homeassistant/components/http/data_validator.py +++ b/homeassistant/components/http/data_validator.py @@ -1,5 +1,4 @@ """Decorator for view methods to help with data validation.""" - from functools import wraps import logging diff --git a/homeassistant/components/http/real_ip.py b/homeassistant/components/http/real_ip.py index 27a8550ab8cac..9bbf30bd9d17b 100644 --- a/homeassistant/components/http/real_ip.py +++ b/homeassistant/components/http/real_ip.py @@ -1,9 +1,8 @@ """Middleware to fetch real IP.""" - from ipaddress import ip_address -from aiohttp.web import middleware from aiohttp.hdrs import X_FORWARDED_FOR +from aiohttp.web import middleware from homeassistant.core import callback diff --git a/homeassistant/components/http/static.py b/homeassistant/components/http/static.py index 54e72c88ff30f..4fac9bf1ae904 100644 --- a/homeassistant/components/http/static.py +++ b/homeassistant/components/http/static.py @@ -1,18 +1,29 @@ """Static file handling for HTTP component.""" +from pathlib import Path + from aiohttp import hdrs from aiohttp.web import FileResponse -from aiohttp.web_exceptions import HTTPNotFound +from aiohttp.web_exceptions import HTTPNotFound, HTTPForbidden from aiohttp.web_urldispatcher import StaticResource -from yarl import URL + +CACHE_TIME = 31 * 86400 # = 1 month +CACHE_HEADERS = {hdrs.CACHE_CONTROL: "public, max-age={}".format(CACHE_TIME)} +# https://github.com/PyCQA/astroid/issues/633 +# pylint: disable=duplicate-bases class CachingStaticResource(StaticResource): """Static Resource handler that will add cache headers.""" async def _handle(self, request): - filename = URL(request.match_info['filename']).path + rel_url = request.match_info['filename'] try: - # PyLint is wrong about resolve not being a member. + filename = Path(rel_url) + if filename.anchor: + # rel_url is an absolute name like + # /static/\\machine_name\c$ or /static/D:\path + # where the static dir is totally different + raise HTTPForbidden() filepath = self._directory.joinpath(filename).resolve() if not self._follow_symlinks: filepath.relative_to(self._directory) @@ -24,30 +35,10 @@ async def _handle(self, request): request.app.logger.exception(error) raise HTTPNotFound() from error + # on opening a dir, load its contents if allowed if filepath.is_dir(): return await super()._handle(request) if filepath.is_file(): - return CachingFileResponse(filepath, chunk_size=self._chunk_size) + return FileResponse( + filepath, chunk_size=self._chunk_size, headers=CACHE_HEADERS) raise HTTPNotFound - - -# pylint: disable=too-many-ancestors -class CachingFileResponse(FileResponse): - """FileSender class that caches output if not in dev mode.""" - - def __init__(self, *args, **kwargs): - """Initialize the hass file sender.""" - super().__init__(*args, **kwargs) - - orig_sendfile = self._sendfile - - async def sendfile(request, fobj, count): - """Sendfile that includes a cache header.""" - cache_time = 31 * 86400 # = 1 month - self.headers[hdrs.CACHE_CONTROL] = "public, max-age={}".format( - cache_time) - - await orig_sendfile(request, fobj, count) - - # Overwriting like this because __init__ can change implementation. - self._sendfile = sendfile diff --git a/homeassistant/components/http/view.py b/homeassistant/components/http/view.py index beb5c647266f9..9662f3e6c23f5 100644 --- a/homeassistant/components/http/view.py +++ b/homeassistant/components/http/view.py @@ -1,27 +1,21 @@ -""" -This module provides WSGI application to serve the Home Assistant API. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/http/ -""" +"""Support for views.""" import asyncio import json import logging from aiohttp import web from aiohttp.web_exceptions import ( - HTTPUnauthorized, HTTPInternalServerError, HTTPBadRequest) + HTTPBadRequest, HTTPInternalServerError, HTTPUnauthorized) import voluptuous as vol +from homeassistant import exceptions from homeassistant.components.http.ban import process_success_login -from homeassistant.core import Context, is_callback from homeassistant.const import CONTENT_TYPE_JSON -from homeassistant import exceptions +from homeassistant.core import Context, is_callback from homeassistant.helpers.json import JSONEncoder from .const import KEY_AUTHENTICATED, KEY_REAL_IP - _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/huawei_lte/__init__.py b/homeassistant/components/huawei_lte/__init__.py index 9d223df3344b9..2ff21c4d5a7c9 100644 --- a/homeassistant/components/huawei_lte/__init__.py +++ b/homeassistant/components/huawei_lte/__init__.py @@ -1,9 +1,4 @@ -""" -Support for Huawei LTE routers. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/huawei_lte/ -""" +"""Support for Huawei LTE routers.""" from datetime import timedelta from functools import reduce import logging @@ -18,7 +13,6 @@ from homeassistant.helpers import config_validation as cv from homeassistant.util import Throttle - _LOGGER = logging.getLogger(__name__) # dicttoxml (used by huawei-lte-api) has uselessly verbose INFO level. diff --git a/homeassistant/components/huawei_lte/device_tracker.py b/homeassistant/components/huawei_lte/device_tracker.py index d30a413898fcd..69bf42fb3fe5a 100644 --- a/homeassistant/components/huawei_lte/device_tracker.py +++ b/homeassistant/components/huawei_lte/device_tracker.py @@ -1,9 +1,4 @@ -""" -Support for Huawei LTE routers. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/device_tracker.huawei_lte/ -""" +"""Support for device tracking of Huawei LTE routers.""" from typing import Any, Dict, List, Optional import attr @@ -16,7 +11,6 @@ from homeassistant.const import CONF_URL from ..huawei_lte import DATA_KEY, RouterData - DEPENDENCIES = ['huawei_lte'] PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ diff --git a/homeassistant/components/huawei_lte/notify.py b/homeassistant/components/huawei_lte/notify.py index a406a7ec2d827..5e20a774c25d2 100644 --- a/homeassistant/components/huawei_lte/notify.py +++ b/homeassistant/components/huawei_lte/notify.py @@ -1,9 +1,4 @@ -"""Huawei LTE router platform for notify component. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/notify.huawei_lte/ -""" - +"""Support for Huawei LTE router notifications.""" import logging import voluptuous as vol @@ -16,7 +11,6 @@ from ..huawei_lte import DATA_KEY - DEPENDENCIES = ['huawei_lte'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/huawei_lte/sensor.py b/homeassistant/components/huawei_lte/sensor.py index ae3760455441d..42ad4b52f8d81 100644 --- a/homeassistant/components/huawei_lte/sensor.py +++ b/homeassistant/components/huawei_lte/sensor.py @@ -1,9 +1,4 @@ -"""Huawei LTE sensors. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/sensor.huawei_lte/ -""" - +"""Support for Huawei LTE sensors.""" import logging import re @@ -19,7 +14,6 @@ from ..huawei_lte import DATA_KEY, RouterData - _LOGGER = logging.getLogger(__name__) DEPENDENCIES = ['huawei_lte'] @@ -118,8 +112,8 @@ def setup_platform( sensors = [] for path in config.get(CONF_MONITORED_CONDITIONS): data.subscribe(path) - sensors.append(HuaweiLteSensor( - data, path, SENSOR_META.get(path, {}))) + sensors.append(HuaweiLteSensor(data, path, SENSOR_META.get(path, {}))) + add_entities(sensors, True) diff --git a/homeassistant/components/hue/.translations/sv.json b/homeassistant/components/hue/.translations/sv.json index efbcfa544f5d8..a7ffc7bacb2b6 100644 --- a/homeassistant/components/hue/.translations/sv.json +++ b/homeassistant/components/hue/.translations/sv.json @@ -24,6 +24,6 @@ "title": "L\u00e4nka hub" } }, - "title": "Philips Hue Brygga" + "title": "Philips Hue Bridge" } } \ No newline at end of file diff --git a/homeassistant/components/hue/__init__.py b/homeassistant/components/hue/__init__.py index 0871d961a933e..8f5c27f6516e8 100644 --- a/homeassistant/components/hue/__init__.py +++ b/homeassistant/components/hue/__init__.py @@ -1,9 +1,4 @@ -""" -This component provides basic support for the Philips Hue system. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/hue/ -""" +"""Support for the Philips Hue system.""" import ipaddress import logging @@ -19,7 +14,7 @@ # Loading the config flow file will register the flow from .config_flow import configured_hosts -REQUIREMENTS = ['aiohue==1.9.0'] +REQUIREMENTS = ['aiohue==1.9.1'] _LOGGER = logging.getLogger(__name__) @@ -126,11 +121,17 @@ async def async_setup_entry(hass, entry): }, manufacturer='Signify', name=config.name, - # Not yet exposed as properties in aiohue - model=config.raw['modelid'], - sw_version=config.raw['swversion'], + model=config.modelid, + sw_version=config.swversion, ) + if config.swupdate2_bridge_state == "readytoinstall": + err = ( + "Please check for software updates of the bridge " + "in the Philips Hue App." + ) + _LOGGER.warning(err) + return True diff --git a/homeassistant/components/hue/bridge.py b/homeassistant/components/hue/bridge.py index 6e3d818db6826..9df5b0a6730cb 100644 --- a/homeassistant/components/hue/bridge.py +++ b/homeassistant/components/hue/bridge.py @@ -46,7 +46,7 @@ async def async_setup(self, tries=0): self.api = await get_bridge( hass, host, self.config_entry.data['username']) except AuthenticationRequired: - # usernames can become invalid if hub is reset or user removed. + # Usernames can become invalid if hub is reset or user removed. # We are going to fail the config entry setup and initiate a new # linking procedure. When linking succeeds, it will remove the # old config entry. diff --git a/homeassistant/components/hue/light.py b/homeassistant/components/hue/light.py index 51e50f629b504..0725c86bd954e 100644 --- a/homeassistant/components/hue/light.py +++ b/homeassistant/components/hue/light.py @@ -1,9 +1,4 @@ -""" -This component provides light support for the Philips Hue system. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/light.hue/ -""" +"""Support for the Philips Hue lights.""" import asyncio from datetime import timedelta import logging @@ -37,8 +32,8 @@ 'Color light': SUPPORT_HUE_COLOR, 'Dimmable light': SUPPORT_HUE_DIMMABLE, 'On/Off plug-in unit': SUPPORT_HUE_ON_OFF, - 'Color temperature light': SUPPORT_HUE_COLOR_TEMP - } + 'Color temperature light': SUPPORT_HUE_COLOR_TEMP, +} ATTR_IS_HUE_GROUP = 'is_hue_group' GAMUT_TYPE_UNAVAILABLE = 'None' @@ -49,8 +44,8 @@ GROUP_MIN_API_VERSION = (1, 13, 0) -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): """Old way of setting up Hue lights. Can only be called when a user accidentally mentions hue platform in their @@ -232,8 +227,8 @@ def __init__(self, light, request_bridge_update, bridge, is_group=False): _LOGGER.debug("Color gamut of %s: %s", self.name, str(self.gamut)) if self.light.swupdatestate == "readytoinstall": err = ( - "Please check for software updates of the bridge " - "and/or the bulb: %s, in the Philips Hue App." + "Please check for software updates of the %s " + "bulb in the Philips Hue App." ) _LOGGER.warning(err, self.name) if self.gamut: diff --git a/homeassistant/components/hydrawise/__init__.py b/homeassistant/components/hydrawise/__init__.py index 800d19d7efe21..9c7baf6db2e2d 100644 --- a/homeassistant/components/hydrawise/__init__.py +++ b/homeassistant/components/hydrawise/__init__.py @@ -20,7 +20,8 @@ ALLOWED_WATERING_TIME = [5, 10, 15, 30, 45, 60] -CONF_ATTRIBUTION = "Data provided by hydrawise.com" +ATTRIBUTION = "Data provided by hydrawise.com" + CONF_WATERING_TIME = 'watering_minutes' NOTIFICATION_ID = 'hydrawise_notification' @@ -141,6 +142,6 @@ def unit_of_measurement(self): def device_state_attributes(self): """Return the state attributes.""" return { - ATTR_ATTRIBUTION: CONF_ATTRIBUTION, + ATTR_ATTRIBUTION: ATTRIBUTION, 'identifier': self.data.get('relay'), } diff --git a/homeassistant/components/idteck_prox/__init__.py b/homeassistant/components/idteck_prox/__init__.py index 8ec6f49b95d3c..3de7aa7cc8c5f 100644 --- a/homeassistant/components/idteck_prox/__init__.py +++ b/homeassistant/components/idteck_prox/__init__.py @@ -37,7 +37,7 @@ def setup(hass, config): reader.connect() hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, reader.stop) except OSError as error: - _LOGGER.error('Error creating "%s". %s', name, error) + _LOGGER.error("Error creating %s. %s", name, error) return False return True diff --git a/homeassistant/components/ifttt/.translations/es-419.json b/homeassistant/components/ifttt/.translations/es-419.json new file mode 100644 index 0000000000000..46096bbe63132 --- /dev/null +++ b/homeassistant/components/ifttt/.translations/es-419.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "Su instancia de Home Assistant debe estar accesible desde Internet para recibir mensajes IFTTT.", + "one_instance_allowed": "Solo una instancia es necesaria." + }, + "create_entry": { + "default": "Para enviar eventos a Home Assistant, deber\u00e1 usar la acci\u00f3n \"Realizar una solicitud web\" del [applet de IFTTT Webhook] ( {applet_url} ). \n\n Complete la siguiente informaci\u00f3n: \n\n - URL: ` {webhook_url} ` \n - M\u00e9todo: POST \n - Tipo de contenido: aplicaci\u00f3n / json \n\n Consulte [la documentaci\u00f3n] ( {docs_url} ) sobre c\u00f3mo configurar las automatizaciones para manejar los datos entrantes." + }, + "step": { + "user": { + "description": "\u00bfEst\u00e1s seguro de que quieres configurar IFTTT?" + } + }, + "title": "IFTTT" + } +} \ No newline at end of file diff --git a/homeassistant/components/ifttt/.translations/it.json b/homeassistant/components/ifttt/.translations/it.json index ac81f073347a3..e5dc76b7923cb 100644 --- a/homeassistant/components/ifttt/.translations/it.json +++ b/homeassistant/components/ifttt/.translations/it.json @@ -2,14 +2,14 @@ "config": { "abort": { "not_internet_accessible": "Home Assistant deve essere accessibile da internet per ricevere messaggi IFTTT", - "one_instance_allowed": "E' necessaria una sola istanza." + "one_instance_allowed": "\u00c8 necessaria una sola istanza." }, "create_entry": { "default": "Per inviare eventi a Home Assistant, dovrai utilizzare l'azione \"Esegui una richiesta web\" dall'applet [Weblet di IFTTT] ( {applet_url} ). \n\n Compila le seguenti informazioni: \n\n - URL: ` {webhook_url} ` \n - Metodo: POST \n - Tipo di contenuto: application / json \n\n Vedi [la documentazione] ( {docs_url} ) su come configurare le automazioni per gestire i dati in arrivo." }, "step": { "user": { - "description": "Sei sicuro di voler impostare IFTTT?", + "description": "Sei sicuro di voler configurare IFTTT?", "title": "Configura l'applet WebHook IFTTT" } }, diff --git a/homeassistant/components/ifttt/.translations/ru.json b/homeassistant/components/ifttt/.translations/ru.json index dc846993e2ec0..4184d2dfadc15 100644 --- a/homeassistant/components/ifttt/.translations/ru.json +++ b/homeassistant/components/ifttt/.translations/ru.json @@ -5,7 +5,7 @@ "one_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430." }, "create_entry": { - "default": "\u0414\u043b\u044f \u043e\u0442\u043f\u0440\u0430\u0432\u043a\u0438 \u0441\u043e\u0431\u044b\u0442\u0438\u0439 \u0432 Home Assistant \u0432\u044b \u0434\u043e\u043b\u0436\u043d\u044b \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0435 \"Make a web request\" \u0438\u0437 [IFTTT Webhook applet]({applet_url}).\n\n\u0417\u0430\u043f\u043e\u043b\u043d\u0438\u0442\u0435 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0443\u044e \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e:\n\n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/json\n\n\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0435\u0439]({docs_url}) \u0434\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0437\u0430\u0446\u0438\u0439 \u043f\u043e \u043e\u0431\u0440\u0430\u0431\u043e\u0442\u043a\u0435 \u043f\u043e\u0441\u0442\u0443\u043f\u0430\u044e\u0449\u0438\u0445 \u0434\u0430\u043d\u043d\u044b\u0445." + "default": "\u0414\u043b\u044f \u043e\u0442\u043f\u0440\u0430\u0432\u043a\u0438 \u0441\u043e\u0431\u044b\u0442\u0438\u0439 \u0432 Home Assistant \u0412\u044b \u0434\u043e\u043b\u0436\u043d\u044b \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0435 \"Make a web request\" \u0438\u0437 [IFTTT Webhook applet]({applet_url}).\n\n\u0417\u0430\u043f\u043e\u043b\u043d\u0438\u0442\u0435 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0443\u044e \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e:\n\n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/json\n\n\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0435\u0439]({docs_url}) \u0434\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0437\u0430\u0446\u0438\u0439 \u043f\u043e \u043e\u0431\u0440\u0430\u0431\u043e\u0442\u043a\u0435 \u043f\u043e\u0441\u0442\u0443\u043f\u0430\u044e\u0449\u0438\u0445 \u0434\u0430\u043d\u043d\u044b\u0445." }, "step": { "user": { diff --git a/homeassistant/components/ifttt/__init__.py b/homeassistant/components/ifttt/__init__.py index 7dee93b22608a..0a06947b00f2e 100644 --- a/homeassistant/components/ifttt/__init__.py +++ b/homeassistant/components/ifttt/__init__.py @@ -5,9 +5,9 @@ import requests import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.const import CONF_WEBHOOK_ID from homeassistant.helpers import config_entry_flow +import homeassistant.helpers.config_validation as cv REQUIREMENTS = ['pyfttt==0.3'] DEPENDENCIES = ['webhook'] diff --git a/homeassistant/components/ifttt/alarm_control_panel.py b/homeassistant/components/ifttt/alarm_control_panel.py index bbb9a02c8a130..98a176b1e8245 100644 --- a/homeassistant/components/ifttt/alarm_control_panel.py +++ b/homeassistant/components/ifttt/alarm_control_panel.py @@ -10,9 +10,9 @@ from homeassistant.components.ifttt import ( ATTR_EVENT, DOMAIN as IFTTT_DOMAIN, SERVICE_TRIGGER) from homeassistant.const import ( - ATTR_ENTITY_ID, ATTR_STATE, CONF_NAME, CONF_CODE, - CONF_OPTIMISTIC, STATE_ALARM_DISARMED, STATE_ALARM_ARMED_NIGHT, - STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_AWAY) + ATTR_ENTITY_ID, ATTR_STATE, CONF_CODE, CONF_NAME, CONF_OPTIMISTIC, + STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT, + STATE_ALARM_DISARMED) import homeassistant.helpers.config_validation as cv DEPENDENCIES = ['ifttt'] diff --git a/homeassistant/components/ihc/__init__.py b/homeassistant/components/ihc/__init__.py index 823f9d2657dbe..bd45a52944cc8 100644 --- a/homeassistant/components/ihc/__init__.py +++ b/homeassistant/components/ihc/__init__.py @@ -1,9 +1,4 @@ -""" -Support for IHC devices. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/ihc/ -""" +"""Support for IHC devices.""" import logging import os.path @@ -23,7 +18,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.typing import HomeAssistantType -REQUIREMENTS = ['ihcsdk==2.2.0', 'defusedxml==0.5.0'] +REQUIREMENTS = ['ihcsdk==2.3.0', 'defusedxml==0.5.0'] _LOGGER = logging.getLogger(__name__) @@ -224,7 +219,7 @@ def autosetup_ihc_products(hass: HomeAssistantType, config, ihc_controller, return False project = ElementTree.fromstring(project_xml) - # if an auto setup file exist in the configuration it will override + # If an auto setup file exist in the configuration it will override yaml_path = hass.config.path(AUTO_SETUP_YAML) if not os.path.isfile(yaml_path): yaml_path = os.path.join(os.path.dirname(__file__), AUTO_SETUP_YAML) diff --git a/homeassistant/components/ihc/binary_sensor.py b/homeassistant/components/ihc/binary_sensor.py index fb5b4c0bfc227..7e3371a834c1a 100644 --- a/homeassistant/components/ihc/binary_sensor.py +++ b/homeassistant/components/ihc/binary_sensor.py @@ -1,17 +1,9 @@ -"""IHC binary sensor platform. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/binary_sensor.ihc/ -""" -from homeassistant.components.binary_sensor import ( - BinarySensorDevice) -from homeassistant.components.ihc import ( - IHC_DATA, IHC_CONTROLLER, IHC_INFO) -from homeassistant.components.ihc.const import ( - CONF_INVERTING) +"""Support for IHC binary sensors.""" +from homeassistant.components.binary_sensor import BinarySensorDevice +from homeassistant.components.ihc import IHC_CONTROLLER, IHC_DATA, IHC_INFO +from homeassistant.components.ihc.const import CONF_INVERTING from homeassistant.components.ihc.ihcdevice import IHCDevice -from homeassistant.const import ( - CONF_TYPE) +from homeassistant.const import CONF_TYPE DEPENDENCIES = ['ihc'] @@ -31,10 +23,9 @@ def setup_platform(hass, config, add_entities, discovery_info=None): info = hass.data[ihc_key][IHC_INFO] ihc_controller = hass.data[ihc_key][IHC_CONTROLLER] - sensor = IHCBinarySensor(ihc_controller, name, ihc_id, info, - product_cfg.get(CONF_TYPE), - product_cfg[CONF_INVERTING], - product) + sensor = IHCBinarySensor( + ihc_controller, name, ihc_id, info, product_cfg.get(CONF_TYPE), + product_cfg[CONF_INVERTING], product) devices.append(sensor) add_entities(devices) diff --git a/homeassistant/components/ihc/light.py b/homeassistant/components/ihc/light.py index f80c9b2fd6fd1..2590ea832221e 100644 --- a/homeassistant/components/ihc/light.py +++ b/homeassistant/components/ihc/light.py @@ -1,14 +1,8 @@ -"""IHC light platform. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/light.ihc/ -""" +"""Support for IHC lights.""" import logging -from homeassistant.components.ihc import ( - IHC_DATA, IHC_CONTROLLER, IHC_INFO) -from homeassistant.components.ihc.const import ( - CONF_DIMMABLE) +from homeassistant.components.ihc import IHC_CONTROLLER, IHC_DATA, IHC_INFO +from homeassistant.components.ihc.const import CONF_DIMMABLE from homeassistant.components.ihc.ihcdevice import IHCDevice from homeassistant.components.light import ( ATTR_BRIGHTNESS, SUPPORT_BRIGHTNESS, Light) diff --git a/homeassistant/components/ihc/sensor.py b/homeassistant/components/ihc/sensor.py index f5a45599bb75a..930ac22162948 100644 --- a/homeassistant/components/ihc/sensor.py +++ b/homeassistant/components/ihc/sensor.py @@ -1,13 +1,7 @@ -"""IHC sensor platform. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/sensor.ihc/ -""" -from homeassistant.components.ihc import ( - IHC_DATA, IHC_CONTROLLER, IHC_INFO) +"""Support for IHC sensors.""" +from homeassistant.components.ihc import IHC_CONTROLLER, IHC_DATA, IHC_INFO from homeassistant.components.ihc.ihcdevice import IHCDevice -from homeassistant.const import ( - CONF_UNIT_OF_MEASUREMENT) +from homeassistant.const import CONF_UNIT_OF_MEASUREMENT from homeassistant.helpers.entity import Entity DEPENDENCIES = ['ihc'] @@ -28,8 +22,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): info = hass.data[ihc_key][IHC_INFO] ihc_controller = hass.data[ihc_key][IHC_CONTROLLER] unit = product_cfg[CONF_UNIT_OF_MEASUREMENT] - sensor = IHCSensor(ihc_controller, name, ihc_id, info, - unit, product) + sensor = IHCSensor(ihc_controller, name, ihc_id, info, unit, product) devices.append(sensor) add_entities(devices) diff --git a/homeassistant/components/ihc/switch.py b/homeassistant/components/ihc/switch.py index e217d109cbc9e..bbab9d3e68c0c 100644 --- a/homeassistant/components/ihc/switch.py +++ b/homeassistant/components/ihc/switch.py @@ -1,10 +1,5 @@ -"""IHC switch platform. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/switch.ihc/ -""" -from homeassistant.components.ihc import ( - IHC_DATA, IHC_CONTROLLER, IHC_INFO) +"""Support for IHC switches.""" +from homeassistant.components.ihc import IHC_CONTROLLER, IHC_DATA, IHC_INFO from homeassistant.components.ihc.ihcdevice import IHCDevice from homeassistant.components.switch import SwitchDevice @@ -31,7 +26,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): class IHCSwitch(IHCDevice, SwitchDevice): - """IHC Switch.""" + """Representation of an IHC switch.""" def __init__(self, ihc_controller, name: str, ihc_id: int, info: bool, product=None) -> None: diff --git a/homeassistant/components/image_processing/opencv.py b/homeassistant/components/image_processing/opencv.py index 8ca6e4d8a5357..7cb5184b1163e 100644 --- a/homeassistant/components/image_processing/opencv.py +++ b/homeassistant/components/image_processing/opencv.py @@ -16,7 +16,7 @@ from homeassistant.core import split_entity_id import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['numpy==1.16.0'] +REQUIREMENTS = ['numpy==1.16.1'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/image_processing/tensorflow.py b/homeassistant/components/image_processing/tensorflow.py index cc25756f2d04f..f0e8f5182fcd8 100644 --- a/homeassistant/components/image_processing/tensorflow.py +++ b/homeassistant/components/image_processing/tensorflow.py @@ -1,12 +1,4 @@ -""" -Support for performing TensorFlow classification on images. - -For a quick start, pick a pre-trained COCO model from: -https://github.com/tensorflow/models/blob/master/research/object_detection/g3doc/detection_model_zoo.md - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/image_processing.tensorflow/ -""" +"""Support for performing TensorFlow classification on images.""" import logging import os import sys @@ -20,7 +12,7 @@ from homeassistant.helpers import template import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['numpy==1.16.0', 'pillow==5.4.1', 'protobuf==3.6.1'] +REQUIREMENTS = ['numpy==1.16.1', 'pillow==5.4.1', 'protobuf==3.6.1'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/insteon/__init__.py b/homeassistant/components/insteon/__init__.py index e82e47dc5f4ae..a462ac0f63efe 100644 --- a/homeassistant/components/insteon/__init__.py +++ b/homeassistant/components/insteon/__init__.py @@ -1,22 +1,16 @@ -""" -Support for INSTEON Modems (PLM and Hub). - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/insteon/ -""" +"""Support for INSTEON Modems (PLM and Hub).""" import collections import logging from typing import Dict import voluptuous as vol -import homeassistant.helpers.config_validation as cv -from homeassistant.const import (CONF_PORT, EVENT_HOMEASSISTANT_STOP, - CONF_PLATFORM, - CONF_ENTITY_ID, - CONF_HOST) +from homeassistant.const import ( + CONF_ADDRESS, CONF_ENTITY_ID, CONF_HOST, CONF_PLATFORM, CONF_PORT, + EVENT_HOMEASSISTANT_STOP) from homeassistant.core import callback from homeassistant.helpers import discovery +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity REQUIREMENTS = ['insteonplm==0.15.2'] @@ -31,7 +25,6 @@ CONF_HUB_VERSION = 'hub_version' CONF_OVERRIDE = 'device_override' CONF_PLM_HUB_MSG = 'Must configure either a PLM port or a Hub host' -CONF_ADDRESS = 'address' CONF_CAT = 'cat' CONF_SUBCAT = 'subcat' CONF_FIRMWARE = 'firmware' diff --git a/homeassistant/components/insteon/binary_sensor.py b/homeassistant/components/insteon/binary_sensor.py index 5b0a291e92bd1..06eddb9a0040f 100644 --- a/homeassistant/components/insteon/binary_sensor.py +++ b/homeassistant/components/insteon/binary_sensor.py @@ -1,29 +1,26 @@ -""" -Support for INSTEON dimmers via PowerLinc Modem. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/binary_sensor.insteon/ -""" +"""Support for INSTEON dimmers via PowerLinc Modem.""" import logging from homeassistant.components.binary_sensor import BinarySensorDevice from homeassistant.components.insteon import InsteonEntity -DEPENDENCIES = ['insteon'] - _LOGGER = logging.getLogger(__name__) -SENSOR_TYPES = {'openClosedSensor': 'opening', - 'ioLincSensor': 'opening', - 'motionSensor': 'motion', - 'doorSensor': 'door', - 'wetLeakSensor': 'moisture', - 'lightSensor': 'light', - 'batterySensor': 'battery'} +DEPENDENCIES = ['insteon'] + +SENSOR_TYPES = { + 'openClosedSensor': 'opening', + 'ioLincSensor': 'opening', + 'motionSensor': 'motion', + 'doorSensor': 'door', + 'wetLeakSensor': 'moisture', + 'lightSensor': 'light', + 'batterySensor': 'battery', +} -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): """Set up the INSTEON device class for the hass platform.""" insteon_modem = hass.data['insteon'].get('modem') @@ -32,7 +29,7 @@ async def async_setup_platform(hass, config, async_add_entities, state_key = discovery_info['state_key'] name = device.states[state_key].name if name != 'dryLeakSensor': - _LOGGER.debug('Adding device %s entity %s to Binary Sensor platform', + _LOGGER.debug("Adding device %s entity %s to Binary Sensor platform", device.address.hex, device.states[state_key].name) new_entity = InsteonBinarySensor(device, state_key) @@ -58,8 +55,7 @@ def is_on(self): """Return the boolean response if the node is on.""" on_val = bool(self._insteon_device_state.value) - if self._insteon_device_state.name in ['lightSensor', - 'ioLincSensor']: + if self._insteon_device_state.name in ['lightSensor', 'ioLincSensor']: return not on_val return on_val diff --git a/homeassistant/components/insteon/cover.py b/homeassistant/components/insteon/cover.py index f0cf93c13e980..7de2e872489e8 100644 --- a/homeassistant/components/insteon/cover.py +++ b/homeassistant/components/insteon/cover.py @@ -1,16 +1,11 @@ -""" -Support for Insteon covers via PowerLinc Modem. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/cover.insteon/ -""" +"""Support for Insteon covers via PowerLinc Modem.""" import logging import math +from homeassistant.components.cover import ( + ATTR_POSITION, SUPPORT_CLOSE, SUPPORT_OPEN, SUPPORT_SET_POSITION, + CoverDevice) from homeassistant.components.insteon import InsteonEntity -from homeassistant.components.cover import (CoverDevice, ATTR_POSITION, - SUPPORT_OPEN, SUPPORT_CLOSE, - SUPPORT_SET_POSITION) _LOGGER = logging.getLogger(__name__) @@ -18,8 +13,8 @@ SUPPORTED_FEATURES = SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_SET_POSITION -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): """Set up the Insteon platform.""" if not discovery_info: return diff --git a/homeassistant/components/insteon/fan.py b/homeassistant/components/insteon/fan.py index 604063a9aa34d..2b6097a4ba2f4 100644 --- a/homeassistant/components/insteon/fan.py +++ b/homeassistant/components/insteon/fan.py @@ -1,34 +1,28 @@ -""" -Support for INSTEON fans via PowerLinc Modem. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/fan.insteon/ -""" +"""Support for INSTEON fans via PowerLinc Modem.""" import logging -from homeassistant.components.fan import (SPEED_OFF, - SPEED_LOW, - SPEED_MEDIUM, - SPEED_HIGH, - FanEntity, - SUPPORT_SET_SPEED) -from homeassistant.const import STATE_OFF +from homeassistant.components.fan import ( + SPEED_HIGH, SPEED_LOW, SPEED_MEDIUM, SPEED_OFF, SUPPORT_SET_SPEED, + FanEntity) from homeassistant.components.insteon import InsteonEntity +from homeassistant.const import STATE_OFF + +_LOGGER = logging.getLogger(__name__) DEPENDENCIES = ['insteon'] -SPEED_TO_HEX = {SPEED_OFF: 0x00, - SPEED_LOW: 0x3f, - SPEED_MEDIUM: 0xbe, - SPEED_HIGH: 0xff} +SPEED_TO_HEX = { + SPEED_OFF: 0x00, + SPEED_LOW: 0x3f, + SPEED_MEDIUM: 0xbe, + SPEED_HIGH: 0xff, +} FAN_SPEEDS = [STATE_OFF, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH] -_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): """Set up the INSTEON device class for the hass platform.""" insteon_modem = hass.data['insteon'].get('modem') diff --git a/homeassistant/components/insteon/light.py b/homeassistant/components/insteon/light.py index 4829ce631a67e..e8ffc22671685 100644 --- a/homeassistant/components/insteon/light.py +++ b/homeassistant/components/insteon/light.py @@ -1,9 +1,4 @@ -""" -Support for Insteon lights via PowerLinc Modem. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/light.insteon/ -""" +"""Support for Insteon lights via PowerLinc Modem.""" import logging from homeassistant.components.insteon import InsteonEntity @@ -17,8 +12,8 @@ MAX_BRIGHTNESS = 255 -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): """Set up the Insteon component.""" insteon_modem = hass.data['insteon'].get('modem') diff --git a/homeassistant/components/insteon/sensor.py b/homeassistant/components/insteon/sensor.py index 7854967395bf7..d895d97202700 100644 --- a/homeassistant/components/insteon/sensor.py +++ b/homeassistant/components/insteon/sensor.py @@ -1,9 +1,4 @@ -""" -Support for INSTEON dimmers via PowerLinc Modem. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/sensor.insteon/ -""" +"""Support for INSTEON dimmers via PowerLinc Modem.""" import logging from homeassistant.components.insteon import InsteonEntity @@ -14,8 +9,8 @@ _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): """Set up the INSTEON device class for the hass platform.""" insteon_modem = hass.data['insteon'].get('modem') diff --git a/homeassistant/components/insteon/switch.py b/homeassistant/components/insteon/switch.py index 454b3ef39cb44..2a6b97a39d10b 100644 --- a/homeassistant/components/insteon/switch.py +++ b/homeassistant/components/insteon/switch.py @@ -1,9 +1,4 @@ -""" -Support for INSTEON dimmers via PowerLinc Modem. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/switch.insteon/ -""" +"""Support for INSTEON dimmers via PowerLinc Modem.""" import logging from homeassistant.components.insteon import InsteonEntity @@ -14,8 +9,8 @@ _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): """Set up the INSTEON device class for the hass platform.""" insteon_modem = hass.data['insteon'].get('modem') @@ -25,7 +20,7 @@ async def async_setup_platform(hass, config, async_add_entities, state_name = device.states[state_key].name - _LOGGER.debug('Adding device %s entity %s to Switch platform', + _LOGGER.debug("Adding device %s entity %s to Switch platform", device.address.hex, device.states[state_key].name) new_entity = None diff --git a/homeassistant/components/ios/.translations/es-419.json b/homeassistant/components/ios/.translations/es-419.json new file mode 100644 index 0000000000000..38a12e7411aea --- /dev/null +++ b/homeassistant/components/ios/.translations/es-419.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "S\u00f3lo es necesaria una \u00fanica configuraci\u00f3n de Home Assistant iOS." + }, + "step": { + "confirm": { + "description": "\u00bfDesea configurar el componente iOS de Home Assistant?", + "title": "Home Assistant iOS" + } + }, + "title": "Home Assistant iOS" + } +} \ No newline at end of file diff --git a/homeassistant/components/ios/.translations/it.json b/homeassistant/components/ios/.translations/it.json index 3f587b7ee64d0..c2c5042e29522 100644 --- a/homeassistant/components/ios/.translations/it.json +++ b/homeassistant/components/ios/.translations/it.json @@ -1,5 +1,14 @@ { "config": { + "abort": { + "single_instance_allowed": "\u00c8 necessaria una sola configurazione di Home Assistant iOS." + }, + "step": { + "confirm": { + "description": "Vuoi configurare il componente Home Assistant iOS?", + "title": "Home Assistant iOS" + } + }, "title": "Home Assistant per iOS" } } \ No newline at end of file diff --git a/homeassistant/components/ios/__init__.py b/homeassistant/components/ios/__init__.py index 0b1282b605aea..737216af5c942 100644 --- a/homeassistant/components/ios/__init__.py +++ b/homeassistant/components/ios/__init__.py @@ -1,25 +1,18 @@ -""" -Native Home Assistant iOS app component. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/ecosystem/ios/ -""" -import logging +"""Native Home Assistant iOS app component.""" import datetime +import logging import voluptuous as vol from homeassistant import config_entries from homeassistant.components.http import HomeAssistantView -from homeassistant.const import (HTTP_INTERNAL_SERVER_ERROR, - HTTP_BAD_REQUEST) +from homeassistant.const import HTTP_BAD_REQUEST, HTTP_INTERNAL_SERVER_ERROR from homeassistant.core import callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import ( - config_validation as cv, discovery, config_entry_flow) + config_entry_flow, config_validation as cv, discovery) from homeassistant.util.json import load_json, save_json - _LOGGER = logging.getLogger(__name__) DOMAIN = 'ios' diff --git a/homeassistant/components/ios/notify.py b/homeassistant/components/ios/notify.py index e6a37d707ad2a..1f8aade4ec17a 100644 --- a/homeassistant/components/ios/notify.py +++ b/homeassistant/components/ios/notify.py @@ -1,20 +1,14 @@ -""" -iOS push notification platform for notify component. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/ecosystem/ios/notifications/ -""" -import logging +"""Support for iOS push notifications.""" from datetime import datetime, timezone +import logging + import requests from homeassistant.components import ios - -import homeassistant.util.dt as dt_util - from homeassistant.components.notify import ( - ATTR_TARGET, ATTR_TITLE, ATTR_TITLE_DEFAULT, ATTR_MESSAGE, - ATTR_DATA, BaseNotificationService) + ATTR_DATA, ATTR_MESSAGE, ATTR_TARGET, ATTR_TITLE, ATTR_TITLE_DEFAULT, + BaseNotificationService) +import homeassistant.util.dt as dt_util _LOGGER = logging.getLogger(__name__) @@ -85,7 +79,7 @@ def send_message(self, message="", **kwargs): for target in targets: if target not in ios.enabled_push_ids(self.hass): - _LOGGER.error("The target (%s) does not exist in .ios.conf.", + _LOGGER.error("The target (%s) does not exist in .ios.conf", targets) return diff --git a/homeassistant/components/ios/sensor.py b/homeassistant/components/ios/sensor.py index d206cd1df8785..404b313368ceb 100644 --- a/homeassistant/components/ios/sensor.py +++ b/homeassistant/components/ios/sensor.py @@ -1,9 +1,4 @@ -""" -Support for Home Assistant iOS app sensors. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/ecosystem/ios/ -""" +"""Support for Home Assistant iOS app sensors.""" from homeassistant.components import ios from homeassistant.helpers.entity import Entity from homeassistant.helpers.icon import icon_for_battery_level diff --git a/homeassistant/components/iperf3/__init__.py b/homeassistant/components/iperf3/__init__.py new file mode 100644 index 0000000000000..01ac2194f355e --- /dev/null +++ b/homeassistant/components/iperf3/__init__.py @@ -0,0 +1,185 @@ +"""Support for Iperf3 network measurement tool.""" +import logging +from datetime import timedelta + +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN +from homeassistant.const import CONF_MONITORED_CONDITIONS, CONF_PORT, \ + CONF_HOST, CONF_PROTOCOL, CONF_HOSTS, CONF_SCAN_INTERVAL +from homeassistant.helpers.discovery import async_load_platform +from homeassistant.helpers.dispatcher import dispatcher_send +from homeassistant.helpers.event import async_track_time_interval + +REQUIREMENTS = ['iperf3==0.1.10'] + +DOMAIN = 'iperf3' +DATA_UPDATED = '{}_data_updated'.format(DOMAIN) + +_LOGGER = logging.getLogger(__name__) + +CONF_DURATION = 'duration' +CONF_PARALLEL = 'parallel' +CONF_MANUAL = 'manual' + +DEFAULT_DURATION = 10 +DEFAULT_PORT = 5201 +DEFAULT_PARALLEL = 1 +DEFAULT_PROTOCOL = 'tcp' +DEFAULT_INTERVAL = timedelta(minutes=60) + +ATTR_DOWNLOAD = 'download' +ATTR_UPLOAD = 'upload' +ATTR_VERSION = 'Version' +ATTR_HOST = 'host' + +UNIT_OF_MEASUREMENT = 'Mbit/s' + +SENSOR_TYPES = { + ATTR_DOWNLOAD: [ATTR_DOWNLOAD.capitalize(), UNIT_OF_MEASUREMENT], + ATTR_UPLOAD: [ATTR_UPLOAD.capitalize(), UNIT_OF_MEASUREMENT], +} + +PROTOCOLS = ['tcp', 'udp'] + +HOST_CONFIG_SCHEMA = vol.Schema({ + vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Optional(CONF_DURATION, default=DEFAULT_DURATION): vol.Range(5, 10), + vol.Optional(CONF_PARALLEL, default=DEFAULT_PARALLEL): vol.Range(1, 20), + vol.Optional(CONF_PROTOCOL, default=DEFAULT_PROTOCOL): vol.In(PROTOCOLS), +}) + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Required(CONF_HOSTS): vol.All( + cv.ensure_list, [HOST_CONFIG_SCHEMA] + ), + vol.Optional(CONF_MONITORED_CONDITIONS, default=list(SENSOR_TYPES)): + vol.All(cv.ensure_list, [vol.In(list(SENSOR_TYPES))]), + vol.Optional(CONF_SCAN_INTERVAL, default=DEFAULT_INTERVAL): vol.All( + cv.time_period, cv.positive_timedelta + ), + vol.Optional(CONF_MANUAL, default=False): cv.boolean, + }) +}, extra=vol.ALLOW_EXTRA) + +SERVICE_SCHEMA = vol.Schema({ + vol.Optional(ATTR_HOST, default=None): cv.string, +}) + + +async def async_setup(hass, config): + """Set up the iperf3 component.""" + import iperf3 + + hass.data[DOMAIN] = {} + + 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) + + if not conf[CONF_MANUAL]: + async_track_time_interval( + hass, data.update, conf[CONF_SCAN_INTERVAL] + ) + + def update(call): + """Service call to manually update the data.""" + called_host = call.data[ATTR_HOST] + if called_host in hass.data[DOMAIN]: + hass.data[DOMAIN][called_host].update() + else: + for iperf3_host in hass.data[DOMAIN].values(): + iperf3_host.update() + + hass.services.async_register( + DOMAIN, 'speedtest', update, schema=SERVICE_SCHEMA + ) + + hass.async_create_task( + async_load_platform( + hass, + SENSOR_DOMAIN, + DOMAIN, + conf[CONF_MONITORED_CONDITIONS], + config + ) + ) + + return True + + +class Iperf3Data: + """Get the latest data from iperf3.""" + + def __init__(self, hass, client): + """Initialize the data object.""" + self._hass = hass + self._client = client + self.data = { + ATTR_DOWNLOAD: None, + ATTR_UPLOAD: None, + ATTR_VERSION: None + } + + @property + def protocol(self): + """Return the protocol used for this connection.""" + return self._client.protocol + + @property + def host(self): + """Return the host connected to.""" + return self._client.server_hostname + + @property + def port(self): + """Return the port on the host connected to.""" + return self._client.port + + def update(self, now=None): + """Get the latest data from iperf3.""" + if self.protocol == 'udp': + # UDP only have 1 way attribute + result = self._run_test(ATTR_DOWNLOAD) + self.data[ATTR_DOWNLOAD] = self.data[ATTR_UPLOAD] = getattr( + result, 'Mbps', None) + self.data[ATTR_VERSION] = getattr(result, 'version', None) + else: + result = self._run_test(ATTR_DOWNLOAD) + self.data[ATTR_DOWNLOAD] = getattr( + result, 'received_Mbps', None) + self.data[ATTR_VERSION] = getattr(result, 'version', None) + self.data[ATTR_UPLOAD] = getattr( + self._run_test(ATTR_UPLOAD), 'sent_Mbps', None) + + dispatcher_send(self._hass, DATA_UPDATED, self.host) + + def _run_test(self, test_type): + """Run and return the iperf3 data.""" + self._client.reverse = test_type == ATTR_DOWNLOAD + try: + result = self._client.run() + except (AttributeError, OSError, ValueError) as error: + _LOGGER.error("Iperf3 error: %s", error) + return None + + if result is not None and \ + hasattr(result, 'error') and \ + result.error is not None: + _LOGGER.error("Iperf3 error: %s", result.error) + return None + + return result diff --git a/homeassistant/components/iperf3/sensor.py b/homeassistant/components/iperf3/sensor.py new file mode 100644 index 0000000000000..59813ae0455ae --- /dev/null +++ b/homeassistant/components/iperf3/sensor.py @@ -0,0 +1,100 @@ +"""Support for Iperf3 sensors.""" +from homeassistant.components.iperf3 import ( + DATA_UPDATED, DOMAIN as IPERF3_DOMAIN, SENSOR_TYPES, ATTR_VERSION) +from homeassistant.const import ATTR_ATTRIBUTION +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.restore_state import RestoreEntity + +DEPENDENCIES = ['iperf3'] + +ATTRIBUTION = 'Data retrieved using Iperf3' + +ICON = 'mdi:speedometer' + +ATTR_PROTOCOL = 'Protocol' +ATTR_REMOTE_HOST = 'Remote Server' +ATTR_REMOTE_PORT = 'Remote Port' + + +async def async_setup_platform( + hass, config, async_add_entities, discovery_info): + """Set up the Iperf3 sensor.""" + sensors = [] + for iperf3_host in hass.data[IPERF3_DOMAIN].values(): + sensors.extend( + [Iperf3Sensor(iperf3_host, sensor) for sensor in discovery_info] + ) + async_add_entities(sensors, True) + + +class Iperf3Sensor(RestoreEntity): + """A Iperf3 sensor implementation.""" + + def __init__(self, iperf3_data, sensor_type): + """Initialize the sensor.""" + self._name = \ + "{} {}".format(SENSOR_TYPES[sensor_type][0], iperf3_data.host) + self._state = None + self._sensor_type = sensor_type + self._unit_of_measurement = SENSOR_TYPES[sensor_type][1] + self._iperf3_data = iperf3_data + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @property + def state(self): + """Return the state of the device.""" + return self._state + + @property + def unit_of_measurement(self): + """Return the unit of measurement of this entity, if any.""" + return self._unit_of_measurement + + @property + def icon(self): + """Return icon.""" + return ICON + + @property + def device_state_attributes(self): + """Return the state attributes.""" + return { + ATTR_ATTRIBUTION: ATTRIBUTION, + ATTR_PROTOCOL: self._iperf3_data.protocol, + ATTR_REMOTE_HOST: self._iperf3_data.host, + ATTR_REMOTE_PORT: self._iperf3_data.port, + ATTR_VERSION: self._iperf3_data.data[ATTR_VERSION] + } + + @property + def should_poll(self): + """Return the polling requirement for this sensor.""" + return False + + async def async_added_to_hass(self): + """Handle entity which will be added.""" + await super().async_added_to_hass() + state = await self.async_get_last_state() + if not state: + return + self._state = state.state + + async_dispatcher_connect( + self.hass, DATA_UPDATED, self._schedule_immediate_update + ) + + def update(self): + """Get the latest data and update the states.""" + data = self._iperf3_data.data.get(self._sensor_type) + if data is not None: + self._state = round(data, 2) + + @callback + def _schedule_immediate_update(self, host): + if host == self._iperf3_data.host: + self.async_schedule_update_ha_state(True) diff --git a/homeassistant/components/iperf3/services.yaml b/homeassistant/components/iperf3/services.yaml new file mode 100644 index 0000000000000..c333d7c74c859 --- /dev/null +++ b/homeassistant/components/iperf3/services.yaml @@ -0,0 +1,6 @@ +speedtest: + description: Immediately take a speedest with iperf3 + fields: + host: + description: The host name of the iperf3 server (already configured) to run a test with. + example: 'iperf.he.net' \ No newline at end of file diff --git a/homeassistant/components/ipma/.translations/es-419.json b/homeassistant/components/ipma/.translations/es-419.json new file mode 100644 index 0000000000000..acb8b51a44c81 --- /dev/null +++ b/homeassistant/components/ipma/.translations/es-419.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "name_exists": "El nombre ya existe" + }, + "step": { + "user": { + "data": { + "latitude": "Latitud", + "longitude": "Longitud", + "name": "Nombre" + }, + "description": "Instituto Portugu\u00eas do Mar e Atmosfera", + "title": "Ubicaci\u00f3n" + } + }, + "title": "Servicio meteorol\u00f3gico portugu\u00e9s (IPMA)" + } +} \ No newline at end of file diff --git a/homeassistant/components/ipma/.translations/es.json b/homeassistant/components/ipma/.translations/es.json new file mode 100644 index 0000000000000..c364ca286e3e3 --- /dev/null +++ b/homeassistant/components/ipma/.translations/es.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "name_exists": "El nombre ya existe" + }, + "step": { + "user": { + "data": { + "latitude": "Latitud", + "longitude": "Longitud", + "name": "Nombre" + }, + "title": "Ubicaci\u00f3n" + } + }, + "title": "Servicio meteorol\u00f3gico portugu\u00e9s (IPMA)" + } +} \ No newline at end of file diff --git a/homeassistant/components/ipma/.translations/he.json b/homeassistant/components/ipma/.translations/he.json new file mode 100644 index 0000000000000..4931fcaf94c71 --- /dev/null +++ b/homeassistant/components/ipma/.translations/he.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "name_exists": "\u05d4\u05e9\u05dd \u05db\u05d1\u05e8 \u05e7\u05d9\u05d9\u05dd" + }, + "step": { + "user": { + "data": { + "latitude": "\u05e7\u05d5 \u05e8\u05d5\u05d7\u05d1", + "longitude": "\u05e7\u05d5 \u05d0\u05d5\u05e8\u05da", + "name": "\u05e9\u05dd" + }, + "title": "\u05de\u05d9\u05e7\u05d5\u05dd" + } + }, + "title": "\u05e9\u05d9\u05e8\u05d5\u05ea \u05de\u05d6\u05d2 \u05d0\u05d5\u05d5\u05d9\u05e8 \u05e4\u05d5\u05e8\u05d8\u05d5\u05d2\u05d6\u05d9\u05ea (IPMA)" + } +} \ No newline at end of file diff --git a/homeassistant/components/ipma/.translations/hu.json b/homeassistant/components/ipma/.translations/hu.json new file mode 100644 index 0000000000000..62ddd85e6ef13 --- /dev/null +++ b/homeassistant/components/ipma/.translations/hu.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "name_exists": "A n\u00e9v m\u00e1r l\u00e9tezik" + }, + "step": { + "user": { + "data": { + "latitude": "Sz\u00e9less\u00e9g", + "longitude": "Hossz\u00fas\u00e1g", + "name": "N\u00e9v" + }, + "description": "Portug\u00e1l Atmoszf\u00e9ra Int\u00e9zet", + "title": "Hely" + } + }, + "title": "Portug\u00e1l Meteorol\u00f3giai Szolg\u00e1lat (IPMA)" + } +} \ No newline at end of file diff --git a/homeassistant/components/ipma/.translations/it.json b/homeassistant/components/ipma/.translations/it.json new file mode 100644 index 0000000000000..d751d8a317f2b --- /dev/null +++ b/homeassistant/components/ipma/.translations/it.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "name_exists": "Il nome \u00e8 gi\u00e0 esistente" + }, + "step": { + "user": { + "data": { + "latitude": "Latitudine", + "longitude": "Logitudine", + "name": "Nome" + }, + "description": "Instituto Portugu\u00eas do Mar e Atmosfera", + "title": "Localit\u00e0" + } + }, + "title": "Servizio meteo portoghese (IPMA)" + } +} \ No newline at end of file diff --git a/homeassistant/components/ipma/.translations/no.json b/homeassistant/components/ipma/.translations/no.json new file mode 100644 index 0000000000000..1d5aa9c40cf26 --- /dev/null +++ b/homeassistant/components/ipma/.translations/no.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "name_exists": "Navnet eksisterer allerede" + }, + "step": { + "user": { + "data": { + "latitude": "Breddegrad", + "longitude": "Lengdegrad", + "name": "Navn" + }, + "description": "Instituto Portugu\u00eas do Mar e Atmosfera", + "title": "Plassering" + } + }, + "title": "Portugisisk v\u00e6rtjeneste (IPMA)" + } +} \ No newline at end of file diff --git a/homeassistant/components/ipma/.translations/sl.json b/homeassistant/components/ipma/.translations/sl.json new file mode 100644 index 0000000000000..da6a1dac85904 --- /dev/null +++ b/homeassistant/components/ipma/.translations/sl.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "name_exists": "Ime \u017ee obstaja" + }, + "step": { + "user": { + "data": { + "latitude": "Zemljepisna \u0161irina", + "longitude": "Zemljepisna dol\u017eina", + "name": "Ime" + }, + "description": "Instituto Portugu\u00eas do Mar e Atmosfera", + "title": "Lokacija" + } + }, + "title": "Portugalska vremenska storitev (IPMA)" + } +} \ No newline at end of file diff --git a/homeassistant/components/ipma/.translations/sv.json b/homeassistant/components/ipma/.translations/sv.json new file mode 100644 index 0000000000000..4bdba6f0d0865 --- /dev/null +++ b/homeassistant/components/ipma/.translations/sv.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "name_exists": "Namnet finns redan" + }, + "step": { + "user": { + "data": { + "latitude": "Latitud", + "longitude": "Longitud", + "name": "Namn" + }, + "description": "Portugisiska institutet f\u00f6r hav och atmosf\u00e4ren", + "title": "Location" + } + }, + "title": "Portugisiska weather service (IPMA)" + } +} \ No newline at end of file diff --git a/homeassistant/components/ipma/.translations/uk.json b/homeassistant/components/ipma/.translations/uk.json new file mode 100644 index 0000000000000..bb294cc5d21e0 --- /dev/null +++ b/homeassistant/components/ipma/.translations/uk.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "user": { + "title": "\u0420\u043e\u0437\u0442\u0430\u0448\u0443\u0432\u0430\u043d\u043d\u044f" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ipma/__init__.py b/homeassistant/components/ipma/__init__.py index 87f62371b5557..9bb54a1a01970 100644 --- a/homeassistant/components/ipma/__init__.py +++ b/homeassistant/components/ipma/__init__.py @@ -1,9 +1,4 @@ -""" -Component for the Portuguese weather service - IPMA. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/ipma/ -""" +"""Component for the Portuguese weather service - IPMA.""" from homeassistant.core import Config, HomeAssistant from .config_flow import IpmaFlowHandler # noqa from .const import DOMAIN # noqa @@ -13,7 +8,6 @@ async def async_setup(hass: HomeAssistant, config: Config) -> bool: """Set up configured IPMA.""" - # No support for component configuration return True diff --git a/homeassistant/components/ipma/const.py b/homeassistant/components/ipma/const.py index dbb192945419b..bdd97c74e6aca 100644 --- a/homeassistant/components/ipma/const.py +++ b/homeassistant/components/ipma/const.py @@ -1,4 +1,4 @@ -"""Constants in ipma component.""" +"""Constants for IPMA component.""" import logging from homeassistant.components.weather import DOMAIN as WEATHER_DOMAIN diff --git a/homeassistant/components/ipma/weather.py b/homeassistant/components/ipma/weather.py index ec9b6fec2e8b5..7122957ad12d9 100644 --- a/homeassistant/components/ipma/weather.py +++ b/homeassistant/components/ipma/weather.py @@ -1,9 +1,4 @@ -""" -Support for IPMA weather service. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/weather.ipma/ -""" +"""Support for IPMA weather service.""" import logging from datetime import timedelta @@ -54,13 +49,13 @@ }) -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): """Set up the ipma platform. Deprecated. """ - _LOGGER.warning('Loading IPMA via platform config is deprecated') + _LOGGER.warning("Loading IPMA via platform config is deprecated") latitude = config.get(CONF_LATITUDE, hass.config.latitude) longitude = config.get(CONF_LONGITUDE, hass.config.longitude) diff --git a/homeassistant/components/isy994/__init__.py b/homeassistant/components/isy994/__init__.py index 2b5f8fcb13f16..4eaa71deececa 100644 --- a/homeassistant/components/isy994/__init__.py +++ b/homeassistant/components/isy994/__init__.py @@ -1,19 +1,14 @@ -""" -Support the ISY-994 controllers. - -For configuration details please visit the documentation for this component at -https://home-assistant.io/components/isy994/ -""" +"""Support the ISY-994 controllers.""" from collections import namedtuple import logging from urllib.parse import urlparse import voluptuous as vol -from homeassistant.core import HomeAssistant from homeassistant.const import ( CONF_HOST, CONF_PASSWORD, CONF_USERNAME, EVENT_HOMEASSISTANT_STOP) -from homeassistant.helpers import discovery, config_validation as cv +from homeassistant.core import HomeAssistant +from homeassistant.helpers import config_validation as cv, discovery from homeassistant.helpers.entity import Entity from homeassistant.helpers.typing import ConfigType, Dict @@ -46,8 +41,7 @@ default=DEFAULT_IGNORE_STRING): cv.string, vol.Optional(CONF_SENSOR_STRING, default=DEFAULT_SENSOR_STRING): cv.string, - vol.Optional(CONF_ENABLE_CLIMATE, - default=True): cv.boolean + vol.Optional(CONF_ENABLE_CLIMATE, default=True): cv.boolean, }) }, extra=vol.ALLOW_EXTRA) diff --git a/homeassistant/components/isy994/binary_sensor.py b/homeassistant/components/isy994/binary_sensor.py index b5d676f233f8c..013b99fbb15a4 100644 --- a/homeassistant/components/isy994/binary_sensor.py +++ b/homeassistant/components/isy994/binary_sensor.py @@ -1,20 +1,15 @@ -""" -Support for ISY994 binary sensors. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/binary_sensor.isy994/ -""" -import logging +"""Support for ISY994 binary sensors.""" from datetime import timedelta +import logging from typing import Callable +from homeassistant.components.binary_sensor import DOMAIN, BinarySensorDevice +from homeassistant.components.isy994 import ( + ISY994_NODES, ISY994_PROGRAMS, ISYDevice) +from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.core import callback -from homeassistant.components.binary_sensor import BinarySensorDevice, DOMAIN -from homeassistant.components.isy994 import (ISY994_NODES, ISY994_PROGRAMS, - ISYDevice) -from homeassistant.const import STATE_ON, STATE_OFF -from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.event import async_track_point_in_utc_time +from homeassistant.helpers.typing import ConfigType from homeassistant.util import dt as dt_util _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/isy994/cover.py b/homeassistant/components/isy994/cover.py index 4ead61e6b7a0f..22ea162979423 100644 --- a/homeassistant/components/isy994/cover.py +++ b/homeassistant/components/isy994/cover.py @@ -1,17 +1,12 @@ -""" -Support for ISY994 covers. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/cover.isy994/ -""" +"""Support for ISY994 covers.""" import logging from typing import Callable -from homeassistant.components.cover import CoverDevice, DOMAIN -from homeassistant.components.isy994 import (ISY994_NODES, ISY994_PROGRAMS, - ISYDevice) +from homeassistant.components.cover import DOMAIN, CoverDevice +from homeassistant.components.isy994 import ( + ISY994_NODES, ISY994_PROGRAMS, ISYDevice) from homeassistant.const import ( - STATE_OPEN, STATE_CLOSED, STATE_OPENING, STATE_CLOSING, STATE_UNKNOWN) + STATE_CLOSED, STATE_CLOSING, STATE_OPEN, STATE_OPENING, STATE_UNKNOWN) from homeassistant.helpers.typing import ConfigType _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/isy994/fan.py b/homeassistant/components/isy994/fan.py index 314200ba1c450..142eaedd66b86 100644 --- a/homeassistant/components/isy994/fan.py +++ b/homeassistant/components/isy994/fan.py @@ -1,17 +1,12 @@ -""" -Support for ISY994 fans. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/fan.isy994/ -""" +"""Support for ISY994 fans.""" import logging from typing import Callable -from homeassistant.components.fan import (FanEntity, DOMAIN, SPEED_OFF, - SPEED_LOW, SPEED_MEDIUM, - SPEED_HIGH, SUPPORT_SET_SPEED) -from homeassistant.components.isy994 import (ISY994_NODES, ISY994_PROGRAMS, - ISYDevice) +from homeassistant.components.fan import ( + DOMAIN, SPEED_HIGH, SPEED_LOW, SPEED_MEDIUM, SPEED_OFF, SUPPORT_SET_SPEED, + FanEntity) +from homeassistant.components.isy994 import ( + ISY994_NODES, ISY994_PROGRAMS, ISYDevice) from homeassistant.helpers.typing import ConfigType _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/isy994/light.py b/homeassistant/components/isy994/light.py index d54aa3cd4ce5f..cc39a6d1a3b1f 100644 --- a/homeassistant/components/isy994/light.py +++ b/homeassistant/components/isy994/light.py @@ -1,15 +1,9 @@ -""" -Support for ISY994 lights. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/light.isy994/ -""" +"""Support for ISY994 lights.""" import logging from typing import Callable -from homeassistant.components.light import ( - Light, SUPPORT_BRIGHTNESS, DOMAIN) from homeassistant.components.isy994 import ISY994_NODES, ISYDevice +from homeassistant.components.light import DOMAIN, SUPPORT_BRIGHTNESS, Light from homeassistant.helpers.typing import ConfigType _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/isy994/lock.py b/homeassistant/components/isy994/lock.py index 9481e619a6190..a2e8b1a1e56de 100644 --- a/homeassistant/components/isy994/lock.py +++ b/homeassistant/components/isy994/lock.py @@ -1,23 +1,18 @@ -""" -Support for ISY994 locks. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/lock.isy994/ -""" +"""Support for ISY994 locks.""" import logging from typing import Callable -from homeassistant.components.lock import LockDevice, DOMAIN -from homeassistant.components.isy994 import (ISY994_NODES, ISY994_PROGRAMS, - ISYDevice) -from homeassistant.const import STATE_LOCKED, STATE_UNLOCKED, STATE_UNKNOWN +from homeassistant.components.isy994 import ( + ISY994_NODES, ISY994_PROGRAMS, ISYDevice) +from homeassistant.components.lock import DOMAIN, LockDevice +from homeassistant.const import STATE_LOCKED, STATE_UNKNOWN, STATE_UNLOCKED from homeassistant.helpers.typing import ConfigType _LOGGER = logging.getLogger(__name__) VALUE_TO_STATE = { 0: STATE_UNLOCKED, - 100: STATE_LOCKED + 100: STATE_LOCKED, } diff --git a/homeassistant/components/isy994/sensor.py b/homeassistant/components/isy994/sensor.py index eca7e88a17eb3..60212d081de03 100644 --- a/homeassistant/components/isy994/sensor.py +++ b/homeassistant/components/isy994/sensor.py @@ -1,17 +1,11 @@ -""" -Support for ISY994 sensors. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/sensor.isy994/ -""" +"""Support for ISY994 sensors.""" import logging from typing import Callable +from homeassistant.components.isy994 import ( + ISY994_NODES, ISY994_WEATHER, ISYDevice) from homeassistant.components.sensor import DOMAIN -from homeassistant.components.isy994 import (ISY994_NODES, ISY994_WEATHER, - ISYDevice) -from homeassistant.const import ( - TEMP_CELSIUS, TEMP_FAHRENHEIT, UNIT_UV_INDEX) +from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT, UNIT_UV_INDEX from homeassistant.helpers.typing import ConfigType _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/isy994/switch.py b/homeassistant/components/isy994/switch.py index 6bb9c07de5b8c..96f17c80befd4 100644 --- a/homeassistant/components/isy994/switch.py +++ b/homeassistant/components/isy994/switch.py @@ -1,15 +1,10 @@ -""" -Support for ISY994 switches. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/switch.isy994/ -""" +"""Support for ISY994 switches.""" import logging from typing import Callable -from homeassistant.components.switch import SwitchDevice, DOMAIN -from homeassistant.components.isy994 import (ISY994_NODES, ISY994_PROGRAMS, - ISYDevice) +from homeassistant.components.isy994 import ( + ISY994_NODES, ISY994_PROGRAMS, ISYDevice) +from homeassistant.components.switch import DOMAIN, SwitchDevice from homeassistant.helpers.typing import ConfigType _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/knx/binary_sensor.py b/homeassistant/components/knx/binary_sensor.py index ca7037fe81d16..c84e5820f0461 100644 --- a/homeassistant/components/knx/binary_sensor.py +++ b/homeassistant/components/knx/binary_sensor.py @@ -5,12 +5,10 @@ PLATFORM_SCHEMA, BinarySensorDevice) from homeassistant.components.knx import ( ATTR_DISCOVER_DEVICES, DATA_KNX, KNXAutomation) -from homeassistant.const import CONF_NAME +from homeassistant.const import CONF_ADDRESS, CONF_DEVICE_CLASS, CONF_NAME from homeassistant.core import callback import homeassistant.helpers.config_validation as cv -CONF_ADDRESS = 'address' -CONF_DEVICE_CLASS = 'device_class' CONF_SIGNIFICANT_BIT = 'significant_bit' CONF_DEFAULT_SIGNIFICANT_BIT = 1 CONF_AUTOMATION = 'automation' @@ -32,10 +30,7 @@ vol.Required(CONF_ACTION): cv.SCRIPT_SCHEMA, }) -AUTOMATIONS_SCHEMA = vol.All( - cv.ensure_list, - [AUTOMATION_SCHEMA] -) +AUTOMATIONS_SCHEMA = vol.All(cv.ensure_list, [AUTOMATION_SCHEMA]) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_ADDRESS): cv.string, @@ -48,8 +43,8 @@ }) -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): """Set up binary sensor(s) for KNX platform.""" if discovery_info is not None: async_add_entities_discovery(hass, discovery_info, async_add_entities) diff --git a/homeassistant/components/knx/climate.py b/homeassistant/components/knx/climate.py index 82eaa52ae5a49..96b9f2ea91fed 100644 --- a/homeassistant/components/knx/climate.py +++ b/homeassistant/components/knx/climate.py @@ -1,16 +1,14 @@ """Support for KNX/IP climate devices.""" import voluptuous as vol -import homeassistant.helpers.config_validation as cv -from homeassistant.components.climate import ( - PLATFORM_SCHEMA, SUPPORT_ON_OFF, SUPPORT_OPERATION_MODE, - SUPPORT_TARGET_TEMPERATURE, STATE_HEAT, - STATE_IDLE, STATE_MANUAL, STATE_DRY, - STATE_FAN_ONLY, STATE_ECO, ClimateDevice) -from homeassistant.const import ( - ATTR_TEMPERATURE, CONF_NAME, TEMP_CELSIUS) -from homeassistant.core import callback -from homeassistant.components.knx import DATA_KNX, ATTR_DISCOVER_DEVICES +from homeassistant.components.climate import PLATFORM_SCHEMA, ClimateDevice +from homeassistant.components.climate.const import ( + STATE_DRY, STATE_ECO, STATE_FAN_ONLY, STATE_HEAT, STATE_IDLE, STATE_MANUAL, + SUPPORT_ON_OFF, SUPPORT_OPERATION_MODE, SUPPORT_TARGET_TEMPERATURE) +from homeassistant.components.knx import ATTR_DISCOVER_DEVICES, DATA_KNX +from homeassistant.const import ATTR_TEMPERATURE, CONF_NAME, TEMP_CELSIUS +from homeassistant.core import callback +import homeassistant.helpers.config_validation as cv CONF_SETPOINT_SHIFT_ADDRESS = 'setpoint_shift_address' CONF_SETPOINT_SHIFT_STATE_ADDRESS = 'setpoint_shift_state_address' @@ -80,15 +78,15 @@ vol.Optional(CONF_OPERATION_MODE_COMFORT_ADDRESS): cv.string, vol.Optional(CONF_ON_OFF_ADDRESS): cv.string, vol.Optional(CONF_ON_OFF_STATE_ADDRESS): cv.string, - vol.Optional(CONF_OPERATION_MODES): vol.All(cv.ensure_list, - [vol.In(OPERATION_MODES)]), + vol.Optional(CONF_OPERATION_MODES): + vol.All(cv.ensure_list, [vol.In(OPERATION_MODES)]), vol.Optional(CONF_MIN_TEMP): vol.Coerce(float), vol.Optional(CONF_MAX_TEMP): vol.Coerce(float), }) -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): """Set up climate(s) for KNX platform.""" if discovery_info is not None: async_add_entities_discovery(hass, discovery_info, async_add_entities) @@ -147,10 +145,8 @@ def async_add_entities_config(hass, config, async_add_entities): setpoint_shift_step=config.get(CONF_SETPOINT_SHIFT_STEP), setpoint_shift_max=config.get(CONF_SETPOINT_SHIFT_MAX), setpoint_shift_min=config.get(CONF_SETPOINT_SHIFT_MIN), - group_address_on_off=config.get( - CONF_ON_OFF_ADDRESS), - group_address_on_off_state=config.get( - CONF_ON_OFF_STATE_ADDRESS), + group_address_on_off=config.get(CONF_ON_OFF_ADDRESS), + group_address_on_off_state=config.get(CONF_ON_OFF_STATE_ADDRESS), min_temp=config.get(CONF_MIN_TEMP), max_temp=config.get(CONF_MAX_TEMP), mode=climate_mode) diff --git a/homeassistant/components/knx/light.py b/homeassistant/components/knx/light.py index f2a6f15e08b9e..baba7edd21aa8 100644 --- a/homeassistant/components/knx/light.py +++ b/homeassistant/components/knx/light.py @@ -3,19 +3,15 @@ import voluptuous as vol +from homeassistant.components.knx import ATTR_DISCOVER_DEVICES, DATA_KNX from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_HS_COLOR, PLATFORM_SCHEMA, - SUPPORT_BRIGHTNESS, SUPPORT_COLOR, SUPPORT_COLOR_TEMP, - Light) -from homeassistant.const import CONF_NAME + SUPPORT_BRIGHTNESS, SUPPORT_COLOR, SUPPORT_COLOR_TEMP, Light) +from homeassistant.const import CONF_ADDRESS, CONF_NAME from homeassistant.core import callback import homeassistant.helpers.config_validation as cv import homeassistant.util.color as color_util -from homeassistant.components.knx import ATTR_DISCOVER_DEVICES, DATA_KNX - - -CONF_ADDRESS = 'address' CONF_STATE_ADDRESS = 'state_address' CONF_BRIGHTNESS_ADDRESS = 'brightness_address' CONF_BRIGHTNESS_STATE_ADDRESS = 'brightness_state_address' diff --git a/homeassistant/components/knx/notify.py b/homeassistant/components/knx/notify.py index 2488114aa418b..1e1d7f185f0cf 100644 --- a/homeassistant/components/knx/notify.py +++ b/homeassistant/components/knx/notify.py @@ -1,14 +1,13 @@ """Support for KNX/IP notification services.""" import voluptuous as vol -from homeassistant.components.knx import DATA_KNX, ATTR_DISCOVER_DEVICES -from homeassistant.components.notify import PLATFORM_SCHEMA, \ - BaseNotificationService -from homeassistant.const import CONF_NAME +from homeassistant.components.knx import ATTR_DISCOVER_DEVICES, DATA_KNX +from homeassistant.components.notify import ( + PLATFORM_SCHEMA, BaseNotificationService) +from homeassistant.const import CONF_ADDRESS, CONF_NAME from homeassistant.core import callback import homeassistant.helpers.config_validation as cv -CONF_ADDRESS = 'address' DEFAULT_NAME = 'KNX Notify' DEPENDENCIES = ['knx'] diff --git a/homeassistant/components/knx/scene.py b/homeassistant/components/knx/scene.py index 008e81508b921..b1bb2bf310901 100644 --- a/homeassistant/components/knx/scene.py +++ b/homeassistant/components/knx/scene.py @@ -3,11 +3,10 @@ from homeassistant.components.knx import ATTR_DISCOVER_DEVICES, DATA_KNX from homeassistant.components.scene import CONF_PLATFORM, Scene -from homeassistant.const import CONF_NAME +from homeassistant.const import CONF_ADDRESS, CONF_NAME from homeassistant.core import callback import homeassistant.helpers.config_validation as cv -CONF_ADDRESS = 'address' CONF_SCENE_NUMBER = 'scene_number' DEFAULT_NAME = 'KNX SCENE' diff --git a/homeassistant/components/knx/sensor.py b/homeassistant/components/knx/sensor.py index 6a2d8144b1e81..abbb61e150d69 100644 --- a/homeassistant/components/knx/sensor.py +++ b/homeassistant/components/knx/sensor.py @@ -3,14 +3,11 @@ from homeassistant.components.knx import ATTR_DISCOVER_DEVICES, DATA_KNX from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import CONF_NAME +from homeassistant.const import CONF_ADDRESS, CONF_NAME, CONF_TYPE from homeassistant.core import callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity -CONF_ADDRESS = 'address' -CONF_TYPE = 'type' - DEFAULT_NAME = 'KNX Sensor' DEPENDENCIES = ['knx'] diff --git a/homeassistant/components/knx/switch.py b/homeassistant/components/knx/switch.py index 305234e1eec8b..cef14fb74dc12 100644 --- a/homeassistant/components/knx/switch.py +++ b/homeassistant/components/knx/switch.py @@ -3,11 +3,10 @@ from homeassistant.components.knx import ATTR_DISCOVER_DEVICES, DATA_KNX from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchDevice -from homeassistant.const import CONF_NAME +from homeassistant.const import CONF_ADDRESS, CONF_NAME from homeassistant.core import callback import homeassistant.helpers.config_validation as cv -CONF_ADDRESS = 'address' CONF_STATE_ADDRESS = 'state_address' DEFAULT_NAME = 'KNX Switch' diff --git a/homeassistant/components/lcn/__init__.py b/homeassistant/components/lcn/__init__.py index 941160b63970f..c7c180737f0a7 100644 --- a/homeassistant/components/lcn/__init__.py +++ b/homeassistant/components/lcn/__init__.py @@ -1,39 +1,22 @@ """Support for LCN devices.""" import logging -import re import voluptuous as vol +from homeassistant.components.lcn.const import ( + CONF_CONNECTIONS, CONF_DIM_MODE, CONF_DIMMABLE, CONF_MOTOR, CONF_OUTPUT, + CONF_SK_NUM_TRIES, CONF_TRANSITION, DATA_LCN, DEFAULT_NAME, DIM_MODES, + DOMAIN, MOTOR_PORTS, OUTPUT_PORTS, PATTERN_ADDRESS, RELAY_PORTS) from homeassistant.const import ( - CONF_ADDRESS, CONF_HOST, CONF_LIGHTS, CONF_NAME, CONF_PASSWORD, CONF_PORT, - CONF_SWITCHES, CONF_USERNAME) + CONF_ADDRESS, CONF_COVERS, CONF_HOST, CONF_LIGHTS, CONF_NAME, + CONF_PASSWORD, CONF_PORT, CONF_SWITCHES, CONF_USERNAME) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.discovery import async_load_platform from homeassistant.helpers.entity import Entity -REQUIREMENTS = ['pypck==0.5.9'] - _LOGGER = logging.getLogger(__name__) -DOMAIN = 'lcn' -DATA_LCN = 'lcn' -DEFAULT_NAME = 'pchk' - -CONF_SK_NUM_TRIES = 'sk_num_tries' -CONF_DIM_MODE = 'dim_mode' -CONF_OUTPUT = 'output' -CONF_TRANSITION = 'transition' -CONF_DIMMABLE = 'dimmable' -CONF_CONNECTIONS = 'connections' - -DIM_MODES = ['STEPS50', 'STEPS200'] -OUTPUT_PORTS = ['OUTPUT1', 'OUTPUT2', 'OUTPUT3', 'OUTPUT4'] -RELAY_PORTS = ['RELAY1', 'RELAY2', 'RELAY3', 'RELAY4', - 'RELAY5', 'RELAY6', 'RELAY7', 'RELAY8'] - -# Regex for address validation -PATTERN_ADDRESS = re.compile('^((?P\\w+)\\.)?s?(?P\\d+)' - '\\.(?Pm|g)?(?P\\d+)$') +REQUIREMENTS = ['pypck==0.5.9'] def has_unique_connection_names(connections): @@ -78,6 +61,12 @@ def is_address(value): raise vol.error.Invalid('Not a valid address string.') +COVERS_SCHEMA = vol.Schema({ + vol.Required(CONF_NAME): cv.string, + vol.Required(CONF_ADDRESS): is_address, + vol.Required(CONF_MOTOR): vol.All(vol.Upper, vol.In(MOTOR_PORTS)) + }) + LIGHTS_SCHEMA = vol.Schema({ vol.Required(CONF_NAME): cv.string, vol.Required(CONF_ADDRESS): is_address, @@ -111,8 +100,12 @@ def is_address(value): DOMAIN: vol.Schema({ vol.Required(CONF_CONNECTIONS): vol.All( cv.ensure_list, has_unique_connection_names, [CONNECTION_SCHEMA]), - vol.Optional(CONF_LIGHTS): vol.All(cv.ensure_list, [LIGHTS_SCHEMA]), - vol.Optional(CONF_SWITCHES): vol.All(cv.ensure_list, [SWITCHES_SCHEMA]) + vol.Optional(CONF_COVERS): vol.All( + cv.ensure_list, [COVERS_SCHEMA]), + vol.Optional(CONF_LIGHTS): vol.All( + cv.ensure_list, [LIGHTS_SCHEMA]), + vol.Optional(CONF_SWITCHES): vol.All( + cv.ensure_list, [SWITCHES_SCHEMA]) }) }, extra=vol.ALLOW_EXTRA) @@ -166,14 +159,14 @@ async def async_setup(hass, config): hass.data[DATA_LCN][CONF_CONNECTIONS] = connections - hass.async_create_task( - async_load_platform(hass, 'light', DOMAIN, - config[DOMAIN][CONF_LIGHTS], config)) - - hass.async_create_task( - async_load_platform(hass, 'switch', DOMAIN, - config[DOMAIN][CONF_SWITCHES], config)) - + # load platforms + for component, conf_key in (('cover', CONF_COVERS), + ('light', CONF_LIGHTS), + ('switch', CONF_SWITCHES)): + if conf_key in config[DOMAIN]: + hass.async_create_task( + async_load_platform(hass, component, DOMAIN, + config[DOMAIN][conf_key], config)) return True diff --git a/homeassistant/components/lcn/const.py b/homeassistant/components/lcn/const.py new file mode 100644 index 0000000000000..02b35b06797b0 --- /dev/null +++ b/homeassistant/components/lcn/const.py @@ -0,0 +1,26 @@ +"""Constants for the LCN component.""" +import re + +DOMAIN = 'lcn' +DATA_LCN = 'lcn' +DEFAULT_NAME = 'pchk' + +# Regex for address validation +PATTERN_ADDRESS = re.compile('^((?P\\w+)\\.)?s?(?P\\d+)' + '\\.(?Pm|g)?(?P\\d+)$') + +CONF_CONNECTIONS = 'connections' +CONF_SK_NUM_TRIES = 'sk_num_tries' +CONF_OUTPUT = 'output' +CONF_DIM_MODE = 'dim_mode' +CONF_DIMMABLE = 'dimmable' +CONF_TRANSITION = 'transition' +CONF_MOTOR = 'motor' + +DIM_MODES = ['STEPS50', 'STEPS200'] +OUTPUT_PORTS = ['OUTPUT1', 'OUTPUT2', 'OUTPUT3', 'OUTPUT4'] +RELAY_PORTS = ['RELAY1', 'RELAY2', 'RELAY3', 'RELAY4', + 'RELAY5', 'RELAY6', 'RELAY7', 'RELAY8', + 'MOTORONOFF1', 'MOTORUPDOWN1', 'MOTORONOFF2', 'MOTORUPDOWN2', + 'MOTORONOFF3', 'MOTORUPDOWN3', 'MOTORONOFF4', 'MOTORUPDOWN4'] +MOTOR_PORTS = ['MOTOR1', 'MOTOR2', 'MOTOR3', 'MOTOR4'] diff --git a/homeassistant/components/lcn/cover.py b/homeassistant/components/lcn/cover.py new file mode 100755 index 0000000000000..4b4542fd6236c --- /dev/null +++ b/homeassistant/components/lcn/cover.py @@ -0,0 +1,90 @@ +"""Support for LCN covers.""" +from homeassistant.components.cover import CoverDevice +from homeassistant.components.lcn import LcnDevice, get_connection +from homeassistant.components.lcn.const import ( + CONF_CONNECTIONS, CONF_MOTOR, DATA_LCN) +from homeassistant.const import CONF_ADDRESS + +DEPENDENCIES = ['lcn'] + + +async def async_setup_platform(hass, hass_config, async_add_entities, + discovery_info=None): + """Setups the LCN cover platform.""" + if discovery_info is None: + return + + import pypck + + devices = [] + for config in discovery_info: + address, connection_id = config[CONF_ADDRESS] + addr = pypck.lcn_addr.LcnAddr(*address) + connections = hass.data[DATA_LCN][CONF_CONNECTIONS] + connection = get_connection(connections, connection_id) + address_connection = connection.get_address_conn(addr) + + devices.append(LcnCover(config, address_connection)) + + async_add_entities(devices) + + +class LcnCover(LcnDevice, CoverDevice): + """Representation of a LCN cover.""" + + def __init__(self, config, address_connection): + """Initialize the LCN cover.""" + super().__init__(config, address_connection) + + self.motor = self.pypck.lcn_defs.MotorPort[config[CONF_MOTOR]] + self.motor_port_onoff = self.motor.value * 2 + self.motor_port_updown = self.motor_port_onoff + 1 + + self._closed = None + + async def async_added_to_hass(self): + """Run when entity about to be added to hass.""" + await super().async_added_to_hass() + self.hass.async_create_task( + self.address_connection.activate_status_request_handler( + self.motor)) + + @property + def is_closed(self): + """Return if the cover is closed.""" + return self._closed + + async def async_close_cover(self, **kwargs): + """Close the cover.""" + self._closed = True + states = [self.pypck.lcn_defs.MotorStateModifier.NOCHANGE] * 4 + states[self.motor.value] = self.pypck.lcn_defs.MotorStateModifier.DOWN + self.address_connection.control_motors(states) + await self.async_update_ha_state() + + async def async_open_cover(self, **kwargs): + """Open the cover.""" + self._closed = False + states = [self.pypck.lcn_defs.MotorStateModifier.NOCHANGE] * 4 + states[self.motor.value] = self.pypck.lcn_defs.MotorStateModifier.UP + self.address_connection.control_motors(states) + await self.async_update_ha_state() + + async def async_stop_cover(self, **kwargs): + """Stop the cover.""" + self._closed = None + states = [self.pypck.lcn_defs.MotorStateModifier.NOCHANGE] * 4 + states[self.motor.value] = self.pypck.lcn_defs.MotorStateModifier.STOP + self.address_connection.control_motors(states) + await self.async_update_ha_state() + + def input_received(self, input_obj): + """Set cover states when LCN input object (command) is received.""" + if not isinstance(input_obj, self.pypck.inputs.ModStatusRelays): + return + + states = input_obj.states # list of boolean values (relay on/off) + if states[self.motor_port_onoff]: # motor is on + self._closed = states[self.motor_port_updown] # set direction + + self.async_schedule_update_ha_state() diff --git a/homeassistant/components/lcn/light.py b/homeassistant/components/lcn/light.py index 2b7f4ed4074c4..5f1008cbd57a5 100644 --- a/homeassistant/components/lcn/light.py +++ b/homeassistant/components/lcn/light.py @@ -1,7 +1,8 @@ """Support for LCN lights.""" -from homeassistant.components.lcn import ( +from homeassistant.components.lcn import LcnDevice, get_connection +from homeassistant.components.lcn.const import ( CONF_CONNECTIONS, CONF_DIMMABLE, CONF_OUTPUT, CONF_TRANSITION, DATA_LCN, - OUTPUT_PORTS, LcnDevice, get_connection) + OUTPUT_PORTS) from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_TRANSITION, SUPPORT_BRIGHTNESS, SUPPORT_TRANSITION, Light) diff --git a/homeassistant/components/lcn/switch.py b/homeassistant/components/lcn/switch.py index 60eda2ea77995..09f35d267180c 100755 --- a/homeassistant/components/lcn/switch.py +++ b/homeassistant/components/lcn/switch.py @@ -1,7 +1,7 @@ """Support for LCN switches.""" -from homeassistant.components.lcn import ( - CONF_CONNECTIONS, CONF_OUTPUT, DATA_LCN, OUTPUT_PORTS, LcnDevice, - get_connection) +from homeassistant.components.lcn import LcnDevice, get_connection +from homeassistant.components.lcn.const import ( + CONF_CONNECTIONS, CONF_OUTPUT, DATA_LCN, OUTPUT_PORTS) from homeassistant.components.switch import SwitchDevice from homeassistant.const import CONF_ADDRESS diff --git a/homeassistant/components/lifx/.translations/es-419.json b/homeassistant/components/lifx/.translations/es-419.json new file mode 100644 index 0000000000000..905ec3ce2bfb6 --- /dev/null +++ b/homeassistant/components/lifx/.translations/es-419.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "No se han encontrado dispositivos LIFX en la red.", + "single_instance_allowed": "S\u00f3lo es posible una \u00fanica configuraci\u00f3n de LIFX." + }, + "step": { + "confirm": { + "description": "\u00bfDesea configurar LIFX?", + "title": "LIFX" + } + }, + "title": "LIFX" + } +} \ No newline at end of file diff --git a/homeassistant/components/lifx/.translations/it.json b/homeassistant/components/lifx/.translations/it.json new file mode 100644 index 0000000000000..b4f940bc66b8e --- /dev/null +++ b/homeassistant/components/lifx/.translations/it.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "Nessun dispositivo LIFX trovato in rete.", + "single_instance_allowed": "\u00c8 consentita solo una singola configurazione di LIFX." + }, + "step": { + "confirm": { + "description": "Vuoi configurare LIFX?", + "title": "LIFX" + } + }, + "title": "LIFX" + } +} \ No newline at end of file diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py index 816f93b58815e..93d7a67c6f084 100644 --- a/homeassistant/components/light/__init__.py +++ b/homeassistant/components/light/__init__.py @@ -440,6 +440,9 @@ def state_attributes(self): data[ATTR_MIN_MIREDS] = self.min_mireds data[ATTR_MAX_MIREDS] = self.max_mireds + 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 @@ -461,7 +464,6 @@ def state_attributes(self): data[ATTR_WHITE_VALUE] = self.white_value if supported_features & SUPPORT_EFFECT: - data[ATTR_EFFECT_LIST] = self.effect_list 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/rpi_gpio_pwm.py b/homeassistant/components/light/rpi_gpio_pwm.py index a3fe0f6b71ea7..b0b9ef1b76350 100644 --- a/homeassistant/components/light/rpi_gpio_pwm.py +++ b/homeassistant/components/light/rpi_gpio_pwm.py @@ -1,14 +1,9 @@ -""" -Support for LED lights that can be controlled using PWM. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/light.pwm/ -""" +"""Support for LED lights that can be controlled using PWM.""" import logging import voluptuous as vol -from homeassistant.const import CONF_NAME, CONF_TYPE, STATE_ON +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, SUPPORT_BRIGHTNESS, SUPPORT_COLOR, SUPPORT_TRANSITION, PLATFORM_SCHEMA) @@ -24,7 +19,6 @@ CONF_DRIVER = 'driver' CONF_PINS = 'pins' CONF_FREQUENCY = 'frequency' -CONF_ADDRESS = 'address' CONF_DRIVER_GPIO = 'gpio' CONF_DRIVER_PCA9685 = 'pca9685' @@ -46,11 +40,11 @@ { vol.Required(CONF_NAME): cv.string, vol.Required(CONF_DRIVER): vol.In(CONF_DRIVER_TYPES), - vol.Required(CONF_PINS): vol.All(cv.ensure_list, - [cv.positive_int]), + vol.Required(CONF_PINS): + vol.All(cv.ensure_list, [cv.positive_int]), vol.Required(CONF_TYPE): vol.In(CONF_LED_TYPES), vol.Optional(CONF_FREQUENCY): cv.positive_int, - vol.Optional(CONF_ADDRESS): cv.byte + vol.Optional(CONF_ADDRESS): cv.byte, } ]) }) diff --git a/homeassistant/components/light/services.yaml b/homeassistant/components/light/services.yaml index 9836bf97f9032..10cbeb42aa455 100644 --- a/homeassistant/components/light/services.yaml +++ b/homeassistant/components/light/services.yaml @@ -202,6 +202,9 @@ yeelight_start_flow: count: description: The number of times to run this flow (0 to run forever). example: 0 + action: + description: The action to take after the flow stops. Can be 'recover', 'stay', 'off'. (default 'recover') + example: 'stay' transitions: description: Array of transitions, for desired effect. Examples https://yeelight.readthedocs.io/en/stable/flow.html example: '[{ "TemperatureTransition": [1900, 1000, 80] }, { "TemperatureTransition": [1900, 1000, 10] }]' diff --git a/homeassistant/components/light/yeelight.py b/homeassistant/components/light/yeelight.py index 231821ffc13f4..b4b540f729b6a 100644 --- a/homeassistant/components/light/yeelight.py +++ b/homeassistant/components/light/yeelight.py @@ -46,8 +46,13 @@ ATTR_MODE = 'mode' ATTR_COUNT = 'count' +ATTR_ACTION = 'action' ATTR_TRANSITIONS = 'transitions' +ACTION_RECOVER = 'recover' +ACTION_STAY = 'stay' +ACTION_OFF = 'off' + YEELIGHT_RGB_TRANSITION = 'RGBTransition' YEELIGHT_HSV_TRANSACTION = 'HSVTransition' YEELIGHT_TEMPERATURE_TRANSACTION = 'TemperatureTransition' @@ -59,6 +64,8 @@ YEELIGHT_FLOW_TRANSITION_SCHEMA = { vol.Optional(ATTR_COUNT, default=0): cv.positive_int, + vol.Optional(ATTR_ACTION, default=ACTION_RECOVER): + vol.Any(ACTION_RECOVER, ACTION_OFF, ACTION_STAY), vol.Required(ATTR_TRANSITIONS): [{ vol.Exclusive(YEELIGHT_RGB_TRANSITION, CONF_TRANSITION): vol.All(cv.ensure_list, [cv.positive_int]), @@ -605,13 +612,14 @@ def transitions_config_parser(transitions): return transition_objects - def start_flow(self, transitions, count=0): + def start_flow(self, transitions, count=0, action=ACTION_RECOVER): """Start flow.""" import yeelight try: flow = yeelight.Flow( count=count, + action=yeelight.Flow.actions[action], transitions=self.transitions_config_parser(transitions)) self._bulb.start_flow(flow) diff --git a/homeassistant/components/locative/.translations/de.json b/homeassistant/components/locative/.translations/de.json new file mode 100644 index 0000000000000..14e0523fcf694 --- /dev/null +++ b/homeassistant/components/locative/.translations/de.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "Deine Home-Assistant-Instanz muss aus dem internet erreichbar sein, um Nachrichten von Geofency zu erhalten.", + "one_instance_allowed": "Nur eine einzige Instanz ist notwendig." + }, + "create_entry": { + "default": "Um Standorte Home Assistant zu senden, muss das Webhook Feature in der Locative App konfiguriert werden.\n\n F\u00fcge die folgenden Informationen ein: \n\n - URL: ` {webhook_url} ` \n - Methode: POST \n \n Weitere Informationen finden sich in der [Dokumentation]({docs_url})." + }, + "step": { + "user": { + "description": "M\u00f6chten Sie den Locative Webhook wirklich einrichten?", + "title": "Locative Webhook einrichten" + } + }, + "title": "Locative Webhook" + } +} \ No newline at end of file diff --git a/homeassistant/components/locative/.translations/es-419.json b/homeassistant/components/locative/.translations/es-419.json new file mode 100644 index 0000000000000..8fb63ff18c7af --- /dev/null +++ b/homeassistant/components/locative/.translations/es-419.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "Su instancia de Home Assistant debe ser accesible desde Internet para recibir mensajes de Geofency.", + "one_instance_allowed": "Solo una instancia es necesaria." + }, + "create_entry": { + "default": "Para enviar ubicaciones a Home Assistant, deber\u00e1 configurar la funci\u00f3n de webhook en la aplicaci\u00f3n Locative. \n\n Complete la siguiente informaci\u00f3n: \n\n - URL: ` {webhook_url} ` \n - M\u00e9todo: POST \n\n Vea [la documentaci\u00f3n] ( {docs_url} ) para m\u00e1s detalles." + }, + "step": { + "user": { + "description": "\u00bfEst\u00e1 seguro de que desea configurar el Webhook Locative?", + "title": "Configurar el Webhook Locative" + } + }, + "title": "Webhook Locative" + } +} \ No newline at end of file diff --git a/homeassistant/components/locative/.translations/hu.json b/homeassistant/components/locative/.translations/hu.json new file mode 100644 index 0000000000000..e90910c29a200 --- /dev/null +++ b/homeassistant/components/locative/.translations/hu.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "Az Home Assistantnek el\u00e9rhet\u0151nek kell lennie az internetr\u0151l, hogy megkapja a Geofency \u00fczeneteit.", + "one_instance_allowed": "Csak egy p\u00e9ld\u00e1ny sz\u00fcks\u00e9ges." + }, + "step": { + "user": { + "description": "Biztosan be szeretn\u00e9d be\u00e1ll\u00edtani a Locative Webhookot?", + "title": "Locative Webhook be\u00e1ll\u00edt\u00e1sa" + } + }, + "title": "Locative Webhook" + } +} \ No newline at end of file diff --git a/homeassistant/components/locative/.translations/it.json b/homeassistant/components/locative/.translations/it.json new file mode 100644 index 0000000000000..de62d2ac2f772 --- /dev/null +++ b/homeassistant/components/locative/.translations/it.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "La tua istanza di Home Assistant deve essere accessibile da Internet per ricevere messaggi da Geofency.", + "one_instance_allowed": "\u00c8 necessaria una sola istanza." + }, + "create_entry": { + "default": "Per inviare localit\u00e0 a Home Assistant, dovrai configurare la funzionalit\u00e0 webhook nell'app Locative.\n\n Compila le seguenti informazioni: \n\n - URL: ` {webhook_url} ` \n - Method: POST \n\n Vedi [la documentazione]({docs_url}) for ulteriori dettagli." + }, + "step": { + "user": { + "description": "Sei sicuro di voler configurare il webhook di Locative?", + "title": "Configura il webhook di Locative" + } + }, + "title": "Webhook di Locative" + } +} \ No newline at end of file diff --git a/homeassistant/components/locative/.translations/pt.json b/homeassistant/components/locative/.translations/pt.json new file mode 100644 index 0000000000000..2104ad9060791 --- /dev/null +++ b/homeassistant/components/locative/.translations/pt.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "A sua inst\u00e2ncia Home Assistent precisa de ser acess\u00edvel a partir da internet para receber mensagens Geofency.", + "one_instance_allowed": "Apenas uma inst\u00e2ncia \u00e9 necess\u00e1ria." + }, + "create_entry": { + "default": "Para enviar eventos para o Home Assistant, \u00e9 necess\u00e1rio configurar um webhook no Locative. \n\n Preencha as seguintes informa\u00e7\u00f5es: \n\n - URL: `{webhook_url}`\n - M\u00e9todo: POST \n\n Veja [the documentation]({docs_url}) para obter mais detalhes." + }, + "step": { + "user": { + "description": "Tem certeza de que deseja configurar o Locative Webhook?", + "title": "Configurar o Locative Webhook" + } + }, + "title": "Locative Webhook" + } +} \ No newline at end of file diff --git a/homeassistant/components/locative/.translations/ru.json b/homeassistant/components/locative/.translations/ru.json index ff07393da04b0..70f08595f3a23 100644 --- a/homeassistant/components/locative/.translations/ru.json +++ b/homeassistant/components/locative/.translations/ru.json @@ -5,7 +5,7 @@ "one_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430." }, "create_entry": { - "default": "\u0414\u043b\u044f \u043e\u0442\u043f\u0440\u0430\u0432\u043a\u0438 \u0441\u043e\u0431\u044b\u0442\u0438\u0439 \u0432 Home Assistant \u0432\u044b \u0434\u043e\u043b\u0436\u043d\u044b \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c webhooks \u0434\u043b\u044f Locative.\n\n\u0414\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0443\u044e \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e:\n\n- URL: `{webhook_url}`\n- Method: POST\n\n\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0435\u0439]({docs_url}) \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0431\u043e\u043b\u0435\u0435 \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u043e\u0439 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438." + "default": "\u0414\u043b\u044f \u043e\u0442\u043f\u0440\u0430\u0432\u043a\u0438 \u0441\u043e\u0431\u044b\u0442\u0438\u0439 \u0432 Home Assistant \u0412\u044b \u0434\u043e\u043b\u0436\u043d\u044b \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c webhooks \u0434\u043b\u044f Locative.\n\n\u0414\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0443\u044e \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e:\n\n- URL: `{webhook_url}`\n- Method: POST\n\n\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0435\u0439]({docs_url}) \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0431\u043e\u043b\u0435\u0435 \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u043e\u0439 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438." }, "step": { "user": { diff --git a/homeassistant/components/locative/.translations/sv.json b/homeassistant/components/locative/.translations/sv.json new file mode 100644 index 0000000000000..0296d07993874 --- /dev/null +++ b/homeassistant/components/locative/.translations/sv.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "Din Home Assistant instans m\u00e5ste vara \u00e5tkomlig ifr\u00e5n internet f\u00f6r att ta emot meddelanden ifr\u00e5n Geofency.", + "one_instance_allowed": "Endast en enda instans \u00e4r n\u00f6dv\u00e4ndig." + }, + "create_entry": { + "default": "F\u00f6r att skicka h\u00e4ndelser till Home Assistant m\u00e5ste du konfigurera webhook funktionen i Locative appen.\n\n Fyll i f\u00f6ljande information:\n \n- URL: `{webhook_url}`\n- Method: POST\n\nSe [dokumentation]({docs_url}) om hur du konfigurerar detta f\u00f6r mer information." + }, + "step": { + "user": { + "description": "\u00c4r du s\u00e4ker p\u00e5 att du vill konfigurera Locative Webhook?", + "title": "Konfigurera Locative Webhook" + } + }, + "title": "Locative Webhook" + } +} \ No newline at end of file diff --git a/homeassistant/components/logbook/__init__.py b/homeassistant/components/logbook/__init__.py index 74a90f0f5f0ca..dbedc8c6d7048 100644 --- a/homeassistant/components/logbook/__init__.py +++ b/homeassistant/components/logbook/__init__.py @@ -366,8 +366,7 @@ def _get_related_entity_ids(session, entity_filter): if tryno == RETRIES - 1: raise - else: - time.sleep(QUERY_RETRY_WAIT) + time.sleep(QUERY_RETRY_WAIT) def _generate_filter_from_config(config): diff --git a/homeassistant/components/logi_circle/__init__.py b/homeassistant/components/logi_circle/__init__.py index 50500f47e421b..ef006ef8b4db1 100644 --- a/homeassistant/components/logi_circle/__init__.py +++ b/homeassistant/components/logi_circle/__init__.py @@ -1,19 +1,19 @@ """Support for Logi Circle devices.""" -import logging import asyncio +import logging -import voluptuous as vol import async_timeout +import voluptuous as vol +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME import homeassistant.helpers.config_validation as cv -from homeassistant.const import CONF_USERNAME, CONF_PASSWORD REQUIREMENTS = ['logi_circle==0.1.7'] _LOGGER = logging.getLogger(__name__) _TIMEOUT = 15 # seconds -CONF_ATTRIBUTION = "Data provided by circle.logi.com" +ATTRIBUTION = "Data provided by circle.logi.com" NOTIFICATION_ID = 'logi_notification' NOTIFICATION_TITLE = 'Logi Circle Setup' diff --git a/homeassistant/components/logi_circle/camera.py b/homeassistant/components/logi_circle/camera.py index 51bd7c124a3ff..4f349dd986e44 100644 --- a/homeassistant/components/logi_circle/camera.py +++ b/homeassistant/components/logi_circle/camera.py @@ -7,7 +7,7 @@ from homeassistant.helpers import config_validation as cv from homeassistant.components.logi_circle import ( - DOMAIN as LOGI_CIRCLE_DOMAIN, CONF_ATTRIBUTION) + DOMAIN as LOGI_CIRCLE_DOMAIN, ATTRIBUTION) from homeassistant.components.camera import ( Camera, PLATFORM_SCHEMA, CAMERA_SERVICE_SCHEMA, SUPPORT_ON_OFF, ATTR_ENTITY_ID, ATTR_FILENAME, DOMAIN) @@ -128,7 +128,7 @@ def supported_features(self): def device_state_attributes(self): """Return the state attributes.""" state = { - ATTR_ATTRIBUTION: CONF_ATTRIBUTION, + ATTR_ATTRIBUTION: ATTRIBUTION, 'battery_saving_mode': ( STATE_ON if self._camera.battery_saving else STATE_OFF), 'ip_address': self._camera.ip_address, diff --git a/homeassistant/components/logi_circle/sensor.py b/homeassistant/components/logi_circle/sensor.py index 74c2039c12052..4830219091cdc 100644 --- a/homeassistant/components/logi_circle/sensor.py +++ b/homeassistant/components/logi_circle/sensor.py @@ -5,7 +5,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.components.logi_circle import ( - CONF_ATTRIBUTION, DEFAULT_ENTITY_NAMESPACE, DOMAIN as LOGI_CIRCLE_DOMAIN) + ATTRIBUTION, DEFAULT_ENTITY_NAMESPACE, DOMAIN as LOGI_CIRCLE_DOMAIN) from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( ATTR_ATTRIBUTION, ATTR_BATTERY_CHARGING, @@ -86,7 +86,7 @@ def state(self): def device_state_attributes(self): """Return the state attributes.""" state = { - ATTR_ATTRIBUTION: CONF_ATTRIBUTION, + ATTR_ATTRIBUTION: ATTRIBUTION, 'battery_saving_mode': ( STATE_ON if self._camera.battery_saving else STATE_OFF), 'ip_address': self._camera.ip_address, diff --git a/homeassistant/components/luftdaten/.translations/es-419.json b/homeassistant/components/luftdaten/.translations/es-419.json new file mode 100644 index 0000000000000..8e81e9e52a141 --- /dev/null +++ b/homeassistant/components/luftdaten/.translations/es-419.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "communication_error": "No se puede comunicar con la API de Luftdaten", + "invalid_sensor": "Sensor no disponible o no v\u00e1lido", + "sensor_exists": "Sensor ya registrado" + }, + "step": { + "user": { + "data": { + "show_on_map": "Mostrar en el mapa", + "station_id": "ID del sensor de Luftdaten" + }, + "title": "Definir Luftdaten" + } + }, + "title": "Luftdaten" + } +} \ No newline at end of file diff --git a/homeassistant/components/luftdaten/.translations/hu.json b/homeassistant/components/luftdaten/.translations/hu.json index 48914a944654c..b8b2b1fc0d896 100644 --- a/homeassistant/components/luftdaten/.translations/hu.json +++ b/homeassistant/components/luftdaten/.translations/hu.json @@ -1,7 +1,16 @@ { "config": { + "error": { + "communication_error": "Nem lehet kommunik\u00e1lni a Luftdaten API-val", + "invalid_sensor": "Az \u00e9rz\u00e9kel\u0151 nem el\u00e9rhet\u0151 vagy \u00e9rv\u00e9nytelen", + "sensor_exists": "Az \u00e9rz\u00e9kel\u0151 m\u00e1r regisztr\u00e1lt" + }, "step": { "user": { + "data": { + "show_on_map": "Mutasd a t\u00e9rk\u00e9pen", + "station_id": "Luftdaten \u00e9rz\u00e9kel\u0151 ID" + }, "title": "Luftdaten be\u00e1ll\u00edt\u00e1sa" } }, diff --git a/homeassistant/components/luftdaten/.translations/it.json b/homeassistant/components/luftdaten/.translations/it.json new file mode 100644 index 0000000000000..279513782958f --- /dev/null +++ b/homeassistant/components/luftdaten/.translations/it.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "communication_error": "Impossibile comunicare con l'API Luftdaten", + "invalid_sensor": "Sensore non disponibile o non valido", + "sensor_exists": "Sensore gi\u00e0 registrato" + }, + "step": { + "user": { + "data": { + "show_on_map": "Mostra sulla mappa", + "station_id": "ID del sensore Luftdaten" + }, + "title": "Definisci Luftdaten" + } + }, + "title": "Luftdaten" + } +} \ No newline at end of file diff --git a/homeassistant/components/luftdaten/.translations/sv.json b/homeassistant/components/luftdaten/.translations/sv.json new file mode 100644 index 0000000000000..01fd9ec721b34 --- /dev/null +++ b/homeassistant/components/luftdaten/.translations/sv.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "communication_error": "Det g\u00e5r inte att kommunicera med Luftdaten API", + "invalid_sensor": "Sensor saknas eller \u00e4r ogiltig", + "sensor_exists": "Sensorn \u00e4r redan registrerad" + }, + "step": { + "user": { + "data": { + "show_on_map": "Visa p\u00e5 karta", + "station_id": "Luftdaten Sensor-ID" + }, + "title": "Definiera Luftdaten" + } + }, + "title": "Luftdaten" + } +} \ No newline at end of file diff --git a/homeassistant/components/lutron/__init__.py b/homeassistant/components/lutron/__init__.py index e4ebec4cc5a17..fae44d3584daa 100644 --- a/homeassistant/components/lutron/__init__.py +++ b/homeassistant/components/lutron/__init__.py @@ -134,19 +134,18 @@ 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 + # SingleAction -> single + action = None if self._has_release_event: - # A raise/lower button; we will get callbacks when the button is - # pressed and when it's released, so fire events for each. if event == Button.Event.PRESSED: action = 'pressed' else: action = 'released' - else: - # A single-action button; the Lutron controller won't tell us - # when the button is released, so use a different action name - # than for buttons where we expect a release event. + elif event == Button.Event.PRESSED: action = 'single' - data = {ATTR_ID: self._id, ATTR_ACTION: action} - - self._hass.bus.fire(self._event, data) + if action: + data = {ATTR_ID: self._id, ATTR_ACTION: action} + self._hass.bus.fire(self._event, data) diff --git a/homeassistant/components/mailgun/.translations/es-419.json b/homeassistant/components/mailgun/.translations/es-419.json new file mode 100644 index 0000000000000..fd0c543241b13 --- /dev/null +++ b/homeassistant/components/mailgun/.translations/es-419.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "Su instancia de Home Assistant debe ser accesible desde Internet para recibir mensajes de Mailgun.", + "one_instance_allowed": "Solo una instancia es necesaria." + }, + "create_entry": { + "default": "Para enviar eventos a Home Assistant, deber\u00e1 configurar [Webhooks with Mailgun] ( {mailgun_url} ). \n\n Complete la siguiente informaci\u00f3n: \n\n - URL: ` {webhook_url} ` \n - M\u00e9todo: POST \n - Tipo de contenido: aplicaci\u00f3n / json \n\n Consulte [la documentaci\u00f3n] ( {docs_url} ) sobre c\u00f3mo configurar las automatizaciones para manejar los datos entrantes." + }, + "step": { + "user": { + "description": "\u00bfEst\u00e1s seguro de que quieres configurar Mailgun?", + "title": "Configurar el Webhook de Mailgun" + } + }, + "title": "Mailgun" + } +} \ No newline at end of file diff --git a/homeassistant/components/mailgun/.translations/it.json b/homeassistant/components/mailgun/.translations/it.json new file mode 100644 index 0000000000000..4dea652aa3f8a --- /dev/null +++ b/homeassistant/components/mailgun/.translations/it.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "La tua istanza di Home Assistant deve essere accessibile da Internet per ricevere messaggi da Mailgun.", + "one_instance_allowed": "\u00c8 necessaria una sola istanza." + }, + "create_entry": { + "default": "Per inviare eventi a Home Assistant, dovrai configurare [Webhooks con Mailgun]({mailgun_url})\n\n Compila le seguenti informazioni: \n\n - URL: ` {webhook_url} ` \n - Method: POST \n - Content Type: application/json\n\n Vedi [la documentazione]({docs_url}) su come configurare le automazioni per gestire i dati in arrivo." + }, + "step": { + "user": { + "description": "Sei sicuro di voler configurare Mailgun?", + "title": "Configura il webhook di Mailgun" + } + }, + "title": "Mailgun" + } +} \ No newline at end of file diff --git a/homeassistant/components/mailgun/.translations/ru.json b/homeassistant/components/mailgun/.translations/ru.json index b1828ee28ef39..39503154b6caa 100644 --- a/homeassistant/components/mailgun/.translations/ru.json +++ b/homeassistant/components/mailgun/.translations/ru.json @@ -5,7 +5,7 @@ "one_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430." }, "create_entry": { - "default": "\u0414\u043b\u044f \u043e\u0442\u043f\u0440\u0430\u0432\u043a\u0438 \u0441\u043e\u0431\u044b\u0442\u0438\u0439 \u0432 Home Assistant \u0432\u044b \u0434\u043e\u043b\u0436\u043d\u044b \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c [Webhooks \u0434\u043b\u044f Mailgun]({mailgun_url}).\n\n\u0417\u0430\u043f\u043e\u043b\u043d\u0438\u0442\u0435 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0443\u044e \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e:\n\n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/x-www-form-urlencoded\n\n\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0435\u0439]({docs_url}) \u0434\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0437\u0430\u0446\u0438\u0439 \u043f\u043e \u043e\u0431\u0440\u0430\u0431\u043e\u0442\u043a\u0435 \u043f\u043e\u0441\u0442\u0443\u043f\u0430\u044e\u0449\u0438\u0445 \u0434\u0430\u043d\u043d\u044b\u0445." + "default": "\u0414\u043b\u044f \u043e\u0442\u043f\u0440\u0430\u0432\u043a\u0438 \u0441\u043e\u0431\u044b\u0442\u0438\u0439 \u0432 Home Assistant \u0412\u044b \u0434\u043e\u043b\u0436\u043d\u044b \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c [Webhooks \u0434\u043b\u044f Mailgun]({mailgun_url}).\n\n\u0417\u0430\u043f\u043e\u043b\u043d\u0438\u0442\u0435 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0443\u044e \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e:\n\n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/x-www-form-urlencoded\n\n\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0435\u0439]({docs_url}) \u0434\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0437\u0430\u0446\u0438\u0439 \u043f\u043e \u043e\u0431\u0440\u0430\u0431\u043e\u0442\u043a\u0435 \u043f\u043e\u0441\u0442\u0443\u043f\u0430\u044e\u0449\u0438\u0445 \u0434\u0430\u043d\u043d\u044b\u0445." }, "step": { "user": { diff --git a/homeassistant/components/mailgun/.translations/sv.json b/homeassistant/components/mailgun/.translations/sv.json new file mode 100644 index 0000000000000..f26234e84cf0a --- /dev/null +++ b/homeassistant/components/mailgun/.translations/sv.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "Din Home Assistant instans m\u00e5ste vara tillg\u00e4nglig fr\u00e5n internet f\u00f6r att ta emot Mailgun meddelanden.", + "one_instance_allowed": "Endast en enda instans \u00e4r n\u00f6dv\u00e4ndig." + }, + "create_entry": { + "default": "F\u00f6r att skicka h\u00e4ndelser till Home Assistant m\u00e5ste du konfigurera [Webhooks med Mailgun]({mailgun_url}).\n\n Fyll i f\u00f6ljande information:\n \n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/json\n\n Se [dokumentationen]({docs_url}) om hur du konfigurerar automatiseringar f\u00f6r att hantera inkommande data." + }, + "step": { + "user": { + "description": "\u00c4r du s\u00e4ker p\u00e5 att du vill konfigurera Mailgun?", + "title": "Konfigurera Mailgun Webhook" + } + }, + "title": "Mailgun" + } +} \ No newline at end of file diff --git a/homeassistant/components/maxcube/climate.py b/homeassistant/components/maxcube/climate.py index f5c4533123f17..170a3ba349cc0 100644 --- a/homeassistant/components/maxcube/climate.py +++ b/homeassistant/components/maxcube/climate.py @@ -2,8 +2,9 @@ import socket import logging -from homeassistant.components.climate import ( - ClimateDevice, STATE_AUTO, SUPPORT_TARGET_TEMPERATURE, +from homeassistant.components.climate import ClimateDevice +from homeassistant.components.climate.const import ( + STATE_AUTO, SUPPORT_TARGET_TEMPERATURE, SUPPORT_OPERATION_MODE) from homeassistant.components.maxcube import DATA_KEY from homeassistant.const import TEMP_CELSIUS, ATTR_TEMPERATURE diff --git a/homeassistant/components/media_extractor/__init__.py b/homeassistant/components/media_extractor/__init__.py index efc3e8bddc8f4..d0b3c9e3c003d 100644 --- a/homeassistant/components/media_extractor/__init__.py +++ b/homeassistant/components/media_extractor/__init__.py @@ -12,7 +12,7 @@ ATTR_ENTITY_ID) from homeassistant.helpers import config_validation as cv -REQUIREMENTS = ['youtube_dl==2019.02.08'] +REQUIREMENTS = ['youtube_dl==2019.02.18'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/media_player/anthemav.py b/homeassistant/components/media_player/anthemav.py index d48f90d2bd7eb..f867a10ccd0c3 100644 --- a/homeassistant/components/media_player/anthemav.py +++ b/homeassistant/components/media_player/anthemav.py @@ -18,7 +18,7 @@ STATE_ON) import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['anthemav==1.1.8'] +REQUIREMENTS = ['anthemav==1.1.9'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/media_player/bluesound.py b/homeassistant/components/media_player/bluesound.py index c6a8c51ca58bf..b25916c7f66d9 100644 --- a/homeassistant/components/media_player/bluesound.py +++ b/homeassistant/components/media_player/bluesound.py @@ -345,9 +345,8 @@ async def send_bluesound_command( if raise_timeout: _LOGGER.info("Timeout: %s", self.host) raise - else: - _LOGGER.debug("Failed communicating: %s", self.host) - return None + _LOGGER.debug("Failed communicating: %s", self.host) + return None return data diff --git a/homeassistant/components/media_player/firetv.py b/homeassistant/components/media_player/firetv.py index 58f1913b9f983..fb7df736e5188 100644 --- a/homeassistant/components/media_player/firetv.py +++ b/homeassistant/components/media_player/firetv.py @@ -6,7 +6,6 @@ """ import functools import logging -import threading import voluptuous as vol from homeassistant.components.media_player import ( @@ -20,22 +19,22 @@ STATE_PLAYING, STATE_STANDBY) import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['firetv==1.0.7'] +REQUIREMENTS = ['firetv==1.0.9'] _LOGGER = logging.getLogger(__name__) -SUPPORT_FIRETV = SUPPORT_PAUSE | \ +SUPPORT_FIRETV = SUPPORT_PAUSE | SUPPORT_PLAY | \ SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_PREVIOUS_TRACK | \ - SUPPORT_NEXT_TRACK | SUPPORT_SELECT_SOURCE | SUPPORT_STOP | \ - SUPPORT_PLAY + SUPPORT_NEXT_TRACK | SUPPORT_SELECT_SOURCE | SUPPORT_STOP CONF_ADBKEY = 'adbkey' -CONF_GET_SOURCE = 'get_source' +CONF_ADB_SERVER_IP = 'adb_server_ip' +CONF_ADB_SERVER_PORT = 'adb_server_port' CONF_GET_SOURCES = 'get_sources' DEFAULT_NAME = 'Amazon Fire TV' DEFAULT_PORT = 5555 -DEFAULT_GET_SOURCE = True +DEFAULT_ADB_SERVER_PORT = 5037 DEFAULT_GET_SOURCES = True @@ -52,12 +51,18 @@ def has_adb_files(value): vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, vol.Optional(CONF_ADBKEY): has_adb_files, - vol.Optional(CONF_GET_SOURCE, default=DEFAULT_GET_SOURCE): cv.boolean, + vol.Optional(CONF_ADB_SERVER_IP): cv.string, + vol.Optional( + CONF_ADB_SERVER_PORT, default=DEFAULT_ADB_SERVER_PORT): cv.port, vol.Optional(CONF_GET_SOURCES, default=DEFAULT_GET_SOURCES): cv.boolean }) -PACKAGE_LAUNCHER = "com.amazon.tv.launcher" -PACKAGE_SETTINGS = "com.amazon.tv.settings" +# Translate from `FireTV` reported state to HA state. +FIRETV_STATES = {'off': STATE_OFF, + 'idle': STATE_IDLE, + 'standby': STATE_STANDBY, + 'playing': STATE_PLAYING, + 'paused': STATE_PAUSED} def setup_platform(hass, config, add_entities, discovery_info=None): @@ -66,82 +71,82 @@ def setup_platform(hass, config, add_entities, discovery_info=None): host = '{0}:{1}'.format(config[CONF_HOST], config[CONF_PORT]) - if CONF_ADBKEY in config: - ftv = FireTV(host, config[CONF_ADBKEY]) - adb_log = " using adbkey='{0}'".format(config[CONF_ADBKEY]) + if CONF_ADB_SERVER_IP not in config: + # Use "python-adb" (Python ADB implementation) + if CONF_ADBKEY in config: + ftv = FireTV(host, config[CONF_ADBKEY]) + adb_log = " using adbkey='{0}'".format(config[CONF_ADBKEY]) + else: + ftv = FireTV(host) + adb_log = "" else: - ftv = FireTV(host) - adb_log = "" + # Use "pure-python-adb" (communicate with ADB server) + ftv = FireTV(host, adb_server_ip=config[CONF_ADB_SERVER_IP], + adb_server_port=config[CONF_ADB_SERVER_PORT]) + adb_log = " using ADB server at {0}:{1}".format( + config[CONF_ADB_SERVER_IP], config[CONF_ADB_SERVER_PORT]) if not ftv.available: _LOGGER.warning("Could not connect to Fire TV at %s%s", host, adb_log) return name = config[CONF_NAME] - get_source = config[CONF_GET_SOURCE] get_sources = config[CONF_GET_SOURCES] - device = FireTVDevice(ftv, name, get_source, get_sources) + device = FireTVDevice(ftv, name, get_sources) add_entities([device]) _LOGGER.debug("Setup Fire TV at %s%s", host, adb_log) def adb_decorator(override_available=False): - """Send an ADB command if the device is available and not locked.""" - def adb_wrapper(func): + """Send an ADB command if the device is available and catch exceptions.""" + def _adb_decorator(func): """Wait if previous ADB commands haven't finished.""" @functools.wraps(func) - def _adb_wrapper(self, *args, **kwargs): + def _adb_exception_catcher(self, *args, **kwargs): # If the device is unavailable, don't do anything if not self.available and not override_available: return None - # If an ADB command is already running, skip this command - if not self.adb_lock.acquire(blocking=False): - _LOGGER.info("Skipping an ADB command because a previous " - "command is still running") - return None - - # Additional ADB commands will be prevented while trying this one try: - returns = func(self, *args, **kwargs) + return func(self, *args, **kwargs) except self.exceptions as err: _LOGGER.error( "Failed to execute an ADB command. ADB connection re-" "establishing attempt in the next update. Error: %s", err) - returns = None self._available = False # pylint: disable=protected-access - finally: - self.adb_lock.release() - - return returns + return None - return _adb_wrapper + return _adb_exception_catcher - return adb_wrapper + return _adb_decorator class FireTVDevice(MediaPlayerDevice): """Representation of an Amazon Fire TV device on the network.""" - def __init__(self, ftv, name, get_source, get_sources): + def __init__(self, ftv, name, get_sources): """Initialize the FireTV device.""" - from adb.adb_protocol import ( - InvalidChecksumError, InvalidCommandError, InvalidResponseError) - self.firetv = ftv self._name = name - self._get_source = get_source self._get_sources = get_sources - # whether or not the ADB connection is currently in use - self.adb_lock = threading.Lock() - # ADB exceptions to catch - self.exceptions = ( - AttributeError, BrokenPipeError, TypeError, ValueError, - InvalidChecksumError, InvalidCommandError, InvalidResponseError) + if not self.firetv.adb_server_ip: + # Using "python-adb" (Python ADB implementation) + from adb.adb_protocol import (InvalidChecksumError, + InvalidCommandError, + InvalidResponseError) + from adb.usb_exceptions import TcpTimeoutException + + self.exceptions = (AttributeError, BrokenPipeError, TypeError, + ValueError, InvalidChecksumError, + InvalidCommandError, InvalidResponseError, + TcpTimeoutException) + else: + # Using "pure-python-adb" (communicate with ADB server) + self.exceptions = (ConnectionResetError,) self._state = None self._available = self.firetv.available @@ -190,72 +195,24 @@ def source_list(self): @adb_decorator(override_available=True) def update(self): - """Get the latest date and update device state.""" + """Update the device state and, if necessary, re-connect.""" # Check if device is disconnected. if not self._available: - self._running_apps = None - self._current_app = None - # Try to connect - self.firetv.connect() - self._available = self.firetv.available + self._available = self.firetv.connect() + + # To be safe, wait until the next update to run ADB commands. + return # If the ADB connection is not intact, don't update. if not self._available: return - # Check if device is off. - if not self.firetv.screen_on: - self._state = STATE_OFF - self._running_apps = None - self._current_app = None + # Get the `state`, `current_app`, and `running_apps`. + ftv_state, self._current_app, self._running_apps = \ + self.firetv.update(self._get_sources) - # Check if screen saver is on. - elif not self.firetv.awake: - self._state = STATE_IDLE - self._running_apps = None - self._current_app = None - - else: - # Get the running apps. - if self._get_sources: - self._running_apps = self.firetv.running_apps - - # Get the current app. - if self._get_source: - current_app = self.firetv.current_app - if isinstance(current_app, dict)\ - and 'package' in current_app: - self._current_app = current_app['package'] - else: - self._current_app = current_app - - # Show the current app as the only running app. - if not self._get_sources: - if self._current_app: - self._running_apps = [self._current_app] - else: - self._running_apps = None - - # Check if the launcher is active. - if self._current_app in [PACKAGE_LAUNCHER, PACKAGE_SETTINGS]: - self._state = STATE_STANDBY - - # Check for a wake lock (device is playing). - elif self.firetv.wake_lock: - self._state = STATE_PLAYING - - # Otherwise, device is paused. - else: - self._state = STATE_PAUSED - - # Don't get the current app. - elif self.firetv.wake_lock: - # Check for a wake lock (device is playing). - self._state = STATE_PLAYING - else: - # Assume the devices is on standby. - self._state = STATE_STANDBY + self._state = FIRETV_STATES[ftv_state] @adb_decorator() def turn_on(self): diff --git a/homeassistant/components/media_player/panasonic_viera.py b/homeassistant/components/media_player/panasonic_viera.py index e5ce22e952431..e6546f7c1e276 100644 --- a/homeassistant/components/media_player/panasonic_viera.py +++ b/homeassistant/components/media_player/panasonic_viera.py @@ -59,20 +59,20 @@ def setup_platform(hass, config, add_entities, discovery_info=None): else: uuid = None remote = RemoteControl(host, port) - add_entities([PanasonicVieraTVDevice(mac, name, remote, uuid)]) + add_entities([PanasonicVieraTVDevice(mac, name, remote, host, uuid)]) return True host = config.get(CONF_HOST) remote = RemoteControl(host, port) - add_entities([PanasonicVieraTVDevice(mac, name, remote)]) + add_entities([PanasonicVieraTVDevice(mac, name, remote, host)]) return True class PanasonicVieraTVDevice(MediaPlayerDevice): """Representation of a Panasonic Viera TV.""" - def __init__(self, mac, name, remote, uuid=None): + def __init__(self, mac, name, remote, host, uuid=None): """Initialize the Panasonic device.""" import wakeonlan # Save a reference to the imported class @@ -84,6 +84,7 @@ def __init__(self, mac, name, remote, uuid=None): self._playing = True self._state = None self._remote = remote + self._host = host self._volume = 0 @property @@ -140,7 +141,7 @@ def supported_features(self): def turn_on(self): """Turn on the media player.""" if self._mac: - self._wol.send_magic_packet(self._mac) + self._wol.send_magic_packet(self._mac, ip_address=self._host) self._state = STATE_ON def turn_off(self): diff --git a/homeassistant/components/media_player/philips_js.py b/homeassistant/components/media_player/philips_js.py index 4f8a133978193..97ec758e6cf26 100644 --- a/homeassistant/components/media_player/philips_js.py +++ b/homeassistant/components/media_player/philips_js.py @@ -19,13 +19,12 @@ CONF_API_VERSION, CONF_HOST, CONF_NAME, STATE_OFF, STATE_ON) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.script import Script -from homeassistant.util import Throttle REQUIREMENTS = ['ha-philipsjs==0.0.5'] _LOGGER = logging.getLogger(__name__) -MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=30) +SCAN_INTERVAL = timedelta(seconds=30) SUPPORT_PHILIPS_JS = SUPPORT_TURN_OFF | SUPPORT_VOLUME_STEP | \ SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \ @@ -72,8 +71,6 @@ def __init__(self, tv, name, on_script): self._tv = tv self._name = name self._state = None - self._min_volume = None - self._max_volume = None self._volume = None self._muted = False self._program_name = None @@ -123,10 +120,6 @@ def select_source(self, source): """Set the input source.""" if source in self._source_mapping: self._tv.setSource(self._source_mapping.get(source)) - self._source = source - if not self._tv.on: - self._state = STATE_OFF - self._watching_tv = bool(self._tv.source_id == 'tv') @property def volume_level(self): @@ -146,26 +139,20 @@ def turn_on(self): def turn_off(self): """Turn off the device.""" self._tv.sendKey('Standby') - if not self._tv.on: - self._state = STATE_OFF def volume_up(self): """Send volume up command.""" self._tv.sendKey('VolumeUp') - if not self._tv.on: - self._state = STATE_OFF def volume_down(self): """Send volume down command.""" self._tv.sendKey('VolumeDown') - if not self._tv.on: - self._state = STATE_OFF def mute_volume(self, mute): """Send mute command.""" - self._tv.sendKey('Mute') - if not self._tv.on: - self._state = STATE_OFF + if self._muted != mute: + self._tv.sendKey('Mute') + self._muted = mute def set_volume_level(self, volume): """Set volume level, range 0..1.""" @@ -186,12 +173,9 @@ def media_title(self): return '{} - {}'.format(self._source, self._channel_name) return self._source - @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): """Get the latest data and update device state.""" self._tv.update() - self._min_volume = self._tv.min_volume - self._max_volume = self._tv.max_volume self._volume = self._tv.volume self._muted = self._tv.muted if self._tv.source_id: diff --git a/homeassistant/components/media_player/vizio.py b/homeassistant/components/media_player/vizio.py index 395f5bb369e4d..af3fdd1e15a26 100644 --- a/homeassistant/components/media_player/vizio.py +++ b/homeassistant/components/media_player/vizio.py @@ -92,25 +92,31 @@ def __init__(self, host, token, name, volume_step): def update(self): """Retrieve latest state of the TV.""" is_on = self._device.get_power_state() - if is_on is None: - self._state = None - return - if is_on is False: - self._state = STATE_OFF - else: + + if is_on: self._state = STATE_ON - volume = self._device.get_current_volume() - if volume is not None: - self._volume_level = float(volume) / 100. - input_ = self._device.get_current_input() - if input_ is not None: - self._current_input = input_.meta_name - inputs = self._device.get_inputs() - if inputs is not None: - self._available_inputs = [] - for input_ in inputs: - self._available_inputs.append(input_.name) + volume = self._device.get_current_volume() + if volume is not None: + self._volume_level = float(volume) / 100. + + input_ = self._device.get_current_input() + if input_ is not None: + self._current_input = input_.meta_name + + inputs = self._device.get_inputs() + if inputs is not None: + self._available_inputs = [input_.name for input_ in inputs] + + else: + if is_on is None: + self._state = None + else: + self._state = STATE_OFF + + self._volume_level = None + self._current_input = None + self._available_inputs = None @property def state(self): diff --git a/homeassistant/components/meteo_france/__init__.py b/homeassistant/components/meteo_france/__init__.py new file mode 100644 index 0000000000000..e084cff3c79c7 --- /dev/null +++ b/homeassistant/components/meteo_france/__init__.py @@ -0,0 +1,131 @@ +"""Support for Meteo-France weather data.""" +import datetime +import logging + +import voluptuous as vol + +from homeassistant.const import CONF_MONITORED_CONDITIONS, TEMP_CELSIUS +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.discovery import load_platform +from homeassistant.util import Throttle + +REQUIREMENTS = ['meteofrance==0.3.4'] + +_LOGGER = logging.getLogger(__name__) + +ATTRIBUTION = "Data provided by Météo-France" + +CONF_CITY = 'city' + +DATA_METEO_FRANCE = 'data_meteo_france' +DEFAULT_WEATHER_CARD = True +DOMAIN = 'meteo_france' + +SCAN_INTERVAL = datetime.timedelta(minutes=5) + +SENSOR_TYPES = { + 'rain_chance': ['Rain chance', '%'], + 'freeze_chance': ['Freeze chance', '%'], + 'thunder_chance': ['Thunder chance', '%'], + 'snow_chance': ['Snow chance', '%'], + 'weather': ['Weather', None], + 'wind_speed': ['Wind Speed', 'km/h'], + 'next_rain': ['Next rain', 'min'], + 'temperature': ['Temperature', TEMP_CELSIUS], + 'uv': ['UV', None], +} + +CONDITION_CLASSES = { + 'clear-night': ['Nuit Claire'], + 'cloudy': ['Très nuageux'], + 'fog': ['Brume ou bancs de brouillard', + 'Brouillard', 'Brouillard givrant'], + 'hail': ['Risque de grêle'], + 'lightning': ["Risque d'orages", 'Orages'], + 'lightning-rainy': ['Pluie orageuses', 'Pluies orageuses', + 'Averses orageuses'], + 'partlycloudy': ['Ciel voilé', 'Ciel voilé nuit', 'Éclaircies'], + 'pouring': ['Pluie forte'], + 'rainy': ['Bruine / Pluie faible', 'Bruine', 'Pluie faible', + 'Pluies éparses / Rares averses', 'Pluies éparses', + 'Rares averses', 'Pluie / Averses', 'Averses', 'Pluie'], + 'snowy': ['Neige / Averses de neige', 'Neige', 'Averses de neige', + 'Neige forte', 'Quelques flocons'], + 'snowy-rainy': ['Pluie et neige', 'Pluie verglaçante'], + 'sunny': ['Ensoleillé'], + 'windy': [], + 'windy-variant': [], + 'exceptional': [], +} + + +def has_all_unique_cities(value): + """Validate that all cities are unique.""" + cities = [location[CONF_CITY] for location in value] + vol.Schema(vol.Unique())(cities) + return value + + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.All(cv.ensure_list, [vol.Schema({ + vol.Required(CONF_CITY): cv.string, + vol.Optional(CONF_MONITORED_CONDITIONS): + vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]), + })], has_all_unique_cities) +}, extra=vol.ALLOW_EXTRA) + + +def setup(hass, config): + """Set up the Meteo-France component.""" + hass.data[DATA_METEO_FRANCE] = {} + + for location in config[DOMAIN]: + + city = location[CONF_CITY] + + from meteofrance.client import meteofranceClient, meteofranceError + + try: + client = meteofranceClient(city) + except meteofranceError as exp: + _LOGGER.error(exp) + return + + client.need_rain_forecast = bool( + CONF_MONITORED_CONDITIONS in location and 'next_rain' in + location[CONF_MONITORED_CONDITIONS]) + + hass.data[DATA_METEO_FRANCE][city] = MeteoFranceUpdater(client) + hass.data[DATA_METEO_FRANCE][city].update() + + if CONF_MONITORED_CONDITIONS in location: + monitored_conditions = location[CONF_MONITORED_CONDITIONS] + load_platform( + hass, 'sensor', DOMAIN, { + CONF_CITY: city, + CONF_MONITORED_CONDITIONS: monitored_conditions}, config) + + load_platform(hass, 'weather', DOMAIN, {CONF_CITY: city}, config) + + return True + + +class MeteoFranceUpdater: + """Update data from Meteo-France.""" + + def __init__(self, client): + """Initialize the data object.""" + self._client = client + + def get_data(self): + """Get the latest data from Meteo-France.""" + return self._client.get_data() + + @Throttle(SCAN_INTERVAL) + def update(self): + """Get the latest data from Meteo-France.""" + from meteofrance.client import meteofranceError + try: + self._client.update() + except meteofranceError as exp: + _LOGGER.error(exp) diff --git a/homeassistant/components/meteo_france/sensor.py b/homeassistant/components/meteo_france/sensor.py new file mode 100644 index 0000000000000..f0ef926793e0a --- /dev/null +++ b/homeassistant/components/meteo_france/sensor.py @@ -0,0 +1,73 @@ +"""Support for Meteo-France raining forecast sensor.""" +import logging + +from homeassistant.components.meteo_france import ( + ATTRIBUTION, CONF_CITY, DATA_METEO_FRANCE, SENSOR_TYPES) +from homeassistant.const import ATTR_ATTRIBUTION, CONF_MONITORED_CONDITIONS +from homeassistant.helpers.entity import Entity + +_LOGGER = logging.getLogger(__name__) + +STATE_ATTR_FORECAST = '1h rain forecast' + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the Meteo-France sensor.""" + if discovery_info is None: + return + + city = discovery_info[CONF_CITY] + monitored_conditions = discovery_info[CONF_MONITORED_CONDITIONS] + client = hass.data[DATA_METEO_FRANCE][city] + + add_entities([MeteoFranceSensor(variable, client) + for variable in monitored_conditions], True) + + +class MeteoFranceSensor(Entity): + """Representation of a Meteo-France sensor.""" + + def __init__(self, condition, client): + """Initialize the Meteo-France sensor.""" + self._condition = condition + self._client = client + self._state = None + self._data = {} + + @property + def name(self): + """Return the name of the sensor.""" + return "{} {}".format( + self._data['name'], SENSOR_TYPES[self._condition][0]) + + @property + def state(self): + """Return the state of the sensor.""" + return self._state + + @property + def device_state_attributes(self): + """Return the state attributes of the sensor.""" + if self._condition == 'next_rain' and 'rain_forecast' in self._data: + return { + **{STATE_ATTR_FORECAST: self._data['rain_forecast']}, + ** self._data['next_rain_intervals'], + **{ATTR_ATTRIBUTION: ATTRIBUTION} + } + return {ATTR_ATTRIBUTION: ATTRIBUTION} + + @property + def unit_of_measurement(self): + """Return the unit of measurement.""" + return SENSOR_TYPES[self._condition][1] + + def update(self): + """Fetch new state data for the sensor.""" + try: + self._client.update() + self._data = self._client.get_data() + self._state = self._data[self._condition] + except KeyError: + _LOGGER.error("No condition %s for location %s", + self._condition, self._data['name']) + self._state = None diff --git a/homeassistant/components/meteo_france/weather.py b/homeassistant/components/meteo_france/weather.py new file mode 100644 index 0000000000000..849c9d9da10de --- /dev/null +++ b/homeassistant/components/meteo_france/weather.py @@ -0,0 +1,104 @@ +"""Support for Meteo-France weather service.""" +from datetime import datetime, timedelta +import logging + +from homeassistant.components.meteo_france import ( + ATTRIBUTION, CONDITION_CLASSES, CONF_CITY, DATA_METEO_FRANCE) +from homeassistant.components.weather import ( + ATTR_FORECAST_CONDITION, ATTR_FORECAST_TEMP, ATTR_FORECAST_TEMP_LOW, + ATTR_FORECAST_TIME, WeatherEntity) +from homeassistant.const import TEMP_CELSIUS + +_LOGGER = logging.getLogger(__name__) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the Meteo-France weather platform.""" + if discovery_info is None: + return + + city = discovery_info[CONF_CITY] + client = hass.data[DATA_METEO_FRANCE][city] + + add_entities([MeteoFranceWeather(client)], True) + + +class MeteoFranceWeather(WeatherEntity): + """Representation of a weather condition.""" + + def __init__(self, client): + """Initialise the platform with a data instance and station name.""" + self._client = client + self._data = {} + + def update(self): + """Update current conditions.""" + self._client.update() + self._data = self._client.get_data() + + @property + def name(self): + """Return the name of the sensor.""" + return self._data['name'] + + @property + def condition(self): + """Return the current condition.""" + return self.format_condition(self._data['weather']) + + @property + def temperature(self): + """Return the temperature.""" + return self._data['temperature'] + + @property + def humidity(self): + """Return the humidity.""" + return None + + @property + def temperature_unit(self): + """Return the unit of measurement.""" + return TEMP_CELSIUS + + @property + def wind_speed(self): + """Return the wind speed.""" + return self._data['wind_speed'] + + @property + def wind_bearing(self): + """Return the wind bearing.""" + return self._data['wind_bearing'] + + @property + def attribution(self): + """Return the attribution.""" + return ATTRIBUTION + + @property + def forecast(self): + """Return the forecast.""" + reftime = datetime.now().replace(hour=12, minute=00) + reftime += timedelta(hours=24) + forecast_data = [] + for key in self._data['forecast']: + value = self._data['forecast'][key] + data_dict = { + ATTR_FORECAST_TIME: reftime.isoformat(), + ATTR_FORECAST_TEMP: int(value['max_temp']), + ATTR_FORECAST_TEMP_LOW: int(value['min_temp']), + ATTR_FORECAST_CONDITION: + self.format_condition(value['weather']) + } + reftime = reftime + timedelta(hours=24) + forecast_data.append(data_dict) + return forecast_data + + @staticmethod + def format_condition(condition): + """Return condition from dict CONDITION_CLASSES.""" + for key, value in CONDITION_CLASSES.items(): + if condition in value: + return key + return condition diff --git a/homeassistant/components/mobile_app/__init__.py b/homeassistant/components/mobile_app/__init__.py new file mode 100644 index 0000000000000..19a81b4aa452f --- /dev/null +++ b/homeassistant/components/mobile_app/__init__.py @@ -0,0 +1,355 @@ +"""Support for native mobile apps.""" +import logging +import json +from functools import partial + +import voluptuous as vol +from aiohttp.web import json_response, Response +from aiohttp.web_exceptions import HTTPBadRequest + +from homeassistant import config_entries +from homeassistant.auth.util import generate_secret +import homeassistant.core as ha +from homeassistant.core import Context +from homeassistant.components import webhook +from homeassistant.components.http import HomeAssistantView +from homeassistant.components.http.data_validator import RequestDataValidator +from homeassistant.components.device_tracker import ( + DOMAIN as DEVICE_TRACKER_DOMAIN, SERVICE_SEE as DEVICE_TRACKER_SEE, + SERVICE_SEE_PAYLOAD_SCHEMA as SEE_SCHEMA) +from homeassistant.const import (ATTR_DOMAIN, ATTR_SERVICE, ATTR_SERVICE_DATA, + HTTP_BAD_REQUEST, HTTP_CREATED, + HTTP_INTERNAL_SERVER_ERROR, CONF_WEBHOOK_ID) +from homeassistant.exceptions import (HomeAssistantError, ServiceNotFound, + TemplateError) +from homeassistant.helpers import config_validation as cv, template +from homeassistant.helpers.typing import HomeAssistantType + +REQUIREMENTS = ['PyNaCl==1.3.0'] + +_LOGGER = logging.getLogger(__name__) + +DOMAIN = 'mobile_app' + +DEPENDENCIES = ['device_tracker', 'http', 'webhook'] + +STORAGE_KEY = DOMAIN +STORAGE_VERSION = 1 + +CONF_SECRET = 'secret' +CONF_USER_ID = 'user_id' + +ATTR_APP_DATA = 'app_data' +ATTR_APP_ID = 'app_id' +ATTR_APP_NAME = 'app_name' +ATTR_APP_VERSION = 'app_version' +ATTR_DEVICE_NAME = 'device_name' +ATTR_MANUFACTURER = 'manufacturer' +ATTR_MODEL = 'model' +ATTR_OS_VERSION = 'os_version' +ATTR_SUPPORTS_ENCRYPTION = 'supports_encryption' + +ATTR_EVENT_DATA = 'event_data' +ATTR_EVENT_TYPE = 'event_type' + +ATTR_TEMPLATE = 'template' +ATTR_TEMPLATE_VARIABLES = 'variables' + +ATTR_WEBHOOK_DATA = 'data' +ATTR_WEBHOOK_ENCRYPTED = 'encrypted' +ATTR_WEBHOOK_ENCRYPTED_DATA = 'encrypted_data' +ATTR_WEBHOOK_TYPE = 'type' + +WEBHOOK_TYPE_CALL_SERVICE = 'call_service' +WEBHOOK_TYPE_FIRE_EVENT = 'fire_event' +WEBHOOK_TYPE_RENDER_TEMPLATE = 'render_template' +WEBHOOK_TYPE_UPDATE_LOCATION = 'update_location' +WEBHOOK_TYPE_UPDATE_REGISTRATION = 'update_registration' + +WEBHOOK_TYPES = [WEBHOOK_TYPE_CALL_SERVICE, WEBHOOK_TYPE_FIRE_EVENT, + WEBHOOK_TYPE_RENDER_TEMPLATE, WEBHOOK_TYPE_UPDATE_LOCATION, + WEBHOOK_TYPE_UPDATE_REGISTRATION] + +REGISTER_DEVICE_SCHEMA = vol.Schema({ + vol.Optional(ATTR_APP_DATA, default={}): dict, + vol.Required(ATTR_APP_ID): cv.string, + vol.Optional(ATTR_APP_NAME): cv.string, + vol.Required(ATTR_APP_VERSION): cv.string, + vol.Required(ATTR_DEVICE_NAME): cv.string, + vol.Required(ATTR_MANUFACTURER): cv.string, + vol.Required(ATTR_MODEL): cv.string, + vol.Optional(ATTR_OS_VERSION): cv.string, + vol.Required(ATTR_SUPPORTS_ENCRYPTION, default=False): cv.boolean, +}) + +UPDATE_DEVICE_SCHEMA = vol.Schema({ + vol.Optional(ATTR_APP_DATA, default={}): dict, + vol.Required(ATTR_APP_VERSION): cv.string, + vol.Required(ATTR_DEVICE_NAME): cv.string, + vol.Required(ATTR_MANUFACTURER): cv.string, + vol.Required(ATTR_MODEL): cv.string, + vol.Optional(ATTR_OS_VERSION): cv.string, +}) + +WEBHOOK_PAYLOAD_SCHEMA = vol.Schema({ + vol.Required(ATTR_WEBHOOK_TYPE): vol.In(WEBHOOK_TYPES), + vol.Required(ATTR_WEBHOOK_DATA, default={}): dict, + vol.Optional(ATTR_WEBHOOK_ENCRYPTED, default=False): cv.boolean, + vol.Optional(ATTR_WEBHOOK_ENCRYPTED_DATA): cv.string, +}) + +CALL_SERVICE_SCHEMA = vol.Schema({ + vol.Required(ATTR_DOMAIN): cv.string, + vol.Required(ATTR_SERVICE): cv.string, + vol.Optional(ATTR_SERVICE_DATA, default={}): dict, +}) + +FIRE_EVENT_SCHEMA = vol.Schema({ + vol.Required(ATTR_EVENT_TYPE): cv.string, + vol.Optional(ATTR_EVENT_DATA, default={}): dict, +}) + +RENDER_TEMPLATE_SCHEMA = vol.Schema({ + vol.Required(ATTR_TEMPLATE): cv.string, + vol.Optional(ATTR_TEMPLATE_VARIABLES, default={}): dict, +}) + +WEBHOOK_SCHEMAS = { + WEBHOOK_TYPE_CALL_SERVICE: CALL_SERVICE_SCHEMA, + WEBHOOK_TYPE_FIRE_EVENT: FIRE_EVENT_SCHEMA, + WEBHOOK_TYPE_RENDER_TEMPLATE: RENDER_TEMPLATE_SCHEMA, + WEBHOOK_TYPE_UPDATE_LOCATION: SEE_SCHEMA, + WEBHOOK_TYPE_UPDATE_REGISTRATION: UPDATE_DEVICE_SCHEMA, +} + + +def get_cipher(): + """Return decryption function and length of key. + + Async friendly. + """ + from nacl.secret import SecretBox + from nacl.encoding import Base64Encoder + + def decrypt(ciphertext, key): + """Decrypt ciphertext using key.""" + return SecretBox(key).decrypt(ciphertext, encoder=Base64Encoder) + return (SecretBox.KEY_SIZE, decrypt) + + +def _decrypt_payload(key, ciphertext): + """Decrypt encrypted payload.""" + try: + keylen, decrypt = get_cipher() + except OSError: + _LOGGER.warning( + "Ignoring encrypted payload because libsodium not installed") + return None + + if key is None: + _LOGGER.warning( + "Ignoring encrypted payload because no decryption key known") + return None + + key = key.encode("utf-8") + key = key[:keylen] + key = key.ljust(keylen, b'\0') + + try: + message = decrypt(ciphertext, key) + message = json.loads(message.decode("utf-8")) + _LOGGER.debug("Successfully decrypted mobile_app payload") + return message + except ValueError: + _LOGGER.warning("Ignoring encrypted payload because unable to decrypt") + return None + + +def context(device): + """Generate a context from a request.""" + return Context(user_id=device[CONF_USER_ID]) + + +async def handle_webhook(store, hass: HomeAssistantType, webhook_id: str, + request): + """Handle webhook callback.""" + device = hass.data[DOMAIN][webhook_id] + + try: + req_data = await request.json() + except ValueError: + _LOGGER.warning('Received invalid JSON from mobile_app') + return json_response([], status=HTTP_BAD_REQUEST) + + try: + req_data = WEBHOOK_PAYLOAD_SCHEMA(req_data) + except vol.Invalid as ex: + err = vol.humanize.humanize_error(req_data, ex) + _LOGGER.error('Received invalid webhook payload: %s', err) + return Response(status=200) + + webhook_type = req_data[ATTR_WEBHOOK_TYPE] + + webhook_payload = req_data.get(ATTR_WEBHOOK_DATA, {}) + + if req_data[ATTR_WEBHOOK_ENCRYPTED]: + enc_data = req_data[ATTR_WEBHOOK_ENCRYPTED_DATA] + webhook_payload = _decrypt_payload(device[CONF_SECRET], enc_data) + + try: + data = WEBHOOK_SCHEMAS[webhook_type](webhook_payload) + except vol.Invalid as ex: + err = vol.humanize.humanize_error(webhook_payload, ex) + _LOGGER.error('Received invalid webhook payload: %s', err) + return Response(status=200) + + if webhook_type == WEBHOOK_TYPE_CALL_SERVICE: + try: + await hass.services.async_call(data[ATTR_DOMAIN], + data[ATTR_SERVICE], + data[ATTR_SERVICE_DATA], + blocking=True, + context=context(device)) + except (vol.Invalid, ServiceNotFound): + raise HTTPBadRequest() + + return Response(status=200) + + if webhook_type == WEBHOOK_TYPE_FIRE_EVENT: + event_type = data[ATTR_EVENT_TYPE] + hass.bus.async_fire(event_type, data[ATTR_EVENT_DATA], + ha.EventOrigin.remote, context=context(device)) + return Response(status=200) + + if webhook_type == WEBHOOK_TYPE_RENDER_TEMPLATE: + try: + tpl = template.Template(data[ATTR_TEMPLATE], hass) + rendered = tpl.async_render(data.get(ATTR_TEMPLATE_VARIABLES)) + return json_response({"rendered": rendered}) + except (ValueError, TemplateError) as ex: + return json_response(({"error": ex}), status=HTTP_BAD_REQUEST) + + if webhook_type == WEBHOOK_TYPE_UPDATE_LOCATION: + await hass.services.async_call(DEVICE_TRACKER_DOMAIN, + DEVICE_TRACKER_SEE, data, + blocking=True, context=context(device)) + return Response(status=200) + + if webhook_type == WEBHOOK_TYPE_UPDATE_REGISTRATION: + data[ATTR_APP_ID] = device[ATTR_APP_ID] + data[ATTR_APP_NAME] = device[ATTR_APP_NAME] + data[ATTR_SUPPORTS_ENCRYPTION] = device[ATTR_SUPPORTS_ENCRYPTION] + data[CONF_SECRET] = device[CONF_SECRET] + data[CONF_USER_ID] = device[CONF_USER_ID] + data[CONF_WEBHOOK_ID] = device[CONF_WEBHOOK_ID] + + hass.data[DOMAIN][webhook_id] = data + + try: + await store.async_save(hass.data[DOMAIN]) + except HomeAssistantError as ex: + _LOGGER.error("Error updating mobile_app registration: %s", ex) + return Response(status=200) + + return json_response(safe_device(data)) + + +def supports_encryption(): + """Test if we support encryption.""" + try: + import nacl # noqa pylint: disable=unused-import + return True + except OSError: + return False + + +def safe_device(device: dict): + """Return a device without webhook_id or secret.""" + return { + ATTR_APP_DATA: device[ATTR_APP_DATA], + ATTR_APP_ID: device[ATTR_APP_ID], + ATTR_APP_NAME: device[ATTR_APP_NAME], + ATTR_APP_VERSION: device[ATTR_APP_VERSION], + ATTR_DEVICE_NAME: device[ATTR_DEVICE_NAME], + ATTR_MANUFACTURER: device[ATTR_MANUFACTURER], + ATTR_MODEL: device[ATTR_MODEL], + ATTR_OS_VERSION: device[ATTR_OS_VERSION], + ATTR_SUPPORTS_ENCRYPTION: device[ATTR_SUPPORTS_ENCRYPTION], + } + + +def register_device_webhook(hass: HomeAssistantType, store, device): + """Register the webhook for a device.""" + device_name = 'Mobile App: {}'.format(device[ATTR_DEVICE_NAME]) + webhook_id = device[CONF_WEBHOOK_ID] + webhook.async_register(hass, DOMAIN, device_name, webhook_id, + partial(handle_webhook, store)) + + +async def async_setup(hass, config): + """Set up the mobile app component.""" + conf = config.get(DOMAIN) + + store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY) + app_config = await store.async_load() + if app_config is None: + app_config = {} + + hass.data[DOMAIN] = app_config + + for device in app_config.values(): + register_device_webhook(hass, store, device) + + if conf is not None: + hass.async_create_task(hass.config_entries.flow.async_init( + DOMAIN, context={'source': config_entries.SOURCE_IMPORT})) + + hass.http.register_view(DevicesView(store)) + + return True + + +async def async_setup_entry(hass, entry): + """Set up an mobile_app entry.""" + return True + + +class DevicesView(HomeAssistantView): + """A view that accepts device registration requests.""" + + url = '/api/mobile_app/devices' + name = 'api:mobile_app:register-device' + + def __init__(self, store): + """Initialize the view.""" + self._store = store + + @RequestDataValidator(REGISTER_DEVICE_SCHEMA) + async def post(self, request, data): + """Handle the POST request for device registration.""" + hass = request.app['hass'] + + resp = {} + + webhook_id = generate_secret() + + data[CONF_WEBHOOK_ID] = resp[CONF_WEBHOOK_ID] = webhook_id + + if data[ATTR_SUPPORTS_ENCRYPTION] and supports_encryption(): + secret = generate_secret(16) + + data[CONF_SECRET] = resp[CONF_SECRET] = secret + + data[CONF_USER_ID] = request['hass_user'].id + + hass.data[DOMAIN][webhook_id] = data + + try: + await self._store.async_save(hass.data[DOMAIN]) + except HomeAssistantError: + return self.json_message("Error saving device.", + HTTP_INTERNAL_SERVER_ERROR) + + register_device_webhook(hass, self._store, data) + + return self.json(resp, status_code=HTTP_CREATED) diff --git a/homeassistant/components/modbus/__init__.py b/homeassistant/components/modbus/__init__.py index f42423bf9a871..182e3dc28fa4a 100644 --- a/homeassistant/components/modbus/__init__.py +++ b/homeassistant/components/modbus/__init__.py @@ -4,27 +4,34 @@ import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.const import ( - EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, - CONF_HOST, CONF_METHOD, CONF_NAME, CONF_PORT, CONF_TYPE, CONF_TIMEOUT, - ATTR_STATE) - -DOMAIN = 'modbus' + ATTR_STATE, CONF_HOST, CONF_METHOD, CONF_NAME, CONF_PORT, CONF_TIMEOUT, + CONF_TYPE, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP) +import homeassistant.helpers.config_validation as cv REQUIREMENTS = ['pymodbus==1.5.2'] -CONF_HUB = 'hub' -# Type of network +_LOGGER = logging.getLogger(__name__) + +ATTR_ADDRESS = 'address' +ATTR_HUB = 'hub' +ATTR_UNIT = 'unit' +ATTR_VALUE = 'value' + CONF_BAUDRATE = 'baudrate' CONF_BYTESIZE = 'bytesize' -CONF_STOPBITS = 'stopbits' +CONF_HUB = 'hub' CONF_PARITY = 'parity' +CONF_STOPBITS = 'stopbits' DEFAULT_HUB = 'default' +DOMAIN = 'modbus' + +SERVICE_WRITE_COIL = 'write_coil' +SERVICE_WRITE_REGISTER = 'write_register' BASE_SCHEMA = vol.Schema({ - vol.Optional(CONF_NAME, default=DEFAULT_HUB): cv.string + vol.Optional(CONF_NAME, default=DEFAULT_HUB): cv.string, }) SERIAL_SCHEMA = BASE_SCHEMA.extend({ @@ -49,16 +56,6 @@ DOMAIN: vol.All(cv.ensure_list, [vol.Any(SERIAL_SCHEMA, ETHERNET_SCHEMA)]) }, extra=vol.ALLOW_EXTRA,) -_LOGGER = logging.getLogger(__name__) - -SERVICE_WRITE_REGISTER = 'write_register' -SERVICE_WRITE_COIL = 'write_coil' - -ATTR_ADDRESS = 'address' -ATTR_HUB = 'hub' -ATTR_UNIT = 'unit' -ATTR_VALUE = 'value' - SERVICE_WRITE_REGISTER_SCHEMA = vol.Schema({ vol.Optional(ATTR_HUB, default=DEFAULT_HUB): cv.string, vol.Required(ATTR_UNIT): cv.positive_int, @@ -109,14 +106,13 @@ def setup_client(client_config): def setup(hass, config): """Set up Modbus component.""" - # Modbus connection type hass.data[DOMAIN] = hub_collect = {} for client_config in config[DOMAIN]: client = setup_client(client_config) name = client_config[CONF_NAME] hub_collect[name] = ModbusHub(client, name) - _LOGGER.debug('Setting up hub: %s', client_config) + _LOGGER.debug("Setting up hub: %s", client_config) def stop_modbus(event): """Stop Modbus service.""" @@ -139,24 +135,20 @@ def start_modbus(event): schema=SERVICE_WRITE_COIL_SCHEMA) def write_register(service): - """Write modbus registers.""" + """Write Modbus registers.""" unit = int(float(service.data.get(ATTR_UNIT))) address = int(float(service.data.get(ATTR_ADDRESS))) value = service.data.get(ATTR_VALUE) client_name = service.data.get(ATTR_HUB) if isinstance(value, list): hub_collect[client_name].write_registers( - unit, - address, - [int(float(i)) for i in value]) + unit, address, [int(float(i)) for i in value]) else: hub_collect[client_name].write_register( - unit, - address, - int(float(value))) + unit, address, int(float(value))) def write_coil(service): - """Write modbus coil.""" + """Write Modbus coil.""" unit = service.data.get(ATTR_UNIT) address = service.data.get(ATTR_ADDRESS) state = service.data.get(ATTR_STATE) @@ -172,7 +164,7 @@ class ModbusHub: """Thread safe wrapper class for pymodbus.""" def __init__(self, modbus_client, name): - """Initialize the modbus hub.""" + """Initialize the Modbus hub.""" self._client = modbus_client self._lock = threading.Lock() self._name = name @@ -196,52 +188,36 @@ def read_coils(self, unit, address, count): """Read coils.""" with self._lock: kwargs = {'unit': unit} if unit else {} - return self._client.read_coils( - address, - count, - **kwargs) + return self._client.read_coils(address, count, **kwargs) def read_input_registers(self, unit, address, count): """Read input registers.""" with self._lock: kwargs = {'unit': unit} if unit else {} return self._client.read_input_registers( - address, - count, - **kwargs) + address, count, **kwargs) def read_holding_registers(self, unit, address, count): """Read holding registers.""" with self._lock: kwargs = {'unit': unit} if unit else {} return self._client.read_holding_registers( - address, - count, - **kwargs) + address, count, **kwargs) def write_coil(self, unit, address, value): """Write coil.""" with self._lock: kwargs = {'unit': unit} if unit else {} - self._client.write_coil( - address, - value, - **kwargs) + self._client.write_coil(address, value, **kwargs) def write_register(self, unit, address, value): """Write register.""" with self._lock: kwargs = {'unit': unit} if unit else {} - self._client.write_register( - address, - value, - **kwargs) + self._client.write_register(address, value, **kwargs) def write_registers(self, unit, address, values): """Write registers.""" with self._lock: kwargs = {'unit': unit} if unit else {} - self._client.write_registers( - address, - values, - **kwargs) + self._client.write_registers(address, values, **kwargs) diff --git a/homeassistant/components/modbus/binary_sensor.py b/homeassistant/components/modbus/binary_sensor.py index 38511ffed7ec0..4e0ab74445d2f 100644 --- a/homeassistant/components/modbus/binary_sensor.py +++ b/homeassistant/components/modbus/binary_sensor.py @@ -1,25 +1,27 @@ """Support for Modbus Coil sensors.""" import logging + import voluptuous as vol +from homeassistant.components.binary_sensor import BinarySensorDevice from homeassistant.components.modbus import ( CONF_HUB, DEFAULT_HUB, DOMAIN as MODBUS_DOMAIN) +from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import CONF_NAME, CONF_SLAVE -from homeassistant.components.binary_sensor import BinarySensorDevice from homeassistant.helpers import config_validation as cv -from homeassistant.components.sensor import PLATFORM_SCHEMA _LOGGER = logging.getLogger(__name__) -DEPENDENCIES = ['modbus'] CONF_COIL = 'coil' CONF_COILS = 'coils' +DEPENDENCIES = ['modbus'] + PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_COILS): [{ - vol.Optional(CONF_HUB, default=DEFAULT_HUB): cv.string, vol.Required(CONF_COIL): cv.positive_int, vol.Required(CONF_NAME): cv.string, + vol.Optional(CONF_HUB, default=DEFAULT_HUB): cv.string, vol.Optional(CONF_SLAVE): cv.positive_int, }] }) @@ -33,6 +35,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): sensors.append(ModbusCoilSensor( hub, coil.get(CONF_NAME), coil.get(CONF_SLAVE), coil.get(CONF_COIL))) + add_entities(sensors) @@ -40,7 +43,7 @@ class ModbusCoilSensor(BinarySensorDevice): """Modbus coil sensor.""" def __init__(self, hub, name, slave, coil): - """Initialize the modbus coil sensor.""" + """Initialize the Modbus coil sensor.""" self._hub = hub self._name = name self._slave = int(slave) if slave else None diff --git a/homeassistant/components/modbus/climate.py b/homeassistant/components/modbus/climate.py index ed8cbda863f7f..44daedac9c11b 100644 --- a/homeassistant/components/modbus/climate.py +++ b/homeassistant/components/modbus/climate.py @@ -4,19 +4,15 @@ import voluptuous as vol -from homeassistant.const import ( - CONF_NAME, CONF_SLAVE, ATTR_TEMPERATURE) -from homeassistant.components.climate import ( - ClimateDevice, PLATFORM_SCHEMA, SUPPORT_TARGET_TEMPERATURE) +from homeassistant.components.climate import PLATFORM_SCHEMA, ClimateDevice +from homeassistant.components.climate.const import 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 import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) -DEPENDENCIES = ['modbus'] - -# Parameters not defined by homeassistant.const CONF_TARGET_TEMP = 'target_temp_register' CONF_CURRENT_TEMP = 'current_temp_register' CONF_DATA_TYPE = 'data_type' @@ -26,21 +22,22 @@ DATA_TYPE_INT = 'int' DATA_TYPE_UINT = 'uint' DATA_TYPE_FLOAT = 'float' +DEPENDENCIES = ['modbus'] + +SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Optional(CONF_HUB, default=DEFAULT_HUB): cv.string, + vol.Required(CONF_CURRENT_TEMP): cv.positive_int, vol.Required(CONF_NAME): cv.string, vol.Required(CONF_SLAVE): cv.positive_int, vol.Required(CONF_TARGET_TEMP): cv.positive_int, - vol.Required(CONF_CURRENT_TEMP): cv.positive_int, + vol.Optional(CONF_COUNT, default=2): cv.positive_int, vol.Optional(CONF_DATA_TYPE, default=DATA_TYPE_FLOAT): vol.In([DATA_TYPE_INT, DATA_TYPE_UINT, DATA_TYPE_FLOAT]), - vol.Optional(CONF_COUNT, default=2): cv.positive_int, - vol.Optional(CONF_PRECISION, default=1): cv.positive_int + vol.Optional(CONF_HUB, default=DEFAULT_HUB): cv.string, + vol.Optional(CONF_PRECISION, default=1): cv.positive_int, }) -SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE - def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Modbus Thermostat Platform.""" @@ -77,12 +74,14 @@ def __init__(self, hub, name, modbus_slave, target_temp_register, self._precision = precision self._structure = '>f' - data_types = {DATA_TYPE_INT: {1: 'h', 2: 'i', 4: 'q'}, - DATA_TYPE_UINT: {1: 'H', 2: 'I', 4: 'Q'}, - DATA_TYPE_FLOAT: {1: 'e', 2: 'f', 4: 'd'}} + data_types = { + DATA_TYPE_INT: {1: 'h', 2: 'i', 4: 'q'}, + DATA_TYPE_UINT: {1: 'H', 2: 'I', 4: 'Q'}, + DATA_TYPE_FLOAT: {1: 'e', 2: 'f', 4: 'd'}, + } - self._structure = '>{}'.format(data_types[self._data_type] - [self._count]) + self._structure = '>{}'.format( + data_types[self._data_type][self._count]) @property def supported_features(self): @@ -108,7 +107,7 @@ def current_temperature(self): @property def target_temperature(self): - """Return the temperature we try to reach.""" + """Return the target temperature.""" return self._target_temperature def set_temperature(self, **kwargs): @@ -120,16 +119,16 @@ def set_temperature(self, **kwargs): register_value = struct.unpack('>h', byte_string[0:2])[0] try: - self.write_register(self._target_temperature_register, - register_value) + self.write_register( + self._target_temperature_register, register_value) except AttributeError as ex: _LOGGER.error(ex) def read_register(self, register): - """Read holding register using the modbus hub slave.""" + """Read holding register using the Modbus hub slave.""" try: - result = self._hub.read_holding_registers(self._slave, register, - self._count) + result = self._hub.read_holding_registers( + self._slave, register, self._count) except AttributeError as ex: _LOGGER.error(ex) byte_string = b''.join( @@ -139,5 +138,5 @@ def read_register(self, register): return register_value def write_register(self, register, value): - """Write register using the modbus hub slave.""" + """Write register using the Modbus hub slave.""" self._hub.write_registers(self._slave, register, [value, 0]) diff --git a/homeassistant/components/modbus/sensor.py b/homeassistant/components/modbus/sensor.py index 6ba8d92d15533..3f8c68b25ff14 100644 --- a/homeassistant/components/modbus/sensor.py +++ b/homeassistant/components/modbus/sensor.py @@ -6,50 +6,50 @@ from homeassistant.components.modbus import ( CONF_HUB, DEFAULT_HUB, DOMAIN as MODBUS_DOMAIN) +from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( - CONF_NAME, CONF_OFFSET, CONF_UNIT_OF_MEASUREMENT, CONF_SLAVE, - CONF_STRUCTURE) -from homeassistant.helpers.restore_state import RestoreEntity + CONF_NAME, CONF_OFFSET, CONF_SLAVE, CONF_STRUCTURE, + CONF_UNIT_OF_MEASUREMENT) from homeassistant.helpers import config_validation as cv -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.helpers.restore_state import RestoreEntity _LOGGER = logging.getLogger(__name__) -DEPENDENCIES = ['modbus'] - CONF_COUNT = 'count' -CONF_REVERSE_ORDER = 'reverse_order' +CONF_DATA_TYPE = 'data_type' CONF_PRECISION = 'precision' CONF_REGISTER = 'register' +CONF_REGISTER_TYPE = 'register_type' CONF_REGISTERS = 'registers' +CONF_REVERSE_ORDER = 'reverse_order' CONF_SCALE = 'scale' -CONF_DATA_TYPE = 'data_type' -CONF_REGISTER_TYPE = 'register_type' - -REGISTER_TYPE_HOLDING = 'holding' -REGISTER_TYPE_INPUT = 'input' +DATA_TYPE_CUSTOM = 'custom' +DATA_TYPE_FLOAT = 'float' DATA_TYPE_INT = 'int' DATA_TYPE_UINT = 'uint' -DATA_TYPE_FLOAT = 'float' -DATA_TYPE_CUSTOM = 'custom' + +DEPENDENCIES = ['modbus'] + +REGISTER_TYPE_HOLDING = 'holding' +REGISTER_TYPE_INPUT = 'input' PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_REGISTERS): [{ - vol.Optional(CONF_HUB, default=DEFAULT_HUB): cv.string, vol.Required(CONF_NAME): cv.string, vol.Required(CONF_REGISTER): cv.positive_int, - vol.Optional(CONF_REGISTER_TYPE, default=REGISTER_TYPE_HOLDING): - vol.In([REGISTER_TYPE_HOLDING, REGISTER_TYPE_INPUT]), vol.Optional(CONF_COUNT, default=1): cv.positive_int, - vol.Optional(CONF_REVERSE_ORDER, default=False): cv.boolean, + vol.Optional(CONF_DATA_TYPE, default=DATA_TYPE_INT): + vol.In([DATA_TYPE_INT, DATA_TYPE_UINT, DATA_TYPE_FLOAT, + DATA_TYPE_CUSTOM]), + vol.Optional(CONF_HUB, default=DEFAULT_HUB): cv.string, vol.Optional(CONF_OFFSET, default=0): vol.Coerce(float), vol.Optional(CONF_PRECISION, default=0): cv.positive_int, + vol.Optional(CONF_REGISTER_TYPE, default=REGISTER_TYPE_HOLDING): + vol.In([REGISTER_TYPE_HOLDING, REGISTER_TYPE_INPUT]), + vol.Optional(CONF_REVERSE_ORDER, default=False): cv.boolean, vol.Optional(CONF_SCALE, default=1): vol.Coerce(float), vol.Optional(CONF_SLAVE): cv.positive_int, - vol.Optional(CONF_DATA_TYPE, default=DATA_TYPE_INT): - vol.In([DATA_TYPE_INT, DATA_TYPE_UINT, DATA_TYPE_FLOAT, - DATA_TYPE_CUSTOM]), vol.Optional(CONF_STRUCTURE): cv.string, vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string, }] @@ -93,17 +93,11 @@ def setup_platform(hass, config, add_entities, discovery_info=None): hub_name = register.get(CONF_HUB) hub = hass.data[MODBUS_DOMAIN][hub_name] sensors.append(ModbusRegisterSensor( - hub, - register.get(CONF_NAME), - register.get(CONF_SLAVE), - register.get(CONF_REGISTER), - register.get(CONF_REGISTER_TYPE), - register.get(CONF_UNIT_OF_MEASUREMENT), - register.get(CONF_COUNT), - register.get(CONF_REVERSE_ORDER), - register.get(CONF_SCALE), - register.get(CONF_OFFSET), - structure, + hub, register.get(CONF_NAME), register.get(CONF_SLAVE), + register.get(CONF_REGISTER), register.get(CONF_REGISTER_TYPE), + register.get(CONF_UNIT_OF_MEASUREMENT), register.get(CONF_COUNT), + register.get(CONF_REVERSE_ORDER), register.get(CONF_SCALE), + register.get(CONF_OFFSET), structure, register.get(CONF_PRECISION))) if not sensors: @@ -158,14 +152,10 @@ def update(self): """Update the state of the sensor.""" if self._register_type == REGISTER_TYPE_INPUT: result = self._hub.read_input_registers( - self._slave, - self._register, - self._count) + self._slave, self._register, self._count) else: result = self._hub.read_holding_registers( - self._slave, - self._register, - self._count) + self._slave, self._register, self._count) val = 0 try: diff --git a/homeassistant/components/modbus/switch.py b/homeassistant/components/modbus/switch.py index 47ad8e98958ba..b7039a01da339 100644 --- a/homeassistant/components/modbus/switch.py +++ b/homeassistant/components/modbus/switch.py @@ -1,54 +1,54 @@ """Support for Modbus switches.""" import logging + import voluptuous as vol from homeassistant.components.modbus import ( CONF_HUB, DEFAULT_HUB, DOMAIN as MODBUS_DOMAIN) +from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( - CONF_NAME, CONF_SLAVE, CONF_COMMAND_ON, CONF_COMMAND_OFF, STATE_ON) + CONF_COMMAND_OFF, CONF_COMMAND_ON, CONF_NAME, CONF_SLAVE, STATE_ON) +from homeassistant.helpers import config_validation as cv from homeassistant.helpers.entity import ToggleEntity from homeassistant.helpers.restore_state import RestoreEntity -from homeassistant.helpers import config_validation as cv -from homeassistant.components.sensor import PLATFORM_SCHEMA _LOGGER = logging.getLogger(__name__) -DEPENDENCIES = ['modbus'] +CONF_COIL = 'coil' +CONF_COILS = 'coils' +CONF_REGISTER = 'register' +CONF_REGISTER_TYPE = 'register_type' +CONF_REGISTERS = 'registers' +CONF_STATE_OFF = 'state_off' +CONF_STATE_ON = 'state_on' +CONF_VERIFY_REGISTER = 'verify_register' +CONF_VERIFY_STATE = 'verify_state' -CONF_COIL = "coil" -CONF_COILS = "coils" -CONF_REGISTER = "register" -CONF_REGISTERS = "registers" -CONF_VERIFY_STATE = "verify_state" -CONF_VERIFY_REGISTER = "verify_register" -CONF_REGISTER_TYPE = "register_type" -CONF_STATE_ON = "state_on" -CONF_STATE_OFF = "state_off" +DEPENDENCIES = ['modbus'] REGISTER_TYPE_HOLDING = 'holding' REGISTER_TYPE_INPUT = 'input' REGISTERS_SCHEMA = vol.Schema({ - vol.Optional(CONF_HUB, default=DEFAULT_HUB): cv.string, + vol.Required(CONF_COMMAND_OFF): cv.positive_int, + vol.Required(CONF_COMMAND_ON): cv.positive_int, vol.Required(CONF_NAME): cv.string, - vol.Optional(CONF_SLAVE): cv.positive_int, vol.Required(CONF_REGISTER): cv.positive_int, - vol.Required(CONF_COMMAND_ON): cv.positive_int, - vol.Required(CONF_COMMAND_OFF): cv.positive_int, - vol.Optional(CONF_VERIFY_STATE, default=True): cv.boolean, - vol.Optional(CONF_VERIFY_REGISTER): - cv.positive_int, + vol.Optional(CONF_HUB, default=DEFAULT_HUB): cv.string, vol.Optional(CONF_REGISTER_TYPE, default=REGISTER_TYPE_HOLDING): vol.In([REGISTER_TYPE_HOLDING, REGISTER_TYPE_INPUT]), - vol.Optional(CONF_STATE_ON): cv.positive_int, + vol.Optional(CONF_SLAVE): cv.positive_int, vol.Optional(CONF_STATE_OFF): cv.positive_int, + vol.Optional(CONF_STATE_ON): cv.positive_int, + vol.Optional(CONF_VERIFY_REGISTER): cv.positive_int, + vol.Optional(CONF_VERIFY_STATE, default=True): cv.boolean, }) COILS_SCHEMA = vol.Schema({ - vol.Optional(CONF_HUB, default=DEFAULT_HUB): cv.string, vol.Required(CONF_COIL): cv.positive_int, vol.Required(CONF_NAME): cv.string, vol.Required(CONF_SLAVE): cv.positive_int, + vol.Optional(CONF_HUB, default=DEFAULT_HUB): cv.string, }) PLATFORM_SCHEMA = vol.All( @@ -141,9 +141,9 @@ class ModbusRegisterSwitch(ModbusCoilSwitch): """Representation of a Modbus register switch.""" # pylint: disable=super-init-not-called - def __init__(self, hub, name, slave, register, command_on, - command_off, verify_state, verify_register, - register_type, state_on, state_off): + def __init__(self, hub, name, slave, register, command_on, command_off, + verify_state, verify_register, register_type, state_on, + state_off): """Initialize the register switch.""" self._hub = hub self._name = name diff --git a/homeassistant/components/mqtt/.translations/es-419.json b/homeassistant/components/mqtt/.translations/es-419.json index e9e869ae96662..4f54e11a1126d 100644 --- a/homeassistant/components/mqtt/.translations/es-419.json +++ b/homeassistant/components/mqtt/.translations/es-419.json @@ -1,9 +1,31 @@ { "config": { + "abort": { + "single_instance_allowed": "S\u00f3lo se permite una \u00fanica configuraci\u00f3n de MQTT." + }, + "error": { + "cannot_connect": "No se puede conectar con el broker." + }, "step": { + "broker": { + "data": { + "broker": "Broker", + "discovery": "Habilitar descubrimiento", + "password": "Contrase\u00f1a", + "port": "Puerto", + "username": "Nombre de usuario" + }, + "description": "Por favor ingrese la informaci\u00f3n de conexi\u00f3n de su agente MQTT.", + "title": "MQTT" + }, "hassio_confirm": { + "data": { + "discovery": "Habilitar descubrimiento" + }, + "description": "\u00bfDesea configurar el Asistente del Hogar para que se conecte al broker MQTT proporcionado por el complemento hass.io {addon}?", "title": "MQTT Broker a trav\u00e9s del complemento Hass.io" } - } + }, + "title": "MQTT" } } \ No newline at end of file diff --git a/homeassistant/components/mqtt/.translations/hu.json b/homeassistant/components/mqtt/.translations/hu.json index f08c601633e21..26361b0e3634f 100644 --- a/homeassistant/components/mqtt/.translations/hu.json +++ b/homeassistant/components/mqtt/.translations/hu.json @@ -22,7 +22,8 @@ "data": { "discovery": "Felfedez\u00e9s enged\u00e9lyez\u00e9se" }, - "description": "Szeretn\u00e9d, hogy a Home Assistant csatlakozzon a hass.io addon {addon} \u00e1ltal biztos\u00edtott MQTT br\u00f3kerhez?" + "description": "Szeretn\u00e9d, hogy a Home Assistant csatlakozzon a hass.io addon {addon} \u00e1ltal biztos\u00edtott MQTT br\u00f3kerhez?", + "title": "MQTT Broker a Hass.io b\u0151v\u00edtm\u00e9nyen kereszt\u00fcl" } }, "title": "MQTT" diff --git a/homeassistant/components/mqtt/.translations/it.json b/homeassistant/components/mqtt/.translations/it.json index e56860cd6757d..ed33b182a9693 100644 --- a/homeassistant/components/mqtt/.translations/it.json +++ b/homeassistant/components/mqtt/.translations/it.json @@ -10,13 +10,20 @@ "broker": { "data": { "broker": "Broker", - "discovery": "Attiva l'individuazione" - } + "discovery": "Attiva l'individuazione", + "password": "Password", + "port": "Porta", + "username": "Nome utente" + }, + "description": "Inserisci le informazioni di connessione del tuo broker MQTT.", + "title": "MQTT" }, "hassio_confirm": { "data": { "discovery": "Attiva l'individuazione" - } + }, + "description": "Vuoi configurare Home Assistant per connettersi al broker MQTT fornito dall'add-on di Hass.io {addon}?", + "title": "Broker MQTT tramite l'add-on di Hass.io" } }, "title": "MQTT" diff --git a/homeassistant/components/mqtt/.translations/ru.json b/homeassistant/components/mqtt/.translations/ru.json index 663d79f3c14e6..ad3a90383b12d 100644 --- a/homeassistant/components/mqtt/.translations/ru.json +++ b/homeassistant/components/mqtt/.translations/ru.json @@ -15,7 +15,7 @@ "port": "\u041f\u043e\u0440\u0442", "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" }, - "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e \u043e \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0438 \u043a \u0432\u0430\u0448\u0435\u043c\u0443 \u0431\u0440\u043e\u043a\u0435\u0440\u0443 MQTT.", + "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e \u043e \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0438 \u043a \u0412\u0430\u0448\u0435\u043c\u0443 \u0431\u0440\u043e\u043a\u0435\u0440\u0443 MQTT.", "title": "MQTT" }, "hassio_confirm": { diff --git a/homeassistant/components/mqtt/climate.py b/homeassistant/components/mqtt/climate.py index c028ca5a6f694..7be47185322dd 100644 --- a/homeassistant/components/mqtt/climate.py +++ b/homeassistant/components/mqtt/climate.py @@ -10,11 +10,13 @@ from homeassistant.components import climate, mqtt from homeassistant.components.climate import ( + ClimateDevice, PLATFORM_SCHEMA as CLIMATE_PLATFORM_SCHEMA) +from homeassistant.components.climate.const import ( ATTR_OPERATION_MODE, DEFAULT_MAX_TEMP, DEFAULT_MIN_TEMP, - PLATFORM_SCHEMA as CLIMATE_PLATFORM_SCHEMA, STATE_AUTO, STATE_COOL, + STATE_AUTO, STATE_COOL, STATE_DRY, STATE_FAN_ONLY, STATE_HEAT, SUPPORT_AUX_HEAT, SUPPORT_AWAY_MODE, SUPPORT_FAN_MODE, SUPPORT_HOLD_MODE, SUPPORT_OPERATION_MODE, - SUPPORT_SWING_MODE, SUPPORT_TARGET_TEMPERATURE, ClimateDevice) + SUPPORT_SWING_MODE, SUPPORT_TARGET_TEMPERATURE) from homeassistant.components.fan import SPEED_HIGH, SPEED_LOW, SPEED_MEDIUM from homeassistant.components.mqtt import ( ATTR_DISCOVERY_HASH, CONF_QOS, CONF_RETAIN, CONF_STATE_TOPIC, @@ -91,6 +93,7 @@ SCHEMA_BASE = CLIMATE_PLATFORM_SCHEMA.extend(MQTT_BASE_PLATFORM_SCHEMA.schema) PLATFORM_SCHEMA = SCHEMA_BASE.extend({ + vol.Optional(CONF_RETAIN, default=mqtt.DEFAULT_RETAIN): cv.boolean, vol.Optional(CONF_POWER_COMMAND_TOPIC): mqtt.valid_publish_topic, vol.Optional(CONF_MODE_COMMAND_TOPIC): mqtt.valid_publish_topic, vol.Optional(CONF_TEMPERATURE_COMMAND_TOPIC): mqtt.valid_publish_topic, diff --git a/homeassistant/components/mysensors/climate.py b/homeassistant/components/mysensors/climate.py index 20d608e1ca5f4..f8c52f65cdaa3 100644 --- a/homeassistant/components/mysensors/climate.py +++ b/homeassistant/components/mysensors/climate.py @@ -1,12 +1,13 @@ """MySensors platform that offers a Climate (MySensors-HVAC) component.""" from homeassistant.components import mysensors -from homeassistant.components.climate import ( +from homeassistant.components.climate import ClimateDevice +from homeassistant.components.climate.const import ( ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, DOMAIN, STATE_AUTO, - STATE_COOL, STATE_HEAT, STATE_OFF, SUPPORT_FAN_MODE, + STATE_COOL, STATE_HEAT, SUPPORT_FAN_MODE, SUPPORT_OPERATION_MODE, SUPPORT_TARGET_TEMPERATURE, - SUPPORT_TARGET_TEMPERATURE_HIGH, SUPPORT_TARGET_TEMPERATURE_LOW, - ClimateDevice) -from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT + SUPPORT_TARGET_TEMPERATURE_HIGH, SUPPORT_TARGET_TEMPERATURE_LOW) +from homeassistant.const import ( + ATTR_TEMPERATURE, STATE_OFF, TEMP_CELSIUS, TEMP_FAHRENHEIT) DICT_HA_TO_MYS = { STATE_AUTO: 'AutoChangeOver', diff --git a/homeassistant/components/neato/__init__.py b/homeassistant/components/neato/__init__.py index 2b4af3e1e9129..bb717b8d230b2 100644 --- a/homeassistant/components/neato/__init__.py +++ b/homeassistant/components/neato/__init__.py @@ -18,6 +18,7 @@ NEATO_ROBOTS = 'neato_robots' NEATO_LOGIN = 'neato_login' NEATO_MAP_DATA = 'neato_map_data' +NEATO_PERSISTENT_MAPS = 'neato_persistent_maps' CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.Schema({ @@ -197,6 +198,7 @@ def __init__(self, hass, domain_config, neato): domain_config[CONF_USERNAME], domain_config[CONF_PASSWORD]) self._hass.data[NEATO_ROBOTS] = self.my_neato.robots + self._hass.data[NEATO_PERSISTENT_MAPS] = self.my_neato.persistent_maps self._hass.data[NEATO_MAP_DATA] = self.my_neato.maps def login(self): @@ -216,6 +218,7 @@ def update_robots(self): _LOGGER.debug("Running HUB.update_robots %s", self._hass.data[NEATO_ROBOTS]) self._hass.data[NEATO_ROBOTS] = self.my_neato.robots + self._hass.data[NEATO_PERSISTENT_MAPS] = self.my_neato.persistent_maps self._hass.data[NEATO_MAP_DATA] = self.my_neato.maps def download_map(self, url): diff --git a/homeassistant/components/neato/vacuum.py b/homeassistant/components/neato/vacuum.py index 45cfd273aca42..ff78a087de846 100644 --- a/homeassistant/components/neato/vacuum.py +++ b/homeassistant/components/neato/vacuum.py @@ -2,15 +2,21 @@ import logging from datetime import timedelta import requests +import voluptuous as vol +from homeassistant.const import (ATTR_ENTITY_ID) from homeassistant.components.vacuum import ( StateVacuumDevice, SUPPORT_BATTERY, SUPPORT_PAUSE, SUPPORT_RETURN_HOME, SUPPORT_STATE, SUPPORT_STOP, SUPPORT_START, STATE_IDLE, STATE_PAUSED, STATE_CLEANING, STATE_DOCKED, STATE_RETURNING, STATE_ERROR, SUPPORT_MAP, ATTR_STATUS, ATTR_BATTERY_LEVEL, ATTR_BATTERY_ICON, - SUPPORT_LOCATE, SUPPORT_CLEAN_SPOT) + SUPPORT_LOCATE, SUPPORT_CLEAN_SPOT, DOMAIN) from homeassistant.components.neato import ( - NEATO_ROBOTS, NEATO_LOGIN, NEATO_MAP_DATA, ACTION, ERRORS, MODE, ALERTS) + NEATO_ROBOTS, NEATO_LOGIN, NEATO_MAP_DATA, ACTION, ERRORS, MODE, ALERTS, + NEATO_PERSISTENT_MAPS) + +from homeassistant.helpers.service import extract_entity_ids +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -19,8 +25,8 @@ SCAN_INTERVAL = timedelta(minutes=5) SUPPORT_NEATO = SUPPORT_BATTERY | SUPPORT_PAUSE | SUPPORT_RETURN_HOME | \ - SUPPORT_STOP | SUPPORT_START | SUPPORT_CLEAN_SPOT | \ - SUPPORT_STATE | SUPPORT_MAP | SUPPORT_LOCATE + SUPPORT_STOP | SUPPORT_START | SUPPORT_CLEAN_SPOT | \ + SUPPORT_STATE | SUPPORT_MAP | SUPPORT_LOCATE ATTR_CLEAN_START = 'clean_start' ATTR_CLEAN_STOP = 'clean_stop' @@ -30,15 +36,56 @@ ATTR_CLEAN_SUSP_COUNT = 'clean_suspension_count' ATTR_CLEAN_SUSP_TIME = 'clean_suspension_time' +ATTR_MODE = 'mode' +ATTR_NAVIGATION = 'navigation' +ATTR_CATEGORY = 'category' +ATTR_ZONE = 'zone' + +SERVICE_NEATO_CUSTOM_CLEANING = 'neato_custom_cleaning' + +SERVICE_NEATO_CUSTOM_CLEANING_SCHEMA = vol.Schema({ + vol.Required(ATTR_ENTITY_ID): cv.entity_ids, + vol.Optional(ATTR_MODE, default=2): cv.positive_int, + vol.Optional(ATTR_NAVIGATION, default=1): cv.positive_int, + vol.Optional(ATTR_CATEGORY, default=4): cv.positive_int, + vol.Optional(ATTR_ZONE): cv.string +}) + def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Neato vacuum.""" dev = [] for robot in hass.data[NEATO_ROBOTS]: dev.append(NeatoConnectedVacuum(hass, robot)) + + if not dev: + return + _LOGGER.debug("Adding vacuums %s", dev) add_entities(dev, True) + def neato_custom_cleaning_service(call): + """Zone cleaning service that allows user to change options.""" + for robot in service_to_entities(call): + if call.service == SERVICE_NEATO_CUSTOM_CLEANING: + mode = call.data.get(ATTR_MODE) + navigation = call.data.get(ATTR_NAVIGATION) + category = call.data.get(ATTR_CATEGORY) + zone = call.data.get(ATTR_ZONE) + robot.neato_custom_cleaning( + mode, navigation, category, zone) + + def service_to_entities(call): + """Return the known devices that a service call mentions.""" + entity_ids = extract_entity_ids(hass, call) + entities = [entity for entity in dev + if entity.entity_id in entity_ids] + return entities + + hass.services.register(DOMAIN, SERVICE_NEATO_CUSTOM_CLEANING, + neato_custom_cleaning_service, + schema=SERVICE_NEATO_CUSTOM_CLEANING_SCHEMA) + class NeatoConnectedVacuum(StateVacuumDevice): """Representation of a Neato Connected Vacuum.""" @@ -62,6 +109,9 @@ def __init__(self, hass, robot): self._available = False self._battery_level = None self._robot_serial = self.robot.serial + self._robot_maps = hass.data[NEATO_PERSISTENT_MAPS] + self._robot_boundaries = {} + self._robot_has_map = self.robot.has_persistent_maps def update(self): """Update the states of Neato Vacuums.""" @@ -129,12 +179,18 @@ def update(self): ['time_in_suspended_cleaning']) self.clean_battery_start = ( self._mapdata[self._robot_serial]['maps'][0]['run_charge_at_start'] - ) + ) self.clean_battery_end = ( self._mapdata[self._robot_serial]['maps'][0]['run_charge_at_end']) self._battery_level = self._state['details']['charge'] + if self._robot_has_map: + robot_map_id = self._robot_maps[self._robot_serial][0]['id'] + + self._robot_boundaries = self.robot.get_map_boundaries( + robot_map_id).json() + @property def name(self): """Return the name of the device.""" @@ -224,3 +280,20 @@ def locate(self, **kwargs): def clean_spot(self, **kwargs): """Run a spot cleaning starting from the base.""" self.robot.start_spot_cleaning() + + def neato_custom_cleaning(self, mode, navigation, category, + zone=None, **kwargs): + """Zone cleaning service call.""" + boundary_id = None + if zone is not None: + for boundary in self._robot_boundaries['data']['boundaries']: + if zone in boundary['name']: + boundary_id = boundary['id'] + if boundary_id is None: + _LOGGER.error( + "Zone '%s' was not found for the robot '%s'", + zone, self._name) + return + + self._clean_state = STATE_CLEANING + self.robot.start_cleaning(mode, navigation, category, boundary_id) diff --git a/homeassistant/components/ness_alarm/__init__.py b/homeassistant/components/ness_alarm/__init__.py index dae244ece3fd9..c7175a0c3c770 100644 --- a/homeassistant/components/ness_alarm/__init__.py +++ b/homeassistant/components/ness_alarm/__init__.py @@ -10,7 +10,7 @@ from homeassistant.helpers.discovery import async_load_platform from homeassistant.helpers.dispatcher import async_dispatcher_send -REQUIREMENTS = ['nessclient==0.9.9'] +REQUIREMENTS = ['nessclient==0.9.13'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/nest/.translations/es-419.json b/homeassistant/components/nest/.translations/es-419.json index 0dfb5283d8f31..117a4500d5872 100644 --- a/homeassistant/components/nest/.translations/es-419.json +++ b/homeassistant/components/nest/.translations/es-419.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_setup": "Solo puedes configurar una sola cuenta Nest.", "no_flows": "Debe configurar Nest antes de poder autenticarse con \u00e9l. [Lea las instrucciones] (https://www.home-assistant.io/components/nest/)." }, "error": { diff --git a/homeassistant/components/nest/.translations/hu.json b/homeassistant/components/nest/.translations/hu.json index e24c38f860861..dc26862f5ea3f 100644 --- a/homeassistant/components/nest/.translations/hu.json +++ b/homeassistant/components/nest/.translations/hu.json @@ -23,6 +23,7 @@ "data": { "code": "PIN-k\u00f3d" }, + "description": "A Nest-fi\u00f3k \u00f6sszekapcsol\u00e1s\u00e1hoz [enged\u00e9lyezze fi\u00f3kj\u00e1t] ( {url} ). \n\n Az enged\u00e9lyez\u00e9s ut\u00e1n m\u00e1solja be az al\u00e1bbi PIN k\u00f3dot.", "title": "Nest fi\u00f3k \u00f6sszekapcsol\u00e1sa" } }, diff --git a/homeassistant/components/nest/.translations/it.json b/homeassistant/components/nest/.translations/it.json index e4a19ebd52122..b55c6d00683c4 100644 --- a/homeassistant/components/nest/.translations/it.json +++ b/homeassistant/components/nest/.translations/it.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_setup": "\u00c8 possibile configurare un solo account Nest.", - "authorize_url_fail": "Errore sconoscioto nel generare l'url di autorizzazione", + "authorize_url_fail": "Errore sconosciuto nel generare l'url di autorizzazione", "authorize_url_timeout": "Tempo scaduto nel generare l'url di autorizzazione", "no_flows": "Devi configurare Nest prima di poter eseguire l'autenticazione. [Si prega di leggere le istruzioni] (https://www.home-assistant.io/components/nest/)." }, diff --git a/homeassistant/components/nest/.translations/ru.json b/homeassistant/components/nest/.translations/ru.json index 54ff1dff9999d..ff86c34ac719a 100644 --- a/homeassistant/components/nest/.translations/ru.json +++ b/homeassistant/components/nest/.translations/ru.json @@ -17,7 +17,7 @@ "data": { "flow_impl": "\u041f\u0440\u043e\u0432\u0430\u0439\u0434\u0435\u0440" }, - "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435, \u0447\u0435\u0440\u0435\u0437 \u043a\u0430\u043a\u043e\u0433\u043e \u043f\u0440\u043e\u0432\u0430\u0439\u0434\u0435\u0440\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438 \u0432\u044b \u0445\u043e\u0442\u0438\u0442\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0438\u0442\u044c \u0432\u0445\u043e\u0434 \u0432 Nest.", + "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435, \u0447\u0435\u0440\u0435\u0437 \u043a\u0430\u043a\u043e\u0433\u043e \u043f\u0440\u043e\u0432\u0430\u0439\u0434\u0435\u0440\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438 \u0412\u044b \u0445\u043e\u0442\u0438\u0442\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0438\u0442\u044c \u0432\u0445\u043e\u0434 \u0432 Nest.", "title": "\u041f\u0440\u043e\u0432\u0430\u0439\u0434\u0435\u0440 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438" }, "link": { diff --git a/homeassistant/components/nest/__init__.py b/homeassistant/components/nest/__init__.py index fe6a34cf4044b..21aaa2109a10c 100644 --- a/homeassistant/components/nest/__init__.py +++ b/homeassistant/components/nest/__init__.py @@ -7,7 +7,7 @@ import voluptuous as vol from homeassistant import config_entries -from homeassistant.components.climate import ( +from homeassistant.components.climate.const import ( ATTR_AWAY_MODE, SERVICE_SET_AWAY_MODE) from homeassistant.const import ( CONF_BINARY_SENSORS, CONF_FILENAME, CONF_MONITORED_CONDITIONS, diff --git a/homeassistant/components/nest/climate.py b/homeassistant/components/nest/climate.py index 8746a1959ae2d..88b6cbbbeb00e 100644 --- a/homeassistant/components/nest/climate.py +++ b/homeassistant/components/nest/climate.py @@ -5,14 +5,15 @@ from homeassistant.components.nest import ( DATA_NEST, SIGNAL_NEST_UPDATE, DOMAIN as NEST_DOMAIN) -from homeassistant.components.climate import ( - STATE_AUTO, STATE_COOL, STATE_HEAT, STATE_ECO, ClimateDevice, - PLATFORM_SCHEMA, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, - ATTR_TEMPERATURE, SUPPORT_TARGET_TEMPERATURE, +from homeassistant.components.climate import ClimateDevice, PLATFORM_SCHEMA +from homeassistant.components.climate.const import ( + STATE_AUTO, STATE_COOL, STATE_HEAT, STATE_ECO, + ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, + SUPPORT_TARGET_TEMPERATURE, SUPPORT_TARGET_TEMPERATURE_HIGH, SUPPORT_TARGET_TEMPERATURE_LOW, SUPPORT_OPERATION_MODE, SUPPORT_AWAY_MODE, SUPPORT_FAN_MODE) from homeassistant.const import ( - TEMP_CELSIUS, TEMP_FAHRENHEIT, + ATTR_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT, CONF_SCAN_INTERVAL, STATE_ON, STATE_OFF) from homeassistant.helpers.dispatcher import async_dispatcher_connect diff --git a/homeassistant/components/nest/local_auth.py b/homeassistant/components/nest/local_auth.py index 393a36e4a9ce2..5cb63956aeaab 100644 --- a/homeassistant/components/nest/local_auth.py +++ b/homeassistant/components/nest/local_auth.py @@ -41,6 +41,5 @@ async def resolve_auth_code(hass, client_id, client_secret, code): except AuthorizationError as err: if err.response.status_code == 401: raise config_flow.CodeInvalid() - else: - raise config_flow.NestAuthError('Unknown error: {} ({})'.format( - err, err.response.status_code)) + raise config_flow.NestAuthError('Unknown error: {} ({})'.format( + err, err.response.status_code)) diff --git a/homeassistant/components/nest/sensor.py b/homeassistant/components/nest/sensor.py index 10fa83d23e0fa..bde3f681c2b52 100644 --- a/homeassistant/components/nest/sensor.py +++ b/homeassistant/components/nest/sensor.py @@ -1,7 +1,7 @@ """Support for Nest Thermostat sensors.""" import logging -from homeassistant.components.climate import ( +from homeassistant.components.climate.const import ( STATE_COOL, STATE_HEAT) from homeassistant.components.nest import ( DATA_NEST, DATA_NEST_CONFIG, CONF_SENSORS, NestSensorDevice) diff --git a/homeassistant/components/netatmo/__init__.py b/homeassistant/components/netatmo/__init__.py index 495e22aae24fe..2e580627543d3 100644 --- a/homeassistant/components/netatmo/__init__.py +++ b/homeassistant/components/netatmo/__init__.py @@ -6,21 +6,60 @@ import voluptuous as vol from homeassistant.const import ( - CONF_API_KEY, CONF_PASSWORD, CONF_USERNAME, CONF_DISCOVERY) + CONF_API_KEY, CONF_PASSWORD, CONF_USERNAME, CONF_DISCOVERY, CONF_URL, + EVENT_HOMEASSISTANT_STOP) from homeassistant.helpers import discovery import homeassistant.helpers.config_validation as cv from homeassistant.util import Throttle REQUIREMENTS = ['pyatmo==1.8'] +DEPENDENCIES = ['webhook'] _LOGGER = logging.getLogger(__name__) +DATA_PERSONS = 'netatmo_persons' +DATA_WEBHOOK_URL = 'netatmo_webhook_url' + CONF_SECRET_KEY = 'secret_key' +CONF_WEBHOOKS = 'webhooks' DOMAIN = 'netatmo' +SERVICE_ADDWEBHOOK = 'addwebhook' +SERVICE_DROPWEBHOOK = 'dropwebhook' + NETATMO_AUTH = None +NETATMO_WEBHOOK_URL = None + +DEFAULT_PERSON = 'Unknown' DEFAULT_DISCOVERY = True +DEFAULT_WEBHOOKS = False + +EVENT_PERSON = 'person' +EVENT_MOVEMENT = 'movement' +EVENT_HUMAN = 'human' +EVENT_ANIMAL = 'animal' +EVENT_VEHICLE = 'vehicle' + +EVENT_BUS_PERSON = 'netatmo_person' +EVENT_BUS_MOVEMENT = 'netatmo_movement' +EVENT_BUS_HUMAN = 'netatmo_human' +EVENT_BUS_ANIMAL = 'netatmo_animal' +EVENT_BUS_VEHICLE = 'netatmo_vehicle' +EVENT_BUS_OTHER = 'netatmo_other' + +ATTR_ID = 'id' +ATTR_PSEUDO = 'pseudo' +ATTR_NAME = 'name' +ATTR_EVENT_TYPE = 'event_type' +ATTR_MESSAGE = 'message' +ATTR_CAMERA_ID = 'camera_id' +ATTR_HOME_NAME = 'home_name' +ATTR_PERSONS = 'persons' +ATTR_IS_KNOWN = 'is_known' +ATTR_FACE_URL = 'face_url' +ATTR_SNAPSHOT_URL = 'snapshot_url' +ATTR_VIGNETTE_URL = 'vignette_url' MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=10) MIN_TIME_BETWEEN_EVENT_UPDATES = timedelta(seconds=10) @@ -31,16 +70,24 @@ vol.Required(CONF_PASSWORD): cv.string, vol.Required(CONF_SECRET_KEY): cv.string, vol.Required(CONF_USERNAME): cv.string, + vol.Optional(CONF_WEBHOOKS, default=DEFAULT_WEBHOOKS): cv.boolean, vol.Optional(CONF_DISCOVERY, default=DEFAULT_DISCOVERY): cv.boolean, }) }, extra=vol.ALLOW_EXTRA) +SCHEMA_SERVICE_ADDWEBHOOK = vol.Schema({ + vol.Optional(CONF_URL): cv.string, +}) + +SCHEMA_SERVICE_DROPWEBHOOK = vol.Schema({}) + def setup(hass, config): """Set up the Netatmo devices.""" import pyatmo global NETATMO_AUTH + hass.data[DATA_PERSONS] = {} try: NETATMO_AUTH = pyatmo.ClientAuth( config[DOMAIN][CONF_API_KEY], config[DOMAIN][CONF_SECRET_KEY], @@ -56,14 +103,94 @@ def setup(hass, config): for component in 'camera', 'sensor', 'binary_sensor', 'climate': discovery.load_platform(hass, component, DOMAIN, {}, config) + if config[DOMAIN][CONF_WEBHOOKS]: + webhook_id = hass.components.webhook.async_generate_id() + hass.data[ + DATA_WEBHOOK_URL] = hass.components.webhook.async_generate_url( + webhook_id) + hass.components.webhook.async_register( + DOMAIN, 'Netatmo', webhook_id, handle_webhook) + NETATMO_AUTH.addwebhook(hass.data[DATA_WEBHOOK_URL]) + hass.bus.listen_once( + EVENT_HOMEASSISTANT_STOP, dropwebhook) + + def _service_addwebhook(service): + """Service to (re)add webhooks during runtime.""" + url = service.data.get(CONF_URL) + if url is None: + url = hass.data[DATA_WEBHOOK_URL] + _LOGGER.info("Adding webhook for URL: %s", url) + NETATMO_AUTH.addwebhook(url) + + hass.services.register( + DOMAIN, SERVICE_ADDWEBHOOK, _service_addwebhook, + schema=SCHEMA_SERVICE_ADDWEBHOOK) + + def _service_dropwebhook(service): + """Service to drop webhooks during runtime.""" + _LOGGER.info("Dropping webhook") + NETATMO_AUTH.dropwebhook() + + hass.services.register( + DOMAIN, SERVICE_DROPWEBHOOK, _service_dropwebhook, + schema=SCHEMA_SERVICE_DROPWEBHOOK) + return True +def dropwebhook(hass): + """Drop the webhook subscription.""" + NETATMO_AUTH.dropwebhook() + + +async def handle_webhook(hass, webhook_id, request): + """Handle webhook callback.""" + try: + data = await request.json() + except ValueError: + return None + + _LOGGER.debug("Got webhook data: %s", data) + published_data = { + ATTR_EVENT_TYPE: data.get(ATTR_EVENT_TYPE), + ATTR_HOME_NAME: data.get(ATTR_HOME_NAME), + ATTR_CAMERA_ID: data.get(ATTR_CAMERA_ID), + ATTR_MESSAGE: data.get(ATTR_MESSAGE) + } + if data.get(ATTR_EVENT_TYPE) == EVENT_PERSON: + for person in data[ATTR_PERSONS]: + published_data[ATTR_ID] = person.get(ATTR_ID) + published_data[ATTR_NAME] = hass.data[DATA_PERSONS].get( + published_data[ATTR_ID], DEFAULT_PERSON) + published_data[ATTR_IS_KNOWN] = person.get(ATTR_IS_KNOWN) + published_data[ATTR_FACE_URL] = person.get(ATTR_FACE_URL) + hass.bus.async_fire(EVENT_BUS_PERSON, published_data) + elif data.get(ATTR_EVENT_TYPE) == EVENT_MOVEMENT: + published_data[ATTR_VIGNETTE_URL] = data.get(ATTR_VIGNETTE_URL) + published_data[ATTR_SNAPSHOT_URL] = data.get(ATTR_SNAPSHOT_URL) + hass.bus.async_fire(EVENT_BUS_MOVEMENT, published_data) + elif data.get(ATTR_EVENT_TYPE) == EVENT_HUMAN: + published_data[ATTR_VIGNETTE_URL] = data.get(ATTR_VIGNETTE_URL) + published_data[ATTR_SNAPSHOT_URL] = data.get(ATTR_SNAPSHOT_URL) + hass.bus.async_fire(EVENT_BUS_HUMAN, published_data) + elif data.get(ATTR_EVENT_TYPE) == EVENT_ANIMAL: + published_data[ATTR_VIGNETTE_URL] = data.get(ATTR_VIGNETTE_URL) + published_data[ATTR_SNAPSHOT_URL] = data.get(ATTR_SNAPSHOT_URL) + hass.bus.async_fire(EVENT_BUS_ANIMAL, published_data) + elif data.get(ATTR_EVENT_TYPE) == EVENT_VEHICLE: + hass.bus.async_fire(EVENT_BUS_VEHICLE, published_data) + published_data[ATTR_VIGNETTE_URL] = data.get(ATTR_VIGNETTE_URL) + published_data[ATTR_SNAPSHOT_URL] = data.get(ATTR_SNAPSHOT_URL) + else: + hass.bus.async_fire(EVENT_BUS_OTHER, data) + + class CameraData: """Get the latest data from Netatmo.""" - def __init__(self, auth, home=None): + def __init__(self, hass, auth, home=None): """Initialize the data object.""" + self._hass = hass self.auth = auth self.camera_data = None self.camera_names = [] @@ -101,6 +228,12 @@ def get_camera_type(self, camera=None, home=None, cid=None): home=home, cid=cid) return self.camera_type + def get_persons(self): + """Gather person data for webhooks.""" + for person_id, person_data in self.camera_data.persons.items(): + self._hass.data[DATA_PERSONS][person_id] = person_data.get( + ATTR_PSEUDO) + @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): """Call the Netatmo API to update the data.""" diff --git a/homeassistant/components/netatmo/binary_sensor.py b/homeassistant/components/netatmo/binary_sensor.py index 727ed0a68c7b5..7986010ef6469 100644 --- a/homeassistant/components/netatmo/binary_sensor.py +++ b/homeassistant/components/netatmo/binary_sensor.py @@ -62,7 +62,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): import pyatmo try: - data = CameraData(netatmo.NETATMO_AUTH, home) + data = CameraData(hass, netatmo.NETATMO_AUTH, home) if not data.get_camera_names(): return None except pyatmo.NoDevice: diff --git a/homeassistant/components/netatmo/camera.py b/homeassistant/components/netatmo/camera.py index a3a5461631d3c..57d30d6cbc98f 100644 --- a/homeassistant/components/netatmo/camera.py +++ b/homeassistant/components/netatmo/camera.py @@ -31,7 +31,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): verify_ssl = config.get(CONF_VERIFY_SSL, True) import pyatmo try: - data = CameraData(netatmo.NETATMO_AUTH, home) + data = CameraData(hass, netatmo.NETATMO_AUTH, home) for camera_name in data.get_camera_names(): camera_type = data.get_camera_type(camera=camera_name, home=home) if CONF_CAMERAS in config: @@ -40,6 +40,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): continue add_entities([NetatmoCamera(data, camera_name, home, camera_type, verify_ssl)]) + data.get_persons() except pyatmo.NoDevice: return None diff --git a/homeassistant/components/netatmo/climate.py b/homeassistant/components/netatmo/climate.py index 2b9bcbebaf29e..1e16f2d3e050a 100644 --- a/homeassistant/components/netatmo/climate.py +++ b/homeassistant/components/netatmo/climate.py @@ -4,8 +4,9 @@ import voluptuous as vol from homeassistant.const import TEMP_CELSIUS, ATTR_TEMPERATURE -from homeassistant.components.climate import ( - STATE_HEAT, STATE_IDLE, ClimateDevice, PLATFORM_SCHEMA, +from homeassistant.components.climate import ClimateDevice, PLATFORM_SCHEMA +from homeassistant.components.climate.const import ( + STATE_HEAT, STATE_IDLE, SUPPORT_TARGET_TEMPERATURE, SUPPORT_OPERATION_MODE, SUPPORT_AWAY_MODE) from homeassistant.util import Throttle import homeassistant.helpers.config_validation as cv diff --git a/homeassistant/components/netatmo/sensor.py b/homeassistant/components/netatmo/sensor.py index 78a118528b97f..307b76ca434bd 100644 --- a/homeassistant/components/netatmo/sensor.py +++ b/homeassistant/components/netatmo/sensor.py @@ -47,6 +47,7 @@ 'rf_status_lvl': ['Radio_lvl', '', 'mdi:signal', None], 'wifi_status': ['Wifi', '', 'mdi:wifi', None], 'wifi_status_lvl': ['Wifi_lvl', 'dBm', 'mdi:wifi', None], + 'health_idx': ['Health', '', 'mdi:cloud', None], } MODULE_SCHEMA = vol.Schema({ @@ -67,23 +68,55 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the available Netatmo weather sensors.""" netatmo = hass.components.netatmo - data = NetAtmoData(netatmo.NETATMO_AUTH, config.get(CONF_STATION, None)) dev = [] + if CONF_MODULES in config: + manual_config(netatmo, config, dev) + else: + auto_config(netatmo, config, dev) + + if dev: + add_entities(dev, True) + + +def manual_config(netatmo, config, dev): + """Handle manual configuration.""" import pyatmo - try: - if CONF_MODULES in config: + + all_classes = all_product_classes() + not_handled = {} + for data_class in all_classes: + data = NetAtmoData(netatmo.NETATMO_AUTH, data_class, + config.get(CONF_STATION)) + try: # Iterate each module for module_name, monitored_conditions in \ config[CONF_MODULES].items(): # Test if module exists if module_name not in data.get_module_names(): - _LOGGER.error('Module name: "%s" not found', module_name) - continue - # Only create sensors for monitored properties - for variable in monitored_conditions: - dev.append(NetAtmoSensor(data, module_name, variable)) - else: + not_handled[module_name] = \ + not_handled[module_name]+1 \ + if module_name in not_handled else 1 + else: + # Only create sensors for monitored properties + for variable in monitored_conditions: + dev.append(NetAtmoSensor(data, module_name, variable)) + except pyatmo.NoDevice: + continue + + for module_name, count in not_handled.items(): + if count == len(all_classes): + _LOGGER.error('Module name: "%s" not found', module_name) + + +def auto_config(netatmo, config, dev): + """Handle auto configuration.""" + import pyatmo + + for data_class in all_product_classes(): + data = NetAtmoData(netatmo.NETATMO_AUTH, data_class, + config.get(CONF_STATION)) + try: for module_name in data.get_module_names(): for variable in \ data.station_data.monitoredConditions(module_name): @@ -92,10 +125,15 @@ def setup_platform(hass, config, add_entities, discovery_info=None): else: _LOGGER.warning("Ignoring unknown var %s for mod %s", variable, module_name) - except pyatmo.NoDevice: - return None + except pyatmo.NoDevice: + continue + + +def all_product_classes(): + """Provide all handled Netatmo product classes.""" + import pyatmo - add_entities(dev, True) + return [pyatmo.WeatherStationData, pyatmo.HomeCoachData] class NetAtmoSensor(Entity): @@ -151,6 +189,13 @@ def unique_id(self): def update(self): """Get the latest data from NetAtmo API and updates the states.""" self.netatmo_data.update() + if self.netatmo_data.data is None: + if self._state is None: + return + _LOGGER.warning("No data found for %s", self.module_name) + self._state = None + return + data = self.netatmo_data.data.get(self.module_name) if data is None: @@ -299,6 +344,17 @@ def update(self): self._state = "High" elif data['wifi_status'] <= 55: self._state = "Full" + elif self.type == 'health_idx': + if data['health_idx'] == 0: + self._state = "Healthy" + elif data['health_idx'] == 1: + self._state = "Fine" + elif data['health_idx'] == 2: + self._state = "Fair" + elif data['health_idx'] == 3: + self._state = "Poor" + elif data['health_idx'] == 4: + self._state = "Unhealthy" except KeyError: _LOGGER.error("No %s data found for %s", self.type, self.module_name) @@ -309,9 +365,10 @@ def update(self): class NetAtmoData: """Get the latest data from NetAtmo.""" - def __init__(self, auth, station): + def __init__(self, auth, data_class, station): """Initialize the data object.""" self.auth = auth + self.data_class = data_class self.data = None self.station_data = None self.station = station @@ -321,6 +378,8 @@ def __init__(self, auth, station): def get_module_names(self): """Return all module available on the API as a list.""" self.update() + if not self.data: + return [] return self.data.keys() def _detect_platform_type(self): @@ -328,14 +387,12 @@ def _detect_platform_type(self): The return can be a WeatherStationData or a HomeCoachData. """ - import pyatmo - for data_class in [pyatmo.WeatherStationData, pyatmo.HomeCoachData]: - try: - station_data = data_class(self.auth) - _LOGGER.debug("%s detected!", str(data_class.__name__)) - return station_data - except TypeError: - continue + try: + station_data = self.data_class(self.auth) + _LOGGER.debug("%s detected!", str(self.data_class.__name__)) + return station_data + except TypeError: + return def update(self): """Call the Netatmo API to update the data. @@ -366,7 +423,7 @@ def update(self): newinterval = self.data[module]['When'] break except TypeError: - _LOGGER.error("No modules found!") + _LOGGER.debug("No %s modules found", self.data_class.__name__) if newinterval: # Try and estimate when fresh data will be available diff --git a/homeassistant/components/netatmo/services.yaml b/homeassistant/components/netatmo/services.yaml new file mode 100644 index 0000000000000..7bb990caf97f3 --- /dev/null +++ b/homeassistant/components/netatmo/services.yaml @@ -0,0 +1,8 @@ +addwebhook: + description: Add webhook during runtime (e.g. if it has been banned). + fields: + url: + description: URL for which to add the webhook. + example: https://yourdomain.com:443/api/webhook/webhook_id +dropwebhook: + description: Drop active webhooks. \ No newline at end of file diff --git a/homeassistant/components/nissan_leaf/__init__.py b/homeassistant/components/nissan_leaf/__init__.py new file mode 100644 index 0000000000000..cb101c0a5309c --- /dev/null +++ b/homeassistant/components/nissan_leaf/__init__.py @@ -0,0 +1,496 @@ +"""Support for the Nissan Leaf Carwings/Nissan Connect API.""" +from datetime import datetime, timedelta +import asyncio +import logging +import sys + +import voluptuous as vol + +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.core import callback +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.discovery import load_platform +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, async_dispatcher_send) +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.event import async_track_point_in_utc_time +from homeassistant.util.dt import utcnow + +REQUIREMENTS = ['pycarwings2==2.8'] + +_LOGGER = logging.getLogger(__name__) + +DOMAIN = 'nissan_leaf' +DATA_LEAF = 'nissan_leaf_data' + +DATA_BATTERY = 'battery' +DATA_LOCATION = 'location' +DATA_CHARGING = 'charging' +DATA_PLUGGED_IN = 'plugged_in' +DATA_CLIMATE = 'climate' +DATA_RANGE_AC = 'range_ac_on' +DATA_RANGE_AC_OFF = 'range_ac_off' + +CONF_NCONNECT = 'nissan_connect' +CONF_INTERVAL = 'update_interval' +CONF_CHARGING_INTERVAL = 'update_interval_charging' +CONF_CLIMATE_INTERVAL = 'update_interval_climate' +CONF_REGION = 'region' +CONF_VALID_REGIONS = ['NNA', 'NE', 'NCI', 'NMA', 'NML'] +CONF_FORCE_MILES = 'force_miles' + +INITIAL_UPDATE = timedelta(seconds=15) +MIN_UPDATE_INTERVAL = timedelta(minutes=2) +DEFAULT_INTERVAL = timedelta(hours=1) +DEFAULT_CHARGING_INTERVAL = timedelta(minutes=15) +DEFAULT_CLIMATE_INTERVAL = timedelta(minutes=5) +RESTRICTED_BATTERY = 2 +RESTRICTED_INTERVAL = timedelta(hours=12) + +MAX_RESPONSE_ATTEMPTS = 10 + +PYCARWINGS2_SLEEP = 30 + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.All(cv.ensure_list, [vol.Schema({ + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Required(CONF_REGION): vol.In(CONF_VALID_REGIONS), + vol.Optional(CONF_NCONNECT, default=True): cv.boolean, + vol.Optional(CONF_INTERVAL, default=DEFAULT_INTERVAL): ( + vol.All(cv.time_period, vol.Clamp(min=MIN_UPDATE_INTERVAL))), + vol.Optional(CONF_CHARGING_INTERVAL, + default=DEFAULT_CHARGING_INTERVAL): ( + vol.All(cv.time_period, + vol.Clamp(min=MIN_UPDATE_INTERVAL))), + vol.Optional(CONF_CLIMATE_INTERVAL, + default=DEFAULT_CLIMATE_INTERVAL): ( + vol.All(cv.time_period, + vol.Clamp(min=MIN_UPDATE_INTERVAL))), + vol.Optional(CONF_FORCE_MILES, default=False): cv.boolean + })]) +}, extra=vol.ALLOW_EXTRA) + +LEAF_COMPONENTS = [ + 'sensor', 'switch', 'binary_sensor', 'device_tracker' +] + +SIGNAL_UPDATE_LEAF = 'nissan_leaf_update' + +SERVICE_UPDATE_LEAF = 'update' +SERVICE_START_CHARGE_LEAF = 'start_charge' +ATTR_VIN = 'vin' + +UPDATE_LEAF_SCHEMA = vol.Schema({ + vol.Required(ATTR_VIN): cv.string, +}) +START_CHARGE_LEAF_SCHEMA = vol.Schema({ + vol.Required(ATTR_VIN): cv.string, +}) + + +def setup(hass, config): + """Set up the Nissan Leaf component.""" + import pycarwings2 + + async def async_handle_update(service): + """Handle service to update leaf data from Nissan servers.""" + # It would be better if this was changed to use nickname, or + # an entity name rather than a vin. + vin = service.data[ATTR_VIN] + + if vin in hass.data[DATA_LEAF]: + data_store = hass.data[DATA_LEAF][vin] + await data_store.async_update_data(utcnow()) + else: + _LOGGER.debug("Vin %s not recognised for update", vin) + + async def async_handle_start_charge(service): + """Handle service to start charging.""" + # It would be better if this was changed to use nickname, or + # an entity name rather than a vin. + vin = service.data[ATTR_VIN] + + if vin in hass.data[DATA_LEAF]: + data_store = hass.data[DATA_LEAF][vin] + + # Send the command to request charging is started to Nissan + # servers. If that completes OK then trigger a fresh update to + # pull the charging status from the car after waiting a minute + # for the charging request to reach the car. + result = await hass.async_add_executor_job( + data_store.leaf.start_charging) + if result: + _LOGGER.debug("Start charging sent, " + "request updated data in 1 minute") + check_charge_at = utcnow() + timedelta(minutes=1) + data_store.next_update = check_charge_at + async_track_point_in_utc_time( + hass, data_store.async_update_data, check_charge_at) + + else: + _LOGGER.debug("Vin %s not recognised for update", vin) + + def setup_leaf(car_config): + """Set up a car.""" + _LOGGER.debug("Logging into You+Nissan...") + + username = car_config[CONF_USERNAME] + password = car_config[CONF_PASSWORD] + region = car_config[CONF_REGION] + leaf = None + + try: + # This might need to be made async (somehow) causes + # homeassistant to be slow to start + sess = pycarwings2.Session(username, password, region) + leaf = sess.get_leaf() + except KeyError: + _LOGGER.error( + "Unable to fetch car details..." + " do you actually have a Leaf connected to your account?") + return False + except pycarwings2.CarwingsError: + _LOGGER.error( + "An unknown error occurred while connecting to Nissan: %s", + sys.exc_info()[0]) + return False + + _LOGGER.warning( + "WARNING: This may poll your Leaf too often, and drain the 12V" + " battery. If you drain your cars 12V battery it WILL NOT START" + " as the drive train battery won't connect." + " Don't set the intervals too low.") + + data_store = LeafDataStore(hass, leaf, car_config) + hass.data[DATA_LEAF][leaf.vin] = data_store + + for component in LEAF_COMPONENTS: + if component != 'device_tracker' or car_config[CONF_NCONNECT]: + load_platform(hass, component, DOMAIN, {}, car_config) + + async_track_point_in_utc_time(hass, data_store.async_update_data, + utcnow() + INITIAL_UPDATE) + + hass.data[DATA_LEAF] = {} + for car in config[DOMAIN]: + setup_leaf(car) + + hass.services.register( + DOMAIN, SERVICE_UPDATE_LEAF, + async_handle_update, schema=UPDATE_LEAF_SCHEMA) + hass.services.register( + DOMAIN, SERVICE_START_CHARGE_LEAF, + async_handle_start_charge, schema=START_CHARGE_LEAF_SCHEMA) + + return True + + +class LeafDataStore: + """Nissan Leaf Data Store.""" + + def __init__(self, hass, leaf, car_config): + """Initialise the data store.""" + self.hass = hass + self.leaf = leaf + self.car_config = car_config + self.nissan_connect = car_config[CONF_NCONNECT] + self.force_miles = car_config[CONF_FORCE_MILES] + self.data = {} + self.data[DATA_CLIMATE] = False + self.data[DATA_BATTERY] = 0 + self.data[DATA_CHARGING] = False + self.data[DATA_LOCATION] = False + self.data[DATA_RANGE_AC] = 0 + self.data[DATA_RANGE_AC_OFF] = 0 + self.data[DATA_PLUGGED_IN] = False + self.next_update = None + self.last_check = None + self.request_in_progress = False + # Timestamp of last successful response from battery, + # climate or location. + self.last_battery_response = None + self.last_climate_response = None + self.last_location_response = None + self._remove_listener = None + + async def async_update_data(self, now): + """Update data from nissan leaf.""" + # Prevent against a previously scheduled update and an ad-hoc update + # started from an update from both being triggered. + if self._remove_listener: + self._remove_listener() + self._remove_listener = None + + # Clear next update whilst this update is underway + self.next_update = None + + await self.async_refresh_data(now) + self.next_update = self.get_next_interval() + _LOGGER.debug("Next update=%s", self.next_update) + self._remove_listener = async_track_point_in_utc_time( + self.hass, self.async_update_data, self.next_update) + + def get_next_interval(self): + """Calculate when the next update should occur.""" + base_interval = self.car_config[CONF_INTERVAL] + climate_interval = self.car_config[CONF_CLIMATE_INTERVAL] + charging_interval = self.car_config[CONF_CHARGING_INTERVAL] + + # The 12V battery is used when communicating with Nissan servers. + # The 12V battery is charged from the traction battery when not + # connected and when the traction battery has enough charge. To + # avoid draining the 12V battery we shall restrict the update + # frequency if low battery detected. + if (self.last_battery_response is not None and + self.data[DATA_CHARGING] is False and + self.data[DATA_BATTERY] <= RESTRICTED_BATTERY): + _LOGGER.debug("Low battery so restricting refresh frequency (%s)", + self.leaf.nickname) + interval = RESTRICTED_INTERVAL + else: + intervals = [base_interval] + + if self.data[DATA_CHARGING]: + intervals.append(charging_interval) + + if self.data[DATA_CLIMATE]: + intervals.append(climate_interval) + + interval = min(intervals) + + return utcnow() + interval + + async def async_refresh_data(self, now): + """Refresh the leaf data and update the datastore.""" + from pycarwings2 import CarwingsError + + if self.request_in_progress: + _LOGGER.debug("Refresh currently in progress for %s", + self.leaf.nickname) + return + + _LOGGER.debug("Updating Nissan Leaf Data") + + self.last_check = datetime.today() + self.request_in_progress = True + + server_response = await self.async_get_battery() + + if server_response is not None: + _LOGGER.debug("Server Response: %s", server_response.__dict__) + + if server_response.answer['status'] == 200: + self.data[DATA_BATTERY] = server_response.battery_percent + + # pycarwings2 library doesn't always provide cruising rnages + # so we have to check if they exist before we can use them. + # Root cause: the nissan servers don't always send the data. + if hasattr(server_response, 'cruising_range_ac_on_km'): + self.data[DATA_RANGE_AC] = ( + server_response.cruising_range_ac_on_km + ) + else: + self.data[DATA_RANGE_AC] = None + + if hasattr(server_response, 'cruising_range_ac_off_km'): + self.data[DATA_RANGE_AC_OFF] = ( + server_response.cruising_range_ac_off_km + ) + else: + self.data[DATA_RANGE_AC_OFF] = None + + self.data[DATA_PLUGGED_IN] = ( + server_response.is_connected + ) + async_dispatcher_send(self.hass, SIGNAL_UPDATE_LEAF) + self.last_battery_response = utcnow() + + # Climate response only updated if battery data updated first. + if server_response is not None: + try: + climate_response = await self.async_get_climate() + if climate_response is not None: + _LOGGER.debug("Got climate data for Leaf: %s", + climate_response.__dict__) + self.data[DATA_CLIMATE] = climate_response.is_hvac_running + self.last_climate_response = utcnow() + except CarwingsError: + _LOGGER.error("Error fetching climate info") + + if self.nissan_connect: + try: + location_response = await self.async_get_location() + + if location_response is None: + _LOGGER.debug("Empty Location Response Received") + self.data[DATA_LOCATION] = None + else: + _LOGGER.debug("Location Response: %s", + location_response.__dict__) + self.data[DATA_LOCATION] = location_response + self.last_location_response = utcnow() + except CarwingsError: + _LOGGER.error("Error fetching location info") + + self.request_in_progress = False + async_dispatcher_send(self.hass, SIGNAL_UPDATE_LEAF) + + @staticmethod + def _extract_start_date(battery_info): + """Extract the server date from the battery response.""" + try: + return battery_info.answer[ + "BatteryStatusRecords"]["OperationDateAndTime"] + except KeyError: + return None + + async def async_get_battery(self): + """Request battery update from Nissan servers.""" + from pycarwings2 import CarwingsError + try: + # First, check nissan servers for the latest data + start_server_info = await self.hass.async_add_executor_job( + self.leaf.get_latest_battery_status) + + # Store the date from the nissan servers + start_date = self._extract_start_date(start_server_info) + if start_date is None: + _LOGGER.info("No start date from servers. Aborting") + return None + + _LOGGER.debug("Start server date=%s", start_date) + + # Request battery update from the car + _LOGGER.debug("Requesting battery update, %s", self.leaf.vin) + request = await self.hass.async_add_executor_job( + self.leaf.request_update) + if not request: + _LOGGER.error("Battery update request failed") + return None + + for attempt in range(MAX_RESPONSE_ATTEMPTS): + _LOGGER.debug( + "Waiting %s seconds for battery update (%s) (%s)", + PYCARWINGS2_SLEEP, self.leaf.vin, attempt) + await asyncio.sleep(PYCARWINGS2_SLEEP) + + # Note leaf.get_status_from_update is always returning 0, so + # don't try to use it anymore. + server_info = await self.hass.async_add_executor_job( + self.leaf.get_latest_battery_status) + + latest_date = self._extract_start_date(server_info) + _LOGGER.debug("Latest server date=%s", latest_date) + if latest_date is not None and latest_date != start_date: + return server_info + + _LOGGER.debug( + "%s attempts exceeded return latest data from server", + MAX_RESPONSE_ATTEMPTS) + return server_info + except CarwingsError: + _LOGGER.error("An error occurred getting battery status.") + return None + + async def async_get_climate(self): + """Request climate data from Nissan servers.""" + from pycarwings2 import CarwingsError + try: + return await self.hass.async_add_executor_job( + self.leaf.get_latest_hvac_status) + except CarwingsError: + _LOGGER.error( + "An error occurred communicating with the car %s", + self.leaf.vin) + return None + + async def async_set_climate(self, toggle): + """Set climate control mode via Nissan servers.""" + climate_result = None + if toggle: + _LOGGER.debug("Requesting climate turn on for %s", self.leaf.vin) + set_function = self.leaf.start_climate_control + result_function = self.leaf.get_start_climate_control_result + else: + _LOGGER.debug("Requesting climate turn off for %s", self.leaf.vin) + set_function = self.leaf.stop_climate_control + result_function = self.leaf.get_stop_climate_control_result + + request = await self.hass.async_add_executor_job(set_function) + for attempt in range(MAX_RESPONSE_ATTEMPTS): + if attempt > 0: + _LOGGER.debug("Climate data not in yet (%s) (%s). " + "Waiting (%s) seconds", self.leaf.vin, + attempt, PYCARWINGS2_SLEEP) + await asyncio.sleep(PYCARWINGS2_SLEEP) + + climate_result = await self.hass.async_add_executor_job( + result_function, request) + + if climate_result is not None: + break + + if climate_result is not None: + _LOGGER.debug("Climate result: %s", climate_result.__dict__) + async_dispatcher_send(self.hass, SIGNAL_UPDATE_LEAF) + return climate_result.is_hvac_running == toggle + + _LOGGER.debug("Climate result not returned by Nissan servers") + return False + + async def async_get_location(self): + """Get location from Nissan servers.""" + request = await self.hass.async_add_executor_job( + self.leaf.request_location) + for attempt in range(MAX_RESPONSE_ATTEMPTS): + if attempt > 0: + _LOGGER.debug("Location data not in yet. (%s) (%s). " + "Waiting %s seconds", self.leaf.vin, + attempt, PYCARWINGS2_SLEEP) + await asyncio.sleep(PYCARWINGS2_SLEEP) + + location_status = await self.hass.async_add_executor_job( + self.leaf.get_status_from_location, request) + + if location_status is not None: + _LOGGER.debug("Location_status=%s", location_status.__dict__) + break + + return location_status + + +class LeafEntity(Entity): + """Base class for Nissan Leaf entity.""" + + def __init__(self, car): + """Store LeafDataStore upon init.""" + self.car = car + + def log_registration(self): + """Log registration.""" + _LOGGER.debug( + "Registered %s component for VIN %s", + self.__class__.__name__, self.car.leaf.vin) + + @property + def device_state_attributes(self): + """Return default attributes for Nissan leaf entities.""" + return { + 'next_update': self.car.next_update, + 'last_attempt': self.car.last_check, + 'updated_on': self.car.last_battery_response, + 'update_in_progress': self.car.request_in_progress, + 'vin': self.car.leaf.vin, + } + + async def async_added_to_hass(self): + """Register callbacks.""" + self.log_registration() + async_dispatcher_connect( + self.car.hass, SIGNAL_UPDATE_LEAF, self._update_callback) + + @callback + def _update_callback(self): + """Update the state.""" + self.async_schedule_update_ha_state(True) diff --git a/homeassistant/components/nissan_leaf/binary_sensor.py b/homeassistant/components/nissan_leaf/binary_sensor.py new file mode 100644 index 0000000000000..2397405ec201f --- /dev/null +++ b/homeassistant/components/nissan_leaf/binary_sensor.py @@ -0,0 +1,66 @@ +"""Plugged In Status Support for the Nissan Leaf.""" +import logging + +from homeassistant.components.nissan_leaf import ( + DATA_CHARGING, DATA_LEAF, DATA_PLUGGED_IN, LeafEntity) +from homeassistant.components.binary_sensor import BinarySensorDevice + +_LOGGER = logging.getLogger(__name__) + +DEPENDENCIES = ['nissan_leaf'] + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up of a Nissan Leaf binary sensor.""" + if discovery_info is None: + return + + devices = [] + for vin, datastore in hass.data[DATA_LEAF].items(): + _LOGGER.debug("Adding binary_sensors for vin=%s", vin) + devices.append(LeafPluggedInSensor(datastore)) + devices.append(LeafChargingSensor(datastore)) + + add_entities(devices, True) + + +class LeafPluggedInSensor(LeafEntity, BinarySensorDevice): + """Plugged In Sensor class.""" + + @property + def name(self): + """Sensor name.""" + return "{} {}".format(self.car.leaf.nickname, "Plug Status") + + @property + def is_on(self): + """Return true if plugged in.""" + return self.car.data[DATA_PLUGGED_IN] + + @property + def icon(self): + """Icon handling.""" + if self.car.data[DATA_PLUGGED_IN]: + return 'mdi:power-plug' + return 'mdi:power-plug-off' + + +class LeafChargingSensor(LeafEntity, BinarySensorDevice): + """Charging Sensor class.""" + + @property + def name(self): + """Sensor name.""" + return "{} {}".format(self.car.leaf.nickname, "Charging Status") + + @property + def is_on(self): + """Return true if charging.""" + return self.car.data[DATA_CHARGING] + + @property + def icon(self): + """Icon handling.""" + if self.car.data[DATA_CHARGING]: + return 'mdi:flash' + return 'mdi:flash-off' diff --git a/homeassistant/components/nissan_leaf/device_tracker.py b/homeassistant/components/nissan_leaf/device_tracker.py new file mode 100644 index 0000000000000..1ca7fceb91188 --- /dev/null +++ b/homeassistant/components/nissan_leaf/device_tracker.py @@ -0,0 +1,46 @@ +"""Support for tracking a Nissan Leaf.""" +import logging + +from homeassistant.components.nissan_leaf import ( + DATA_LEAF, DATA_LOCATION, SIGNAL_UPDATE_LEAF) +from homeassistant.helpers.dispatcher import dispatcher_connect +from homeassistant.util import slugify + +_LOGGER = logging.getLogger(__name__) + +DEPENDENCIES = ['nissan_leaf'] + +ICON_CAR = "mdi:car" + + +def setup_scanner(hass, config, see, discovery_info=None): + """Set up the Nissan Leaf tracker.""" + if discovery_info is None: + return False + + def see_vehicle(): + """Handle the reporting of the vehicle position.""" + for vin, datastore in hass.data[DATA_LEAF].items(): + host_name = datastore.leaf.nickname + dev_id = 'nissan_leaf_{}'.format(slugify(host_name)) + if not datastore.data[DATA_LOCATION]: + _LOGGER.debug("No position found for vehicle %s", vin) + return + _LOGGER.debug("Updating device_tracker for %s with position %s", + datastore.leaf.nickname, + datastore.data[DATA_LOCATION].__dict__) + attrs = { + 'updated_on': datastore.last_location_response, + } + see(dev_id=dev_id, + host_name=host_name, + gps=( + datastore.data[DATA_LOCATION].latitude, + datastore.data[DATA_LOCATION].longitude + ), + attributes=attrs, + icon=ICON_CAR) + + dispatcher_connect(hass, SIGNAL_UPDATE_LEAF, see_vehicle) + + return True diff --git a/homeassistant/components/nissan_leaf/sensor.py b/homeassistant/components/nissan_leaf/sensor.py new file mode 100644 index 0000000000000..f6206f1f4efc5 --- /dev/null +++ b/homeassistant/components/nissan_leaf/sensor.py @@ -0,0 +1,113 @@ +"""Battery Charge and Range Support for the Nissan Leaf.""" +import logging + +from homeassistant.components.nissan_leaf import ( + DATA_BATTERY, DATA_CHARGING, DATA_LEAF, DATA_RANGE_AC, DATA_RANGE_AC_OFF, + LeafEntity) +from homeassistant.const import DEVICE_CLASS_BATTERY +from homeassistant.helpers.icon import icon_for_battery_level +from homeassistant.util.distance import LENGTH_KILOMETERS, LENGTH_MILES +from homeassistant.util.unit_system import IMPERIAL_SYSTEM, METRIC_SYSTEM + +_LOGGER = logging.getLogger(__name__) + +DEPENDENCIES = ['nissan_leaf'] + +ICON_RANGE = 'mdi:speedometer' + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Sensors setup.""" + if discovery_info is None: + return + + devices = [] + for vin, datastore in hass.data[DATA_LEAF].items(): + _LOGGER.debug("Adding sensors for vin=%s", vin) + devices.append(LeafBatterySensor(datastore)) + devices.append(LeafRangeSensor(datastore, True)) + devices.append(LeafRangeSensor(datastore, False)) + + add_devices(devices, True) + + +class LeafBatterySensor(LeafEntity): + """Nissan Leaf Battery Sensor.""" + + @property + def name(self): + """Sensor Name.""" + return self.car.leaf.nickname + " Charge" + + @property + def device_class(self): + """Return the device class of the sensor.""" + return DEVICE_CLASS_BATTERY + + @property + def state(self): + """Battery state percentage.""" + return round(self.car.data[DATA_BATTERY]) + + @property + def unit_of_measurement(self): + """Battery state measured in percentage.""" + return '%' + + @property + def icon(self): + """Battery state icon handling.""" + chargestate = self.car.data[DATA_CHARGING] + return icon_for_battery_level( + battery_level=self.state, + charging=chargestate + ) + + +class LeafRangeSensor(LeafEntity): + """Nissan Leaf Range Sensor.""" + + def __init__(self, car, ac_on): + """Set-up range sensor. Store if AC on.""" + self._ac_on = ac_on + super().__init__(car) + + @property + def name(self): + """Update sensor name depending on AC.""" + if self._ac_on is True: + return self.car.leaf.nickname + " Range (AC)" + return self.car.leaf.nickname + " Range" + + def log_registration(self): + """Log registration.""" + _LOGGER.debug( + "Registered LeafRangeSensor component with HASS for VIN %s", + self.car.leaf.vin) + + @property + def state(self): + """Battery range in miles or kms.""" + if self._ac_on: + ret = self.car.data[DATA_RANGE_AC] + else: + ret = self.car.data[DATA_RANGE_AC_OFF] + + if (not self.car.hass.config.units.is_metric or + self.car.force_miles): + ret = IMPERIAL_SYSTEM.length(ret, METRIC_SYSTEM.length_unit) + + return round(ret) + + @property + def unit_of_measurement(self): + """Battery range unit.""" + if (not self.car.hass.config.units.is_metric or + self.car.force_miles): + return LENGTH_MILES + return LENGTH_KILOMETERS + + @property + def icon(self): + """Nice icon for range.""" + return ICON_RANGE diff --git a/homeassistant/components/nissan_leaf/services.yaml b/homeassistant/components/nissan_leaf/services.yaml new file mode 100644 index 0000000000000..ef60dfb4a654d --- /dev/null +++ b/homeassistant/components/nissan_leaf/services.yaml @@ -0,0 +1,21 @@ +# Describes the format for available services for nissan_leaf + +start_charge: + description: > + Start the vehicle charging. It must be plugged in first! + fields: + vin: + description: > + The vehicle identification number (VIN) of the vehicle, 17 characters + example: WBANXXXXXX1234567 + +update: + description: > + Fetch the last state of the vehicle of all your accounts, requesting + an update from of the state from the car if possible. + fields: + vin: + description: > + The vehicle identification number (VIN) of the vehicle, 17 characters + example: WBANXXXXXX1234567 + diff --git a/homeassistant/components/nissan_leaf/switch.py b/homeassistant/components/nissan_leaf/switch.py new file mode 100644 index 0000000000000..60b9a6630cd63 --- /dev/null +++ b/homeassistant/components/nissan_leaf/switch.py @@ -0,0 +1,60 @@ +"""Charge and Climate Control Support for the Nissan Leaf.""" +import logging + +from homeassistant.components.nissan_leaf import ( + DATA_CLIMATE, DATA_LEAF, LeafEntity) +from homeassistant.helpers.entity import ToggleEntity + +_LOGGER = logging.getLogger(__name__) + +DEPENDENCIES = ['nissan_leaf'] + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Nissan Leaf switch platform setup.""" + if discovery_info is None: + return + + devices = [] + for vin, datastore in hass.data[DATA_LEAF].items(): + _LOGGER.debug("Adding switch for vin=%s", vin) + devices.append(LeafClimateSwitch(datastore)) + + add_devices(devices, True) + + +class LeafClimateSwitch(LeafEntity, ToggleEntity): + """Nissan Leaf Climate Control switch.""" + + @property + def name(self): + """Switch name.""" + return "{} {}".format(self.car.leaf.nickname, "Climate Control") + + def log_registration(self): + """Log registration.""" + _LOGGER.debug( + "Registered LeafClimateSwitch component with HASS for VIN %s", + self.car.leaf.vin) + + @property + def device_state_attributes(self): + """Return climate control attributes.""" + attrs = super().device_state_attributes + attrs["updated_on"] = self.car.last_climate_response + return attrs + + @property + def is_on(self): + """Return true if climate control is on.""" + return self.car.data[DATA_CLIMATE] + + async def async_turn_on(self, **kwargs): + """Turn on climate control.""" + if await self.car.async_set_climate(True): + self.car.data[DATA_CLIMATE] = True + + async def async_turn_off(self, **kwargs): + """Turn off climate control.""" + if await self.car.async_set_climate(False): + self.car.data[DATA_CLIMATE] = False diff --git a/homeassistant/components/notify/kodi.py b/homeassistant/components/notify/kodi.py index 74bfe61d3f2f9..50d2246cd29c4 100644 --- a/homeassistant/components/notify/kodi.py +++ b/homeassistant/components/notify/kodi.py @@ -90,7 +90,7 @@ async def async_send_message(self, message="", **kwargs): try: data = kwargs.get(ATTR_DATA) or {} - displaytime = data.get(ATTR_DISPLAYTIME, 10000) + displaytime = int(data.get(ATTR_DISPLAYTIME, 10000)) icon = data.get(ATTR_ICON, "info") title = kwargs.get(ATTR_TITLE, ATTR_TITLE_DEFAULT) await self._server.GUI.ShowNotification( diff --git a/homeassistant/components/notify/nfandroidtv.py b/homeassistant/components/notify/nfandroidtv.py index faf5e90e0166c..f99d97574b4eb 100644 --- a/homeassistant/components/notify/nfandroidtv.py +++ b/homeassistant/components/notify/nfandroidtv.py @@ -248,7 +248,7 @@ def load_file(self, url=None, local_path=None, username=None, req = requests.get(url, timeout=DEFAULT_TIMEOUT) return req.content - elif local_path is not None: + if local_path is not None: # Check whether path is whitelisted in configuration.yaml if self.is_allowed_path(local_path): return open(local_path, "rb") diff --git a/homeassistant/components/notify/pushsafer.py b/homeassistant/components/notify/pushsafer.py index 443d56521c1e2..94dc08a811385 100644 --- a/homeassistant/components/notify/pushsafer.py +++ b/homeassistant/components/notify/pushsafer.py @@ -149,8 +149,7 @@ def load_from_url(self, url=None, username=None, password=None, auth=None): response = requests.get(url, timeout=CONF_TIMEOUT) return self.get_base64(response.content, response.headers['content-type']) - else: - _LOGGER.warning("url not found in param") + _LOGGER.warning("url not found in param") return None diff --git a/homeassistant/components/notify/slack.py b/homeassistant/components/notify/slack.py index 8e23c9f4fa0fd..961f671203ffd 100644 --- a/homeassistant/components/notify/slack.py +++ b/homeassistant/components/notify/slack.py @@ -152,7 +152,7 @@ def load_file(self, url=None, local_path=None, username=None, req = requests.get(url, timeout=CONF_TIMEOUT) return req.content - elif local_path: + if local_path: # Check whether path is whitelisted in configuration.yaml if self.is_allowed_path(local_path): return open(local_path, 'rb') diff --git a/homeassistant/components/opentherm_gw/climate.py b/homeassistant/components/opentherm_gw/climate.py index ff6acc1a8845d..584be4c0c6483 100644 --- a/homeassistant/components/opentherm_gw/climate.py +++ b/homeassistant/components/opentherm_gw/climate.py @@ -1,9 +1,9 @@ """Support for OpenTherm Gateway climate devices.""" import logging -from homeassistant.components.climate import (ClimateDevice, STATE_IDLE, - STATE_HEAT, STATE_COOL, - SUPPORT_TARGET_TEMPERATURE) +from homeassistant.components.climate import ClimateDevice +from homeassistant.components.climate.const import ( + STATE_IDLE, STATE_HEAT, STATE_COOL, SUPPORT_TARGET_TEMPERATURE) from homeassistant.components.opentherm_gw import ( CONF_FLOOR_TEMP, CONF_PRECISION, DATA_DEVICE, DATA_GW_VARS, DATA_OPENTHERM_GW, SIGNAL_OPENTHERM_GW_UPDATE) diff --git a/homeassistant/components/openuv/.translations/es-419.json b/homeassistant/components/openuv/.translations/es-419.json index 6b391c20a0a22..332a21f99f54d 100644 --- a/homeassistant/components/openuv/.translations/es-419.json +++ b/homeassistant/components/openuv/.translations/es-419.json @@ -7,11 +7,14 @@ "step": { "user": { "data": { + "api_key": "Clave API de OpenUV", "elevation": "Elevaci\u00f3n", "latitude": "Latitud", "longitude": "Longitud" - } + }, + "title": "Completa tu informaci\u00f3n" } - } + }, + "title": "OpenUV" } } \ No newline at end of file diff --git a/homeassistant/components/openuv/.translations/it.json b/homeassistant/components/openuv/.translations/it.json index a18d36693d5d1..82dfd63184ab1 100644 --- a/homeassistant/components/openuv/.translations/it.json +++ b/homeassistant/components/openuv/.translations/it.json @@ -1,15 +1,20 @@ { "config": { "error": { + "identifier_exists": "Coordinate gi\u00e0 registrate", "invalid_api_key": "Chiave API non valida" }, "step": { "user": { "data": { + "api_key": "API Key di OpenUV", + "elevation": "Altitudine", "latitude": "Latitudine", "longitude": "Logitudine" - } + }, + "title": "Inserisci i tuoi dati" } - } + }, + "title": "OpenUV" } } \ No newline at end of file diff --git a/homeassistant/components/owlet/__init__.py b/homeassistant/components/owlet/__init__.py new file mode 100644 index 0000000000000..b7ad7ab915240 --- /dev/null +++ b/homeassistant/components/owlet/__init__.py @@ -0,0 +1,71 @@ +"""Support for Owlet baby monitors.""" +import logging + +import voluptuous as vol + +from homeassistant.const import CONF_NAME, CONF_PASSWORD, CONF_USERNAME +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.discovery import load_platform + +from .const import ( + SENSOR_BASE_STATION, SENSOR_HEART_RATE, SENSOR_MOVEMENT, + SENSOR_OXYGEN_LEVEL) + +REQUIREMENTS = ['pyowlet==1.0.2'] + +_LOGGER = logging.getLogger(__name__) + +DOMAIN = 'owlet' + +SENSOR_TYPES = [ + SENSOR_OXYGEN_LEVEL, + SENSOR_HEART_RATE, + SENSOR_BASE_STATION, + SENSOR_MOVEMENT, +] + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Optional(CONF_NAME): cv.string, + }), +}, extra=vol.ALLOW_EXTRA) + + +def setup(hass, config): + """Set up owlet component.""" + from pyowlet.PyOwlet import PyOwlet + + username = config[DOMAIN][CONF_USERNAME] + password = config[DOMAIN][CONF_PASSWORD] + name = config[DOMAIN].get(CONF_NAME) + + try: + device = PyOwlet(username, password) + except KeyError: + _LOGGER.error("Owlet authentication failed. Please verify your " + "credentials are correct") + return False + + device.update_properties() + + if not name: + name = '{}\'s Owlet'.format(device.baby_name) + + hass.data[DOMAIN] = OwletDevice(device, name, SENSOR_TYPES) + + load_platform(hass, 'sensor', DOMAIN, {}, config) + load_platform(hass, 'binary_sensor', DOMAIN, {}, config) + + return True + + +class OwletDevice(): + """Represents a configured Owlet device.""" + + def __init__(self, device, name, monitor): + """Initialize device.""" + self.name = name + self.monitor = monitor + self.device = device diff --git a/homeassistant/components/owlet/binary_sensor.py b/homeassistant/components/owlet/binary_sensor.py new file mode 100644 index 0000000000000..cb66278150aea --- /dev/null +++ b/homeassistant/components/owlet/binary_sensor.py @@ -0,0 +1,82 @@ +"""Support for Owlet binary sensors.""" +from datetime import timedelta + +from homeassistant.components.binary_sensor import BinarySensorDevice +from homeassistant.components.owlet import DOMAIN as OWLET_DOMAIN +from homeassistant.util import dt as dt_util + +from .const import SENSOR_BASE_STATION, SENSOR_MOVEMENT + +SCAN_INTERVAL = timedelta(seconds=120) + +BINARY_CONDITIONS = { + SENSOR_BASE_STATION: { + 'name': 'Base Station', + 'device_class': 'power' + }, + SENSOR_MOVEMENT: { + 'name': 'Movement', + 'device_class': 'motion' + } +} + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up owlet binary sensor.""" + if discovery_info is None: + return + + device = hass.data[OWLET_DOMAIN] + + entities = [] + for condition in BINARY_CONDITIONS: + if condition in device.monitor: + entities.append(OwletBinarySensor(device, condition)) + + add_entities(entities, True) + + +class OwletBinarySensor(BinarySensorDevice): + """Representation of owlet binary sensor.""" + + def __init__(self, device, condition): + """Init owlet binary sensor.""" + self._device = device + self._condition = condition + self._state = None + self._base_on = False + self._prop_expiration = None + self._is_charging = None + + @property + def name(self): + """Return sensor name.""" + return '{} {}'.format(self._device.name, + BINARY_CONDITIONS[self._condition]['name']) + + @property + def is_on(self): + """Return current state of sensor.""" + return self._state + + @property + def device_class(self): + """Return the device class.""" + return BINARY_CONDITIONS[self._condition]['device_class'] + + def update(self): + """Update state of sensor.""" + self._base_on = self._device.device.base_station_on + self._prop_expiration = self._device.device.prop_expire_time + self._is_charging = self._device.device.charge_status > 0 + + # handle expired values + if self._prop_expiration < dt_util.now().timestamp(): + self._state = False + return + + if self._condition == 'movement': + if not self._base_on or self._is_charging: + return False + + self._state = getattr(self._device.device, self._condition) diff --git a/homeassistant/components/owlet/const.py b/homeassistant/components/owlet/const.py new file mode 100644 index 0000000000000..f8d4db3ec1e19 --- /dev/null +++ b/homeassistant/components/owlet/const.py @@ -0,0 +1,6 @@ +"""Constants for Owlet component.""" +SENSOR_OXYGEN_LEVEL = 'oxygen_level' +SENSOR_HEART_RATE = 'heart_rate' + +SENSOR_BASE_STATION = 'base_station_on' +SENSOR_MOVEMENT = 'movement' diff --git a/homeassistant/components/owlet/sensor.py b/homeassistant/components/owlet/sensor.py new file mode 100644 index 0000000000000..b91cc38771864 --- /dev/null +++ b/homeassistant/components/owlet/sensor.py @@ -0,0 +1,103 @@ +"""Support for Owlet sensors.""" +from datetime import timedelta + +from homeassistant.components.owlet import DOMAIN as OWLET_DOMAIN +from homeassistant.helpers.entity import Entity +from homeassistant.util import dt as dt_util + +from .const import SENSOR_HEART_RATE, SENSOR_OXYGEN_LEVEL + +SCAN_INTERVAL = timedelta(seconds=120) + +SENSOR_CONDITIONS = { + SENSOR_OXYGEN_LEVEL: { + 'name': 'Oxygen Level', + 'device_class': None + }, + SENSOR_HEART_RATE: { + 'name': 'Heart Rate', + 'device_class': None + } +} + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up owlet binary sensor.""" + if discovery_info is None: + return + + device = hass.data[OWLET_DOMAIN] + + entities = [] + for condition in SENSOR_CONDITIONS: + if condition in device.monitor: + entities.append(OwletSensor(device, condition)) + + add_entities(entities, True) + + +class OwletSensor(Entity): + """Representation of Owlet sensor.""" + + def __init__(self, device, condition): + """Init owlet binary sensor.""" + self._device = device + self._condition = condition + self._state = None + self._prop_expiration = None + self.is_charging = None + self.battery_level = None + self.sock_off = None + self.sock_connection = None + self._movement = None + + @property + def name(self): + """Return sensor name.""" + return '{} {}'.format(self._device.name, + SENSOR_CONDITIONS[self._condition]['name']) + + @property + def state(self): + """Return current state of sensor.""" + return self._state + + @property + def device_class(self): + """Return the device class.""" + return SENSOR_CONDITIONS[self._condition]['device_class'] + + @property + def device_state_attributes(self): + """Return state attributes.""" + attributes = { + 'battery_charging': self.is_charging, + 'battery_level': self.battery_level, + 'sock_off': self.sock_off, + 'sock_connection': self.sock_connection + } + + return attributes + + def update(self): + """Update state of sensor.""" + self.is_charging = self._device.device.charge_status + self.battery_level = self._device.device.batt_level + self.sock_off = self._device.device.sock_off + self.sock_connection = self._device.device.sock_connection + self._movement = self._device.device.movement + self._prop_expiration = self._device.device.prop_expire_time + + value = getattr(self._device.device, self._condition) + + if self._condition == 'batt_level': + self._state = min(100, value) + return + + if not self._device.device.base_station_on \ + or self._device.device.charge_status > 0 \ + or self._prop_expiration < dt_util.now().timestamp() \ + or self._movement: + value = None + + self._state = value diff --git a/homeassistant/components/owntracks/.translations/es-419.json b/homeassistant/components/owntracks/.translations/es-419.json new file mode 100644 index 0000000000000..f56cff977d022 --- /dev/null +++ b/homeassistant/components/owntracks/.translations/es-419.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "one_instance_allowed": "Solo una instancia es necesaria." + }, + "create_entry": { + "default": "\n\n En Android, abra [la aplicaci\u00f3n OwnTracks] ( {android_url} ), vaya a preferencias - > conexi\u00f3n. Cambia las siguientes configuraciones: \n - Modo: HTTP privado \n - Anfitri\u00f3n: {webhook_url} \n - Identificaci\u00f3n: \n - Nombre de usuario: ` ` \n - ID del dispositivo: ` ` \n\n En iOS, abra [la aplicaci\u00f3n OwnTracks] ( {ios_url} ), toque el icono (i) en la parte superior izquierda - > configuraci\u00f3n. Cambia las siguientes configuraciones: \n - Modo: HTTP \n - URL: {webhook_url} \n - Activar autenticaci\u00f3n \n - ID de usuario: ` ` \n\n {secret} \n \n Consulte [la documentaci\u00f3n] ( {docs_url} ) para obtener m\u00e1s informaci\u00f3n." + }, + "step": { + "user": { + "description": "\u00bfEst\u00e1s seguro de que quieres configurar OwnTracks?", + "title": "Configurar OwnTracks" + } + }, + "title": "OwnTracks" + } +} \ No newline at end of file diff --git a/homeassistant/components/owntracks/.translations/es.json b/homeassistant/components/owntracks/.translations/es.json new file mode 100644 index 0000000000000..f866aa6e4037b --- /dev/null +++ b/homeassistant/components/owntracks/.translations/es.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "one_instance_allowed": "Solo se necesita una instancia." + }, + "create_entry": { + "default": "\n\nEn Android, abra[la aplicaci\u00f3n OwnTracks]({android_url}), vaya a Preferencias -> Conexi\u00f3n. Cambie los siguientes ajustes:\n - Mode: HTTP privado\n - URL: {webhook_url}\n - Identificaci\u00f3n:\n - Nombre de usuario: \n - ID de dispositivo: \n\nEn iOS, abra[la aplicaci\u00f3n OwnTracks] ({ios_url}), toque el icono (i) en la parte superior izquierda -> configuraci\u00f3n. Cambie los siguientes ajustes:\n - Mode: HTTP\n - URL: {webhook_url}\n - Activar la autenticaci\u00f3n\n - UserID: \n\n{secret}\n\nConsulte[la documentaci\u00f3n]({docs_url}) para obtener m\u00e1s informaci\u00f3n." + }, + "step": { + "user": { + "description": "\u00bfEst\u00e1s seguro de que quieres configurar OwnTracks?", + "title": "Configurar OwnTracks" + } + }, + "title": "OwnTracks" + } +} \ No newline at end of file diff --git a/homeassistant/components/owntracks/.translations/it.json b/homeassistant/components/owntracks/.translations/it.json new file mode 100644 index 0000000000000..9b66b693c333a --- /dev/null +++ b/homeassistant/components/owntracks/.translations/it.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "one_instance_allowed": "\u00c8 necessaria una sola istanza." + }, + "step": { + "user": { + "description": "Sei sicuro di voler configurare OwnTracks?", + "title": "Configura OwnTracks" + } + }, + "title": "OwnTracks" + } +} \ No newline at end of file diff --git a/homeassistant/components/owntracks/.translations/ru.json b/homeassistant/components/owntracks/.translations/ru.json index bb9c7f39c5b14..6ebaa31cacf04 100644 --- a/homeassistant/components/owntracks/.translations/ru.json +++ b/homeassistant/components/owntracks/.translations/ru.json @@ -4,7 +4,7 @@ "one_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430." }, "create_entry": { - "default": "\u0415\u0441\u043b\u0438 \u0432\u0430\u0448\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442 \u043d\u0430 \u043e\u043f\u0435\u0440\u0430\u0446\u0438\u043e\u043d\u043d\u043e\u0439 \u0441\u0438\u0441\u0442\u0435\u043c\u0435 Android, \u043e\u0442\u043a\u0440\u043e\u0439\u0442\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435 [OwnTracks]({android_url}), \u0437\u0430\u0442\u0435\u043c preferences -> connection. \u0418\u0437\u043c\u0435\u043d\u0438\u0442\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u0442\u0430\u043a, \u043a\u0430\u043a \u0443\u043a\u0430\u0437\u0430\u043d\u043e \u043d\u0438\u0436\u0435:\n - Mode: Private HTTP\n - Host: {webhook_url}\n - Identification:\n - Username: ``\n - Device ID: ``\n\n\u0415\u0441\u043b\u0438 \u0432\u0430\u0448\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442 \u043d\u0430 iOS, \u043e\u0442\u043a\u0440\u043e\u0439\u0442\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435 [OwnTracks]({ios_url}), \u043d\u0430\u0436\u043c\u0438\u0442\u0435 \u043d\u0430 \u0437\u043d\u0430\u0447\u0435\u043a (i) \u0432 \u043b\u0435\u0432\u043e\u043c \u0432\u0435\u0440\u0445\u043d\u0435\u043c \u0443\u0433\u043b\u0443 -> settings. \u0418\u0437\u043c\u0435\u043d\u0438\u0442\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u0442\u0430\u043a, \u043a\u0430\u043a \u0443\u043a\u0430\u0437\u0430\u043d\u043e \u043d\u0438\u0436\u0435:\n - Mode: HTTP\n - URL: {webhook_url}\n - Turn on authentication\n - UserID: ``\n\n{secret}\n\n\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0435\u0439]({docs_url}) \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0431\u043e\u043b\u0435\u0435 \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u043e\u0439 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438." + "default": "\u0415\u0441\u043b\u0438 \u0412\u0430\u0448\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442 \u043d\u0430 \u043e\u043f\u0435\u0440\u0430\u0446\u0438\u043e\u043d\u043d\u043e\u0439 \u0441\u0438\u0441\u0442\u0435\u043c\u0435 Android, \u043e\u0442\u043a\u0440\u043e\u0439\u0442\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435 [OwnTracks]({android_url}), \u0437\u0430\u0442\u0435\u043c preferences -> connection. \u0418\u0437\u043c\u0435\u043d\u0438\u0442\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u0442\u0430\u043a, \u043a\u0430\u043a \u0443\u043a\u0430\u0437\u0430\u043d\u043e \u043d\u0438\u0436\u0435:\n - Mode: Private HTTP\n - Host: {webhook_url}\n - Identification:\n - Username: ``\n - Device ID: ``\n\n\u0415\u0441\u043b\u0438 \u0412\u0430\u0448\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442 \u043d\u0430 iOS, \u043e\u0442\u043a\u0440\u043e\u0439\u0442\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435 [OwnTracks]({ios_url}), \u043d\u0430\u0436\u043c\u0438\u0442\u0435 \u043d\u0430 \u0437\u043d\u0430\u0447\u0435\u043a (i) \u0432 \u043b\u0435\u0432\u043e\u043c \u0432\u0435\u0440\u0445\u043d\u0435\u043c \u0443\u0433\u043b\u0443 -> settings. \u0418\u0437\u043c\u0435\u043d\u0438\u0442\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u0442\u0430\u043a, \u043a\u0430\u043a \u0443\u043a\u0430\u0437\u0430\u043d\u043e \u043d\u0438\u0436\u0435:\n - Mode: HTTP\n - URL: {webhook_url}\n - Turn on authentication\n - UserID: ``\n\n{secret}\n\n\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0435\u0439]({docs_url}) \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0431\u043e\u043b\u0435\u0435 \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u043e\u0439 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438." }, "step": { "user": { diff --git a/homeassistant/components/owntracks/.translations/sv.json b/homeassistant/components/owntracks/.translations/sv.json new file mode 100644 index 0000000000000..2077cceeb4d4f --- /dev/null +++ b/homeassistant/components/owntracks/.translations/sv.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "one_instance_allowed": "Endast en enda instans \u00e4r n\u00f6dv\u00e4ndig." + }, + "create_entry": { + "default": "\n\n P\u00e5 Android, \u00f6ppna [OwnTracks-appen]({android_url}), g\u00e5 till inst\u00e4llningar -> anslutning. \u00c4ndra f\u00f6ljande inst\u00e4llningar: \n - L\u00e4ge: Privat HTTP \n - V\u00e4rden: {webhook_url}\n - Identifiering: \n - Anv\u00e4ndarnamn: ``\n - Enhets-ID: `` \n\n P\u00e5 IOS, \u00f6ppna [OwnTracks-appen]({ios_url}), tryck p\u00e5 (i) ikonen i \u00f6vre v\u00e4nstra h\u00f6rnet -> inst\u00e4llningarna. \u00c4ndra f\u00f6ljande inst\u00e4llningar: \n - L\u00e4ge: HTTP \n - URL: {webhook_url}\n - Sl\u00e5 p\u00e5 autentisering \n - UserID: `` \n\n {secret} \n \n Se [dokumentationen]({docs_url}) f\u00f6r mer information." + }, + "step": { + "user": { + "description": "\u00c4r du s\u00e4ker p\u00e5 att du vill konfigurera OwnTracks?", + "title": "Konfigurera OwnTracks" + } + }, + "title": "OwnTracks" + } +} \ No newline at end of file diff --git a/homeassistant/components/owntracks/__init__.py b/homeassistant/components/owntracks/__init__.py index cc918dcf674e6..c0d3d152270a3 100644 --- a/homeassistant/components/owntracks/__init__.py +++ b/homeassistant/components/owntracks/__init__.py @@ -16,7 +16,7 @@ from .config_flow import CONF_SECRET -REQUIREMENTS = ['libnacl==1.6.1'] +REQUIREMENTS = ['PyNaCl==1.3.0'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/owntracks/config_flow.py b/homeassistant/components/owntracks/config_flow.py index 6818efbbf7575..59e8c4825dfea 100644 --- a/homeassistant/components/owntracks/config_flow.py +++ b/homeassistant/components/owntracks/config_flow.py @@ -9,7 +9,7 @@ def supports_encryption(): """Test if we support encryption.""" try: - import libnacl # noqa pylint: disable=unused-import + import nacl # noqa pylint: disable=unused-import return True except OSError: return False diff --git a/homeassistant/components/owntracks/device_tracker.py b/homeassistant/components/owntracks/device_tracker.py index e85ebbe6fe1ff..be8698a47b1a7 100644 --- a/homeassistant/components/owntracks/device_tracker.py +++ b/homeassistant/components/owntracks/device_tracker.py @@ -4,7 +4,6 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/device_tracker.owntracks/ """ -import base64 import json import logging @@ -37,13 +36,13 @@ def get_cipher(): Async friendly. """ - from libnacl import crypto_secretbox_KEYBYTES as KEYLEN - from libnacl.secret import SecretBox + from nacl.secret import SecretBox + from nacl.encoding import Base64Encoder def decrypt(ciphertext, key): """Decrypt ciphertext using key.""" - return SecretBox(key).decrypt(ciphertext) - return (KEYLEN, decrypt) + return SecretBox(key).decrypt(ciphertext, encoder=Base64Encoder) + return (SecretBox.KEY_SIZE, decrypt) def _parse_topic(topic, subscribe_topic): @@ -141,7 +140,6 @@ def _decrypt_payload(secret, topic, ciphertext): key = key.ljust(keylen, b'\0') try: - ciphertext = base64.b64decode(ciphertext) message = decrypt(ciphertext, key) message = message.decode("utf-8") _LOGGER.debug("Decrypted payload: %s", message) diff --git a/homeassistant/components/panel_custom/__init__.py b/homeassistant/components/panel_custom/__init__.py index f6602169eb21f..2fce5d9857c99 100644 --- a/homeassistant/components/panel_custom/__init__.py +++ b/homeassistant/components/panel_custom/__init__.py @@ -81,7 +81,7 @@ async def async_register_panel( """Register a new custom panel.""" if js_url is None and html_url is None and module_url is None: raise ValueError('Either js_url, module_url or html_url is required.') - elif (js_url and html_url) or (module_url and html_url): + if (js_url and html_url) or (module_url and html_url): raise ValueError('Pass in only one of JS url, Module url or HTML url.') if config is not None and not isinstance(config, dict): diff --git a/homeassistant/components/person/__init__.py b/homeassistant/components/person/__init__.py index f2bca91205c2e..e6f83b80ba418 100644 --- a/homeassistant/components/person/__init__.py +++ b/homeassistant/components/person/__init__.py @@ -2,17 +2,19 @@ from collections import OrderedDict from itertools import chain import logging +from typing import Optional import uuid import voluptuous as vol from homeassistant.components import websocket_api from homeassistant.components.device_tracker import ( - DOMAIN as DEVICE_TRACKER_DOMAIN) + DOMAIN as DEVICE_TRACKER_DOMAIN, ATTR_SOURCE_TYPE, SOURCE_TYPE_GPS) from homeassistant.const import ( - ATTR_ID, ATTR_LATITUDE, ATTR_LONGITUDE, CONF_ID, CONF_NAME, - EVENT_HOMEASSISTANT_START, STATE_UNKNOWN, STATE_UNAVAILABLE) -from homeassistant.core import callback, Event + ATTR_ID, ATTR_LATITUDE, ATTR_LONGITUDE, ATTR_GPS_ACCURACY, + CONF_ID, CONF_NAME, EVENT_HOMEASSISTANT_START, + STATE_UNKNOWN, STATE_UNAVAILABLE, STATE_HOME, STATE_NOT_HOME) +from homeassistant.core import callback, Event, State from homeassistant.auth import EVENT_USER_REMOVED import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_component import EntityComponent @@ -285,6 +287,7 @@ def __init__(self, config, editable): self._editable = editable self._latitude = None self._longitude = None + self._gps_accuracy = None self._source = None self._state = None self._unsub_track_device = None @@ -315,9 +318,11 @@ def state_attributes(self): ATTR_ID: self.unique_id, } if self._latitude is not None: - data[ATTR_LATITUDE] = round(self._latitude, 5) + data[ATTR_LATITUDE] = self._latitude if self._longitude is not None: - data[ATTR_LONGITUDE] = round(self._longitude, 5) + data[ATTR_LONGITUDE] = self._longitude + if self._gps_accuracy is not None: + data[ATTR_GPS_ACCURACY] = self._gps_accuracy if self._source is not None: data[ATTR_SOURCE] = self._source user_id = self._config.get(CONF_USER_ID) @@ -376,15 +381,26 @@ def _async_handle_tracker_update(self, entity, old_state, new_state): @callback def _update_state(self): """Update the state.""" - latest = None + latest_non_gps_home = latest_not_home = latest_gps = latest = None for entity_id in self._config.get(CONF_DEVICE_TRACKERS, []): state = self.hass.states.get(entity_id) if not state or state.state in IGNORE_STATES: continue - if latest is None or state.last_updated > latest.last_updated: - latest = state + if state.attributes.get(ATTR_SOURCE_TYPE) == SOURCE_TYPE_GPS: + latest_gps = _get_latest(latest_gps, state) + elif state.state == STATE_HOME: + latest_non_gps_home = _get_latest(latest_non_gps_home, state) + elif state.state == STATE_NOT_HOME: + latest_not_home = _get_latest(latest_not_home, state) + + if latest_non_gps_home: + latest = latest_non_gps_home + elif latest_gps: + latest = latest_gps + else: + latest = latest_not_home if latest: self._parse_source_state(latest) @@ -393,6 +409,7 @@ def _update_state(self): self._source = None self._latitude = None self._longitude = None + self._gps_accuracy = None self.async_schedule_update_ha_state() @@ -406,6 +423,7 @@ def _parse_source_state(self, state): self._source = state.entity_id self._latitude = state.attributes.get(ATTR_LATITUDE) self._longitude = state.attributes.get(ATTR_LONGITUDE) + self._gps_accuracy = state.attributes.get(ATTR_GPS_ACCURACY) @websocket_api.websocket_command({ @@ -486,3 +504,10 @@ async def ws_delete_person(hass: HomeAssistantType, manager = hass.data[DOMAIN] # type: PersonManager await manager.async_delete_person(msg['person_id']) connection.send_result(msg['id']) + + +def _get_latest(prev: Optional[State], curr: State): + """Get latest state.""" + if prev is None or curr.last_updated > prev.last_updated: + return curr + return prev diff --git a/homeassistant/components/point/.translations/ca.json b/homeassistant/components/point/.translations/ca.json index 6a66735e6d094..b50a1169a53a1 100644 --- a/homeassistant/components/point/.translations/ca.json +++ b/homeassistant/components/point/.translations/ca.json @@ -5,7 +5,7 @@ "authorize_url_fail": "S'ha produ\u00eft un error desconegut al generar l'URL d'autoritzaci\u00f3.", "authorize_url_timeout": "S'ha acabat el temps d'espera mentre \u00e9s generava l'URL d'autoritzaci\u00f3.", "external_setup": "Point s'ha configurat correctament des d'un altre lloc.", - "no_flows": "Necessites configurar Point abans de poder autenticar-t'hi. [Llegiu les instruccions](https://www.home-assistant.io/components/point/)." + "no_flows": "Necessites configurar Point abans de poder autenticar-t'hi. Llegeix les [instruccions](https://www.home-assistant.io/components/point/)." }, "create_entry": { "default": "Autenticaci\u00f3 exitosa amb Minut per als teus dispositiu/s Point." diff --git a/homeassistant/components/point/.translations/es-419.json b/homeassistant/components/point/.translations/es-419.json new file mode 100644 index 0000000000000..c20e3350272d8 --- /dev/null +++ b/homeassistant/components/point/.translations/es-419.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "external_setup": "Punto configurado con \u00e9xito desde otro flujo." + }, + "error": { + "follow_link": "Por favor, siga el enlace y autent\u00edquese antes de presionar Enviar", + "no_token": "No autenticado con Minut" + }, + "step": { + "auth": { + "description": "Siga el enlace a continuaci\u00f3n y Aceptar acceso a su cuenta de Minut, luego vuelva y presione Enviar continuaci\u00f3n. \n\n [Enlace] ( {authorization_url} )" + }, + "user": { + "data": { + "flow_impl": "Proveedor" + }, + "description": "Elija a trav\u00e9s de qu\u00e9 proveedor de autenticaci\u00f3n desea autenticarse con Point.", + "title": "Proveedor de autenticaci\u00f3n" + } + }, + "title": "Minut Point" + } +} \ No newline at end of file diff --git a/homeassistant/components/point/.translations/es.json b/homeassistant/components/point/.translations/es.json new file mode 100644 index 0000000000000..815f8fbf9afab --- /dev/null +++ b/homeassistant/components/point/.translations/es.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "already_setup": "S\u00f3lo se puede configurar una cuenta de Point." + }, + "step": { + "user": { + "data": { + "flow_impl": "Proveedor" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/point/.translations/hu.json b/homeassistant/components/point/.translations/hu.json index 2d52069d5ba48..3192454550dc0 100644 --- a/homeassistant/components/point/.translations/hu.json +++ b/homeassistant/components/point/.translations/hu.json @@ -1,7 +1,16 @@ { "config": { + "abort": { + "authorize_url_fail": "Ismeretlen hiba t\u00f6rt\u00e9nt a hiteles\u00edt\u00e9si link gener\u00e1l\u00e1sa sor\u00e1n.", + "authorize_url_timeout": "Id\u0151t\u00fall\u00e9p\u00e9s az \u00e9rv\u00e9nyes\u00edt\u00e9si url gener\u00e1l\u00e1sa sor\u00e1n." + }, + "error": { + "follow_link": "K\u00e9rlek, k\u00f6vesd a hivatkoz\u00e1st \u00e9s hiteles\u00edtsd magad miel\u0151tt megnyomod a K\u00fcld\u00e9s gombot", + "no_token": "A Minut nincs hiteles\u00edtve" + }, "step": { "auth": { + "description": "K\u00e9rlek k\u00f6vesd az al\u00e1bbi linket \u00e9s a Fogadd el a hozz\u00e1f\u00e9r\u00e9st a Minut fi\u00f3kj\u00e1hoz, majd t\u00e9rj vissza \u00e9s nyomd meg a K\u00fcld\u00e9s gombot. \n\n [Link] ( {authorization_url} )", "title": "Point hiteles\u00edt\u00e9se" }, "user": { diff --git a/homeassistant/components/point/.translations/it.json b/homeassistant/components/point/.translations/it.json index 00e2cb02358f7..324801009ca5a 100644 --- a/homeassistant/components/point/.translations/it.json +++ b/homeassistant/components/point/.translations/it.json @@ -1,12 +1,31 @@ { "config": { + "abort": { + "already_setup": "\u00c8 possibile configurare un solo account Point.", + "authorize_url_fail": "Errore sconosciuto nel generare l'url di autorizzazione", + "authorize_url_timeout": "Tempo scaduto nel generare l'url di autorizzazione", + "external_setup": "Point configurato correttamente da un altro flusso.", + "no_flows": "Devi configurare Point prima di poter eseguire l'autenticazione. [Si prega di leggere le istruzioni] (https://www.home-assistant.io/components/point/)." + }, + "create_entry": { + "default": "Autenticato con successo con Minut per i tuoi dispositivi Point" + }, + "error": { + "follow_link": "Segui il link e autenticati prima di premere Invio", + "no_token": "Non autenticato con Minut" + }, "step": { + "auth": { + "title": "Autenticare Point" + }, "user": { "data": { "flow_impl": "Provider" }, + "description": "Scegli tramite quale provider di autenticazione vuoi autenticarti con Point.", "title": "Provider di autenticazione" } - } + }, + "title": "Minut Point" } } \ No newline at end of file diff --git a/homeassistant/components/point/.translations/ru.json b/homeassistant/components/point/.translations/ru.json index 7bcf275f96ed2..60c1d62ab911b 100644 --- a/homeassistant/components/point/.translations/ru.json +++ b/homeassistant/components/point/.translations/ru.json @@ -23,7 +23,7 @@ "data": { "flow_impl": "\u041f\u0440\u043e\u0432\u0430\u0439\u0434\u0435\u0440" }, - "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435, \u0447\u0435\u0440\u0435\u0437 \u043a\u0430\u043a\u043e\u0433\u043e \u043f\u0440\u043e\u0432\u0430\u0439\u0434\u0435\u0440\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438 \u0432\u044b \u0445\u043e\u0442\u0438\u0442\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0438\u0442\u044c \u0432\u0445\u043e\u0434 \u0432 Point.", + "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435, \u0447\u0435\u0440\u0435\u0437 \u043a\u0430\u043a\u043e\u0433\u043e \u043f\u0440\u043e\u0432\u0430\u0439\u0434\u0435\u0440\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438 \u0412\u044b \u0445\u043e\u0442\u0438\u0442\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0438\u0442\u044c \u0432\u0445\u043e\u0434 \u0432 Point.", "title": "\u041f\u0440\u043e\u0432\u0430\u0439\u0434\u0435\u0440 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438" } }, diff --git a/homeassistant/components/point/.translations/sv.json b/homeassistant/components/point/.translations/sv.json index 6464434eda4d2..c68fd29f7fcb9 100644 --- a/homeassistant/components/point/.translations/sv.json +++ b/homeassistant/components/point/.translations/sv.json @@ -1,33 +1,32 @@ { - "config": { - "title": "Minut Point", - "step": { - "user": { - "title": "Autentiseringsleverant\u00f6r", - "description": "V\u00e4lj den autentiseringsleverant\u00f6r som du vill autentisera med mot Point.", - "data": { - "flow_impl": "Leverant\u00f6r" - } - }, - "auth": { - "title": "Autentisera Point", - "description": "F\u00f6lj l\u00e4nken nedan och klicka p\u00e5 Accept f\u00f6r att tilll\u00e5ta tillg\u00e5ng till ditt Minut konto, kom d\u00f6refter tillbaka hit och kicka p\u00e5 Submit nedan.\n\n[L\u00e4nk]({authorization_url})" - } - }, - "create_entry": { - "default": "Autentiserad med Minut f\u00f6r era Point enheter." - }, - "error": { - "no_token": "Inte autentiserad hos Minut", - "follow_link": "F\u00f6lj l\u00e4nken och autentisera innan du kickar på Submit" - }, - "abort": { - "already_setup": "Du kan endast konfigurera ett Point-konto.", - "external_setup": "Point har lyckats konfigureras fr\u00e5n ett annat fl\u00f6de.", - "no_flows": "Du m\u00e5ste konfigurera Nest innan du kan autentisera med det. [V\u00e4nligen l\u00e4s instruktionerna] (https://www.home-assistant.io/components/point/).", - "authorize_url_timeout": "Timeout vid generering av en autentisieringsadress.", - "authorize_url_fail": "Ok\u00e4nt fel vid generering av autentisieringsadress." + "config": { + "abort": { + "already_setup": "Du kan endast konfigurera ett Point-konto.", + "authorize_url_fail": "Ok\u00e4nt fel n\u00e4r f\u00f6rs\u00f6ker generera en url f\u00f6r auktorisering.", + "authorize_url_timeout": "Timeout n\u00e4r genererar url f\u00f6r auktorisering.", + "external_setup": "Point har lyckats med konfigurering ifr\u00e5n ett annat fl\u00f6de.", + "no_flows": "Du beh\u00f6ver konfigurera Point innan de kan autentisera med den. [L\u00e4s instruktioner](https://www.home-assistant.io/components/point/)." + }, + "create_entry": { + "default": "Lyckad autentisering med Minut f\u00f6r din(a) Point-enhet(er)" + }, + "error": { + "follow_link": "F\u00f6lj l\u00e4nken och autentisera innan du trycker p\u00e5 Skicka", + "no_token": "Ej autentiserad med Minut" + }, + "step": { + "auth": { + "description": "V\u00e4nligen f\u00f6lj l\u00e4nken nedan och Acceptera tillg\u00e5ng till ditt Minut-konto, kom tillbaka och tryck p\u00e5 Skicka nedan. \n\n [L\u00e4nk]({authorization_url})", + "title": "Autentisera Point" + }, + "user": { + "data": { + "flow_impl": "Leverant\u00f6r" + }, + "description": "V\u00e4lj via vilken autentiseringsleverant\u00f6r du vill autentisera med Point.", + "title": "Autentiseringsleverant\u00f6r" + } + }, + "title": "Minut Point" } - } -} - \ No newline at end of file +} \ No newline at end of file diff --git a/homeassistant/components/point/__init__.py b/homeassistant/components/point/__init__.py index f223ded998f1a..dc839756469dd 100644 --- a/homeassistant/components/point/__init__.py +++ b/homeassistant/components/point/__init__.py @@ -20,7 +20,7 @@ CONF_WEBHOOK_URL, DOMAIN, EVENT_RECEIVED, POINT_DISCOVERY_NEW, SCAN_INTERVAL, SIGNAL_UPDATE_ENTITY, SIGNAL_WEBHOOK) -REQUIREMENTS = ['pypoint==1.0.8'] +REQUIREMENTS = ['pypoint==1.1.1'] _LOGGER = logging.getLogger(__name__) @@ -159,6 +159,7 @@ def __init__(self, hass: HomeAssistantType, config_entry: ConfigEntry, session): """Initialize the Minut data object.""" self._known_devices = set() + self._known_homes = set() self._hass = hass self._config_entry = config_entry self._is_available = True @@ -194,6 +195,10 @@ async def new_device(device_id, component): device_id) self._is_available = True + for home_id in self._client.homes: + if home_id not in self._known_homes: + await new_device(home_id, 'alarm_control_panel') + self._known_homes.add(home_id) for device in self._client.devices: if device.device_id not in self._known_devices: for component in ('sensor', 'binary_sensor'): @@ -213,6 +218,19 @@ def remove_webhook(self): """Remove the session webhook.""" return self._client.remove_webhook() + @property + def homes(self): + """Return known homes.""" + return self._client.homes + + def alarm_disarm(self, home_id): + """Send alarm disarm command.""" + return self._client.alarm_disarm(home_id) + + def alarm_arm(self, home_id): + """Send alarm arm command.""" + return self._client.alarm_arm(home_id) + class MinutPointEntity(Entity): """Base Entity used by the sensors.""" @@ -286,6 +304,7 @@ def device_info(self): 'model': 'Point v{}'.format(device['hardware_version']), 'name': device['description'], 'sw_version': device['firmware']['installed'], + 'via_hub': (DOMAIN, device['home']), } @property diff --git a/homeassistant/components/point/alarm_control_panel.py b/homeassistant/components/point/alarm_control_panel.py new file mode 100644 index 0000000000000..a50dffe42b9a8 --- /dev/null +++ b/homeassistant/components/point/alarm_control_panel.py @@ -0,0 +1,116 @@ +"""Support for Minut Point.""" +import logging + +from homeassistant.components.alarm_control_panel import ( + DOMAIN, AlarmControlPanel) +from homeassistant.components.point.const import ( + DOMAIN as POINT_DOMAIN, POINT_DISCOVERY_NEW, SIGNAL_WEBHOOK) +from homeassistant.const import ( + STATE_ALARM_ARMED_AWAY, STATE_ALARM_DISARMED, STATE_ALARM_TRIGGERED) +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect + +_LOGGER = logging.getLogger(__name__) + + +EVENT_MAP = { + 'off': STATE_ALARM_DISARMED, + 'alarm_silenced': STATE_ALARM_ARMED_AWAY, + 'alarm_grace_period_expired': STATE_ALARM_TRIGGERED, +} + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up a Point's alarm_control_panel based on a config entry.""" + async def async_discover_home(home_id): + """Discover and add a discovered home.""" + client = hass.data[POINT_DOMAIN][config_entry.entry_id] + async_add_entities([MinutPointAlarmControl(client, home_id)], True) + + async_dispatcher_connect( + hass, POINT_DISCOVERY_NEW.format(DOMAIN, POINT_DOMAIN), + async_discover_home) + + +class MinutPointAlarmControl(AlarmControlPanel): + """The platform class required by Home Assistant.""" + + def __init__(self, point_client, home_id): + """Initialize the entity.""" + self._client = point_client + self._home_id = home_id + self._async_unsub_hook_dispatcher_connect = None + self._changed_by = None + + async def async_added_to_hass(self): + """Call when entity is added to HOme Assistant.""" + await super().async_added_to_hass() + self._async_unsub_hook_dispatcher_connect = async_dispatcher_connect( + self.hass, SIGNAL_WEBHOOK, self._webhook_event) + + async def async_will_remove_from_hass(self): + """Disconnect dispatcher listener when removed.""" + await super().async_will_remove_from_hass() + if self._async_unsub_hook_dispatcher_connect: + self._async_unsub_hook_dispatcher_connect() + + @callback + def _webhook_event(self, data, webhook): + """Process new event from the webhook.""" + _type = data.get('event', {}).get('type') + _device_id = data.get('event', {}).get('device_id') + if _device_id not in self._home['devices'] or _type not in EVENT_MAP: + return + _LOGGER.debug("Recieved webhook: %s", _type) + self._home['alarm_status'] = EVENT_MAP[_type] + self._changed_by = _device_id + self.async_schedule_update_ha_state() + + @property + def _home(self): + """Return the home object.""" + return self._client.homes[self._home_id] + + @property + def name(self): + """Return name of the device.""" + return self._home['name'] + + @property + def state(self): + """Return state of the device.""" + return EVENT_MAP.get( + self._home['alarm_status'], + STATE_ALARM_ARMED_AWAY, + ) + + @property + def changed_by(self): + """Return the user the last change was triggered by.""" + return self._changed_by + + def alarm_disarm(self, code=None): + """Send disarm command.""" + status = self._client.alarm_disarm(self._home_id) + if status: + self._home['alarm_status'] = 'off' + + def alarm_arm_away(self, code=None): + """Send arm away command.""" + status = self._client.alarm_arm(self._home_id) + if status: + self._home['alarm_status'] = 'on' + + @property + def unique_id(self): + """Return the unique id of the sensor.""" + return 'point.{}'.format(self._home_id) + + @property + def device_info(self): + """Return a device description for device registry.""" + return { + 'identifiers': {(POINT_DOMAIN, self._home_id)}, + 'name': self.name, + 'manufacturer': 'Minut', + } diff --git a/homeassistant/components/prometheus/__init__.py b/homeassistant/components/prometheus/__init__.py index 4508611e51b46..9053a87213405 100644 --- a/homeassistant/components/prometheus/__init__.py +++ b/homeassistant/components/prometheus/__init__.py @@ -5,7 +5,7 @@ import voluptuous as vol from homeassistant import core as hacore -from homeassistant.components.climate import ATTR_CURRENT_TEMPERATURE +from homeassistant.components.climate.const import ATTR_CURRENT_TEMPERATURE from homeassistant.components.http import HomeAssistantView from homeassistant.const import ( ATTR_TEMPERATURE, ATTR_UNIT_OF_MEASUREMENT, CONTENT_TYPE_TEXT_PLAIN, @@ -151,6 +151,15 @@ def _handle_device_tracker(self, state): value = state_helper.state_as_number(state) metric.labels(**self._labels(state)).set(value) + def _handle_person(self, state): + metric = self._metric( + 'person_state', + self.prometheus_client.Gauge, + 'State of the person (0/1)', + ) + value = state_helper.state_as_number(state) + metric.labels(**self._labels(state)).set(value) + def _handle_light(self, state): metric = self._metric( 'light_state', diff --git a/homeassistant/components/ps4/.translations/ca.json b/homeassistant/components/ps4/.translations/ca.json new file mode 100644 index 0000000000000..350b65ca815d4 --- /dev/null +++ b/homeassistant/components/ps4/.translations/ca.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "credential_error": "Error en l'obtenci\u00f3 de les credencials.", + "devices_configured": "Tots els dispositius trobats ja estan configurats.", + "no_devices_found": "No s'han trobat dispositius PlayStation 4 a la xarxa.", + "port_987_bind_error": "No s'ha pogut vincular amb el port 987.", + "port_997_bind_error": "No s'ha pogut vincular amb el port 997." + }, + "error": { + "login_failed": "No s'ha pogut sincronitzar amb la PlayStation 4. Verifica el codi PIN.", + "not_ready": "La PlayStation 4 no est\u00e0 engegada o no s'ha connectada a la xarxa." + }, + "step": { + "creds": { + "description": "Credencials necess\u00e0ries. Prem 'Enviar' i, a continuaci\u00f3, a la segona pantalla de l'aplicaci\u00f3 de la PS4, actualitza els dispositius i selecciona 'Home-Assistant' per continuar.", + "title": "PlayStation 4" + }, + "link": { + "data": { + "code": "PIN", + "ip_address": "Adre\u00e7a IP", + "name": "Nom", + "region": "Regi\u00f3" + }, + "description": "Introdueix la informaci\u00f3 de la teva PlayStation 4. Pel 'PIN', ves a 'Configuraci\u00f3' de la PlayStation 4, despr\u00e9s navega fins a 'Configuraci\u00f3 de la connexi\u00f3 de l'aplicaci\u00f3 m\u00f2bil' i selecciona 'Afegir dispositiu'. Introdueix el PIN que es mostra.", + "title": "PlayStation 4" + } + }, + "title": "PlayStation 4" + } +} \ No newline at end of file diff --git a/homeassistant/components/ps4/.translations/cs.json b/homeassistant/components/ps4/.translations/cs.json new file mode 100644 index 0000000000000..5c4e67a324cfe --- /dev/null +++ b/homeassistant/components/ps4/.translations/cs.json @@ -0,0 +1,13 @@ +{ + "config": { + "step": { + "link": { + "data": { + "region": "Region" + }, + "title": "PlayStation 4" + } + }, + "title": "PlayStation 4" + } +} \ No newline at end of file diff --git a/homeassistant/components/ps4/.translations/da.json b/homeassistant/components/ps4/.translations/da.json new file mode 100644 index 0000000000000..7c5f9e7621cd7 --- /dev/null +++ b/homeassistant/components/ps4/.translations/da.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "credential_error": "Fejl ved hentning af legitimationsoplysninger.", + "devices_configured": "Alle de fundne enheder er allerede konfigureret.", + "no_devices_found": "Ingen PlayStation 4 enheder fundet p\u00e5 netv\u00e6rket.", + "port_987_bind_error": "Kunne ikke binde til port 987.", + "port_997_bind_error": "Kunne ikke binde til port 997." + }, + "error": { + "login_failed": "Kunne ikke parre med PlayStation 4. Kontroller PIN er korrekt.", + "not_ready": "PlayStation 4 er ikke t\u00e6ndt eller tilsluttet til netv\u00e6rket." + }, + "step": { + "creds": { + "description": "Legitimationsoplysninger er n\u00f8dvendige. Tryk p\u00e5 'Send' og derefter i PS4 2nd Screen App, v\u00e6lg opdater enheder og v\u00e6lg 'Home-Assistant' -enheden for at forts\u00e6tte.", + "title": "PlayStation 4" + }, + "link": { + "data": { + "code": "PIN", + "ip_address": "IP-adresse", + "name": "Navn", + "region": "Omr\u00e5de" + }, + "description": "Indtast dine PlayStation 4 oplysninger. For 'PIN' skal du navigere til 'Indstillinger' p\u00e5 din PlayStation 4 konsol. G\u00e5 derefter til 'Indstillinger for mobilapp-forbindelse' og v\u00e6lg 'Tilf\u00f8j enhed'. Indtast den PIN der vises.", + "title": "PlayStation 4" + } + }, + "title": "PlayStation 4" + } +} \ No newline at end of file diff --git a/homeassistant/components/ps4/.translations/de.json b/homeassistant/components/ps4/.translations/de.json new file mode 100644 index 0000000000000..8f4e883867317 --- /dev/null +++ b/homeassistant/components/ps4/.translations/de.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "credential_error": "Fehler beim Abrufen der Anmeldeinformationen.", + "devices_configured": "Alle gefundenen Ger\u00e4te sind bereits konfiguriert.", + "no_devices_found": "Es wurden keine PlayStation 4 im Netzwerk gefunden.", + "port_987_bind_error": "Bind to Port 987 nicht m\u00f6glich.", + "port_997_bind_error": "Bind to Port 997 nicht m\u00f6glich." + }, + "error": { + "login_failed": "Fehler beim Koppeln mit PlayStation 4. \u00dcberpr\u00fcfe, ob die PIN korrekt ist.", + "not_ready": "PlayStation 4 ist nicht eingeschaltet oder mit dem Netzwerk verbunden." + }, + "step": { + "creds": { + "description": "Anmeldeinformationen ben\u00f6tigt. Klicke auf \"Senden\" und dann in der PS4 Second Screen app, aktualisiere die Ger\u00e4te und w\u00e4hle das \"Home-Assistant\"-Ger\u00e4t aus, um fortzufahren.", + "title": "PlayStation 4" + }, + "link": { + "data": { + "code": "PIN", + "ip_address": "IP-Adresse", + "name": "Name", + "region": "Region" + }, + "description": "Geben Sie Ihre PlayStation 4-Informationen ein. Navigiere f\u00fcr \"PIN\" auf der PlayStation 4-Konsole zu \"Einstellungen\". Navigiere dann zu \"Mobile App-Verbindungseinstellungen\" und w\u00e4hle \"Ger\u00e4t hinzuf\u00fcgen\" aus. Gib die angezeigte PIN ein.", + "title": "PlayStation 4" + } + }, + "title": "PlayStation 4" + } +} \ No newline at end of file diff --git a/homeassistant/components/ps4/.translations/en.json b/homeassistant/components/ps4/.translations/en.json new file mode 100644 index 0000000000000..c0b476ff4e2aa --- /dev/null +++ b/homeassistant/components/ps4/.translations/en.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "credential_error": "Error fetching credentials.", + "devices_configured": "All devices found are already configured.", + "no_devices_found": "No PlayStation 4 devices found on the network.", + "port_987_bind_error": "Could not bind to port 987.", + "port_997_bind_error": "Could not bind to port 997." + }, + "error": { + "login_failed": "Failed to pair to PlayStation 4. Verify PIN is correct.", + "not_ready": "PlayStation 4 is not on or connected to network." + }, + "step": { + "creds": { + "description": "Credentials needed. Press 'Submit' and then in the PS4 2nd Screen App, refresh devices and select the 'Home-Assistant' device to continue.", + "title": "PlayStation 4" + }, + "link": { + "data": { + "code": "PIN", + "ip_address": "IP Address", + "name": "Name", + "region": "Region" + }, + "description": "Enter your PlayStation 4 information. For 'PIN', navigate to 'Settings' on your PlayStation 4 console. Then navigate to 'Mobile App Connection Settings' and select 'Add Device'. Enter the PIN that is displayed.", + "title": "PlayStation 4" + } + }, + "title": "PlayStation 4" + } +} \ No newline at end of file diff --git a/homeassistant/components/ps4/.translations/es-419.json b/homeassistant/components/ps4/.translations/es-419.json new file mode 100644 index 0000000000000..093ee55295178 --- /dev/null +++ b/homeassistant/components/ps4/.translations/es-419.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "credential_error": "Error al obtener las credenciales.", + "devices_configured": "Todos los dispositivos encontrados ya est\u00e1n configurados.", + "no_devices_found": "No se encontraron dispositivos PlayStation 4 en la red.", + "port_987_bind_error": "No se pudo enlazar al puerto 987.", + "port_997_bind_error": "No se pudo enlazar al puerto 997." + }, + "error": { + "login_failed": "No se ha podido emparejar con PlayStation 4. Verifique que el PIN sea correcto.", + "not_ready": "PlayStation 4 no est\u00e1 encendida o conectada a la red." + }, + "step": { + "creds": { + "description": "Credenciales necesarias. Presione 'Enviar' y luego en la aplicaci\u00f3n de la segunda pantalla de PS4, actualice los dispositivos y seleccione el dispositivo 'Home-Assistant' para continuar.", + "title": "Playstation 4" + }, + "link": { + "data": { + "code": "PIN", + "ip_address": "Direcci\u00f3n IP", + "name": "Nombre", + "region": "Regi\u00f3n" + }, + "description": "Ingresa tu informaci\u00f3n de PlayStation 4. Para 'PIN', navegue hasta 'Configuraci\u00f3n' en su consola PlayStation 4. Luego navegue a 'Configuraci\u00f3n de conexi\u00f3n de la aplicaci\u00f3n m\u00f3vil' y seleccione 'Agregar dispositivo'. Ingrese el PIN que se muestra.", + "title": "Playstation 4" + } + }, + "title": "Playstation 4" + } +} \ No newline at end of file diff --git a/homeassistant/components/ps4/.translations/es.json b/homeassistant/components/ps4/.translations/es.json new file mode 100644 index 0000000000000..41cbd28492a8f --- /dev/null +++ b/homeassistant/components/ps4/.translations/es.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "credential_error": "Error al obtener las credenciales.", + "devices_configured": "Todos los dispositivos encontrados ya est\u00e1n configurados.", + "no_devices_found": "No se encuentran dispositivos PlayStation 4 en la red." + }, + "error": { + "not_ready": "PlayStation 4 no est\u00e1 encendido o conectado a la red." + }, + "step": { + "creds": { + "title": "PlayStation 4" + }, + "link": { + "data": { + "code": "PIN", + "ip_address": "Direcci\u00f3n IP", + "name": "Nombre", + "region": "Regi\u00f3n" + }, + "title": "PlayStation 4" + } + }, + "title": "PlayStation 4" + } +} \ No newline at end of file diff --git a/homeassistant/components/ps4/.translations/he.json b/homeassistant/components/ps4/.translations/he.json new file mode 100644 index 0000000000000..d9fa42b9e470f --- /dev/null +++ b/homeassistant/components/ps4/.translations/he.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "devices_configured": "\u05db\u05dc \u05d4\u05d4\u05ea\u05e7\u05e0\u05d9\u05dd \u05e9\u05e0\u05de\u05e6\u05d0\u05d5 \u05db\u05d1\u05e8 \u05de\u05d5\u05d2\u05d3\u05e8\u05d9\u05dd.", + "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05de\u05db\u05e9\u05d9\u05e8\u05d9 \u05e4\u05dc\u05d9\u05d9\u05e1\u05d8\u05d9\u05d9\u05e9\u05df 4 \u05d1\u05e8\u05e9\u05ea." + }, + "error": { + "not_ready": "PlayStation 4 \u05d0\u05d9\u05e0\u05d5 \u05e4\u05d5\u05e2\u05dc \u05d0\u05d5 \u05de\u05d7\u05d5\u05d1\u05e8 \u05dc\u05e8\u05e9\u05ea." + }, + "step": { + "creds": { + "title": "\u05e4\u05dc\u05d9\u05d9\u05e1\u05d8\u05d9\u05d9\u05e9\u05df 4" + }, + "link": { + "data": { + "ip_address": "\u05db\u05ea\u05d5\u05d1\u05ea \u05d4 - IP", + "name": "\u05e9\u05dd", + "region": "\u05d0\u05d9\u05d6\u05d5\u05e8" + }, + "title": "\u05e4\u05dc\u05d9\u05d9\u05e1\u05d8\u05d9\u05d9\u05e9\u05df 4" + } + }, + "title": "\u05e4\u05dc\u05d9\u05d9\u05e1\u05d8\u05d9\u05d9\u05e9\u05df 4" + } +} \ No newline at end of file diff --git a/homeassistant/components/ps4/.translations/it.json b/homeassistant/components/ps4/.translations/it.json new file mode 100644 index 0000000000000..5e83d7bd39c22 --- /dev/null +++ b/homeassistant/components/ps4/.translations/it.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "credential_error": "Errore nel recupero delle credenziali.", + "devices_configured": "Tutti i dispositivi trovati sono gi\u00e0 configurati.", + "no_devices_found": "Nessun dispositivo PlayStation 4 trovato in rete.", + "port_987_bind_error": "Impossibile connettersi alla porta 987.", + "port_997_bind_error": "Impossibile connettersi alla porta 997." + }, + "error": { + "login_failed": "Accoppiamento alla PlayStation 4 fallito. Verifica che il PIN sia corretto.", + "not_ready": "La PlayStation 4 non \u00e8 accesa o non \u00e8 collegata alla rete." + }, + "step": { + "creds": { + "description": "Credenziali necessarie. Premi 'Invia' e poi, nella seconda schermata della App PS4, aggiorna i dispositivi e seleziona il dispositivo 'Home-Assistant' per continuare.", + "title": "PlayStation 4" + }, + "link": { + "data": { + "code": "PIN", + "ip_address": "Indirizzo IP", + "name": "Nome", + "region": "Area geografica" + }, + "description": "Inserisci le informazioni della tua PlayStation 4. Per il \"PIN\", vai su \"Impostazioni\" sulla tua console PlayStation 4. Quindi accedi a \"Impostazioni connessione app mobile\" e seleziona \"Aggiungi dispositivo\". Inserisci il PIN che viene visualizzato.", + "title": "PlayStation 4" + } + }, + "title": "PlayStation 4" + } +} \ No newline at end of file diff --git a/homeassistant/components/ps4/.translations/ko.json b/homeassistant/components/ps4/.translations/ko.json new file mode 100644 index 0000000000000..ca77537e4e111 --- /dev/null +++ b/homeassistant/components/ps4/.translations/ko.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "credential_error": "\uc790\uaca9 \uc99d\uba85\uc744 \uac00\uc838\uc624\ub294 \uc911 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4.", + "devices_configured": "\ubc1c\uacac \ub41c \ubaa8\ub4e0 \uae30\uae30\ub294 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", + "no_devices_found": "PlayStation 4 \uae30\uae30\uac00 \ub124\ud2b8\uc6cc\ud06c\uc5d0\uc11c \ubc1c\uacac\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4.", + "port_987_bind_error": "\ud3ec\ud2b8 987 \uc5d0 \ubc14\uc778\ub529 \ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4.", + "port_997_bind_error": "\ud3ec\ud2b8 997 \uc5d0 \ubc14\uc778\ub529 \ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4." + }, + "error": { + "login_failed": "PlayStation 4 \uc640 \ud398\uc5b4\ub9c1\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4. PIN \uc774 \uc62c\ubc14\ub978\uc9c0 \ud655\uc778\ud574\uc8fc\uc138\uc694.", + "not_ready": "PlayStation 4 \uac00 \ucf1c\uc838 \uc788\uc9c0 \uc54a\uac70\ub098 \ub124\ud2b8\uc6cc\ud06c\uc5d0 \uc5f0\uacb0\ub418\uc5b4 \uc788\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4." + }, + "step": { + "creds": { + "description": "\uc790\uaca9 \uc99d\uba85\uc774 \ud544\uc694\ud569\ub2c8\ub2e4. 'Submit' \uc744 \ub204\ub978 \ub2e4\uc74c PS4 Second Screen \uc571\uc5d0\uc11c \uae30\uae30\ub97c \uc0c8\ub85c \uace0\uce68\ud558\uace0 'Home-Assistant' \uae30\uae30\ub97c \uc120\ud0dd\ud574\uc8fc\uc138\uc694.", + "title": "PlayStation 4" + }, + "link": { + "data": { + "code": "PIN", + "ip_address": "IP \uc8fc\uc18c", + "name": "\uc774\ub984", + "region": "\uc9c0\uc5ed" + }, + "description": "PlayStation 4 \uc815\ubcf4\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694. 'PIN' \uc744 \ud655\uc778\ud558\ub824\uba74, PlayStation 4 \ucf58\uc194\uc5d0\uc11c '\uc124\uc815' \uc73c\ub85c \uc774\ub3d9\ud55c \ub4a4 '\ubaa8\ubc14\uc77c \uc571 \uc811\uc18d \uc124\uc815' \uc73c\ub85c \uc774\ub3d9\ud558\uc5ec '\uae30\uae30 \ub4f1\ub85d\ud558\uae30' \ub97c \uc120\ud0dd\ud574\uc8fc\uc138\uc694. \ud654\uba74\uc5d0 \ud45c\uc2dc\ub41c 8\uc790\ub9ac \uc22b\uc790\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694.", + "title": "PlayStation 4" + } + }, + "title": "PlayStation 4" + } +} \ No newline at end of file diff --git a/homeassistant/components/ps4/.translations/lb.json b/homeassistant/components/ps4/.translations/lb.json new file mode 100644 index 0000000000000..15b90cb6b6ba5 --- /dev/null +++ b/homeassistant/components/ps4/.translations/lb.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "credential_error": "Feeler beim ausliesen vun den Umeldungs Informatiounen.", + "devices_configured": "All Apparater sinn schonn konfigur\u00e9iert", + "no_devices_found": "Keng Playstation 4 am Netzwierk fonnt.", + "port_987_bind_error": "Konnt sech net mam Port 987 verbannen.", + "port_997_bind_error": "Konnt sech net mam Port 997 verbannen." + }, + "error": { + "login_failed": "Feeler beim verbanne mat der Playstation 4. Iwwerpr\u00e9ift op de PIN korrekt ass.", + "not_ready": "PlayStation 4 ass net un oder mam Netzwierk verbonnen." + }, + "step": { + "creds": { + "description": "Umeldungsinformatioun sinn n\u00e9ideg. Dr\u00e9ckt op 'Ofsch\u00e9cken' , dann an der PS4 App, 2ten Ecran, erneiert Apparater an wielt den Home-Assistant Apparat aus fir weider ze fueren.", + "title": "PlayStation 4" + }, + "link": { + "data": { + "code": "PIN", + "ip_address": "IP Adresse", + "name": "Numm", + "region": "Regioun" + }, + "description": "Gitt \u00e4r Playstation 4 Informatiounen an. Fir 'PIN', gitt an d'Astellunge vun der Playstation 4 Konsole. Dann op 'Mobile App Verbindungs Astellungen' a wielt \"Apparat dob\u00e4isetzen' aus. Gitt de PIN an deen ugewise g\u00ebtt.", + "title": "PlayStation 4" + } + }, + "title": "PlayStation 4" + } +} \ No newline at end of file diff --git a/homeassistant/components/ps4/.translations/no.json b/homeassistant/components/ps4/.translations/no.json new file mode 100644 index 0000000000000..32687882da2e4 --- /dev/null +++ b/homeassistant/components/ps4/.translations/no.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "credential_error": "Feil ved henting av legitimasjon.", + "devices_configured": "Alle enheter som ble funnet er allerede konfigurert.", + "no_devices_found": "Ingen PlayStation 4 enheter funnet p\u00e5 nettverket.", + "port_987_bind_error": "Kunne ikke binde til port 987.", + "port_997_bind_error": "Kunne ikke binde til port 997." + }, + "error": { + "login_failed": "Klarte ikke \u00e5 koble til PlayStation 4. Bekreft at PIN koden er riktig.", + "not_ready": "PlayStation 4 er ikke p\u00e5sl\u00e5tt eller koblet til nettverk." + }, + "step": { + "creds": { + "description": "Legitimasjon n\u00f8dvendig. Trykk \"Send\" og deretter i PS4-ens andre skjerm app, kan du oppdatere enheter, og velg \"Home-Assistent' enheten for \u00e5 fortsette.", + "title": "PlayStation 4" + }, + "link": { + "data": { + "code": "PIN", + "ip_address": "IP adresse", + "name": "Navn", + "region": "Region" + }, + "description": "Skriv inn PlayStation 4 informasjonen din. For 'PIN', naviger til 'Innstillinger' p\u00e5 PlayStation 4 konsollen, deretter navigerer du til 'Innstillinger for mobilapp forbindelse' og velger 'Legg til enhet'. Skriv inn PIN-koden som vises.", + "title": "PlayStation 4" + } + }, + "title": "PlayStation 4" + } +} \ No newline at end of file diff --git a/homeassistant/components/ps4/.translations/pl.json b/homeassistant/components/ps4/.translations/pl.json new file mode 100644 index 0000000000000..eea4eda0810b5 --- /dev/null +++ b/homeassistant/components/ps4/.translations/pl.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "credential_error": "B\u0142\u0105d podczas pobierania danych logowania.", + "devices_configured": "Wszystkie znalezione urz\u0105dzenia s\u0105 ju\u017c skonfigurowane.", + "no_devices_found": "W sieci nie znaleziono urz\u0105dze\u0144 PlayStation 4.", + "port_987_bind_error": "Nie mo\u017cna powi\u0105za\u0107 z portem 987.", + "port_997_bind_error": "Nie mo\u017cna powi\u0105za\u0107 z portem 997." + }, + "error": { + "login_failed": "Nie uda\u0142o si\u0119 sparowa\u0107 z PlayStation 4. Sprawd\u017a, czy PIN jest poprawny.", + "not_ready": "PlayStation 4 nie jest w\u0142\u0105czona lub po\u0142\u0105czona z sieci\u0105." + }, + "step": { + "creds": { + "description": "Wymagane s\u0105 po\u015bwiadczenia. Naci\u015bnij przycisk 'Prze\u015blij', a nast\u0119pnie w aplikacji PS4 Second Screen, od\u015bwie\u017c urz\u0105dzenia i wybierz urz\u0105dzenie 'Home-Assistant', aby kontynuowa\u0107.", + "title": "PlayStation 4" + }, + "link": { + "data": { + "code": "PIN", + "ip_address": "Adres IP", + "name": "Nazwa", + "region": "Region" + }, + "description": "Wprowad\u017a informacje o PlayStation 4. Aby uzyska\u0107 'PIN', przejd\u017a do 'Ustawienia' na konsoli PlayStation 4. Nast\u0119pnie przejd\u017a do 'Ustawienia po\u0142\u0105czenia aplikacji mobilnej' i wybierz 'Dodaj urz\u0105dzenie'. Wprowad\u017a wy\u015bwietlony kod PIN.", + "title": "PlayStation 4" + } + }, + "title": "PlayStation 4" + } +} \ No newline at end of file diff --git a/homeassistant/components/ps4/.translations/pt.json b/homeassistant/components/ps4/.translations/pt.json new file mode 100644 index 0000000000000..34a5ebfc4db14 --- /dev/null +++ b/homeassistant/components/ps4/.translations/pt.json @@ -0,0 +1,18 @@ +{ + "config": { + "step": { + "creds": { + "title": "PlayStation 4" + }, + "link": { + "data": { + "code": "PIN", + "name": "Nome", + "region": "Regi\u00e3o" + }, + "title": "PlayStation 4" + } + }, + "title": "PlayStation 4" + } +} \ No newline at end of file diff --git a/homeassistant/components/ps4/.translations/ru.json b/homeassistant/components/ps4/.translations/ru.json new file mode 100644 index 0000000000000..41232ddc2d47f --- /dev/null +++ b/homeassistant/components/ps4/.translations/ru.json @@ -0,0 +1,32 @@ +{ + "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.", + "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": "\u0412 \u0441\u0435\u0442\u0438 \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432 PlayStation 4.", + "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.", + "port_997_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 997." + }, + "error": { + "login_failed": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0432\u044b\u043f\u043e\u043b\u043d\u0438\u0442\u044c \u0441\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u0435 \u0441 PlayStation 4. \u0423\u0431\u0435\u0434\u0438\u0442\u0435\u0441\u044c, \u0447\u0442\u043e PIN-\u043a\u043e\u0434 \u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u044b\u0439.", + "not_ready": "PlayStation 4 \u043d\u0435 \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u0430 \u0438\u043b\u0438 \u043d\u0435 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0430 \u043a \u0441\u0435\u0442\u0438." + }, + "step": { + "creds": { + "description": "\u041d\u0430\u0436\u043c\u0438\u0442\u0435 **\u041f\u041e\u0414\u0422\u0412\u0415\u0420\u0414\u0418\u0422\u042c**, \u0430 \u0437\u0430\u0442\u0435\u043c \u0432 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0438 'PS4 Second Screen' \u043e\u0431\u043d\u043e\u0432\u0438\u0442\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0438 \u0432\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e 'Home-Assistant', \u0447\u0442\u043e\u0431\u044b \u043f\u0440\u043e\u0434\u043e\u043b\u0436\u0438\u0442\u044c.", + "title": "PlayStation 4" + }, + "link": { + "data": { + "code": "PIN-\u043a\u043e\u0434", + "ip_address": "IP-\u0430\u0434\u0440\u0435\u0441", + "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435", + "region": "\u0420\u0435\u0433\u0438\u043e\u043d" + }, + "description": "\u0414\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f PIN-\u043a\u043e\u0434\u0430 \u043f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043a \u043f\u0443\u043d\u043a\u0442\u0443 **\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438** \u043d\u0430 \u043a\u043e\u043d\u0441\u043e\u043b\u0438 PlayStation 4. \u0417\u0430\u0442\u0435\u043c \u043e\u0442\u043a\u0440\u043e\u0439\u0442\u0435 **\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043c\u043e\u0431\u0438\u043b\u044c\u043d\u043e\u0433\u043e \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f** \u0438 \u0432\u044b\u0431\u0435\u0440\u0438\u0442\u0435 **\u0414\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e**.", + "title": "PlayStation 4" + } + }, + "title": "PlayStation 4" + } +} \ No newline at end of file diff --git a/homeassistant/components/ps4/.translations/sv.json b/homeassistant/components/ps4/.translations/sv.json new file mode 100644 index 0000000000000..d35efbd4b0071 --- /dev/null +++ b/homeassistant/components/ps4/.translations/sv.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "credential_error": "Fel n\u00e4r f\u00f6rs\u00f6ker h\u00e4mta autentiseringsuppgifter.", + "devices_configured": "Alla enheter som hittats \u00e4r redan konfigurerade.", + "no_devices_found": "Inga PlayStation 4 enheter hittades p\u00e5 n\u00e4tverket.", + "port_987_bind_error": "Kunde inte binda till port 987.", + "port_997_bind_error": "Kunde inte binda till port 997." + }, + "error": { + "login_failed": "Misslyckades med att para till PlayStation 4. Verifiera PIN-koden \u00e4r korrekt.", + "not_ready": "PlayStation 4 \u00e4r inte p\u00e5slagen eller ansluten till n\u00e4tverket." + }, + "step": { + "creds": { + "description": "Autentiseringsuppgifter beh\u00f6vs. Tryck p\u00e5 'Skicka' och sedan uppdatera enheter i appen \"PS4 Second Screen\" p\u00e5 din mobiltelefon eller surfplatta och v\u00e4lj 'Home Assistent' enheten att forts\u00e4tta.", + "title": "PlayStation 4" + }, + "link": { + "data": { + "code": "PIN-kod", + "ip_address": "IP-adress", + "name": "Namn", + "region": "Region" + }, + "description": "Ange din PlayStation 4 information. F\u00f6r 'PIN', navigera till 'Inst\u00e4llningar' p\u00e5 din PlayStation 4 konsol. Navigera sedan till \"Inst\u00e4llningar f\u00f6r mobilappanslutning\" och v\u00e4lj \"L\u00e4gg till enhet\". Ange PIN-koden som visas.", + "title": "PlayStation 4" + } + }, + "title": "PlayStation 4" + } +} \ No newline at end of file diff --git a/homeassistant/components/ps4/.translations/zh-Hant.json b/homeassistant/components/ps4/.translations/zh-Hant.json new file mode 100644 index 0000000000000..b4f45986c1e96 --- /dev/null +++ b/homeassistant/components/ps4/.translations/zh-Hant.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "credential_error": "\u53d6\u5f97\u6191\u8b49\u932f\u8aa4\u3002", + "devices_configured": "\u6240\u6709\u88dd\u7f6e\u90fd\u5df2\u8a2d\u5b9a\u5b8c\u6210\u3002", + "no_devices_found": "\u5728\u7db2\u8def\u4e0a\u627e\u4e0d\u5230 PlayStation 4 \u88dd\u7f6e\u3002", + "port_987_bind_error": "\u7121\u6cd5\u7d81\u5b9a\u901a\u8a0a\u57e0 987\u3002", + "port_997_bind_error": "\u7121\u6cd5\u7d81\u5b9a\u901a\u8a0a\u57e0 997\u3002" + }, + "error": { + "login_failed": "PlayStation 4 \u914d\u5c0d\u5931\u6557\uff0c\u8acb\u78ba\u8a8d PIN \u78bc\u3002", + "not_ready": "PlayStation 4 \u4e26\u672a\u958b\u555f\u6216\u672a\u9023\u7dda\u81f3\u7db2\u8def\u3002" + }, + "step": { + "creds": { + "description": "\u9700\u8981\u6191\u8b49\u3002\u6309\u4e0b\u300c\u50b3\u9001\u300d\u5f8c\u3001\u65bc PS4 \u7b2c\u4e8c\u756b\u9762 App\uff0c\u66f4\u65b0\u88dd\u7f6e\u4e26\u9078\u64c7\u300cHome-Assistant\u300d\u4ee5\u7e7c\u7e8c\u3002", + "title": "PlayStation 4" + }, + "link": { + "data": { + "code": "PIN", + "ip_address": "IP \u4f4d\u5740", + "name": "\u540d\u7a31", + "region": "\u5340\u57df" + }, + "description": "\u8f38\u5165\u60a8\u7684 PlayStation 4 \u8cc7\u8a0a\uff0c\u300cPIN\u300d\u65bc PlayStation 4 \u4e3b\u6a5f\u7684\u300c\u8a2d\u5b9a\u300d\u5167\uff0c\u4e26\u65bc\u300c\u884c\u52d5\u7a0b\u5f0f\u9023\u7dda\u8a2d\u5b9a\uff08Mobile App Connection Settings\uff09\u300d\u4e2d\u9078\u64c7\u300c\u65b0\u589e\u88dd\u7f6e\u300d\u3002\u8f38\u5165\u6240\u986f\u793a\u7684 PIN \u78bc\u3002", + "title": "PlayStation 4" + } + }, + "title": "PlayStation 4" + } +} \ No newline at end of file diff --git a/homeassistant/components/ps4/__init__.py b/homeassistant/components/ps4/__init__.py new file mode 100644 index 0000000000000..51260f5d86e90 --- /dev/null +++ b/homeassistant/components/ps4/__init__.py @@ -0,0 +1,33 @@ +""" +Support for PlayStation 4 consoles. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/ps4/ +""" +import logging + +from homeassistant.components.ps4.config_flow import PlayStation4FlowHandler # noqa: pylint: disable=unused-import +from homeassistant.components.ps4.const import DOMAIN # noqa: pylint: disable=unused-import + +_LOGGER = logging.getLogger(__name__) + +REQUIREMENTS = ['pyps4-homeassistant==0.3.0'] + + +async def async_setup(hass, config): + """Set up the PS4 Component.""" + return True + + +async def async_setup_entry(hass, config_entry): + """Set up PS4 from a config entry.""" + hass.async_create_task(hass.config_entries.async_forward_entry_setup( + config_entry, 'media_player')) + return True + + +async def async_unload_entry(hass, entry): + """Unload a PS4 config entry.""" + await hass.config_entries.async_forward_entry_unload( + entry, 'media_player') + return True diff --git a/homeassistant/components/ps4/config_flow.py b/homeassistant/components/ps4/config_flow.py new file mode 100644 index 0000000000000..3557c3fd9301a --- /dev/null +++ b/homeassistant/components/ps4/config_flow.py @@ -0,0 +1,123 @@ +"""Config Flow for PlayStation 4.""" +from collections import OrderedDict +import logging + +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.components.ps4.const import ( + DEFAULT_NAME, DEFAULT_REGION, DOMAIN, REGIONS) +from homeassistant.const import ( + CONF_CODE, CONF_HOST, CONF_IP_ADDRESS, CONF_NAME, CONF_REGION, CONF_TOKEN) + +_LOGGER = logging.getLogger(__name__) + +UDP_PORT = 987 +TCP_PORT = 997 +PORT_MSG = {UDP_PORT: 'port_987_bind_error', TCP_PORT: 'port_997_bind_error'} + + +@config_entries.HANDLERS.register(DOMAIN) +class PlayStation4FlowHandler(config_entries.ConfigFlow): + """Handle a PlayStation 4 config flow.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL + + def __init__(self): + """Initialize the config flow.""" + from pyps4_homeassistant import Helper + + self.helper = Helper() + self.creds = None + self.name = None + self.host = None + self.region = None + self.pin = None + + async def async_step_user(self, user_input=None): + """Handle a user config flow.""" + # Abort if device is configured. + if self.hass.config_entries.async_entries(DOMAIN): + return self.async_abort(reason='devices_configured') + + # Check if able to bind to ports: UDP 987, TCP 997. + ports = PORT_MSG.keys() + failed = await self.hass.async_add_executor_job( + self.helper.port_bind, ports) + if failed in ports: + reason = PORT_MSG[failed] + return self.async_abort(reason=reason) + return await self.async_step_creds() + + async def async_step_creds(self, user_input=None): + """Return PS4 credentials from 2nd Screen App.""" + if user_input is not None: + self.creds = await self.hass.async_add_executor_job( + self.helper.get_creds) + + if self.creds is not None: + return await self.async_step_link() + return self.async_abort(reason='credential_error') + + return self.async_show_form( + step_id='creds') + + async def async_step_link(self, user_input=None): + """Prompt user input. Create or edit entry.""" + errors = {} + + # Search for device. + devices = await self.hass.async_add_executor_job( + self.helper.has_devices) + + # Abort if can't find device. + if not devices: + return self.async_abort(reason='no_devices_found') + + device_list = [ + device['host-ip'] for device in devices] + + # Login to PS4 with user data. + if user_input is not None: + self.region = user_input[CONF_REGION] + self.name = user_input[CONF_NAME] + self.pin = user_input[CONF_CODE] + self.host = user_input[CONF_IP_ADDRESS] + + is_ready, is_login = await self.hass.async_add_executor_job( + self.helper.link, self.host, self.creds, self.pin) + + if is_ready is False: + errors['base'] = 'not_ready' + elif is_login is False: + errors['base'] = 'login_failed' + else: + device = { + CONF_HOST: self.host, + CONF_NAME: self.name, + CONF_REGION: self.region + } + + # Create entry. + return self.async_create_entry( + title='PlayStation 4', + data={ + CONF_TOKEN: self.creds, + 'devices': [device], + }, + ) + + # Show User Input form. + link_schema = OrderedDict() + link_schema[vol.Required(CONF_IP_ADDRESS)] = vol.In(list(device_list)) + link_schema[vol.Required( + CONF_REGION, default=DEFAULT_REGION)] = vol.In(list(REGIONS)) + link_schema[vol.Required(CONF_CODE)] = str + link_schema[vol.Required(CONF_NAME, default=DEFAULT_NAME)] = str + + return self.async_show_form( + step_id='link', + data_schema=vol.Schema(link_schema), + errors=errors, + ) diff --git a/homeassistant/components/ps4/const.py b/homeassistant/components/ps4/const.py new file mode 100644 index 0000000000000..0618ca9675f8d --- /dev/null +++ b/homeassistant/components/ps4/const.py @@ -0,0 +1,5 @@ +"""Constants for PlayStation 4.""" +DEFAULT_NAME = "PlayStation 4" +DEFAULT_REGION = "R1" +DOMAIN = 'ps4' +REGIONS = ('R1', 'R2', 'R3', 'R4', 'R5') diff --git a/homeassistant/components/ps4/media_player.py b/homeassistant/components/ps4/media_player.py new file mode 100644 index 0000000000000..bf7be1bbf9181 --- /dev/null +++ b/homeassistant/components/ps4/media_player.py @@ -0,0 +1,372 @@ +""" +Support for PlayStation 4 consoles. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/media_player.ps4/ +""" +from datetime import timedelta +import logging +import socket + +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +import homeassistant.util as util +from homeassistant.components.media_player import ( + MediaPlayerDevice, ENTITY_IMAGE_URL) +from homeassistant.components.media_player.const import ( + MEDIA_TYPE_MUSIC, SUPPORT_SELECT_SOURCE, + SUPPORT_STOP, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, +) +from homeassistant.components.ps4.const import DOMAIN as PS4_DOMAIN +from homeassistant.const import ( + ATTR_ENTITY_ID, ATTR_COMMAND, CONF_HOST, CONF_NAME, CONF_REGION, + CONF_TOKEN, STATE_IDLE, STATE_OFF, STATE_PLAYING, +) +from homeassistant.util.json import load_json, save_json + + +DEPENDENCIES = ['ps4'] + +_LOGGER = logging.getLogger(__name__) + +SUPPORT_PS4 = SUPPORT_TURN_OFF | SUPPORT_TURN_ON | \ + SUPPORT_STOP | SUPPORT_SELECT_SOURCE + +PS4_DATA = 'ps4_data' +ICON = 'mdi:playstation' +GAMES_FILE = '.ps4-games.json' +MEDIA_IMAGE_DEFAULT = None + +MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5) +MIN_TIME_BETWEEN_FORCED_SCANS = timedelta(seconds=10) + +COMMANDS = ( + 'up', + 'down', + 'right', + 'left', + 'enter', + 'back', + 'option', + 'ps', +) + +SERVICE_COMMAND = 'send_command' + +PS4_COMMAND_SCHEMA = vol.Schema({ + vol.Required(ATTR_ENTITY_ID): cv.entity_ids, + vol.Required(ATTR_COMMAND): vol.In(list(COMMANDS)) +}) + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up PS4 from a config entry.""" + config = config_entry + + def add_entities(entities, update_before_add=False): + """Sync version of async add devices.""" + hass.add_job(async_add_entities, entities, update_before_add) + + await hass.async_add_executor_job( + setup_platform, hass, config, + add_entities, None) + + async def async_service_handle(hass): + """Handle for services.""" + def service_command(call): + entity_ids = call.data[ATTR_ENTITY_ID] + command = call.data[ATTR_COMMAND] + for device in hass.data[PS4_DATA].devices: + if device.entity_id in entity_ids: + device.send_command(command) + + hass.services.async_register( + PS4_DOMAIN, SERVICE_COMMAND, service_command, + schema=PS4_COMMAND_SCHEMA) + + await async_service_handle(hass) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up PS4 Platform.""" + import pyps4_homeassistant as pyps4 + hass.data[PS4_DATA] = PS4Data() + games_file = hass.config.path(GAMES_FILE) + creds = config.data[CONF_TOKEN] + device_list = [] + for device in config.data['devices']: + host = device[CONF_HOST] + region = device[CONF_REGION] + name = device[CONF_NAME] + ps4 = pyps4.Ps4(host, creds) + device_list.append(PS4Device( + name, host, region, ps4, games_file)) + add_entities(device_list, True) + + +class PS4Data(): + """Init Data Class.""" + + def __init__(self): + """Init Class.""" + self.devices = [] + + +class PS4Device(MediaPlayerDevice): + """Representation of a PS4.""" + + def __init__(self, name, host, region, ps4, games_file): + """Initialize the ps4 device.""" + self._ps4 = ps4 + self._host = host + self._name = name + self._region = region + self._state = None + self._games_filename = games_file + self._media_content_id = None + self._media_title = None + self._media_image = None + self._source = None + self._games = {} + self._source_list = [] + self._retry = 0 + self._info = None + self._unique_id = None + + async def async_added_to_hass(self): + """Subscribe PS4 events.""" + self.hass.data[PS4_DATA].devices.append(self) + + @util.Throttle(MIN_TIME_BETWEEN_SCANS, MIN_TIME_BETWEEN_FORCED_SCANS) + def update(self): + """Retrieve the latest data.""" + try: + status = self._ps4.get_status() + if self._info is None: + self.get_device_info(status) + self._games = self.load_games() + if self._games is not None: + self._source_list = list(sorted(self._games.values())) + except socket.timeout: + status = None + if status is not None: + self._retry = 0 + if status.get('status') == 'Ok': + title_id = status.get('running-app-titleid') + name = status.get('running-app-name') + if title_id and name is not None: + self._state = STATE_PLAYING + if self._media_content_id != title_id: + self._media_content_id = title_id + self.get_title_data(title_id, name) + else: + self.idle() + else: + self.state_off() + elif self._retry > 5: + self.state_unknown() + else: + self._retry += 1 + + def idle(self): + """Set states for state idle.""" + self.reset_title() + self._state = STATE_IDLE + + def state_off(self): + """Set states for state off.""" + self.reset_title() + self._state = STATE_OFF + + def state_unknown(self): + """Set states for state unknown.""" + self.reset_title() + self._state = None + _LOGGER.warning("PS4 could not be reached") + self._retry = 0 + + def reset_title(self): + """Update if there is no title.""" + self._media_title = None + self._media_content_id = None + self._source = None + + def get_title_data(self, title_id, name): + """Get PS Store Data.""" + app_name = None + art = None + try: + app_name, art = self._ps4.get_ps_store_data( + name, title_id, self._region) + except TypeError: + _LOGGER.error( + "Could not find data in region: %s for PS ID: %s", + self._region, title_id) + finally: + self._media_title = app_name or name + self._source = self._media_title + self._media_image = art + self.update_list() + + def update_list(self): + """Update Game List, Correct data if different.""" + if self._media_content_id in self._games: + store = self._games[self._media_content_id] + if store != self._media_title: + self._games.pop(self._media_content_id) + if self._media_content_id not in self._games: + self.add_games(self._media_content_id, self._media_title) + self._games = self.load_games() + self._source_list = list(sorted(self._games.values())) + + def load_games(self): + """Load games for sources.""" + g_file = self._games_filename + try: + games = load_json(g_file) + + # If file does not exist, create empty file. + except FileNotFoundError: + games = {} + self.save_games(games) + return games + + def save_games(self, games): + """Save games to file.""" + g_file = self._games_filename + try: + save_json(g_file, games) + except OSError as error: + _LOGGER.error("Could not save game list, %s", error) + + # Retry loading file + if games is None: + self.load_games() + + def add_games(self, title_id, app_name): + """Add games to list.""" + games = self._games + if title_id is not None and title_id not in games: + game = {title_id: app_name} + games.update(game) + self.save_games(games) + + def get_device_info(self, status): + """Return device info for registry.""" + _sw_version = status['system-version'] + _sw_version = _sw_version[1:4] + sw_version = "{}.{}".format(_sw_version[0], _sw_version[1:]) + self._info = { + 'name': status['host-name'], + 'model': 'PlayStation 4', + 'identifiers': { + (PS4_DOMAIN, status['host-id']) + }, + 'manufacturer': 'Sony Interactive Entertainment Inc.', + 'sw_version': sw_version + } + self._unique_id = status['host-id'] + + @property + def device_info(self): + """Return information about the device.""" + return self._info + + @property + def unique_id(self): + """Return Unique ID for entity.""" + return self._unique_id + + @property + def entity_picture(self): + """Return picture.""" + if self._state == STATE_PLAYING and self._media_content_id is not None: + image_hash = self.media_image_hash + if image_hash is not None: + return ENTITY_IMAGE_URL.format( + self.entity_id, self.access_token, image_hash) + return MEDIA_IMAGE_DEFAULT + + @property + def name(self): + """Return the name of the device.""" + return self._name + + @property + def state(self): + """Return the state of the device.""" + return self._state + + @property + def icon(self): + """Icon.""" + return ICON + + @property + def media_content_id(self): + """Content ID of current playing media.""" + return self._media_content_id + + @property + def media_content_type(self): + """Content type of current playing media.""" + return MEDIA_TYPE_MUSIC + + @property + def media_image_url(self): + """Image url of current playing media.""" + if self._media_content_id is None: + return MEDIA_IMAGE_DEFAULT + return self._media_image + + @property + def media_title(self): + """Title of current playing media.""" + return self._media_title + + @property + def supported_features(self): + """Media player features that are supported.""" + return SUPPORT_PS4 + + @property + def source(self): + """Return the current input source.""" + return self._source + + @property + def source_list(self): + """List of available input sources.""" + return self._source_list + + def turn_off(self): + """Turn off media player.""" + self._ps4.standby() + + def turn_on(self): + """Turn on the media player.""" + self._ps4.wakeup() + + def media_pause(self): + """Send keypress ps to return to menu.""" + self._ps4.remote_control('ps') + + def media_stop(self): + """Send keypress ps to return to menu.""" + self._ps4.remote_control('ps') + + def select_source(self, source): + """Select input source.""" + for title_id, game in self._games.items(): + if source == game: + _LOGGER.debug( + "Starting PS4 game %s (%s) using source %s", + game, title_id, source) + self._ps4.start_title( + title_id, running_id=self._media_content_id) + return + + def send_command(self, command): + """Send Button Command.""" + self._ps4.remote_control(command) diff --git a/homeassistant/components/ps4/services.yaml b/homeassistant/components/ps4/services.yaml new file mode 100644 index 0000000000000..b7d1e8df96f3b --- /dev/null +++ b/homeassistant/components/ps4/services.yaml @@ -0,0 +1,9 @@ +send_command: + description: Emulate button press for PlayStation 4. + fields: + entity_id: + description: Name(s) of entities to send command. + example: 'media_player.playstation_4' + command: + description: Button to press. + example: 'ps' diff --git a/homeassistant/components/ps4/strings.json b/homeassistant/components/ps4/strings.json new file mode 100644 index 0000000000000..5f4e2a7c8b418 --- /dev/null +++ b/homeassistant/components/ps4/strings.json @@ -0,0 +1,32 @@ +{ + "config": { + "title": "PlayStation 4", + "step": { + "creds": { + "title": "PlayStation 4", + "description": "Credentials needed. Press 'Submit' and then in the PS4 2nd Screen App, refresh devices and select the 'Home-Assistant' device to continue." + }, + "link": { + "title": "PlayStation 4", + "description": "Enter your PlayStation 4 information. For 'PIN', navigate to 'Settings' on your PlayStation 4 console. Then navigate to 'Mobile App Connection Settings' and select 'Add Device'. Enter the PIN that is displayed.", + "data": { + "region": "Region", + "name": "Name", + "code": "PIN", + "ip_address": "IP Address" + } + } + }, + "error": { + "not_ready": "PlayStation 4 is not on or connected to network.", + "login_failed": "Failed to pair to PlayStation 4. Verify PIN is correct." + }, + "abort": { + "credential_error": "Error fetching credentials.", + "no_devices_found": "No PlayStation 4 devices found on the network.", + "devices_configured": "All devices found are already configured.", + "port_987_bind_error": "Could not bind to port 987.", + "port_997_bind_error": "Could not bind to port 997." + } + } +} diff --git a/homeassistant/components/python_script/__init__.py b/homeassistant/components/python_script/__init__.py index 3d0952b89fbac..d639b638033dc 100644 --- a/homeassistant/components/python_script/__init__.py +++ b/homeassistant/components/python_script/__init__.py @@ -125,13 +125,13 @@ def protected_getattr(obj, name, default=None): # pylint: disable=too-many-boolean-expressions if name.startswith('async_'): raise ScriptError("Not allowed to access async methods") - elif (obj is hass and name not in ALLOWED_HASS or - obj is hass.bus and name not in ALLOWED_EVENTBUS or - obj is hass.states and name not in ALLOWED_STATEMACHINE or - obj is hass.services and name not in ALLOWED_SERVICEREGISTRY or - obj is dt_util and name not in ALLOWED_DT_UTIL or - obj is datetime and name not in ALLOWED_DATETIME or - isinstance(obj, TimeWrapper) and name not in ALLOWED_TIME): + if (obj is hass and name not in ALLOWED_HASS or + obj is hass.bus and name not in ALLOWED_EVENTBUS or + obj is hass.states and name not in ALLOWED_STATEMACHINE or + obj is hass.services and name not in ALLOWED_SERVICEREGISTRY or + obj is dt_util and name not in ALLOWED_DT_UTIL or + obj is datetime and name not in ALLOWED_DATETIME or + isinstance(obj, TimeWrapper) and name not in ALLOWED_TIME): raise ScriptError("Not allowed to access {}.{}".format( obj.__class__.__name__, name)) diff --git a/homeassistant/components/raincloud/__init__.py b/homeassistant/components/raincloud/__init__.py index 47f6176d5f8ad..7ccf9f33adacd 100644 --- a/homeassistant/components/raincloud/__init__.py +++ b/homeassistant/components/raincloud/__init__.py @@ -24,7 +24,8 @@ ALLOWED_WATERING_TIME = [5, 10, 15, 30, 45, 60] -CONF_ATTRIBUTION = "Data provided by Melnor Aquatimer.com" +ATTRIBUTION = "Data provided by Melnor Aquatimer.com" + CONF_WATERING_TIME = 'watering_minutes' NOTIFICATION_ID = 'raincloud_notification' @@ -165,7 +166,7 @@ def unit_of_measurement(self): def device_state_attributes(self): """Return the state attributes.""" return { - ATTR_ATTRIBUTION: CONF_ATTRIBUTION, + ATTR_ATTRIBUTION: ATTRIBUTION, 'identifier': self.data.serial, } diff --git a/homeassistant/components/raincloud/switch.py b/homeassistant/components/raincloud/switch.py index 969169edc6474..1b76e8974b09d 100644 --- a/homeassistant/components/raincloud/switch.py +++ b/homeassistant/components/raincloud/switch.py @@ -1,16 +1,11 @@ -""" -Support for Melnor RainCloud sprinkler water timer. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/switch.raincloud/ -""" +"""Support for Melnor RainCloud sprinkler water timer.""" import logging import voluptuous as vol import homeassistant.helpers.config_validation as cv from homeassistant.components.raincloud import ( - ALLOWED_WATERING_TIME, CONF_ATTRIBUTION, CONF_WATERING_TIME, + ALLOWED_WATERING_TIME, ATTRIBUTION, CONF_WATERING_TIME, DATA_RAINCLOUD, DEFAULT_WATERING_TIME, RainCloudEntity, SWITCHES) from homeassistant.components.switch import SwitchDevice, PLATFORM_SCHEMA from homeassistant.const import ( @@ -38,12 +33,9 @@ def setup_platform(hass, config, add_entities, discovery_info=None): # create a sensor for each zone managed by faucet for zone in raincloud.controller.faucet.zones: sensors.append( - RainCloudSwitch(default_watering_timer, - zone, - sensor_type)) + RainCloudSwitch(default_watering_timer, zone, sensor_type)) add_entities(sensors, True) - return True class RainCloudSwitch(RainCloudEntity, SwitchDevice): @@ -87,7 +79,7 @@ def update(self): def device_state_attributes(self): """Return the state attributes.""" return { - ATTR_ATTRIBUTION: CONF_ATTRIBUTION, + ATTR_ATTRIBUTION: ATTRIBUTION, 'default_manual_timer': self._default_watering_timer, 'identifier': self.data.serial } diff --git a/homeassistant/components/rainmachine/.translations/es-419.json b/homeassistant/components/rainmachine/.translations/es-419.json new file mode 100644 index 0000000000000..2cb49dc0ac105 --- /dev/null +++ b/homeassistant/components/rainmachine/.translations/es-419.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "identifier_exists": "Cuenta ya registrada", + "invalid_credentials": "Credenciales no v\u00e1lidas" + }, + "step": { + "user": { + "data": { + "ip_address": "Nombre de host o direcci\u00f3n IP", + "password": "Contrase\u00f1a", + "port": "Puerto" + }, + "title": "Completa tu informaci\u00f3n" + } + }, + "title": "RainMachine" + } +} \ No newline at end of file diff --git a/homeassistant/components/rainmachine/.translations/es.json b/homeassistant/components/rainmachine/.translations/es.json new file mode 100644 index 0000000000000..2cb49dc0ac105 --- /dev/null +++ b/homeassistant/components/rainmachine/.translations/es.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "identifier_exists": "Cuenta ya registrada", + "invalid_credentials": "Credenciales no v\u00e1lidas" + }, + "step": { + "user": { + "data": { + "ip_address": "Nombre de host o direcci\u00f3n IP", + "password": "Contrase\u00f1a", + "port": "Puerto" + }, + "title": "Completa tu informaci\u00f3n" + } + }, + "title": "RainMachine" + } +} \ No newline at end of file diff --git a/homeassistant/components/rainmachine/.translations/hu.json b/homeassistant/components/rainmachine/.translations/hu.json index ff98eccbe5aa6..0f5b6b7112620 100644 --- a/homeassistant/components/rainmachine/.translations/hu.json +++ b/homeassistant/components/rainmachine/.translations/hu.json @@ -1,6 +1,7 @@ { "config": { "error": { + "identifier_exists": "A fi\u00f3k m\u00e1r regisztr\u00e1lt", "invalid_credentials": "\u00c9rv\u00e9nytelen hiteles\u00edt\u0151 adatok" }, "step": { @@ -9,7 +10,8 @@ "ip_address": "Kiszolg\u00e1l\u00f3 neve vagy IP c\u00edme", "password": "Jelsz\u00f3", "port": "Port" - } + }, + "title": "T\u00f6ltsd ki az adataid" } }, "title": "Rainmachine" diff --git a/homeassistant/components/rainmachine/.translations/it.json b/homeassistant/components/rainmachine/.translations/it.json new file mode 100644 index 0000000000000..40b49a926c760 --- /dev/null +++ b/homeassistant/components/rainmachine/.translations/it.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "identifier_exists": "Account gi\u00e0 registrato", + "invalid_credentials": "Credenziali non valide" + }, + "step": { + "user": { + "data": { + "ip_address": "Nome dell'host o indirizzo IP", + "password": "Password", + "port": "Porta" + }, + "title": "Inserisci i tuoi dati" + } + }, + "title": "RainMachine" + } +} \ No newline at end of file diff --git a/homeassistant/components/rainmachine/.translations/sv.json b/homeassistant/components/rainmachine/.translations/sv.json new file mode 100644 index 0000000000000..03f9c671c3564 --- /dev/null +++ b/homeassistant/components/rainmachine/.translations/sv.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "identifier_exists": "Kontot \u00e4r redan registrerat", + "invalid_credentials": "Ogiltiga autentiseringsuppgifter" + }, + "step": { + "user": { + "data": { + "ip_address": "V\u00e4rdnamn eller IP-adress", + "password": "L\u00f6senord", + "port": "Port" + }, + "title": "Fyll i dina uppgifter" + } + }, + "title": "RainMachine" + } +} \ No newline at end of file diff --git a/homeassistant/components/raspihats/__init__.py b/homeassistant/components/raspihats/__init__.py index 69b03a36769e6..622b98223aad6 100644 --- a/homeassistant/components/raspihats/__init__.py +++ b/homeassistant/components/raspihats/__init__.py @@ -14,7 +14,6 @@ CONF_I2C_HATS = 'i2c_hats' CONF_BOARD = 'board' -CONF_ADDRESS = 'address' CONF_CHANNELS = 'channels' CONF_INDEX = 'index' CONF_INVERT_LOGIC = 'invert_logic' diff --git a/homeassistant/components/raspihats/binary_sensor.py b/homeassistant/components/raspihats/binary_sensor.py index 04885402e722a..b0ebc2e3579ac 100644 --- a/homeassistant/components/raspihats/binary_sensor.py +++ b/homeassistant/components/raspihats/binary_sensor.py @@ -6,10 +6,10 @@ from homeassistant.components.binary_sensor import ( PLATFORM_SCHEMA, BinarySensorDevice) from homeassistant.components.raspihats import ( - CONF_ADDRESS, CONF_BOARD, CONF_CHANNELS, CONF_I2C_HATS, CONF_INDEX, - CONF_INVERT_LOGIC, I2C_HAT_NAMES, I2C_HATS_MANAGER, I2CHatsException) + CONF_BOARD, CONF_CHANNELS, CONF_I2C_HATS, CONF_INDEX, CONF_INVERT_LOGIC, + I2C_HAT_NAMES, I2C_HATS_MANAGER, I2CHatsException) from homeassistant.const import ( - CONF_DEVICE_CLASS, CONF_NAME, DEVICE_DEFAULT_NAME) + CONF_ADDRESS, CONF_DEVICE_CLASS, CONF_NAME, DEVICE_DEFAULT_NAME) import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/raspihats/switch.py b/homeassistant/components/raspihats/switch.py index 10bb2f748c4a1..26fcda3c8d7c6 100644 --- a/homeassistant/components/raspihats/switch.py +++ b/homeassistant/components/raspihats/switch.py @@ -4,11 +4,10 @@ import voluptuous as vol from homeassistant.components.raspihats import ( - CONF_ADDRESS, CONF_BOARD, CONF_CHANNELS, CONF_I2C_HATS, CONF_INDEX, - CONF_INITIAL_STATE, CONF_INVERT_LOGIC, I2C_HAT_NAMES, I2C_HATS_MANAGER, - I2CHatsException) + CONF_BOARD, CONF_CHANNELS, CONF_I2C_HATS, CONF_INDEX, CONF_INITIAL_STATE, + CONF_INVERT_LOGIC, I2C_HAT_NAMES, I2C_HATS_MANAGER, I2CHatsException) from homeassistant.components.switch import PLATFORM_SCHEMA -from homeassistant.const import CONF_NAME, DEVICE_DEFAULT_NAME +from homeassistant.const import CONF_ADDRESS, CONF_NAME, DEVICE_DEFAULT_NAME import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import ToggleEntity diff --git a/homeassistant/components/recorder/__init__.py b/homeassistant/components/recorder/__init__.py index 9b852b4a00a1e..6c338457b3472 100644 --- a/homeassistant/components/recorder/__init__.py +++ b/homeassistant/components/recorder/__init__.py @@ -25,7 +25,7 @@ from .const import DATA_INSTANCE from .util import session_scope -REQUIREMENTS = ['sqlalchemy==1.2.17'] +REQUIREMENTS = ['sqlalchemy==1.2.18'] _LOGGER = logging.getLogger(__name__) @@ -318,6 +318,10 @@ def async_purge(now): CONNECT_RETRY_WAIT) tries += 1 + except exc.SQLAlchemyError: + updated = True + _LOGGER.exception("Error saving event: %s", event) + if not updated: _LOGGER.error("Error in database update. Could not save " "after %d tries. Giving up", tries) diff --git a/homeassistant/components/recorder/util.py b/homeassistant/components/recorder/util.py index c6390e5d8e2c7..449f910fda961 100644 --- a/homeassistant/components/recorder/util.py +++ b/homeassistant/components/recorder/util.py @@ -76,5 +76,4 @@ def execute(qry): if tryno == RETRIES - 1: raise - else: - time.sleep(QUERY_RETRY_WAIT) + time.sleep(QUERY_RETRY_WAIT) diff --git a/homeassistant/components/reddit/__init__.py b/homeassistant/components/reddit/__init__.py new file mode 100644 index 0000000000000..3c810cdb1d839 --- /dev/null +++ b/homeassistant/components/reddit/__init__.py @@ -0,0 +1 @@ +"""Reddit Component.""" diff --git a/homeassistant/components/reddit/sensor.py b/homeassistant/components/reddit/sensor.py new file mode 100644 index 0000000000000..1b6a960669cd6 --- /dev/null +++ b/homeassistant/components/reddit/sensor.py @@ -0,0 +1,125 @@ +"""Support for Reddit.""" +from datetime import timedelta +import logging + +import voluptuous as vol + +from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import (CONF_USERNAME, CONF_PASSWORD, CONF_MAXIMUM) +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity + +REQUIREMENTS = ['praw==6.1.1'] + +_LOGGER = logging.getLogger(__name__) + +CONF_CLIENT_ID = 'client_id' +CONF_CLIENT_SECRET = 'client_secret' +CONF_SUBREDDITS = 'subreddits' + +ATTR_ID = 'id' +ATTR_BODY = 'body' +ATTR_COMMENTS_NUMBER = 'comms_num' +ATTR_CREATED = 'created' +ATTR_POSTS = 'posts' +ATTR_SUBREDDIT = 'subreddit' +ATTR_SCORE = 'score' +ATTR_TITLE = 'title' +ATTR_URL = 'url' + +DEFAULT_NAME = 'Reddit' + +SCAN_INTERVAL = timedelta(seconds=300) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_CLIENT_ID): cv.string, + vol.Required(CONF_CLIENT_SECRET): cv.string, + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Required(CONF_SUBREDDITS): vol.All(cv.ensure_list, [cv.string]), + vol.Optional(CONF_MAXIMUM, default=10): cv.positive_int +}) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the Reddit sensor platform.""" + import praw + + subreddits = config[CONF_SUBREDDITS] + user_agent = '{}_home_assistant_sensor'.format(config[CONF_USERNAME]) + limit = config[CONF_MAXIMUM] + + try: + reddit = praw.Reddit( + client_id=config[CONF_CLIENT_ID], + client_secret=config[CONF_CLIENT_SECRET], + username=config[CONF_USERNAME], + password=config[CONF_PASSWORD], + user_agent=user_agent) + + _LOGGER.debug('Connected to praw') + + except praw.exceptions.PRAWException as err: + _LOGGER.error("Reddit error %s", err) + return + + sensors = [RedditSensor(reddit, sub, limit) for sub in subreddits] + add_entities(sensors, True) + + +class RedditSensor(Entity): + """Representation of a Reddit sensor.""" + + def __init__(self, reddit, subreddit: str, limit: int): + """Initialize the Reddit sensor.""" + self._reddit = reddit + self._limit = limit + self._subreddit = subreddit + + self._subreddit_data = [] + + @property + def name(self): + """Return the name of the sensor.""" + return 'reddit_{}'.format(self._subreddit) + + @property + def state(self): + """Return the state of the sensor.""" + return len(self._subreddit_data) + + @property + def device_state_attributes(self): + """Return the state attributes.""" + return { + ATTR_SUBREDDIT: self._subreddit, + ATTR_POSTS: self._subreddit_data + } + + @property + def icon(self): + """Return the icon to use in the frontend.""" + return 'mdi:reddit' + + def update(self): + """Update data from Reddit API.""" + import praw + + self._subreddit_data = [] + + try: + subreddit = self._reddit.subreddit(self._subreddit) + + for submission in subreddit.top(limit=self._limit): + self._subreddit_data.append({ + ATTR_ID: submission.id, + ATTR_URL: submission.url, + ATTR_TITLE: submission.title, + ATTR_SCORE: submission.score, + ATTR_COMMENTS_NUMBER: submission.num_comments, + ATTR_CREATED: submission.created, + ATTR_BODY: submission.selftext + }) + + except praw.exceptions.PRAWException as err: + _LOGGER.error("Reddit error %s", err) diff --git a/homeassistant/components/ring/__init__.py b/homeassistant/components/ring/__init__.py index 526388a0918d2..94f3be305fa69 100644 --- a/homeassistant/components/ring/__init__.py +++ b/homeassistant/components/ring/__init__.py @@ -1,17 +1,17 @@ """Support for Ring Doorbell/Chimes.""" import logging -from requests.exceptions import HTTPError, ConnectTimeout +from requests.exceptions import ConnectTimeout, HTTPError import voluptuous as vol +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME import homeassistant.helpers.config_validation as cv -from homeassistant.const import CONF_USERNAME, CONF_PASSWORD REQUIREMENTS = ['ring_doorbell==0.2.2'] _LOGGER = logging.getLogger(__name__) -CONF_ATTRIBUTION = "Data provided by Ring.com" +ATTRIBUTION = "Data provided by Ring.com" NOTIFICATION_ID = 'ring_notification' NOTIFICATION_TITLE = 'Ring Setup' diff --git a/homeassistant/components/sensor/.translations/moon.it.json b/homeassistant/components/sensor/.translations/moon.it.json index f22a6d340ae19..39c7f22f7af5f 100644 --- a/homeassistant/components/sensor/.translations/moon.it.json +++ b/homeassistant/components/sensor/.translations/moon.it.json @@ -3,7 +3,7 @@ "first_quarter": "Primo quarto", "full_moon": "Luna piena", "last_quarter": "Ultimo quarto", - "new_moon": "Nuova luna", + "new_moon": "Luna nuova", "waning_crescent": "Luna calante", "waning_gibbous": "Gibbosa calante", "waxing_crescent": "Luna crescente", diff --git a/homeassistant/components/sensor/airvisual.py b/homeassistant/components/sensor/airvisual.py index ff99dce5e06ab..b9e7a3315e3e9 100644 --- a/homeassistant/components/sensor/airvisual.py +++ b/homeassistant/components/sensor/airvisual.py @@ -18,7 +18,7 @@ from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle -REQUIREMENTS = ['pyairvisual==2.0.1'] +REQUIREMENTS = ['pyairvisual==3.0.1'] _LOGGER = getLogger(__name__) ATTR_CITY = 'city' @@ -141,7 +141,7 @@ async def async_setup_platform( "Using city, state, and country: %s, %s, %s", city, state, country) location_id = ','.join((city, state, country)) data = AirVisualData( - Client(config[CONF_API_KEY], websession), + Client(websession, api_key=config[CONF_API_KEY]), city=city, state=state, country=country, @@ -152,7 +152,7 @@ async def async_setup_platform( "Using latitude and longitude: %s, %s", latitude, longitude) location_id = ','.join((str(latitude), str(longitude))) data = AirVisualData( - Client(config[CONF_API_KEY], websession), + Client(websession, api_key=config[CONF_API_KEY]), latitude=latitude, longitude=longitude, show_on_map=config[CONF_SHOW_ON_MAP], @@ -278,11 +278,11 @@ async def _async_update(self): try: if self.city and self.state and self.country: - resp = await self._client.data.city( + resp = await self._client.api.city( self.city, self.state, self.country) self.longitude, self.latitude = resp['location']['coordinates'] else: - resp = await self._client.data.nearest_city( + resp = await self._client.api.nearest_city( self.latitude, self.longitude) _LOGGER.debug("New data retrieved: %s", resp) diff --git a/homeassistant/components/sensor/alpha_vantage.py b/homeassistant/components/sensor/alpha_vantage.py index 79943a8b08472..774a3fe95f6b9 100644 --- a/homeassistant/components/sensor/alpha_vantage.py +++ b/homeassistant/components/sensor/alpha_vantage.py @@ -23,7 +23,8 @@ ATTR_HIGH = 'high' ATTR_LOW = 'low' -CONF_ATTRIBUTION = "Stock market information provided by Alpha Vantage" +ATTRIBUTION = "Stock market information provided by Alpha Vantage" + CONF_FOREIGN_EXCHANGE = 'foreign_exchange' CONF_FROM = 'from' CONF_SYMBOL = 'symbol' @@ -143,7 +144,7 @@ def device_state_attributes(self): """Return the state attributes.""" if self.values is not None: return { - ATTR_ATTRIBUTION: CONF_ATTRIBUTION, + ATTR_ATTRIBUTION: ATTRIBUTION, ATTR_CLOSE: self.values['4. close'], ATTR_HIGH: self.values['2. high'], ATTR_LOW: self.values['3. low'], @@ -203,7 +204,7 @@ def device_state_attributes(self): """Return the state attributes.""" if self.values is not None: return { - ATTR_ATTRIBUTION: CONF_ATTRIBUTION, + ATTR_ATTRIBUTION: ATTRIBUTION, CONF_FROM: self._from_currency, CONF_TO: self._to_currency, } diff --git a/homeassistant/components/sensor/bitcoin.py b/homeassistant/components/sensor/bitcoin.py index 34855d19104e8..e654f29f42a56 100644 --- a/homeassistant/components/sensor/bitcoin.py +++ b/homeassistant/components/sensor/bitcoin.py @@ -19,7 +19,7 @@ _LOGGER = logging.getLogger(__name__) -CONF_ATTRIBUTION = "Data provided by blockchain.info" +ATTRIBUTION = "Data provided by blockchain.info" DEFAULT_CURRENCY = 'USD' @@ -112,7 +112,7 @@ def icon(self): def device_state_attributes(self): """Return the state attributes of the sensor.""" return { - ATTR_ATTRIBUTION: CONF_ATTRIBUTION, + ATTR_ATTRIBUTION: ATTRIBUTION, } def update(self): diff --git a/homeassistant/components/sensor/blockchain.py b/homeassistant/components/sensor/blockchain.py index e51db7edcad0d..241c98d23284f 100644 --- a/homeassistant/components/sensor/blockchain.py +++ b/homeassistant/components/sensor/blockchain.py @@ -18,8 +18,9 @@ _LOGGER = logging.getLogger(__name__) +ATTRIBUTION = "Data provided by blockchain.info" + CONF_ADDRESSES = 'addresses' -CONF_ATTRIBUTION = "Data provided by blockchain.info" DEFAULT_NAME = 'Bitcoin Balance' @@ -82,7 +83,7 @@ def icon(self): def device_state_attributes(self): """Return the state attributes of the sensor.""" return { - ATTR_ATTRIBUTION: CONF_ATTRIBUTION, + ATTR_ATTRIBUTION: ATTRIBUTION, } def update(self): diff --git a/homeassistant/components/sensor/bom.py b/homeassistant/components/sensor/bom.py index df8b539135992..62a3706034ad3 100644 --- a/homeassistant/components/sensor/bom.py +++ b/homeassistant/components/sensor/bom.py @@ -34,7 +34,8 @@ ATTR_STATION_NAME = 'station_name' ATTR_ZONE_ID = 'zone_id' -CONF_ATTRIBUTION = "Data provided by the Australian Bureau of Meteorology" +ATTRIBUTION = "Data provided by the Australian Bureau of Meteorology" + CONF_STATION = 'station' CONF_ZONE_ID = 'zone_id' CONF_WMO_ID = 'wmo_id' @@ -158,7 +159,7 @@ def state(self): def device_state_attributes(self): """Return the state attributes of the device.""" attr = { - ATTR_ATTRIBUTION: CONF_ATTRIBUTION, + ATTR_ATTRIBUTION: ATTRIBUTION, ATTR_LAST_UPDATE: self.bom_data.last_updated, ATTR_SENSOR_ID: self._condition, ATTR_STATION_ID: self.bom_data.latest_data['wmo'], diff --git a/homeassistant/components/sensor/buienradar.py b/homeassistant/components/sensor/buienradar.py index 36585b8e103ed..4ceb1b221c005 100644 --- a/homeassistant/components/sensor/buienradar.py +++ b/homeassistant/components/sensor/buienradar.py @@ -137,6 +137,7 @@ 'Latitude and longitude must exist together'): cv.longitude, vol.Optional(CONF_TIMEFRAME, default=60): vol.All(vol.Coerce(int), vol.Range(min=5, max=120)), + vol.Optional(CONF_NAME, default='br'): cv.string, }) @@ -161,7 +162,7 @@ async def async_setup_platform(hass, config, async_add_entities, dev = [] for sensor_type in config[CONF_MONITORED_CONDITIONS]: - dev.append(BrSensor(sensor_type, config.get(CONF_NAME, 'br'), + dev.append(BrSensor(sensor_type, config.get(CONF_NAME), coordinates)) async_add_entities(dev) diff --git a/homeassistant/components/sensor/coinbase.py b/homeassistant/components/sensor/coinbase.py index d25b7b786f8a5..54af94944d6c2 100644 --- a/homeassistant/components/sensor/coinbase.py +++ b/homeassistant/components/sensor/coinbase.py @@ -16,9 +16,10 @@ 'LTC': 'mdi:litecoin', 'USD': 'mdi:currency-usd' } + DEFAULT_COIN_ICON = 'mdi:coin' -CONF_ATTRIBUTION = "Data provided by coinbase.com" +ATTRIBUTION = "Data provided by coinbase.com" DATA_COINBASE = 'coinbase_cache' DEPENDENCIES = ['coinbase'] @@ -77,7 +78,7 @@ def icon(self): def device_state_attributes(self): """Return the state attributes of the sensor.""" return { - ATTR_ATTRIBUTION: CONF_ATTRIBUTION, + ATTR_ATTRIBUTION: ATTRIBUTION, ATTR_NATIVE_BALANCE: "{} {}".format( self._native_balance, self._native_currency), } @@ -127,7 +128,7 @@ def icon(self): def device_state_attributes(self): """Return the state attributes of the sensor.""" return { - ATTR_ATTRIBUTION: CONF_ATTRIBUTION + ATTR_ATTRIBUTION: ATTRIBUTION } def update(self): diff --git a/homeassistant/components/sensor/coinmarketcap.py b/homeassistant/components/sensor/coinmarketcap.py index 18d3f0a3d009c..9143405a553a5 100644 --- a/homeassistant/components/sensor/coinmarketcap.py +++ b/homeassistant/components/sensor/coinmarketcap.py @@ -32,7 +32,8 @@ ATTR_SYMBOL = 'symbol' ATTR_TOTAL_SUPPLY = 'total_supply' -CONF_ATTRIBUTION = "Data provided by CoinMarketCap" +ATTRIBUTION = "Data provided by CoinMarketCap" + CONF_CURRENCY_ID = 'currency_id' CONF_DISPLAY_CURRENCY_DECIMALS = 'display_currency_decimals' @@ -115,7 +116,7 @@ def device_state_attributes(self): ATTR_VOLUME_24H: self._ticker.get('quotes').get(self.data.display_currency) .get('volume_24h'), - ATTR_ATTRIBUTION: CONF_ATTRIBUTION, + ATTR_ATTRIBUTION: ATTRIBUTION, ATTR_CIRCULATING_SUPPLY: self._ticker.get('circulating_supply'), ATTR_MARKET_CAP: self._ticker.get('quotes').get(self.data.display_currency) diff --git a/homeassistant/components/sensor/comed_hourly_pricing.py b/homeassistant/components/sensor/comed_hourly_pricing.py index 12b8e917f9db9..1771fd0f1a3dc 100644 --- a/homeassistant/components/sensor/comed_hourly_pricing.py +++ b/homeassistant/components/sensor/comed_hourly_pricing.py @@ -25,7 +25,8 @@ SCAN_INTERVAL = timedelta(minutes=5) -CONF_ATTRIBUTION = "Data provided by ComEd Hourly Pricing service" +ATTRIBUTION = "Data provided by ComEd Hourly Pricing service" + CONF_CURRENT_HOUR_AVERAGE = 'current_hour_average' CONF_FIVE_MINUTE = 'five_minute' CONF_MONITORED_FEEDS = 'monitored_feeds' @@ -97,8 +98,7 @@ def unit_of_measurement(self): @property def device_state_attributes(self): """Return the state attributes.""" - attrs = {ATTR_ATTRIBUTION: CONF_ATTRIBUTION} - return attrs + return {ATTR_ATTRIBUTION: ATTRIBUTION} async def async_update(self): """Get the ComEd Hourly Pricing data from the web service.""" diff --git a/homeassistant/components/sensor/crimereports.py b/homeassistant/components/sensor/crimereports.py index 2f1db42a12788..139346755178e 100644 --- a/homeassistant/components/sensor/crimereports.py +++ b/homeassistant/components/sensor/crimereports.py @@ -1,9 +1,4 @@ -""" -Sensor for Crime Reports. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/sensor.crimereports/ -""" +"""Sensor for Crime Reports.""" from collections import defaultdict from datetime import timedelta import logging @@ -21,7 +16,7 @@ from homeassistant.util.dt import now import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['crimereports==1.0.0'] +REQUIREMENTS = ['crimereports==1.0.1'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/sensor/currencylayer.py b/homeassistant/components/sensor/currencylayer.py index 67c9c7bbf19c5..9b7186e8e0952 100644 --- a/homeassistant/components/sensor/currencylayer.py +++ b/homeassistant/components/sensor/currencylayer.py @@ -19,7 +19,7 @@ _LOGGER = logging.getLogger(__name__) _RESOURCE = 'http://apilayer.net/api/live' -CONF_ATTRIBUTION = "Data provided by currencylayer.com" +ATTRIBUTION = "Data provided by currencylayer.com" DEFAULT_BASE = 'USD' DEFAULT_NAME = 'CurrencyLayer Sensor' @@ -91,7 +91,7 @@ def state(self): def device_state_attributes(self): """Return the state attributes of the sensor.""" return { - ATTR_ATTRIBUTION: CONF_ATTRIBUTION, + ATTR_ATTRIBUTION: ATTRIBUTION, } def update(self): @@ -119,10 +119,9 @@ def update(self): self._resource, params=self._parameters, timeout=10) if 'error' in result.json(): raise ValueError(result.json()['error']['info']) - else: - self.data = result.json()['quotes'] - _LOGGER.debug("Currencylayer data updated: %s", - result.json()['timestamp']) + self.data = result.json()['quotes'] + _LOGGER.debug("Currencylayer data updated: %s", + result.json()['timestamp']) except ValueError as err: _LOGGER.error("Check Currencylayer API %s", err.args) self.data = None diff --git a/homeassistant/components/sensor/discogs.py b/homeassistant/components/sensor/discogs.py index 70d7155fec787..ecbd6d9cab142 100644 --- a/homeassistant/components/sensor/discogs.py +++ b/homeassistant/components/sensor/discogs.py @@ -6,11 +6,13 @@ """ from datetime import timedelta import logging +import random import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import ATTR_ATTRIBUTION, CONF_NAME, CONF_TOKEN +from homeassistant.const import ( + ATTR_ATTRIBUTION, CONF_MONITORED_CONDITIONS, CONF_NAME, CONF_TOKEN) from homeassistant.helpers.aiohttp_client import SERVER_SOFTWARE import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity @@ -21,51 +23,89 @@ ATTR_IDENTITY = 'identity' -CONF_ATTRIBUTION = "Data provided by Discogs" +ATTRIBUTION = "Data provided by Discogs" DEFAULT_NAME = 'Discogs' -ICON = 'mdi:album' - -SCAN_INTERVAL = timedelta(hours=2) +ICON_RECORD = 'mdi:album' +ICON_PLAYER = 'mdi:record-player' +UNIT_RECORDS = 'records' + +SCAN_INTERVAL = timedelta(minutes=10) + +SENSOR_COLLECTION_TYPE = 'collection' +SENSOR_WANTLIST_TYPE = 'wantlist' +SENSOR_RANDOM_RECORD_TYPE = 'random_record' + +SENSORS = { + SENSOR_COLLECTION_TYPE: { + 'name': 'Collection', + 'icon': 'mdi:album', + 'unit_of_measurement': 'records' + }, + SENSOR_WANTLIST_TYPE: { + 'name': 'Wantlist', + 'icon': 'mdi:album', + 'unit_of_measurement': 'records' + }, + SENSOR_RANDOM_RECORD_TYPE: { + 'name': 'Random Record', + 'icon': 'mdi:record_player', + 'unit_of_measurement': None + }, +} PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_TOKEN): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_MONITORED_CONDITIONS, default=list(SENSORS)): + vol.All(cv.ensure_list, [vol.In(SENSORS)]) }) -async def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Discogs sensor.""" import discogs_client - name = config.get(CONF_NAME) - token = config.get(CONF_TOKEN) + token = config[CONF_TOKEN] + name = config[CONF_NAME] try: - discogs = discogs_client.Client(SERVER_SOFTWARE, user_token=token) - identity = discogs.identity() + discogs_client = discogs_client.Client( + SERVER_SOFTWARE, user_token=token) + + discogs_data = { + 'user': discogs_client.identity().name, + 'folders': discogs_client.identity().collection_folders, + 'collection_count': discogs_client.identity().num_collection, + 'wantlist_count': discogs_client.identity().num_wantlist + } except discogs_client.exceptions.HTTPError: _LOGGER.error("API token is not valid") return - async_add_entities([DiscogsSensor(identity, name)], True) + sensors = [] + for sensor_type in config.get(CONF_MONITORED_CONDITIONS): + sensors.append(DiscogsSensor(discogs_data, name, sensor_type)) + + add_entities(sensors, True) class DiscogsSensor(Entity): - """Get a user's number of records in collection.""" + """Create a new Discogs sensor for a specific type.""" - def __init__(self, identity, name): + def __init__(self, discogs_data, name, sensor_type): """Initialize the Discogs sensor.""" - self._identity = identity + self._discogs_data = discogs_data self._name = name + self._type = sensor_type self._state = None + self._attrs = {} @property def name(self): """Return the name of the sensor.""" - return self._name + return "{} {}".format(self._name, SENSORS[self._type]['name']) @property def state(self): @@ -75,21 +115,54 @@ def state(self): @property def icon(self): """Return the icon to use in the frontend, if any.""" - return ICON + return SENSORS[self._type]['icon'] @property def unit_of_measurement(self): """Return the unit this state is expressed in.""" - return 'records' + return SENSORS[self._type]['unit_of_measurement'] @property def device_state_attributes(self): """Return the state attributes of the sensor.""" + if self._state is None or self._attrs is None: + return None + + if self._type != SENSOR_RANDOM_RECORD_TYPE: + return { + ATTR_ATTRIBUTION: ATTRIBUTION, + ATTR_IDENTITY: self._discogs_data['user'], + } + return { - ATTR_ATTRIBUTION: CONF_ATTRIBUTION, - ATTR_IDENTITY: self._identity.name, + 'cat_no': self._attrs['labels'][0]['catno'], + 'cover_image': self._attrs['cover_image'], + 'format': "{} ({})".format( + self._attrs['formats'][0]['name'], + self._attrs['formats'][0]['descriptions'][0]), + 'label': self._attrs['labels'][0]['name'], + 'released': self._attrs['year'], + ATTR_ATTRIBUTION: ATTRIBUTION, + ATTR_IDENTITY: self._discogs_data['user'], } - async def async_update(self): + def get_random_record(self): + """Get a random record suggestion from the user's collection.""" + # Index 0 in the folders is the 'All' folder + collection = self._discogs_data['folders'][0] + random_index = random.randrange(collection.count) + random_record = collection.releases[random_index].release + + self._attrs = random_record.data + return "{} - {}".format( + random_record.data['artists'][0]['name'], + random_record.data['title']) + + def update(self): """Set state to the amount of records in user's collection.""" - self._state = self._identity.num_collection + if self._type == SENSOR_COLLECTION_TYPE: + self._state = self._discogs_data['collection_count'] + elif self._type == SENSOR_WANTLIST_TYPE: + self._state = self._discogs_data['wantlist_count'] + else: + self._state = self.get_random_record() diff --git a/homeassistant/components/sensor/dsmr.py b/homeassistant/components/sensor/dsmr.py index 8b7d78aa038d9..1bb7b44cab611 100644 --- a/homeassistant/components/sensor/dsmr.py +++ b/homeassistant/components/sensor/dsmr.py @@ -147,6 +147,18 @@ async def async_setup_platform(hass, config, async_add_entities, 'Voltage Swells Phase L3', obis_ref.VOLTAGE_SWELL_L3_COUNT ], + [ + 'Voltage Phase L1', + obis_ref.INSTANTANEOUS_VOLTAGE_L1 + ], + [ + 'Voltage Phase L2', + obis_ref.INSTANTANEOUS_VOLTAGE_L2 + ], + [ + 'Voltage Phase L3', + obis_ref.INSTANTANEOUS_VOLTAGE_L3 + ], ] # Generate device entities diff --git a/homeassistant/components/sensor/dublin_bus_transport.py b/homeassistant/components/sensor/dublin_bus_transport.py index 02527f1e33360..7a70d7af3a7bb 100644 --- a/homeassistant/components/sensor/dublin_bus_transport.py +++ b/homeassistant/components/sensor/dublin_bus_transport.py @@ -28,7 +28,8 @@ ATTR_DUE_AT = 'Due at' ATTR_NEXT_UP = 'Later Bus' -CONF_ATTRIBUTION = "Data provided by data.dublinked.ie" +ATTRIBUTION = "Data provided by data.dublinked.ie" + CONF_STOP_ID = 'stopid' CONF_ROUTE = 'route' @@ -101,7 +102,7 @@ def device_state_attributes(self): ATTR_DUE_AT: self._times[0][ATTR_DUE_AT], ATTR_STOP_ID: self._stop, ATTR_ROUTE: self._times[0][ATTR_ROUTE], - ATTR_ATTRIBUTION: CONF_ATTRIBUTION, + ATTR_ATTRIBUTION: ATTRIBUTION, ATTR_NEXT_UP: next_up } diff --git a/homeassistant/components/sensor/entur_public_transport.py b/homeassistant/components/sensor/entur_public_transport.py index 64884764523f0..330f5f8cc56ca 100644 --- a/homeassistant/components/sensor/entur_public_transport.py +++ b/homeassistant/components/sensor/entur_public_transport.py @@ -26,7 +26,8 @@ API_CLIENT_NAME = 'homeassistant-homeassistant' -CONF_ATTRIBUTION = "Data provided by entur.org under NLOD." +ATTRIBUTION = "Data provided by entur.org under NLOD" + CONF_STOP_IDS = 'stop_ids' CONF_EXPAND_PLATFORMS = 'expand_platforms' CONF_WHITELIST_LINES = 'line_whitelist' @@ -140,7 +141,7 @@ def __init__( self._state = None self._icon = ICONS[DEFAULT_ICON_KEY] self._attributes = { - ATTR_ATTRIBUTION: CONF_ATTRIBUTION, + ATTR_ATTRIBUTION: ATTRIBUTION, ATTR_STOP_ID: self._stop, } diff --git a/homeassistant/components/sensor/etherscan.py b/homeassistant/components/sensor/etherscan.py index 24cf046cca076..082295bfea547 100644 --- a/homeassistant/components/sensor/etherscan.py +++ b/homeassistant/components/sensor/etherscan.py @@ -1,24 +1,19 @@ -""" -Support for Etherscan sensors. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/sensor.etherscan/ -""" +"""Support for Etherscan sensors.""" from datetime import timedelta import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import ATTR_ATTRIBUTION, CONF_NAME +from homeassistant.const import ( + ATTR_ATTRIBUTION, CONF_ADDRESS, CONF_NAME, CONF_TOKEN) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity REQUIREMENTS = ['python-etherscan-api==0.0.3'] -CONF_ADDRESS = 'address' -CONF_TOKEN = 'token' +ATTRIBUTION = "Data provided by etherscan.io" + CONF_TOKEN_ADDRESS = 'token_address' -CONF_ATTRIBUTION = "Data provided by etherscan.io" SCAN_INTERVAL = timedelta(minutes=5) @@ -77,9 +72,7 @@ def unit_of_measurement(self): @property def device_state_attributes(self): """Return the state attributes of the sensor.""" - return { - ATTR_ATTRIBUTION: CONF_ATTRIBUTION, - } + return {ATTR_ATTRIBUTION: ATTRIBUTION} def update(self): """Get the latest state of the sensor.""" diff --git a/homeassistant/components/sensor/filter.py b/homeassistant/components/sensor/filter.py index 3d05dd28e79de..92e2cc751ac75 100644 --- a/homeassistant/components/sensor/filter.py +++ b/homeassistant/components/sensor/filter.py @@ -313,6 +313,7 @@ def __init__(self, name, window_size=1, precision=None, entity=None): self._entity = entity self._skip_processing = False self._window_size = window_size + self._store_raw = False @property def window_size(self): @@ -337,7 +338,10 @@ def filter_state(self, new_state): """Implement a common interface for filters.""" filtered = self._filter_state(FilterState(new_state)) filtered.set_precision(self.precision) - self.states.append(copy(filtered)) + if self._store_raw: + self.states.append(copy(FilterState(new_state))) + else: + self.states.append(copy(filtered)) new_state.state = filtered.state return new_state @@ -402,12 +406,14 @@ def __init__(self, window_size, precision, entity, radius): super().__init__(FILTER_NAME_OUTLIER, window_size, precision, entity) self._radius = radius self._stats_internal = Counter() + self._store_raw = True def _filter_state(self, new_state): """Implement the outlier filter.""" + median = statistics.median([s.state for s in self.states]) \ + if self.states else 0 if (len(self.states) == self.states.maxlen and - abs(new_state.state - - statistics.median([s.state for s in self.states])) > + abs(new_state.state - median) > self._radius): self._stats_internal['erasures'] += 1 @@ -415,7 +421,7 @@ def _filter_state(self, new_state): _LOGGER.debug("Outlier nr. %s in %s: %s", self._stats_internal['erasures'], self._entity, new_state) - return self.states[-1] + new_state.state = median return new_state diff --git a/homeassistant/components/sensor/fitbit.py b/homeassistant/components/sensor/fitbit.py index f5b44d577a7cb..d5d9150e4e851 100644 --- a/homeassistant/components/sensor/fitbit.py +++ b/homeassistant/components/sensor/fitbit.py @@ -35,7 +35,7 @@ CONF_MONITORED_RESOURCES = 'monitored_resources' CONF_CLOCK_FORMAT = 'clock_format' -CONF_ATTRIBUTION = 'Data provided by Fitbit.com' +ATTRIBUTION = 'Data provided by Fitbit.com' DEPENDENCIES = ['http'] @@ -423,8 +423,8 @@ def icon(self): """Icon to use in the frontend, if any.""" if self.resource_type == 'devices/battery' and self.extra: battery_level = BATTERY_LEVELS[self.extra.get('battery')] - return icon_for_battery_level(battery_level=battery_level, - charging=None) + return icon_for_battery_level( + battery_level=battery_level, charging=None) return 'mdi:{}'.format(FITBIT_RESOURCES_LIST[self.resource_type][2]) @property @@ -432,7 +432,7 @@ def device_state_attributes(self): """Return the state attributes.""" attrs = {} - attrs[ATTR_ATTRIBUTION] = CONF_ATTRIBUTION + attrs[ATTR_ATTRIBUTION] = ATTRIBUTION if self.extra: attrs['model'] = self.extra.get('deviceVersion') diff --git a/homeassistant/components/sensor/fixer.py b/homeassistant/components/sensor/fixer.py index 1bdd9e7127239..c46fa75131957 100644 --- a/homeassistant/components/sensor/fixer.py +++ b/homeassistant/components/sensor/fixer.py @@ -20,8 +20,8 @@ ATTR_EXCHANGE_RATE = 'Exchange rate' ATTR_TARGET = 'Target currency' +ATTRIBUTION = "Data provided by the European Central Bank (ECB)" -CONF_ATTRIBUTION = "Data provided by the European Central Bank (ECB)" CONF_TARGET = 'target' DEFAULT_BASE = 'USD' @@ -86,7 +86,7 @@ def device_state_attributes(self): """Return the state attributes.""" if self.data.rate is not None: return { - ATTR_ATTRIBUTION: CONF_ATTRIBUTION, + ATTR_ATTRIBUTION: ATTRIBUTION, ATTR_EXCHANGE_RATE: self.data.rate['rates'][self._target], ATTR_TARGET: self._target, } diff --git a/homeassistant/components/sensor/gitlab_ci.py b/homeassistant/components/sensor/gitlab_ci.py index 1e55a7d6997ff..7f3b444bb75fa 100644 --- a/homeassistant/components/sensor/gitlab_ci.py +++ b/homeassistant/components/sensor/gitlab_ci.py @@ -28,8 +28,8 @@ ATTR_BUILD_ID = 'build id' ATTR_BUILD_STARTED = 'build_started' ATTR_BUILD_STATUS = 'build_status' +ATTRIBUTION = "Information provided by https://gitlab.com/" -CONF_ATTRIBUTION = "Information provided by https://gitlab.com/" CONF_GITLAB_ID = 'gitlab_id' DEFAULT_NAME = 'GitLab CI Status' @@ -101,7 +101,7 @@ def available(self): def device_state_attributes(self): """Return the state attributes.""" return { - ATTR_ATTRIBUTION: CONF_ATTRIBUTION, + ATTR_ATTRIBUTION: ATTRIBUTION, ATTR_BUILD_STATUS: self._state, ATTR_BUILD_STARTED: self._started_at, ATTR_BUILD_FINISHED: self._finished_at, diff --git a/homeassistant/components/sensor/glances.py b/homeassistant/components/sensor/glances.py index 5ac0816a0c158..53db254e4b393 100644 --- a/homeassistant/components/sensor/glances.py +++ b/homeassistant/components/sensor/glances.py @@ -47,6 +47,7 @@ 'process_total': ['Total', 'Count', 'mdi:memory'], 'process_thread': ['Thread', 'Count', 'mdi:memory'], 'process_sleeping': ['Sleeping', 'Count', 'mdi:memory'], + 'cpu_use_percent': ['CPU used', '%', 'mdi:memory'], 'cpu_temp': ['CPU Temp', TEMP_CELSIUS, 'mdi:thermometer'], 'docker_active': ['Containers active', '', 'mdi:docker'], 'docker_cpu_use': ['Containers CPU used', '%', 'mdi:docker'], @@ -177,6 +178,8 @@ async def async_update(self): self._state = value['processcount']['thread'] elif self.type == 'process_sleeping': self._state = value['processcount']['sleeping'] + elif self.type == 'cpu_use_percent': + self._state = value['quicklook']['cpu'] elif self.type == 'cpu_temp': for sensor in value['sensors']: if sensor['label'] in ['CPU', "Package id 0", diff --git a/homeassistant/components/sensor/google_travel_time.py b/homeassistant/components/sensor/google_travel_time.py index 6c19747565341..1f4d8425d6e28 100644 --- a/homeassistant/components/sensor/google_travel_time.py +++ b/homeassistant/components/sensor/google_travel_time.py @@ -4,21 +4,21 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.google_travel_time/ """ +import logging from datetime import datetime from datetime import timedelta -import logging import voluptuous as vol -from homeassistant.components.sensor import DOMAIN, PLATFORM_SCHEMA -from homeassistant.helpers.entity import Entity +import homeassistant.helpers.config_validation as cv +import homeassistant.util.dt as dt_util +from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( CONF_API_KEY, CONF_NAME, EVENT_HOMEASSISTANT_START, ATTR_LATITUDE, ATTR_LONGITUDE, CONF_MODE) -from homeassistant.util import Throttle -import homeassistant.helpers.config_validation as cv from homeassistant.helpers import location -import homeassistant.util.dt as dt_util +from homeassistant.helpers.entity import Entity +from homeassistant.util import Throttle REQUIREMENTS = ['googlemaps==2.5.1'] @@ -83,18 +83,16 @@ def convert_time_to_utc(timestr): def setup_platform(hass, config, add_entities_callback, discovery_info=None): """Set up the Google travel time platform.""" def run_setup(event): - """Delay the setup until Home Assistant is fully initialized. + """ + Delay the setup until Home Assistant is fully initialized. This allows any entities to be created already """ + hass.data.setdefault(DATA_KEY, []) options = config.get(CONF_OPTIONS) if options.get('units') is None: options['units'] = hass.config.units.name - if DATA_KEY not in hass.data: - hass.data[DATA_KEY] = [] - hass.services.register( - DOMAIN, 'google_travel_sensor_update', update) travel_mode = config.get(CONF_TRAVEL_MODE) mode = options.get(CONF_MODE) @@ -120,14 +118,6 @@ def run_setup(event): if sensor.valid_api_connection: add_entities_callback([sensor]) - def update(service): - """Update service for manual updates.""" - entity_id = service.data.get('entity_id') - for sensor in hass.data[DATA_KEY]: - if sensor.entity_id == entity_id: - sensor.update(no_throttle=True) - sensor.schedule_update_ha_state() - # Wait until start event is sent to load this component. hass.bus.listen_once(EVENT_HOMEASSISTANT_START, run_setup) diff --git a/homeassistant/components/sensor/gtfs.py b/homeassistant/components/sensor/gtfs.py index 94f21287e395a..eec08be093f03 100644 --- a/homeassistant/components/sensor/gtfs.py +++ b/homeassistant/components/sensor/gtfs.py @@ -40,6 +40,7 @@ 7: 'mdi:stairs', } +DATE_FORMAT = '%Y-%m-%d' TIME_FORMAT = '%Y-%m-%d %H:%M:%S' PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ @@ -59,7 +60,7 @@ def get_next_departure(sched, start_station_id, end_station_id, offset): now = datetime.datetime.now() + offset day_name = now.strftime('%A').lower() now_str = now.strftime('%H:%M:%S') - today = now.strftime('%Y-%m-%d') + today = now.strftime(DATE_FORMAT) from sqlalchemy.sql import text @@ -69,7 +70,7 @@ def get_next_departure(sched, start_station_id, end_station_id, offset): time(origin_stop_time.departure_time) AS origin_depart_time, origin_stop_time.drop_off_type AS origin_drop_off_type, origin_stop_time.pickup_type AS origin_pickup_type, - origin_stop_time.shape_dist_traveled AS origin_shape_dist_traveled, + origin_stop_time.shape_dist_traveled AS origin_dist_traveled, origin_stop_time.stop_headsign AS origin_stop_headsign, origin_stop_time.stop_sequence AS origin_stop_sequence, time(destination_stop_time.arrival_time) AS dest_arrival_time, @@ -111,10 +112,27 @@ def get_next_departure(sched, start_station_id, end_station_id, offset): if item == {}: return None - origin_arrival_time = '{} {}'.format(today, item['origin_arrival_time']) + # Format arrival and departure dates and times, accounting for the + # possibility of times crossing over midnight. + origin_arrival = now + if item['origin_arrival_time'] > item['origin_depart_time']: + origin_arrival -= datetime.timedelta(days=1) + origin_arrival_time = '{} {}'.format(origin_arrival.strftime(DATE_FORMAT), + item['origin_arrival_time']) + origin_depart_time = '{} {}'.format(today, item['origin_depart_time']) - dest_arrival_time = '{} {}'.format(today, item['dest_arrival_time']) - dest_depart_time = '{} {}'.format(today, item['dest_depart_time']) + + dest_arrival = now + if item['dest_arrival_time'] < item['origin_depart_time']: + dest_arrival += datetime.timedelta(days=1) + dest_arrival_time = '{} {}'.format(dest_arrival.strftime(DATE_FORMAT), + item['dest_arrival_time']) + + dest_depart = dest_arrival + if item['dest_depart_time'] < item['dest_arrival_time']: + dest_depart += datetime.timedelta(days=1) + dest_depart_time = '{} {}'.format(dest_depart.strftime(DATE_FORMAT), + item['dest_depart_time']) depart_time = datetime.datetime.strptime(origin_depart_time, TIME_FORMAT) arrival_time = datetime.datetime.strptime(dest_arrival_time, TIME_FORMAT) @@ -129,7 +147,7 @@ def get_next_departure(sched, start_station_id, end_station_id, offset): 'Departure Time': origin_depart_time, 'Drop Off Type': item['origin_drop_off_type'], 'Pickup Type': item['origin_pickup_type'], - 'Shape Dist Traveled': item['origin_shape_dist_traveled'], + 'Shape Dist Traveled': item['origin_dist_traveled'], 'Headsign': item['origin_stop_headsign'], 'Sequence': item['origin_stop_sequence'] } diff --git a/homeassistant/components/sensor/haveibeenpwned.py b/homeassistant/components/sensor/haveibeenpwned.py index 4d651ea81c7bf..a4ae2349e2425 100644 --- a/homeassistant/components/sensor/haveibeenpwned.py +++ b/homeassistant/components/sensor/haveibeenpwned.py @@ -12,7 +12,7 @@ import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import CONF_EMAIL +from homeassistant.const import CONF_EMAIL, ATTR_ATTRIBUTION import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import track_point_in_time @@ -21,6 +21,8 @@ _LOGGER = logging.getLogger(__name__) +ATTRIBUTION = "Data provided by Have I Been Pwned (HIBP)" + DATE_STR_FORMAT = "%Y-%m-%d %H:%M:%S" HA_USER_AGENT = "Home Assistant HaveIBeenPwned Sensor Component" @@ -75,7 +77,7 @@ def state(self): @property def device_state_attributes(self): """Return the attributes of the sensor.""" - val = {} + val = {ATTR_ATTRIBUTION: ATTRIBUTION} if self._email not in self._data.data: return val diff --git a/homeassistant/components/sensor/iperf3.py b/homeassistant/components/sensor/iperf3.py deleted file mode 100644 index 0eb6dfaa00c69..0000000000000 --- a/homeassistant/components/sensor/iperf3.py +++ /dev/null @@ -1,195 +0,0 @@ -""" -Support for Iperf3 network measurement tool. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/sensor.iperf3/ -""" -import logging -from datetime import timedelta - -import voluptuous as vol - -from homeassistant.components.sensor import DOMAIN, PLATFORM_SCHEMA -from homeassistant.const import ( - ATTR_ATTRIBUTION, ATTR_ENTITY_ID, CONF_MONITORED_CONDITIONS, - CONF_HOST, CONF_PORT, CONF_PROTOCOL) -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity - -REQUIREMENTS = ['iperf3==0.1.10'] - -_LOGGER = logging.getLogger(__name__) - -ATTR_PROTOCOL = 'Protocol' -ATTR_REMOTE_HOST = 'Remote Server' -ATTR_REMOTE_PORT = 'Remote Port' -ATTR_VERSION = 'Version' - -CONF_ATTRIBUTION = 'Data retrieved using Iperf3' -CONF_DURATION = 'duration' -CONF_PARALLEL = 'parallel' - -DEFAULT_DURATION = 10 -DEFAULT_PORT = 5201 -DEFAULT_PARALLEL = 1 -DEFAULT_PROTOCOL = 'tcp' - -IPERF3_DATA = 'iperf3' - -SCAN_INTERVAL = timedelta(minutes=60) - -SERVICE_NAME = 'iperf3_update' - -ICON = 'mdi:speedometer' - -SENSOR_TYPES = { - 'download': ['Download', 'Mbit/s'], - 'upload': ['Upload', 'Mbit/s'], -} - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_MONITORED_CONDITIONS): - vol.All(cv.ensure_list, [vol.In(list(SENSOR_TYPES))]), - vol.Required(CONF_PORT, default=DEFAULT_PORT): cv.port, - vol.Required(CONF_HOST): cv.string, - vol.Optional(CONF_DURATION, default=DEFAULT_DURATION): vol.Range(5, 10), - vol.Optional(CONF_PARALLEL, default=DEFAULT_PARALLEL): vol.Range(1, 20), - vol.Optional(CONF_PROTOCOL, default=DEFAULT_PROTOCOL): - vol.In(['tcp', 'udp']), -}) - - -SERVICE_SCHEMA = vol.Schema({ - vol.Optional(ATTR_ENTITY_ID): cv.string, -}) - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the Iperf3 sensor.""" - if hass.data.get(IPERF3_DATA) is None: - hass.data[IPERF3_DATA] = {} - hass.data[IPERF3_DATA]['sensors'] = [] - - dev = [] - for sensor in config[CONF_MONITORED_CONDITIONS]: - dev.append( - Iperf3Sensor(config[CONF_HOST], - config[CONF_PORT], - config[CONF_DURATION], - config[CONF_PARALLEL], - config[CONF_PROTOCOL], - sensor)) - - hass.data[IPERF3_DATA]['sensors'].extend(dev) - add_entities(dev) - - def _service_handler(service): - """Update service for manual updates.""" - entity_id = service.data.get('entity_id') - all_iperf3_sensors = hass.data[IPERF3_DATA]['sensors'] - - for sensor in all_iperf3_sensors: - if entity_id is not None: - if sensor.entity_id == entity_id: - sensor.update() - sensor.schedule_update_ha_state() - break - else: - sensor.update() - sensor.schedule_update_ha_state() - - for sensor in dev: - hass.services.register(DOMAIN, SERVICE_NAME, _service_handler, - schema=SERVICE_SCHEMA) - - -class Iperf3Sensor(Entity): - """A Iperf3 sensor implementation.""" - - def __init__(self, server, port, duration, streams, - protocol, sensor_type): - """Initialize the sensor.""" - self._attrs = { - ATTR_ATTRIBUTION: CONF_ATTRIBUTION, - ATTR_PROTOCOL: protocol, - } - self._name = \ - "{} {}".format(SENSOR_TYPES[sensor_type][0], server) - self._state = None - self._sensor_type = sensor_type - self._unit_of_measurement = SENSOR_TYPES[sensor_type][1] - self._port = port - self._server = server - self._duration = duration - self._num_streams = streams - self._protocol = protocol - self.result = None - - @property - def name(self): - """Return the name of the sensor.""" - return self._name - - @property - def state(self): - """Return the state of the device.""" - return self._state - - @property - def unit_of_measurement(self): - """Return the unit of measurement of this entity, if any.""" - return self._unit_of_measurement - - @property - def device_state_attributes(self): - """Return the state attributes.""" - if self.result is not None: - self._attrs[ATTR_ATTRIBUTION] = CONF_ATTRIBUTION - self._attrs[ATTR_REMOTE_HOST] = self.result.remote_host - self._attrs[ATTR_REMOTE_PORT] = self.result.remote_port - self._attrs[ATTR_VERSION] = self.result.version - return self._attrs - - def update(self): - """Get the latest data and update the states.""" - import iperf3 - client = iperf3.Client() - client.duration = self._duration - client.server_hostname = self._server - client.port = self._port - client.verbose = False - client.num_streams = self._num_streams - client.protocol = self._protocol - - # when testing download bandwith, reverse must be True - if self._sensor_type == 'download': - client.reverse = True - - try: - self.result = client.run() - except (AttributeError, OSError, ValueError) as error: - self.result = None - _LOGGER.error("Iperf3 sensor error: %s", error) - return - - if self.result is not None and \ - hasattr(self.result, 'error') and \ - self.result.error is not None: - _LOGGER.error("Iperf3 sensor error: %s", self.result.error) - self.result = None - return - - # UDP only have 1 way attribute - if self._protocol == 'udp': - self._state = round(self.result.Mbps, 2) - - elif self._sensor_type == 'download': - self._state = round(self.result.received_Mbps, 2) - - elif self._sensor_type == 'upload': - self._state = round(self.result.sent_Mbps, 2) - - @property - def icon(self): - """Return icon.""" - return ICON diff --git a/homeassistant/components/sensor/irish_rail_transport.py b/homeassistant/components/sensor/irish_rail_transport.py index 10f4004ae748a..e17ecfde59da5 100644 --- a/homeassistant/components/sensor/irish_rail_transport.py +++ b/homeassistant/components/sensor/irish_rail_transport.py @@ -1,9 +1,4 @@ -""" -Support for Irish Rail RTPI information. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/sensor.irish_rail_transport/ -""" +"""Support for Irish Rail RTPI information.""" import logging from datetime import timedelta @@ -11,7 +6,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import CONF_NAME +from homeassistant.const import CONF_NAME, ATTR_ATTRIBUTION from homeassistant.helpers.entity import Entity REQUIREMENTS = ['pyirishrail==0.0.2'] @@ -28,6 +23,7 @@ ATTR_EXPECT_AT = "Expected at" ATTR_NEXT_UP = "Later Train" ATTR_TRAIN_TYPE = "Train type" +ATTRIBUTION = "Data provided by Irish Rail" CONF_STATION = 'station' CONF_DESTINATION = 'destination' @@ -100,6 +96,7 @@ def device_state_attributes(self): next_up += self._times[1][ATTR_DUE_IN] return { + ATTR_ATTRIBUTION: ATTRIBUTION, ATTR_STATION: self._station, ATTR_ORIGIN: self._times[0][ATTR_ORIGIN], ATTR_DESTINATION: self._times[0][ATTR_DESTINATION], @@ -109,7 +106,7 @@ def device_state_attributes(self): ATTR_DIRECTION: self._times[0][ATTR_DIRECTION], ATTR_STOPS_AT: self._times[0][ATTR_STOPS_AT], ATTR_NEXT_UP: next_up, - ATTR_TRAIN_TYPE: self._times[0][ATTR_TRAIN_TYPE] + ATTR_TRAIN_TYPE: self._times[0][ATTR_TRAIN_TYPE], } @property @@ -146,22 +143,23 @@ def __init__(self, irish_rail, station, direction, destination, stops_at): def update(self): """Get the latest data from irishrail.""" - trains = self._ir_api.get_station_by_name(self.station, - direction=self.direction, - destination=self.destination, - stops_at=self.stops_at) + trains = self._ir_api.get_station_by_name( + self.station, direction=self.direction, + destination=self.destination, stops_at=self.stops_at) stops_at = self.stops_at if self.stops_at else '' self.info = [] for train in trains: - train_data = {ATTR_STATION: self.station, - ATTR_ORIGIN: train.get('origin'), - ATTR_DESTINATION: train.get('destination'), - ATTR_DUE_IN: train.get('due_in_mins'), - ATTR_DUE_AT: train.get('scheduled_arrival_time'), - ATTR_EXPECT_AT: train.get('expected_departure_time'), - ATTR_DIRECTION: train.get('direction'), - ATTR_STOPS_AT: stops_at, - ATTR_TRAIN_TYPE: train.get('type')} + train_data = { + ATTR_STATION: self.station, + ATTR_ORIGIN: train.get('origin'), + ATTR_DESTINATION: train.get('destination'), + ATTR_DUE_IN: train.get('due_in_mins'), + ATTR_DUE_AT: train.get('scheduled_arrival_time'), + ATTR_EXPECT_AT: train.get('expected_departure_time'), + ATTR_DIRECTION: train.get('direction'), + ATTR_STOPS_AT: stops_at, + ATTR_TRAIN_TYPE: train.get('type'), + } self.info.append(train_data) if not self.info: @@ -180,4 +178,5 @@ def _empty_train_data(self): ATTR_EXPECT_AT: 'n/a', ATTR_DIRECTION: direction, ATTR_STOPS_AT: stops_at, - ATTR_TRAIN_TYPE: ''}] + ATTR_TRAIN_TYPE: '', + }] diff --git a/homeassistant/components/sensor/lastfm.py b/homeassistant/components/sensor/lastfm.py index fa69a91649512..bb5a09771c220 100644 --- a/homeassistant/components/sensor/lastfm.py +++ b/homeassistant/components/sensor/lastfm.py @@ -1,16 +1,11 @@ -""" -Sensor for Last.fm account status. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/sensor.lastfm/ -""" +"""Sensor for Last.fm account status.""" import logging import re import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import CONF_API_KEY +from homeassistant.const import CONF_API_KEY, ATTR_ATTRIBUTION import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity @@ -21,6 +16,7 @@ ATTR_LAST_PLAYED = 'last_played' ATTR_PLAY_COUNT = 'play_count' ATTR_TOP_PLAYED = 'top_played' +ATTRIBUTION = "Data provided by Last.fm" CONF_USERS = 'users' @@ -105,6 +101,7 @@ def update(self): def device_state_attributes(self): """Return the state attributes.""" return { + ATTR_ATTRIBUTION: ATTRIBUTION, ATTR_LAST_PLAYED: self._lastplayed, ATTR_PLAY_COUNT: self._playcount, ATTR_TOP_PLAYED: self._topplayed, diff --git a/homeassistant/components/sensor/linky.py b/homeassistant/components/sensor/linky.py index 316da010ae479..8130961bfc020 100644 --- a/homeassistant/components/sensor/linky.py +++ b/homeassistant/components/sensor/linky.py @@ -16,7 +16,7 @@ from homeassistant.util import Throttle import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['pylinky==0.1.8'] +REQUIREMENTS = ['pylinky==0.3.0'] _LOGGER = logging.getLogger(__name__) SCAN_INTERVAL = timedelta(minutes=10) @@ -38,6 +38,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): from pylinky.client import LinkyClient, PyLinkyError client = LinkyClient(username, password, None, timeout) try: + client.login() client.fetch_data() except PyLinkyError as exp: _LOGGER.error(exp) diff --git a/homeassistant/components/sensor/london_underground.py b/homeassistant/components/sensor/london_underground.py index d44806cf48173..1c93d6a1bcb1b 100644 --- a/homeassistant/components/sensor/london_underground.py +++ b/homeassistant/components/sensor/london_underground.py @@ -18,8 +18,11 @@ _LOGGER = logging.getLogger(__name__) ATTRIBUTION = "Powered by TfL Open Data" + CONF_LINE = 'line' + SCAN_INTERVAL = timedelta(seconds=30) + TUBE_LINES = [ 'Bakerloo', 'Central', @@ -34,7 +37,8 @@ 'Piccadilly', 'TfL Rail', 'Victoria', - 'Waterloo & City'] + 'Waterloo & City', +] PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_LINE): diff --git a/homeassistant/components/sensor/meteo_france.py b/homeassistant/components/sensor/meteo_france.py deleted file mode 100644 index 1e18b1518a791..0000000000000 --- a/homeassistant/components/sensor/meteo_france.py +++ /dev/null @@ -1,139 +0,0 @@ -""" -Support for Meteo France raining forecast. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/sensor.meteo_france/ -""" - -import logging -import datetime - -import voluptuous as vol - -from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import ( - CONF_MONITORED_CONDITIONS, ATTR_ATTRIBUTION, TEMP_CELSIUS) -from homeassistant.helpers.entity import Entity -from homeassistant.util import Throttle -import homeassistant.helpers.config_validation as cv - -REQUIREMENTS = ['meteofrance==0.2.7'] -_LOGGER = logging.getLogger(__name__) - -CONF_ATTRIBUTION = "Data provided by Meteo-France" -CONF_POSTAL_CODE = 'postal_code' - -STATE_ATTR_FORECAST = '1h rain forecast' - -SCAN_INTERVAL = datetime.timedelta(minutes=5) - -SENSOR_TYPES = { - 'rain_chance': ['Rain chance', '%'], - 'freeze_chance': ['Freeze chance', '%'], - 'thunder_chance': ['Thunder chance', '%'], - 'snow_chance': ['Snow chance', '%'], - 'weather': ['Weather', None], - 'wind_speed': ['Wind Speed', 'km/h'], - 'next_rain': ['Next rain', 'min'], - 'temperature': ['Temperature', TEMP_CELSIUS], - 'uv': ['UV', None], -} - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_POSTAL_CODE): cv.string, - vol.Required(CONF_MONITORED_CONDITIONS): - vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]), -}) - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the Meteo-France sensor.""" - postal_code = config[CONF_POSTAL_CODE] - - from meteofrance.client import meteofranceClient, meteofranceError - - try: - meteofrance_client = meteofranceClient(postal_code) - except meteofranceError as exp: - _LOGGER.error(exp) - return - - client = MeteoFranceUpdater(meteofrance_client) - - add_entities([MeteoFranceSensor(variable, client) - for variable in config[CONF_MONITORED_CONDITIONS]], - True) - - -class MeteoFranceSensor(Entity): - """Representation of a Sensor.""" - - def __init__(self, condition, client): - """Initialize the sensor.""" - self._condition = condition - self._client = client - self._state = None - self._data = {} - - @property - def name(self): - """Return the name of the sensor.""" - return "{} {}".format(self._data["name"], - SENSOR_TYPES[self._condition][0]) - - @property - def state(self): - """Return the state of the sensor.""" - return self._state - - @property - def device_state_attributes(self): - """Return the state attributes of the sensor.""" - if self._condition == 'next_rain' and "rain_forecast" in self._data: - return { - **{ - STATE_ATTR_FORECAST: self._data["rain_forecast"], - }, - ** self._data["next_rain_intervals"], - **{ - ATTR_ATTRIBUTION: CONF_ATTRIBUTION - } - } - return {ATTR_ATTRIBUTION: CONF_ATTRIBUTION} - - @property - def unit_of_measurement(self): - """Return the unit of measurement.""" - return SENSOR_TYPES[self._condition][1] - - def update(self): - """Fetch new state data for the sensor.""" - try: - self._client.update() - self._data = self._client.get_data() - self._state = self._data[self._condition] - except KeyError: - _LOGGER.error("No condition `%s` for location `%s`", - self._condition, self._data["name"]) - self._state = None - - -class MeteoFranceUpdater: - """Update data from Meteo-France.""" - - def __init__(self, client): - """Initialize the data object.""" - self._client = client - - def get_data(self): - """Get the latest data from Meteo-France.""" - return self._client.get_data() - - @Throttle(SCAN_INTERVAL) - def update(self): - """Get the latest data from Meteo-France.""" - from meteofrance.client import meteofranceError - try: - self._client.update() - except meteofranceError as exp: - _LOGGER.error(exp) diff --git a/homeassistant/components/sensor/metoffice.py b/homeassistant/components/sensor/metoffice.py index 8cebecb71247b..3d9c9485da3fd 100644 --- a/homeassistant/components/sensor/metoffice.py +++ b/homeassistant/components/sensor/metoffice.py @@ -26,7 +26,7 @@ ATTR_SITE_ID = 'site_id' ATTR_SITE_NAME = 'site_name' -CONF_ATTRIBUTION = "Data provided by the Met Office" +ATTRIBUTION = "Data provided by the Met Office" CONDITION_CLASSES = { 'cloudy': ['7', '8'], @@ -162,7 +162,7 @@ def unit_of_measurement(self): def device_state_attributes(self): """Return the state attributes of the device.""" attr = {} - attr[ATTR_ATTRIBUTION] = CONF_ATTRIBUTION + attr[ATTR_ATTRIBUTION] = ATTRIBUTION attr[ATTR_LAST_UPDATE] = self.data.data.date attr[ATTR_SENSOR_ID] = self._condition attr[ATTR_SITE_ID] = self.site.id diff --git a/homeassistant/components/sensor/mitemp_bt.py b/homeassistant/components/sensor/mitemp_bt.py index 15e225fd2c0ed..f8bee17978d15 100644 --- a/homeassistant/components/sensor/mitemp_bt.py +++ b/homeassistant/components/sensor/mitemp_bt.py @@ -12,7 +12,8 @@ from homeassistant.helpers.entity import Entity import homeassistant.helpers.config_validation as cv from homeassistant.const import ( - CONF_FORCE_UPDATE, CONF_MONITORED_CONDITIONS, CONF_NAME, CONF_MAC + CONF_FORCE_UPDATE, CONF_MONITORED_CONDITIONS, CONF_NAME, CONF_MAC, + DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_BATTERY ) @@ -37,9 +38,9 @@ # Sensor types are defined like: Name, units SENSOR_TYPES = { - 'temperature': ['Temperature', '°C'], - 'humidity': ['Humidity', '%'], - 'battery': ['Battery', '%'], + 'temperature': [DEVICE_CLASS_TEMPERATURE, 'Temperature', '°C'], + 'humidity': [DEVICE_CLASS_HUMIDITY, 'Humidity', '%'], + 'battery': [DEVICE_CLASS_BATTERY, 'Battery', '%'], } PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ @@ -80,15 +81,16 @@ def setup_platform(hass, config, add_entities, discovery_info=None): devs = [] for parameter in config[CONF_MONITORED_CONDITIONS]: - name = SENSOR_TYPES[parameter][0] - unit = SENSOR_TYPES[parameter][1] + device = SENSOR_TYPES[parameter][0] + name = SENSOR_TYPES[parameter][1] + unit = SENSOR_TYPES[parameter][2] prefix = config.get(CONF_NAME) if prefix: name = "{} {}".format(prefix, name) devs.append(MiTempBtSensor( - poller, parameter, name, unit, force_update, median)) + poller, parameter, device, name, unit, force_update, median)) add_entities(devs) @@ -96,10 +98,12 @@ def setup_platform(hass, config, add_entities, discovery_info=None): class MiTempBtSensor(Entity): """Implementing the MiTempBt sensor.""" - def __init__(self, poller, parameter, name, unit, force_update, median): + def __init__(self, poller, parameter, device, name, unit, + force_update, median): """Initialize the sensor.""" self.poller = poller self.parameter = parameter + self._device = device self._unit = unit self._name = name self._state = None @@ -125,6 +129,11 @@ def unit_of_measurement(self): """Return the units of measurement.""" return self._unit + @property + def device_class(self): + """Device class of this entity.""" + return self._device + @property def force_update(self): """Force update.""" diff --git a/homeassistant/components/sensor/nederlandse_spoorwegen.py b/homeassistant/components/sensor/nederlandse_spoorwegen.py index 81cfada25f68f..5d9376ad9ebe0 100644 --- a/homeassistant/components/sensor/nederlandse_spoorwegen.py +++ b/homeassistant/components/sensor/nederlandse_spoorwegen.py @@ -21,7 +21,8 @@ _LOGGER = logging.getLogger(__name__) -CONF_ATTRIBUTION = "Data provided by NS" +ATTRIBUTION = "Data provided by NS" + CONF_ROUTES = 'routes' CONF_FROM = 'from' CONF_TO = 'to' @@ -155,7 +156,7 @@ def device_state_attributes(self): 'transfers': self._trips[0].nr_transfers, 'route': route, 'remarks': [r.message for r in self._trips[0].trip_remarks], - ATTR_ATTRIBUTION: CONF_ATTRIBUTION, + ATTR_ATTRIBUTION: ATTRIBUTION, } @Throttle(MIN_TIME_BETWEEN_UPDATES) diff --git a/homeassistant/components/sensor/nmbs.py b/homeassistant/components/sensor/nmbs.py index e13ca18af5f48..e677a072ef3c7 100644 --- a/homeassistant/components/sensor/nmbs.py +++ b/homeassistant/components/sensor/nmbs.py @@ -9,7 +9,9 @@ import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import CONF_NAME, ATTR_ATTRIBUTION +from homeassistant.const import ( + ATTR_ATTRIBUTION, ATTR_LATITUDE, ATTR_LONGITUDE, CONF_NAME, + CONF_SHOW_ON_MAP) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity import homeassistant.util.dt as dt_util @@ -17,7 +19,6 @@ _LOGGER = logging.getLogger(__name__) DEFAULT_NAME = 'NMBS' -DEFAULT_NAME_LIVE = "NMBS Live" DEFAULT_ICON = "mdi:train" DEFAULT_ICON_ALERT = "mdi:alert-octagon" @@ -25,6 +26,7 @@ CONF_STATION_FROM = 'station_from' CONF_STATION_TO = 'station_to' CONF_STATION_LIVE = 'station_live' +CONF_EXCLUDE_VIAS = 'exclude_vias' REQUIREMENTS = ["pyrail==0.0.3"] @@ -32,7 +34,9 @@ vol.Required(CONF_STATION_FROM): cv.string, vol.Required(CONF_STATION_TO): cv.string, vol.Optional(CONF_STATION_LIVE): cv.string, + vol.Optional(CONF_EXCLUDE_VIAS, default=False): cv.boolean, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_SHOW_ON_MAP, default=False): cv.boolean, }) @@ -64,14 +68,17 @@ def setup_platform(hass, config, add_entities, discovery_info=None): api_client = iRail() name = config[CONF_NAME] + show_on_map = config[CONF_SHOW_ON_MAP] station_from = config[CONF_STATION_FROM] station_to = config[CONF_STATION_TO] station_live = config.get(CONF_STATION_LIVE) + excl_vias = config[CONF_EXCLUDE_VIAS] - sensors = [NMBSSensor(name, station_from, station_to, api_client)] + sensors = [NMBSSensor( + api_client, name, show_on_map, station_from, station_to, excl_vias)] if station_live is not None: - sensors.append(NMBSLiveBoard(station_live, api_client)) + sensors.append(NMBSLiveBoard(api_client, station_live)) add_entities(sensors, True) @@ -79,22 +86,23 @@ def setup_platform(hass, config, add_entities, discovery_info=None): class NMBSLiveBoard(Entity): """Get the next train from a station's liveboard.""" - def __init__(self, live_station, api_client): + def __init__(self, api_client, live_station): """Initialize the sensor for getting liveboard data.""" self._station = live_station self._api_client = api_client + self._attrs = {} self._state = None @property def name(self): """Return the sensor default name.""" - return DEFAULT_NAME_LIVE + return "NMBS Live" @property def icon(self): """Return the default icon or an alert icon if delays.""" - if self._attrs is not None and int(self._attrs['delay']) > 0: + if self._attrs and int(self._attrs['delay']) > 0: return DEFAULT_ICON_ALERT return DEFAULT_ICON @@ -107,7 +115,7 @@ def state(self): @property def device_state_attributes(self): """Return the sensor attributes if data is available.""" - if self._state is None or self._attrs is None: + if self._state is None or not self._attrs: return None delay = get_delay_in_minutes(self._attrs["delay"]) @@ -118,6 +126,7 @@ def device_state_attributes(self): 'extra_train': int(self._attrs['isExtra']) > 0, 'occupancy': self._attrs['occupancy']['name'], 'vehicle_id': self._attrs['vehicle'], + 'monitored_station': self._station, ATTR_ATTRIBUTION: "https://api.irail.be/", } @@ -139,12 +148,16 @@ def update(self): class NMBSSensor(Entity): """Get the the total travel time for a given connection.""" - def __init__(self, name, station_from, station_to, api_client): + def __init__(self, api_client, name, show_on_map, + station_from, station_to, excl_vias): """Initialize the NMBS connection sensor.""" self._name = name + self._show_on_map = show_on_map + self._api_client = api_client self._station_from = station_from self._station_to = station_to - self._api_client = api_client + self._excl_vias = excl_vias + self._attrs = {} self._state = None @@ -161,7 +174,7 @@ def unit_of_measurement(self): @property def icon(self): """Return the sensor default icon or an alert icon if any delay.""" - if self._attrs is not None: + if self._attrs: delay = get_delay_in_minutes(self._attrs['departure']['delay']) if delay > 0: return "mdi:alert-octagon" @@ -171,7 +184,7 @@ def icon(self): @property def device_state_attributes(self): """Return sensor attributes if data is available.""" - if self._state is None or self._attrs is None: + if self._state is None or not self._attrs: return None delay = get_delay_in_minutes(self._attrs['departure']['delay']) @@ -179,6 +192,7 @@ def device_state_attributes(self): attrs = { 'departure': "In {} minutes".format(departure), + 'destination': self._station_to, 'direction': self._attrs['departure']['direction']['name'], 'occupancy': self._attrs['departure']['occupancy']['name'], "platform_arriving": self._attrs['arrival']['platform'], @@ -187,6 +201,20 @@ def device_state_attributes(self): ATTR_ATTRIBUTION: "https://api.irail.be/", } + if self._show_on_map and self.station_coordinates: + attrs[ATTR_LATITUDE] = self.station_coordinates[0] + attrs[ATTR_LONGITUDE] = self.station_coordinates[1] + + if self.is_via_connection and not self._excl_vias: + via = self._attrs['vias']['via'][0] + + attrs['via'] = via['station'] + attrs['via_arrival_platform'] = via['arrival']['platform'] + attrs['via_transfer_platform'] = via['departure']['platform'] + attrs['via_transfer_time'] = get_delay_in_minutes( + via['timeBetween'] + ) + get_delay_in_minutes(via['departure']['delay']) + if delay > 0: attrs['delay'] = "{} minutes".format(delay) @@ -197,13 +225,29 @@ def state(self): """Return the state of the device.""" return self._state + @property + def station_coordinates(self): + """Get the lat, long coordinates for station.""" + if self._state is None or not self._attrs: + return [] + + latitude = float(self._attrs['departure']['stationinfo']['locationY']) + longitude = float(self._attrs['departure']['stationinfo']['locationX']) + return [latitude, longitude] + + @property + def is_via_connection(self): + """Return whether the connection goes through another station.""" + if not self._attrs: + return False + + return 'vias' in self._attrs and int(self._attrs['vias']['number']) > 0 + def update(self): """Set the state to the duration of a connection.""" connections = self._api_client.get_connections( self._station_from, self._station_to) - next_connection = None - if int(connections['connection'][0]['departure']['left']) > 0: next_connection = connections['connection'][1] else: @@ -211,6 +255,11 @@ def update(self): self._attrs = next_connection + if self._excl_vias and self.is_via_connection: + _LOGGER.debug("Skipping update of NMBSSensor \ + because this connection is a via") + return + duration = get_ride_duration( next_connection['departure']['time'], next_connection['arrival']['time'], diff --git a/homeassistant/components/sensor/nsw_fuel_station.py b/homeassistant/components/sensor/nsw_fuel_station.py index 60ad83a82f207..f0da619a6e7c7 100644 --- a/homeassistant/components/sensor/nsw_fuel_station.py +++ b/homeassistant/components/sensor/nsw_fuel_station.py @@ -28,7 +28,8 @@ CONF_ALLOWED_FUEL_TYPES = ["E10", "U91", "E85", "P95", "P98", "DL", "PDL", "B20", "LPG", "CNG", "EV"] CONF_DEFAULT_FUEL_TYPES = ["E10", "U91"] -CONF_ATTRIBUTION = "Data provided by NSW Government FuelCheck" + +ATTRIBUTION = "Data provided by NSW Government FuelCheck" PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_STATION_ID): cv.positive_int, @@ -161,7 +162,7 @@ def device_state_attributes(self) -> dict: return { ATTR_STATION_ID: self._station_data.station_id, ATTR_STATION_NAME: self._station_data.get_station_name(), - ATTR_ATTRIBUTION: CONF_ATTRIBUTION + ATTR_ATTRIBUTION: ATTRIBUTION } @property diff --git a/homeassistant/components/sensor/openexchangerates.py b/homeassistant/components/sensor/openexchangerates.py index 01c84c6303438..6361b823dea05 100644 --- a/homeassistant/components/sensor/openexchangerates.py +++ b/homeassistant/components/sensor/openexchangerates.py @@ -20,7 +20,7 @@ _LOGGER = logging.getLogger(__name__) _RESOURCE = 'https://openexchangerates.org/api/latest.json' -CONF_ATTRIBUTION = "Data provided by openexchangerates.org" +ATTRIBUTION = "Data provided by openexchangerates.org" DEFAULT_BASE = 'USD' DEFAULT_NAME = 'Exchange Rate Sensor' @@ -82,7 +82,7 @@ def state(self): def device_state_attributes(self): """Return other attributes of the sensor.""" attr = self.rest.data - attr[ATTR_ATTRIBUTION] = CONF_ATTRIBUTION + attr[ATTR_ATTRIBUTION] = ATTRIBUTION return attr diff --git a/homeassistant/components/sensor/openweathermap.py b/homeassistant/components/sensor/openweathermap.py index b6a4ff0860eb2..a137836138b2a 100644 --- a/homeassistant/components/sensor/openweathermap.py +++ b/homeassistant/components/sensor/openweathermap.py @@ -21,7 +21,8 @@ _LOGGER = logging.getLogger(__name__) -CONF_ATTRIBUTION = "Data provided by OpenWeatherMap" +ATTRIBUTION = "Data provided by OpenWeatherMap" + CONF_FORECAST = 'forecast' CONF_LANGUAGE = 'language' @@ -121,7 +122,7 @@ def unit_of_measurement(self): def device_state_attributes(self): """Return the state attributes.""" return { - ATTR_ATTRIBUTION: CONF_ATTRIBUTION, + ATTR_ATTRIBUTION: ATTRIBUTION, } def update(self): diff --git a/homeassistant/components/sensor/pollen.py b/homeassistant/components/sensor/pollen.py index eab5a14b8ca9d..d553dd8730fd9 100644 --- a/homeassistant/components/sensor/pollen.py +++ b/homeassistant/components/sensor/pollen.py @@ -1,9 +1,4 @@ -""" -Support for Pollen.com allergen and cold/flu sensors. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/sensor.pollen/ -""" +"""Support for Pollen.com allergen and cold/flu sensors.""" from datetime import timedelta import logging from statistics import mean @@ -18,7 +13,7 @@ from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle -REQUIREMENTS = ['numpy==1.16.0', 'pypollencom==2.2.2'] +REQUIREMENTS = ['numpy==1.16.1', 'pypollencom==2.2.2'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/sensor/rejseplanen.py b/homeassistant/components/sensor/rejseplanen.py index bade1bd6315a1..7a8cddb617909 100755 --- a/homeassistant/components/sensor/rejseplanen.py +++ b/homeassistant/components/sensor/rejseplanen.py @@ -31,7 +31,8 @@ ATTR_DUE_AT = 'Due at' ATTR_NEXT_UP = 'Later departure' -CONF_ATTRIBUTION = "Data provided by rejseplanen.dk" +ATTRIBUTION = "Data provided by rejseplanen.dk" + CONF_STOP_ID = 'stop_id' CONF_ROUTE = 'route' CONF_DIRECTION = 'direction' @@ -50,8 +51,8 @@ vol.Optional(CONF_DIRECTION, default=[]): vol.All(cv.ensure_list, [cv.string]), vol.Optional(CONF_DEPARTURE_TYPE, default=[]): - vol.All(cv.ensure_list, [vol.In(list(['BUS', 'EXB', 'M', - 'S', 'REG']))]) + vol.All(cv.ensure_list, + [vol.In(list(['BUS', 'EXB', 'M', 'S', 'REG']))]) }) @@ -75,12 +76,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None): departure_type = config[CONF_DEPARTURE_TYPE] data = PublicTransportData(stop_id, route, direction, departure_type) - add_devices([RejseplanenTransportSensor(data, - stop_id, - route, - direction, - name)], - True) + add_devices([RejseplanenTransportSensor( + data, stop_id, route, direction, name)], True) class RejseplanenTransportSensor(Entity): @@ -111,13 +108,11 @@ def device_state_attributes(self): if self._times is not None: next_up = None if len(self._times) > 1: - next_up = ('{} towards ' - '{} in ' - '{} from ' - '{}'.format(self._times[1][ATTR_ROUTE], - self._times[1][ATTR_DIRECTION], - str(self._times[1][ATTR_DUE_IN]), - self._times[1][ATTR_STOP_NAME])) + next_up = ('{} towards {} in {} from {}'.format( + self._times[1][ATTR_ROUTE], + self._times[1][ATTR_DIRECTION], + str(self._times[1][ATTR_DUE_IN]), + self._times[1][ATTR_STOP_NAME])) params = { ATTR_DUE_IN: str(self._times[0][ATTR_DUE_IN]), ATTR_DUE_AT: self._times[0][ATTR_DUE_AT], @@ -126,9 +121,9 @@ def device_state_attributes(self): ATTR_DIRECTION: self._times[0][ATTR_DIRECTION], ATTR_STOP_NAME: self._times[0][ATTR_STOP_NAME], ATTR_STOP_ID: self._stop_id, - ATTR_ATTRIBUTION: CONF_ATTRIBUTION, + ATTR_ATTRIBUTION: ATTRIBUTION, ATTR_NEXT_UP: next_up - } + } return {k: v for k, v in params.items() if v} @property diff --git a/homeassistant/components/sensor/rest.py b/homeassistant/components/sensor/rest.py index 4eb4b94009575..a9446ee350303 100644 --- a/homeassistant/components/sensor/rest.py +++ b/homeassistant/components/sensor/rest.py @@ -16,7 +16,7 @@ from homeassistant.const import ( CONF_AUTHENTICATION, CONF_FORCE_UPDATE, CONF_HEADERS, CONF_NAME, CONF_METHOD, CONF_PASSWORD, CONF_PAYLOAD, CONF_RESOURCE, - CONF_UNIT_OF_MEASUREMENT, CONF_USERNAME, + CONF_UNIT_OF_MEASUREMENT, CONF_USERNAME, CONF_TIMEOUT, CONF_VALUE_TEMPLATE, CONF_VERIFY_SSL, CONF_DEVICE_CLASS, HTTP_BASIC_AUTHENTICATION, HTTP_DIGEST_AUTHENTICATION) from homeassistant.exceptions import PlatformNotReady @@ -29,6 +29,7 @@ DEFAULT_NAME = 'REST Sensor' DEFAULT_VERIFY_SSL = True DEFAULT_FORCE_UPDATE = False +DEFAULT_TIMEOUT = 10 CONF_JSON_ATTRS = 'json_attributes' METHODS = ['POST', 'GET'] @@ -49,6 +50,7 @@ vol.Optional(CONF_VALUE_TEMPLATE): cv.template, vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): cv.boolean, vol.Optional(CONF_FORCE_UPDATE, default=DEFAULT_FORCE_UPDATE): cv.boolean, + vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int, }) @@ -67,6 +69,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): value_template = config.get(CONF_VALUE_TEMPLATE) json_attrs = config.get(CONF_JSON_ATTRS) force_update = config.get(CONF_FORCE_UPDATE) + timeout = config.get(CONF_TIMEOUT) if value_template is not None: value_template.hass = hass @@ -78,7 +81,8 @@ def setup_platform(hass, config, add_entities, discovery_info=None): auth = HTTPBasicAuth(username, password) else: auth = None - rest = RestData(method, resource, auth, headers, payload, verify_ssl) + rest = RestData(method, resource, auth, headers, payload, verify_ssl, + timeout) rest.update() if rest.data is None: raise PlatformNotReady @@ -174,11 +178,13 @@ def device_state_attributes(self): class RestData: """Class for handling the data retrieval.""" - def __init__(self, method, resource, auth, headers, data, verify_ssl): + 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._verify_ssl = verify_ssl + self._timeout = timeout self.data = None def update(self): @@ -187,7 +193,8 @@ def update(self): try: with requests.Session() as sess: response = sess.send( - self._request, timeout=10, verify=self._verify_ssl) + self._request, timeout=self._timeout, + verify=self._verify_ssl) self.data = response.text except requests.exceptions.RequestException as ex: diff --git a/homeassistant/components/sensor/ring.py b/homeassistant/components/sensor/ring.py index 9478768f889a2..d58e0cf8b3f5d 100644 --- a/homeassistant/components/sensor/ring.py +++ b/homeassistant/components/sensor/ring.py @@ -11,7 +11,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.components.ring import ( - CONF_ATTRIBUTION, DEFAULT_ENTITY_NAMESPACE, DATA_RING) + ATTRIBUTION, DEFAULT_ENTITY_NAMESPACE, DATA_RING) from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( CONF_ENTITY_NAMESPACE, CONF_MONITORED_CONDITIONS, @@ -122,7 +122,7 @@ def device_state_attributes(self): """Return the state attributes.""" attrs = {} - attrs[ATTR_ATTRIBUTION] = CONF_ATTRIBUTION + attrs[ATTR_ATTRIBUTION] = ATTRIBUTION attrs['device_id'] = self._data.id attrs['firmware'] = self._data.firmware attrs['kind'] = self._data.kind diff --git a/homeassistant/components/sensor/ripple.py b/homeassistant/components/sensor/ripple.py index beb7bf2226980..54530571c3eab 100644 --- a/homeassistant/components/sensor/ripple.py +++ b/homeassistant/components/sensor/ripple.py @@ -1,22 +1,16 @@ -""" -Support for Ripple sensors. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/sensor.ripple/ -""" +"""Support for Ripple sensors.""" from datetime import timedelta import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import (CONF_NAME, ATTR_ATTRIBUTION) +from homeassistant.const import ATTR_ATTRIBUTION, CONF_ADDRESS, CONF_NAME +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity REQUIREMENTS = ['python-ripple-api==0.0.3'] -CONF_ADDRESS = 'address' -CONF_ATTRIBUTION = "Data provided by ripple.com" +ATTRIBUTION = "Data provided by ripple.com" DEFAULT_NAME = 'Ripple Balance' @@ -65,7 +59,7 @@ def unit_of_measurement(self): def device_state_attributes(self): """Return the state attributes of the sensor.""" return { - ATTR_ATTRIBUTION: CONF_ATTRIBUTION, + ATTR_ATTRIBUTION: ATTRIBUTION, } def update(self): diff --git a/homeassistant/components/sensor/rova.py b/homeassistant/components/sensor/rova.py index 0b7f43f0973e5..07be331f23f35 100644 --- a/homeassistant/components/sensor/rova.py +++ b/homeassistant/components/sensor/rova.py @@ -17,11 +17,12 @@ from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle -REQUIREMENTS = ['rova==0.0.2'] +REQUIREMENTS = ['rova==0.1.0'] # Config for rova requests. CONF_ZIP_CODE = 'zip_code' CONF_HOUSE_NUMBER = 'house_number' +CONF_HOUSE_NUMBER_SUFFIX = 'house_number_suffix' UPDATE_DELAY = timedelta(hours=12) SCAN_INTERVAL = timedelta(hours=12) @@ -37,6 +38,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_ZIP_CODE): cv.string, vol.Required(CONF_HOUSE_NUMBER): cv.string, + vol.Optional(CONF_HOUSE_NUMBER_SUFFIX, default=''): cv.string, vol.Optional(CONF_NAME, default='Rova'): cv.string, vol.Optional(CONF_MONITORED_CONDITIONS, default=['bio']): vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]) @@ -52,10 +54,11 @@ def setup_platform(hass, config, add_entities, discovery_info=None): zip_code = config[CONF_ZIP_CODE] house_number = config[CONF_HOUSE_NUMBER] + house_number_suffix = config[CONF_HOUSE_NUMBER_SUFFIX] platform_name = config[CONF_NAME] # Create new Rova object to retrieve data - api = Rova(zip_code, house_number) + api = Rova(zip_code, house_number, house_number_suffix) try: if not api.is_rova_area(): diff --git a/homeassistant/components/sensor/scrape.py b/homeassistant/components/sensor/scrape.py index 6dd52789f714a..70dfae392bef5 100644 --- a/homeassistant/components/sensor/scrape.py +++ b/homeassistant/components/sensor/scrape.py @@ -26,6 +26,7 @@ CONF_ATTR = 'attribute' CONF_SELECT = 'select' +CONF_INDEX = 'index' DEFAULT_NAME = 'Web scrape' DEFAULT_VERIFY_SSL = True @@ -34,6 +35,7 @@ vol.Required(CONF_RESOURCE): cv.string, vol.Required(CONF_SELECT): cv.string, vol.Optional(CONF_ATTR): cv.string, + vol.Optional(CONF_INDEX, default=0): cv.positive_int, vol.Optional(CONF_AUTHENTICATION): vol.In([HTTP_BASIC_AUTHENTICATION, HTTP_DIGEST_AUTHENTICATION]), vol.Optional(CONF_HEADERS): vol.Schema({cv.string: cv.string}), @@ -56,6 +58,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): verify_ssl = config.get(CONF_VERIFY_SSL) select = config.get(CONF_SELECT) attr = config.get(CONF_ATTR) + index = config.get(CONF_INDEX) unit = config.get(CONF_UNIT_OF_MEASUREMENT) username = config.get(CONF_USERNAME) password = config.get(CONF_PASSWORD) @@ -77,19 +80,21 @@ def setup_platform(hass, config, add_entities, discovery_info=None): raise PlatformNotReady add_entities([ - ScrapeSensor(rest, name, select, attr, value_template, unit)], True) + ScrapeSensor(rest, name, select, attr, index, value_template, unit)], + True) class ScrapeSensor(Entity): """Representation of a web scrape sensor.""" - def __init__(self, rest, name, select, attr, value_template, unit): + def __init__(self, rest, name, select, attr, index, value_template, unit): """Initialize a web scrape sensor.""" self.rest = rest self._name = name self._state = None self._select = select self._attr = attr + self._index = index self._value_template = value_template self._unit_of_measurement = unit @@ -119,9 +124,9 @@ def update(self): try: if self._attr is not None: - value = raw_data.select(self._select)[0][self._attr] + value = raw_data.select(self._select)[self._index][self._attr] else: - value = raw_data.select(self._select)[0].text + value = raw_data.select(self._select)[self._index].text _LOGGER.debug(value) except IndexError: _LOGGER.error("Unable to extract data from HTML") diff --git a/homeassistant/components/sensor/shodan.py b/homeassistant/components/sensor/shodan.py index 1cce17cf64a7f..ee64eecf3fe02 100644 --- a/homeassistant/components/sensor/shodan.py +++ b/homeassistant/components/sensor/shodan.py @@ -1,9 +1,4 @@ -""" -Sensor for displaying the number of result on Shodan.io. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/sensor.shodan/ -""" +"""Sensor for displaying the number of result on Shodan.io.""" import logging from datetime import timedelta @@ -14,7 +9,7 @@ from homeassistant.const import ATTR_ATTRIBUTION, CONF_API_KEY, CONF_NAME from homeassistant.helpers.entity import Entity -REQUIREMENTS = ['shodan==1.10.4'] +REQUIREMENTS = ['shodan==1.11.1'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/sensor/sochain.py b/homeassistant/components/sensor/sochain.py index b582ba045673e..ef6a53b709199 100644 --- a/homeassistant/components/sensor/sochain.py +++ b/homeassistant/components/sensor/sochain.py @@ -1,27 +1,22 @@ -""" -Support for watching multiple cryptocurrencies. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/sensor.sochain/ -""" -import logging +"""Support for watching multiple cryptocurrencies.""" from datetime import timedelta +import logging 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.helpers.entity import Entity +from homeassistant.const import ATTR_ATTRIBUTION, CONF_ADDRESS, CONF_NAME from homeassistant.helpers.aiohttp_client import async_get_clientsession +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity REQUIREMENTS = ['python-sochain-api==0.0.2'] _LOGGER = logging.getLogger(__name__) -CONF_ADDRESS = 'address' +ATTRIBUTION = "Data provided by chain.so" + CONF_NETWORK = 'network' -CONF_ATTRIBUTION = "Data provided by chain.so" DEFAULT_NAME = 'Crypto Balance' @@ -34,8 +29,8 @@ }) -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): """Set up the sochain sensors.""" from pysochain import ChainSo address = config.get(CONF_ADDRESS) @@ -77,7 +72,7 @@ def unit_of_measurement(self): def device_state_attributes(self): """Return the state attributes of the sensor.""" return { - ATTR_ATTRIBUTION: CONF_ATTRIBUTION, + ATTR_ATTRIBUTION: ATTRIBUTION, } async def async_update(self): diff --git a/homeassistant/components/sensor/sql.py b/homeassistant/components/sensor/sql.py index f780158dd4ee7..bd246c0d01cfd 100644 --- a/homeassistant/components/sensor/sql.py +++ b/homeassistant/components/sensor/sql.py @@ -1,9 +1,4 @@ -""" -Sensor from an SQL Query. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/sensor.sql/ -""" +"""Sensor from an SQL Query.""" import decimal import datetime import logging @@ -20,7 +15,7 @@ _LOGGER = logging.getLogger(__name__) -REQUIREMENTS = ['sqlalchemy==1.2.17'] +REQUIREMENTS = ['sqlalchemy==1.2.18'] CONF_COLUMN_NAME = 'column' CONF_QUERIES = 'queries' diff --git a/homeassistant/components/sensor/starlingbank.py b/homeassistant/components/sensor/starlingbank.py index a0c6f23e496d5..9cb576707402e 100644 --- a/homeassistant/components/sensor/starlingbank.py +++ b/homeassistant/components/sensor/starlingbank.py @@ -14,7 +14,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity -REQUIREMENTS = ['starlingbank==1.2'] +REQUIREMENTS = ['starlingbank==3.0'] _LOGGER = logging.getLogger(__name__) @@ -44,7 +44,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Sterling Bank sensor platform.""" - from starlingbank import StarlingAccount + from starlingbank import StarlingAccount # pylint: disable=syntax-error sensors = [] for account in config[CONF_ACCOUNTS]: @@ -96,8 +96,8 @@ def icon(self): def update(self): """Fetch new state data for the sensor.""" - self._starling_account.balance.update() + self._starling_account.update_balance_data() if self._balance_data_type == 'cleared_balance': - self._state = self._starling_account.balance.cleared_balance + self._state = self._starling_account.cleared_balance / 100 elif self._balance_data_type == 'effective_balance': - self._state = self._starling_account.balance.effective_balance + self._state = self._starling_account.effective_balance / 100 diff --git a/homeassistant/components/sensor/swiss_public_transport.py b/homeassistant/components/sensor/swiss_public_transport.py index 6b34930075afc..d9f2410f8cafa 100644 --- a/homeassistant/components/sensor/swiss_public_transport.py +++ b/homeassistant/components/sensor/swiss_public_transport.py @@ -30,7 +30,8 @@ ATTR_TRAIN_NUMBER = 'train_number' ATTR_TRANSFERS = 'transfers' -CONF_ATTRIBUTION = "Data provided by transport.opendata.ch" +ATTRIBUTION = "Data provided by transport.opendata.ch" + CONF_DESTINATION = 'to' CONF_START = 'from' @@ -113,7 +114,7 @@ def device_state_attributes(self): ATTR_START: self._opendata.from_name, ATTR_TARGET: self._opendata.to_name, ATTR_REMAINING_TIME: '{}'.format(self._remaining_time), - ATTR_ATTRIBUTION: CONF_ATTRIBUTION, + ATTR_ATTRIBUTION: ATTRIBUTION, } return attr diff --git a/homeassistant/components/sensor/synologydsm.py b/homeassistant/components/sensor/synologydsm.py index 39a9e75c47b8e..2b44373823080 100644 --- a/homeassistant/components/sensor/synologydsm.py +++ b/homeassistant/components/sensor/synologydsm.py @@ -1,9 +1,4 @@ -""" -Support for Synology NAS Sensors. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/sensor.synologydsm/ -""" +"""Support for Synology NAS Sensors.""" import logging from datetime import timedelta @@ -22,7 +17,8 @@ _LOGGER = logging.getLogger(__name__) -CONF_ATTRIBUTION = 'Data provided by Synology' +ATTRIBUTION = 'Data provided by Synology' + CONF_VOLUMES = 'volumes' DEFAULT_NAME = 'Synology DSM' DEFAULT_PORT = 5001 @@ -194,7 +190,7 @@ def update(self): def device_state_attributes(self): """Return the state attributes.""" return { - ATTR_ATTRIBUTION: CONF_ATTRIBUTION, + ATTR_ATTRIBUTION: ATTRIBUTION, } diff --git a/homeassistant/components/sensor/systemmonitor.py b/homeassistant/components/sensor/systemmonitor.py index d0b3df5dd0eaf..70fb1f91051aa 100644 --- a/homeassistant/components/sensor/systemmonitor.py +++ b/homeassistant/components/sensor/systemmonitor.py @@ -1,9 +1,4 @@ -""" -Support for monitoring the local system. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/sensor.systemmonitor/ -""" +"""Support for monitoring the local system.""" import logging import os import socket @@ -16,7 +11,7 @@ import homeassistant.helpers.config_validation as cv import homeassistant.util.dt as dt_util -REQUIREMENTS = ['psutil==5.5.0'] +REQUIREMENTS = ['psutil==5.5.1'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/sensor/sytadin.py b/homeassistant/components/sensor/sytadin.py index 082342a0393cc..f8ef18fcffe74 100644 --- a/homeassistant/components/sensor/sytadin.py +++ b/homeassistant/components/sensor/sytadin.py @@ -24,8 +24,7 @@ URL = 'http://www.sytadin.fr/sys/barometres_de_la_circulation.jsp.html' -CONF_ATTRIBUTION = "Data provided by Direction des routes Île-de-France" \ - "(DiRIF)" +ATTRIBUTION = "Data provided by Direction des routes Île-de-France (DiRIF)" DEFAULT_NAME = 'Sytadin' REGEX = r'(\d*\.\d+|\d+)' @@ -95,7 +94,7 @@ def unit_of_measurement(self): def device_state_attributes(self): """Return the state attributes.""" return { - ATTR_ATTRIBUTION: CONF_ATTRIBUTION, + ATTR_ATTRIBUTION: ATTRIBUTION, } def update(self): diff --git a/homeassistant/components/sensor/trafikverket_weatherstation.py b/homeassistant/components/sensor/trafikverket_weatherstation.py index 433bb8e9ed175..38cc23dabbe49 100644 --- a/homeassistant/components/sensor/trafikverket_weatherstation.py +++ b/homeassistant/components/sensor/trafikverket_weatherstation.py @@ -24,12 +24,13 @@ _LOGGER = logging.getLogger(__name__) -SCAN_INTERVAL = timedelta(seconds=300) -MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=10) +ATTRIBUTION = "Data provided by Trafikverket API" -CONF_ATTRIBUTION = "Data provided by Trafikverket API" CONF_STATION = 'station' +MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=10) + +SCAN_INTERVAL = timedelta(seconds=300) SENSOR_TYPES = { 'air_temp': ['Air temperature', '°C', 'air_temp'], @@ -50,8 +51,8 @@ }) -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): """Set up the Trafikverket sensor platform.""" from pytrafikverket.trafikverket_weather import TrafikverketWeather @@ -85,7 +86,7 @@ def __init__(self, weather_api, name, sensor_type, sensor_station): self._station = sensor_station self._weather_api = weather_api self._attributes = { - ATTR_ATTRIBUTION: CONF_ATTRIBUTION, + ATTR_ATTRIBUTION: ATTRIBUTION, } self._weather = None diff --git a/homeassistant/components/sensor/transport_nsw.py b/homeassistant/components/sensor/transport_nsw.py index 2e28d81a2c36f..3c40bf4f709e7 100644 --- a/homeassistant/components/sensor/transport_nsw.py +++ b/homeassistant/components/sensor/transport_nsw.py @@ -1,9 +1,4 @@ -""" -Transport NSW (AU) sensor to query next leave event for a specified stop. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/sensor.transport_nsw/ -""" +"""Support for Transport NSW (AU) to query next leave event.""" from datetime import timedelta import logging @@ -26,7 +21,8 @@ ATTR_DESTINATION = 'destination' ATTR_MODE = 'mode' -CONF_ATTRIBUTION = "Data provided by Transport NSW" +ATTRIBUTION = "Data provided by Transport NSW" + CONF_STOP_ID = 'stop_id' CONF_ROUTE = 'route' CONF_DESTINATION = 'destination' @@ -40,7 +36,7 @@ 'Ferry': 'mdi:ferry', 'Schoolbus': 'mdi:bus', 'n/a': 'mdi:clock', - None: 'mdi:clock' + None: 'mdi:clock', } SCAN_INTERVAL = timedelta(seconds=60) @@ -99,7 +95,7 @@ def device_state_attributes(self): ATTR_REAL_TIME: self._times[ATTR_REAL_TIME], ATTR_DESTINATION: self._times[ATTR_DESTINATION], ATTR_MODE: self._times[ATTR_MODE], - ATTR_ATTRIBUTION: CONF_ATTRIBUTION + ATTR_ATTRIBUTION: ATTRIBUTION } @property diff --git a/homeassistant/components/sensor/travisci.py b/homeassistant/components/sensor/travisci.py index e1bd74b993ce9..c96bb18e95800 100644 --- a/homeassistant/components/sensor/travisci.py +++ b/homeassistant/components/sensor/travisci.py @@ -20,7 +20,8 @@ _LOGGER = logging.getLogger(__name__) -CONF_ATTRIBUTION = "Information provided by https://travis-ci.org/" +ATTRIBUTION = "Information provided by https://travis-ci.org/" + CONF_BRANCH = 'branch' CONF_REPOSITORY = 'repository' @@ -130,7 +131,7 @@ def state(self): def device_state_attributes(self): """Return the state attributes.""" attrs = {} - attrs[ATTR_ATTRIBUTION] = CONF_ATTRIBUTION + attrs[ATTR_ATTRIBUTION] = ATTRIBUTION if self._build and self._state is not None: if self._user and self._sensor_type == 'state': diff --git a/homeassistant/components/sensor/vasttrafik.py b/homeassistant/components/sensor/vasttrafik.py index 124b0ff44ead6..8148a5c2fc7bd 100644 --- a/homeassistant/components/sensor/vasttrafik.py +++ b/homeassistant/components/sensor/vasttrafik.py @@ -24,8 +24,8 @@ ATTR_DIRECTION = 'direction' ATTR_LINE = 'line' ATTR_TRACK = 'track' +ATTRIBUTION = "Data provided by Västtrafik" -CONF_ATTRIBUTION = "Data provided by Västtrafik" CONF_DELAY = 'delay' CONF_DEPARTURES = 'departures' CONF_FROM = 'from' @@ -137,7 +137,7 @@ def update(self): params = { ATTR_ACCESSIBILITY: departure.get('accessibility'), - ATTR_ATTRIBUTION: CONF_ATTRIBUTION, + ATTR_ATTRIBUTION: ATTRIBUTION, ATTR_DIRECTION: departure.get('direction'), ATTR_LINE: departure.get('sname'), ATTR_TRACK: departure.get('track'), diff --git a/homeassistant/components/sensor/viaggiatreno.py b/homeassistant/components/sensor/viaggiatreno.py index 82068c456b60e..2b8de2042facb 100644 --- a/homeassistant/components/sensor/viaggiatreno.py +++ b/homeassistant/components/sensor/viaggiatreno.py @@ -18,7 +18,8 @@ _LOGGER = logging.getLogger(__name__) -CONF_ATTRIBUTION = "Powered by ViaggiaTreno Data" +ATTRIBUTION = "Powered by ViaggiaTreno Data" + VIAGGIATRENO_ENDPOINT = ("http://www.viaggiatreno.it/viaggiatrenonew/" "resteasy/viaggiatreno/andamentoTreno/" "{station_id}/{train_id}") @@ -35,7 +36,7 @@ 'orarioPartenza', 'origine', 'subTitle', - ] +] DEFAULT_NAME = "Train {}" @@ -121,7 +122,7 @@ def unit_of_measurement(self): @property def device_state_attributes(self): """Return extra attributes.""" - self._attributes[ATTR_ATTRIBUTION] = CONF_ATTRIBUTION + self._attributes[ATTR_ATTRIBUTION] = ATTRIBUTION return self._attributes @staticmethod diff --git a/homeassistant/components/sensor/waze_travel_time.py b/homeassistant/components/sensor/waze_travel_time.py index ae38c529fe254..83b4f3ad9340f 100644 --- a/homeassistant/components/sensor/waze_travel_time.py +++ b/homeassistant/components/sensor/waze_travel_time.py @@ -18,7 +18,7 @@ from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle -REQUIREMENTS = ['WazeRouteCalculator==0.6'] +REQUIREMENTS = ['WazeRouteCalculator==0.9'] _LOGGER = logging.getLogger(__name__) @@ -26,7 +26,8 @@ ATTR_DISTANCE = 'distance' ATTR_ROUTE = 'route' -CONF_ATTRIBUTION = "Powered by Waze" +ATTRIBUTION = "Powered by Waze" + CONF_DESTINATION = 'destination' CONF_ORIGIN = 'origin' CONF_INCL_FILTER = 'incl_filter' @@ -43,7 +44,7 @@ SCAN_INTERVAL = timedelta(minutes=5) MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=5) -TRACKABLE_DOMAINS = ['device_tracker', 'sensor', 'zone'] +TRACKABLE_DOMAINS = ['device_tracker', 'sensor', 'zone', 'person'] PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_ORIGIN): cv.string, @@ -69,7 +70,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): sensor = WazeTravelTime(name, origin, destination, region, incl_filter, excl_filter, realtime) - add_entities([sensor], True) + add_entities([sensor]) # Wait until start event is sent to load this component. hass.bus.listen_once( @@ -138,7 +139,7 @@ def device_state_attributes(self): if self._state is None: return None - res = {ATTR_ATTRIBUTION: CONF_ATTRIBUTION} + res = {ATTR_ATTRIBUTION: ATTRIBUTION} if 'duration' in self._state: res[ATTR_DURATION] = self._state['duration'] if 'distance' in self._state: @@ -203,7 +204,8 @@ def update(self): if self._destination is not None and self._origin is not None: try: params = WazeRouteCalculator.WazeRouteCalculator( - self._origin, self._destination, self._region) + self._origin, self._destination, self._region, + log_lvl=logging.DEBUG) routes = params.calc_all_routes_info(real_time=self._realtime) if self._incl_filter is not None: diff --git a/homeassistant/components/sensor/worldtidesinfo.py b/homeassistant/components/sensor/worldtidesinfo.py index fea3e92a140a5..0f7bfeaa90020 100644 --- a/homeassistant/components/sensor/worldtidesinfo.py +++ b/homeassistant/components/sensor/worldtidesinfo.py @@ -19,7 +19,7 @@ _LOGGER = logging.getLogger(__name__) -CONF_ATTRIBUTION = "Data provided by WorldTides" +ATTRIBUTION = "Data provided by WorldTides" DEFAULT_NAME = 'WorldTidesInfo' @@ -72,7 +72,7 @@ def name(self): @property def device_state_attributes(self): """Return the state attributes of this device.""" - attr = {ATTR_ATTRIBUTION: CONF_ATTRIBUTION} + attr = {ATTR_ATTRIBUTION: ATTRIBUTION} if 'High' in str(self.data['extremes'][0]['type']): attr['high_tide_time_utc'] = self.data['extremes'][0]['date'] diff --git a/homeassistant/components/sensor/wsdot.py b/homeassistant/components/sensor/wsdot.py index 84f2e8622c60a..4e53a2c17c4b9 100644 --- a/homeassistant/components/sensor/wsdot.py +++ b/homeassistant/components/sensor/wsdot.py @@ -26,7 +26,7 @@ ATTR_TIME_UPDATED = 'TimeUpdated' ATTR_TRAVEL_TIME_ID = 'TravelTimeID' -CONF_ATTRIBUTION = "Data provided by WSDOT" +ATTRIBUTION = "Data provided by WSDOT" CONF_TRAVEL_TIMES = 'travel_time' @@ -115,7 +115,7 @@ def update(self): def device_state_attributes(self): """Return other details about the sensor state.""" if self._data is not None: - attrs = {ATTR_ATTRIBUTION: CONF_ATTRIBUTION} + attrs = {ATTR_ATTRIBUTION: ATTRIBUTION} for key in [ATTR_AVG_TIME, ATTR_NAME, ATTR_DESCRIPTION, ATTR_TRAVEL_TIME_ID]: attrs[key] = self._data.get(key) diff --git a/homeassistant/components/sensor/wunderground.py b/homeassistant/components/sensor/wunderground.py index be42e10e894e8..74a4c2089b236 100644 --- a/homeassistant/components/sensor/wunderground.py +++ b/homeassistant/components/sensor/wunderground.py @@ -29,7 +29,8 @@ _RESOURCE = 'http://api.wunderground.com/api/{}/{}/{}/q/' _LOGGER = logging.getLogger(__name__) -CONF_ATTRIBUTION = "Data provided by the WUnderground weather service" +ATTRIBUTION = "Data provided by the WUnderground weather service" + CONF_PWS_ID = 'pws_id' CONF_LANG = 'lang' @@ -679,9 +680,7 @@ def __init__(self, hass: HomeAssistantType, rest, condition, self.rest = rest self._condition = condition self._state = None - self._attributes = { - ATTR_ATTRIBUTION: CONF_ATTRIBUTION, - } + self._attributes = {ATTR_ATTRIBUTION: ATTRIBUTION} self._icon = None self._entity_picture = None self._unit_of_measurement = self._cfg_expand("unit_of_measurement") diff --git a/homeassistant/components/sensor/yr.py b/homeassistant/components/sensor/yr.py index 0cb9c3765ecab..665c482f050b5 100644 --- a/homeassistant/components/sensor/yr.py +++ b/homeassistant/components/sensor/yr.py @@ -29,8 +29,8 @@ _LOGGER = logging.getLogger(__name__) -CONF_ATTRIBUTION = "Weather forecast from met.no, delivered " \ - "by the Norwegian Meteorological Institute." +ATTRIBUTION = "Weather forecast from met.no, delivered by the Norwegian " \ + "Meteorological Institute." # https://api.met.no/license_data.html SENSOR_TYPES = { @@ -134,7 +134,7 @@ def entity_picture(self): def device_state_attributes(self): """Return the state attributes.""" return { - ATTR_ATTRIBUTION: CONF_ATTRIBUTION, + ATTR_ATTRIBUTION: ATTRIBUTION, } @property diff --git a/homeassistant/components/sensor/yweather.py b/homeassistant/components/sensor/yweather.py index 243329680e1c7..349ee2c7aaee2 100644 --- a/homeassistant/components/sensor/yweather.py +++ b/homeassistant/components/sensor/yweather.py @@ -21,7 +21,8 @@ _LOGGER = logging.getLogger(__name__) -CONF_ATTRIBUTION = "Weather details provided by Yahoo! Inc." +ATTRIBUTION = "Weather details provided by Yahoo! Inc." + CONF_FORECAST = 'forecast' CONF_WOEID = 'woeid' @@ -131,7 +132,7 @@ def entity_picture(self): @property def device_state_attributes(self): """Return the state attributes.""" - attrs = {ATTR_ATTRIBUTION: CONF_ATTRIBUTION} + attrs = {ATTR_ATTRIBUTION: ATTRIBUTION} if self._code is not None and "weather" in self._type: attrs['condition_code'] = self._code diff --git a/homeassistant/components/sensor/zestimate.py b/homeassistant/components/sensor/zestimate.py index a04df22cf072c..ed3af84d396cc 100644 --- a/homeassistant/components/sensor/zestimate.py +++ b/homeassistant/components/sensor/zestimate.py @@ -22,8 +22,9 @@ _LOGGER = logging.getLogger(__name__) _RESOURCE = 'http://www.zillow.com/webservice/GetZestimate.htm' +ATTRIBUTION = "Data provided by Zillow.com" + CONF_ZPID = 'zpid' -CONF_ATTRIBUTION = "Data provided by Zillow.com" DEFAULT_NAME = 'Zestimate' NAME = 'zestimate' @@ -93,7 +94,7 @@ def device_state_attributes(self): if self.data is not None: attributes = self.data attributes['address'] = self.address - attributes[ATTR_ATTRIBUTION] = CONF_ATTRIBUTION + attributes[ATTR_ATTRIBUTION] = ATTRIBUTION return attributes @property diff --git a/homeassistant/components/simplisafe/.translations/es-419.json b/homeassistant/components/simplisafe/.translations/es-419.json new file mode 100644 index 0000000000000..709d045c3486e --- /dev/null +++ b/homeassistant/components/simplisafe/.translations/es-419.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "identifier_exists": "Cuenta ya registrada", + "invalid_credentials": "Credenciales no v\u00e1lidas" + }, + "step": { + "user": { + "data": { + "code": "C\u00f3digo (para Home Assistant)", + "password": "Contrase\u00f1a", + "username": "Direcci\u00f3n de correo electr\u00f3nico" + }, + "title": "Completa tu informaci\u00f3n" + } + }, + "title": "SimpliSafe" + } +} \ No newline at end of file diff --git a/homeassistant/components/simplisafe/.translations/sv.json b/homeassistant/components/simplisafe/.translations/sv.json new file mode 100644 index 0000000000000..4666a9ea182ec --- /dev/null +++ b/homeassistant/components/simplisafe/.translations/sv.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "identifier_exists": "Kontot \u00e4r redan registrerat", + "invalid_credentials": "Ogiltiga autentiseringsuppgifter" + }, + "step": { + "user": { + "data": { + "code": "Kod (f\u00f6r Home Assistant)", + "password": "L\u00f6senord", + "username": "E-postadress" + }, + "title": "Fyll i din information" + } + }, + "title": "SimpliSafe" + } +} \ No newline at end of file diff --git a/homeassistant/components/simplisafe/__init__.py b/homeassistant/components/simplisafe/__init__.py index fcd9d15839b7c..f494ccf390e30 100644 --- a/homeassistant/components/simplisafe/__init__.py +++ b/homeassistant/components/simplisafe/__init__.py @@ -18,7 +18,7 @@ from .config_flow import configured_instances from .const import DATA_CLIENT, DEFAULT_SCAN_INTERVAL, DOMAIN, TOPIC_UPDATE -REQUIREMENTS = ['simplisafe-python==3.1.14'] +REQUIREMENTS = ['simplisafe-python==3.4.1'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/skybell/__init__.py b/homeassistant/components/skybell/__init__.py index 8724f7d3d667c..31d1339fbcfc8 100644 --- a/homeassistant/components/skybell/__init__.py +++ b/homeassistant/components/skybell/__init__.py @@ -13,7 +13,7 @@ _LOGGER = logging.getLogger(__name__) -CONF_ATTRIBUTION = "Data provided by Skybell.com" +ATTRIBUTION = "Data provided by Skybell.com" NOTIFICATION_ID = 'skybell_notification' NOTIFICATION_TITLE = 'Skybell Sensor Setup' @@ -76,7 +76,7 @@ def update(self): def device_state_attributes(self): """Return the state attributes.""" return { - ATTR_ATTRIBUTION: CONF_ATTRIBUTION, + ATTR_ATTRIBUTION: ATTRIBUTION, 'device_id': self._device.device_id, 'status': self._device.status, 'location': self._device.location, diff --git a/homeassistant/components/smartthings/.translations/ca.json b/homeassistant/components/smartthings/.translations/ca.json index 3c0ca05a8d5cd..1f27e781ee3a5 100644 --- a/homeassistant/components/smartthings/.translations/ca.json +++ b/homeassistant/components/smartthings/.translations/ca.json @@ -7,7 +7,8 @@ "token_already_setup": "El testimoni d'autenticaci\u00f3 ja ha estat configurat.", "token_forbidden": "El testimoni d'autenticaci\u00f3 no t\u00e9 cont\u00e9 els apartats OAuth obligatoris.", "token_invalid_format": "El testimoni d'autenticaci\u00f3 ha d'estar en format UID/GUID", - "token_unauthorized": "El testimoni d'autenticaci\u00f3 no \u00e9s v\u00e0lid o ja no t\u00e9 autoritzaci\u00f3." + "token_unauthorized": "El testimoni d'autenticaci\u00f3 no \u00e9s v\u00e0lid o ja no t\u00e9 autoritzaci\u00f3.", + "webhook_error": "SmartThings no ha pogut validar l'adre\u00e7a final configurada a `base_url`. Revisa els [requisits del component]({component_url})." }, "step": { "user": { diff --git a/homeassistant/components/smartthings/.translations/da.json b/homeassistant/components/smartthings/.translations/da.json index 1c571b4e6392d..1841206939480 100644 --- a/homeassistant/components/smartthings/.translations/da.json +++ b/homeassistant/components/smartthings/.translations/da.json @@ -7,7 +7,8 @@ "token_already_setup": "Token er allerede konfigureret.", "token_forbidden": "Adgangstoken er ikke indenfor OAuth", "token_invalid_format": "Adgangstoken skal v\u00e6re i UID/GUID format", - "token_unauthorized": "Adgangstoken er ugyldigt eller ikke l\u00e6ngere godkendt." + "token_unauthorized": "Adgangstoken er ugyldigt eller ikke l\u00e6ngere godkendt.", + "webhook_error": "SmartThings kunne ikke validere slutpunktet konfigureret i `base_url`. Gennemg\u00e5 venligst komponentkravene." }, "step": { "user": { diff --git a/homeassistant/components/smartthings/.translations/de.json b/homeassistant/components/smartthings/.translations/de.json index f65c338bf03d7..dd672dee9f640 100644 --- a/homeassistant/components/smartthings/.translations/de.json +++ b/homeassistant/components/smartthings/.translations/de.json @@ -1,5 +1,15 @@ { "config": { + "error": { + "app_not_installed": "Stelle sicher, dass du die Home Assistant SmartApp installiert und autorisiert hast, und versuche es erneut.", + "app_setup_error": "SmartApp kann nicht eingerichtet werden. Bitte versuche es erneut.", + "base_url_not_https": "Die `base_url` f\u00fcr die` http`-Komponente muss konfiguriert sein und mit `https://` beginnen.", + "token_already_setup": "Das Token wurde bereits eingerichtet.", + "token_forbidden": "Das Token verf\u00fcgt nicht \u00fcber die erforderlichen OAuth-Bereiche.", + "token_invalid_format": "Das Token muss im UID/GUID-Format vorliegen.", + "token_unauthorized": "Das Token ist ung\u00fcltig oder nicht mehr autorisiert.", + "webhook_error": "SmartThings konnte den in 'base_url' angegebenen Endpunkt nicht validieren. Bitte \u00fcberpr\u00fcfe die Komponentenanforderungen." + }, "step": { "user": { "data": { @@ -9,6 +19,7 @@ "title": "Gib den pers\u00f6nlichen Zugangstoken an" }, "wait_install": { + "description": "Installieren Sie Home-Assistent SmartApp an mindestens einer Stelle, und klicken Sie auf Absenden.", "title": "SmartApp installieren" } }, diff --git a/homeassistant/components/smartthings/.translations/es-419.json b/homeassistant/components/smartthings/.translations/es-419.json new file mode 100644 index 0000000000000..4dc9432469513 --- /dev/null +++ b/homeassistant/components/smartthings/.translations/es-419.json @@ -0,0 +1,25 @@ +{ + "config": { + "error": { + "app_setup_error": "No se puede configurar el SmartApp. Por favor, int\u00e9ntelo de nuevo.", + "base_url_not_https": "El `base_url` para el componente `http` debe estar configurado y empezar por `https://`.", + "token_already_setup": "El token ya ha sido configurado.", + "token_invalid_format": "El token debe estar en formato UID/GUID", + "token_unauthorized": "El token no es v\u00e1lido o ya no est\u00e1 autorizado.", + "webhook_error": "SmartThings no pudo validar el endpoint configurado en `base_url`. Por favor, revise los requisitos de los componentes." + }, + "step": { + "user": { + "data": { + "access_token": "Token de acceso" + }, + "title": "Ingresar token de acceso personal" + }, + "wait_install": { + "description": "Instale la SmartApp de Home Assistant en al menos una ubicaci\u00f3n y haga clic en enviar.", + "title": "Instalar SmartApp" + } + }, + "title": "SmartThings" + } +} \ No newline at end of file diff --git a/homeassistant/components/smartthings/.translations/es.json b/homeassistant/components/smartthings/.translations/es.json new file mode 100644 index 0000000000000..4edeb15392171 --- /dev/null +++ b/homeassistant/components/smartthings/.translations/es.json @@ -0,0 +1,23 @@ +{ + "config": { + "error": { + "app_not_installed": "Por favor aseg\u00farese de haber instalado y autorizado Home Assistant SmartApp y vuelva a intentarlo.", + "app_setup_error": "No se pudo configurar el SmartApp. Por favor, int\u00e9ntelo de nuevo.", + "token_already_setup": "El token ya ha sido configurado.", + "token_invalid_format": "El token debe estar en formato UID/GUID", + "token_unauthorized": "El token no es v\u00e1lido o ya no est\u00e1 autorizado." + }, + "step": { + "user": { + "data": { + "access_token": "Token de acceso" + }, + "title": "Ingresar token de acceso personal" + }, + "wait_install": { + "title": "Instalar SmartApp" + } + }, + "title": "SmartThings" + } +} \ No newline at end of file diff --git a/homeassistant/components/smartthings/.translations/he.json b/homeassistant/components/smartthings/.translations/he.json new file mode 100644 index 0000000000000..c38afd989d250 --- /dev/null +++ b/homeassistant/components/smartthings/.translations/he.json @@ -0,0 +1,28 @@ +{ + "config": { + "error": { + "app_not_installed": "\u05d0\u05e0\u05d0 \u05d5\u05d3\u05d0 \u05e9\u05d4\u05ea\u05e7\u05e0\u05ea \u05d0\u05d9\u05e9\u05e8\u05ea \u05d0\u05ea Home Assistant SmartApp \u05d5\u05dc\u05e0\u05e1\u05d5\u05ea \u05e9\u05d5\u05d1.", + "app_setup_error": "\u05d0\u05d9\u05df \u05d0\u05e4\u05e9\u05e8\u05d5\u05ea \u05dc\u05d4\u05d2\u05d3\u05d9\u05e8 \u05d0\u05ea SmartApp. \u05e0\u05d0 \u05e0\u05e1\u05d4 \u05e9\u05d5\u05d1.", + "base_url_not_https": "\u05d9\u05e9 \u05dc\u05d4\u05d2\u05d3\u05d9\u05e8 \u05d0\u05ea \u05d4- `base_url` \u05e2\u05d1\u05d5\u05e8 \u05e8\u05db\u05d9\u05d1` http` \u05d5\u05dc\u05d4\u05ea\u05d7\u05d9\u05dc \u05d1- `https: //.", + "token_already_setup": "\u05d4\u05d0\u05e1\u05d9\u05de\u05d5\u05df \u05db\u05d1\u05e8 \u05d4\u05d5\u05d2\u05d3\u05e8.", + "token_forbidden": "\u05dc\u05d0\u05e1\u05d9\u05de\u05d5\u05df \u05d0\u05d9\u05df \u05d0\u05ea \u05d8\u05d5\u05d5\u05d7\u05d9 OAuth \u05d4\u05d3\u05e8\u05d5\u05e9\u05d9\u05dd.", + "token_invalid_format": "\u05d4\u05d0\u05e1\u05d9\u05de\u05d5\u05df \u05d7\u05d9\u05d9\u05d1 \u05dc\u05d4\u05d9\u05d5\u05ea \u05d1\u05e4\u05d5\u05e8\u05de\u05d8 UID / GUID", + "token_unauthorized": "\u05d4\u05d0\u05e1\u05d9\u05de\u05d5\u05df \u05d0\u05d9\u05e0\u05d5 \u05d7\u05d5\u05e7\u05d9 \u05d0\u05d5 \u05d0\u05d9\u05e0\u05d5 \u05de\u05d5\u05e8\u05e9\u05d4 \u05e2\u05d5\u05d3.", + "webhook_error": "SmartThings \u05dc\u05d0 \u05d4\u05e6\u05dc\u05d9\u05d7 \u05dc\u05d0\u05de\u05ea \u05d0\u05ea \u05e0\u05e7\u05d5\u05d3\u05ea \u05d4\u05e7\u05e6\u05d4 \u05e9\u05d4\u05d5\u05d2\u05d3\u05e8\u05d4 \u05d1- `base_url`. \u05e2\u05d9\u05d9\u05df \u05d1\u05d3\u05e8\u05d9\u05e9\u05d5\u05ea \u05d4\u05e8\u05db\u05d9\u05d1." + }, + "step": { + "user": { + "data": { + "access_token": "\u05d0\u05e1\u05d9\u05de\u05d5\u05df \u05d2\u05d9\u05e9\u05d4" + }, + "description": "\u05d4\u05d6\u05df SmartThings [\u05d0\u05e1\u05d9\u05de\u05d5\u05df \u05d2\u05d9\u05e9\u05d4 \u05d0\u05d9\u05e9\u05d9\u05ea] ( {token_url} ) \u05e9\u05e0\u05d5\u05e6\u05e8 \u05dc\u05e4\u05d9 [\u05d4\u05d5\u05e8\u05d0\u05d5\u05ea] ( {component_url} ).", + "title": "\u05d4\u05d6\u05df \u05d0\u05e1\u05d9\u05de\u05d5\u05df \u05d2\u05d9\u05e9\u05d4 \u05d0\u05d9\u05e9\u05d9 " + }, + "wait_install": { + "description": "\u05d4\u05ea\u05e7\u05df \u05d0\u05ea \u05d4- Home Assistant SmartApp \u05dc\u05e4\u05d7\u05d5\u05ea \u05d1\u05de\u05d9\u05e7\u05d5\u05dd \u05d0\u05d7\u05d3 \u05d5\u05dc\u05d7\u05e5 \u05e2\u05dc \u05e9\u05dc\u05d7.", + "title": "\u05d4\u05ea\u05e7\u05df \u05d0\u05ea SmartApp" + } + }, + "title": "SmartThings" + } +} \ No newline at end of file diff --git a/homeassistant/components/smartthings/.translations/hu.json b/homeassistant/components/smartthings/.translations/hu.json new file mode 100644 index 0000000000000..e4970780bc0dd --- /dev/null +++ b/homeassistant/components/smartthings/.translations/hu.json @@ -0,0 +1,28 @@ +{ + "config": { + "error": { + "app_not_installed": "Gy\u0151z\u0151dj meg r\u00f3la, hogy telep\u00edtetted \u00e9s enged\u00e9lyezted a SmartApp Home Assistant alkalmaz\u00e1st, \u00e9s pr\u00f3b\u00e1lkozz \u00fajra.", + "app_setup_error": "A SmartApp be\u00e1ll\u00edt\u00e1sa nem siker\u00fclt. K\u00e9rlek pr\u00f3b\u00e1ld \u00fajra.", + "base_url_not_https": "A `http` \u00f6sszetev\u0151 `base_url` be\u00e1ll\u00edt\u00e1s\u00e1t konfigur\u00e1lni kell, \u00e9s `https: //` -vel kell kezdeni.", + "token_already_setup": "A tokent m\u00e1r be\u00e1ll\u00edtottuk.", + "token_forbidden": "A token nem rendelkezik a sz\u00fcks\u00e9ges OAuth-tartom\u00e1nyokkal.", + "token_invalid_format": "A tokennek UID / GUID form\u00e1tumban kell lennie", + "token_unauthorized": "A token \u00e9rv\u00e9nytelen vagy m\u00e1r nem enged\u00e9lyezett.", + "webhook_error": "A SmartThings nem tudta \u00e9rv\u00e9nyes\u00edteni a `base_url`-ben konfigur\u00e1lt v\u00e9gpontot. K\u00e9rlek, tekintsd \u00e1t az \u00f6sszetev\u0151 k\u00f6vetelm\u00e9nyeit." + }, + "step": { + "user": { + "data": { + "access_token": "Hozz\u00e1f\u00e9r\u00e9s a Tokenhez" + }, + "description": "K\u00e9rlek add meg a SmartThings [Personal Access Tokent]({token_url}), amit az [instrukci\u00f3k] ({component_url}) alapj\u00e1n hozt\u00e1l l\u00e9tre.", + "title": "Adja meg a szem\u00e9lyes hozz\u00e1f\u00e9r\u00e9si Tokent" + }, + "wait_install": { + "description": "K\u00e9rj\u00fck, telep\u00edtse a Home Assistant SmartAppot legal\u00e1bb egy helyre, \u00e9s kattintson a K\u00fcld\u00e9s gombra.", + "title": "A SmartApp telep\u00edt\u00e9se" + } + }, + "title": "SmartThings" + } +} \ No newline at end of file diff --git a/homeassistant/components/smartthings/.translations/it.json b/homeassistant/components/smartthings/.translations/it.json new file mode 100644 index 0000000000000..486a61847a71a --- /dev/null +++ b/homeassistant/components/smartthings/.translations/it.json @@ -0,0 +1,26 @@ +{ + "config": { + "error": { + "app_not_installed": "Assicurati di avere installato ed autorizzato la SmartApp Home Assistant e riprova.", + "app_setup_error": "Impossibile configurare SmartApp. Riprovare.", + "base_url_not_https": "Il `base_url` per il componente `http` deve essere configurato e deve iniziare con `https://`.", + "token_already_setup": "Il token \u00e8 gi\u00e0 stato configurato.", + "token_invalid_format": "Il token deve essere nel formato UID/GUID", + "token_unauthorized": "Il token non \u00e8 valido o non \u00e8 pi\u00f9 autorizzato.", + "webhook_error": "SmartThings non ha potuto convalidare l'endpoint configurato in `base_url`. Si prega di rivedere i requisiti del componente." + }, + "step": { + "user": { + "data": { + "access_token": "Token di accesso" + }, + "description": "Inserisci un [Token di Accesso Personale]({token_url}) di SmartThings che \u00e8 stato creato secondo lo [istruzioni]({component_url}).", + "title": "Inserisci il Token di Accesso Personale" + }, + "wait_install": { + "title": "Installa SmartApp" + } + }, + "title": "SmartThings" + } +} \ No newline at end of file diff --git a/homeassistant/components/smartthings/.translations/ko.json b/homeassistant/components/smartthings/.translations/ko.json index e4131543d50b9..f7d86af839485 100644 --- a/homeassistant/components/smartthings/.translations/ko.json +++ b/homeassistant/components/smartthings/.translations/ko.json @@ -3,11 +3,12 @@ "error": { "app_not_installed": "Home Assistant SmartApp \uc744 \uc124\uce58\ud558\uace0 \uc778\uc99d\ud588\ub294\uc9c0 \ud655\uc778\ud558\uace0 \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694.", "app_setup_error": "SmartApp \uc744 \uc124\uc815\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694.", - "base_url_not_https": "`http` \uad6c\uc131\uc694\uc18c\ub97c \uc704\ud55c `base_url` \uc740 `https://`\ub85c \uc2dc\uc791\ud558\ub3c4\ub85d \uad6c\uc131\ub418\uc5b4\uc57c\ud569\ub2c8\ub2e4.", + "base_url_not_https": "`http` \uad6c\uc131\uc694\uc18c\uc758 `base_url` \uc740 \ubc18\ub4dc\uc2dc `https://`\ub85c \uc2dc\uc791\ud558\ub3c4\ub85d \uad6c\uc131\ub418\uc5b4 \uc788\uc5b4\uc57c \ud569\ub2c8\ub2e4.", "token_already_setup": "\ud1a0\ud070\uc774 \uc774\ubbf8 \uc124\uc815\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", "token_forbidden": "\ud1a0\ud070\uc5d0 \ud544\uc694\ud55c OAuth \ubc94\uc704\ubaa9\ub85d\uc774 \uc5c6\uc2b5\ub2c8\ub2e4.", - "token_invalid_format": "\ud1a0\ud070\uc740 UID/GUID \ud615\uc2dd\uc774\uc5b4\uc57c\ud569\ub2c8\ub2e4", - "token_unauthorized": "\ud1a0\ud070\uc774 \uc720\ud6a8\ud558\uc9c0 \uc54a\uac70\ub098 \uc2b9\uc778\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4." + "token_invalid_format": "\ud1a0\ud070\uc740 UID/GUID \ud615\uc2dd\uc774\uc5b4\uc57c \ud569\ub2c8\ub2e4", + "token_unauthorized": "\ud1a0\ud070\uc774 \uc720\ud6a8\ud558\uc9c0 \uc54a\uac70\ub098 \uc2b9\uc778\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4.", + "webhook_error": "SmartThings \ub294 `base_url` \uc5d0 \uc124\uc815\ub41c \uc5d4\ub4dc\ud3ec\uc778\ud2b8\uc758 \uc720\ud6a8\uc131\uc744 \uac80\uc0ac \ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4. \uad6c\uc131\uc694\uc18c\uc758 \uc694\uad6c \uc0ac\ud56d\uc744 \uac80\ud1a0\ud574\uc8fc\uc138\uc694." }, "step": { "user": { diff --git a/homeassistant/components/smartthings/.translations/lb.json b/homeassistant/components/smartthings/.translations/lb.json index fd59d187314b5..fc80ba9f722e1 100644 --- a/homeassistant/components/smartthings/.translations/lb.json +++ b/homeassistant/components/smartthings/.translations/lb.json @@ -7,7 +7,8 @@ "token_already_setup": "Den Jeton gouf schonn ageriicht.", "token_forbidden": "De Jeton huet net d\u00e9i n\u00e9ideg OAuth M\u00e9iglechkeeten.", "token_invalid_format": "De Jeton muss am UID/GUID Format sinn", - "token_unauthorized": "De Jeton ass ong\u00eblteg oder net m\u00e9i autoris\u00e9iert." + "token_unauthorized": "De Jeton ass ong\u00eblteg oder net m\u00e9i autoris\u00e9iert.", + "webhook_error": "SmartThings konnt den an der 'base_url' defin\u00e9ierten Endpoint net valid\u00e9ieren. Iwwerpr\u00e9ift d'Viraussetzunge vun d\u00ebser Komponente" }, "step": { "user": { diff --git a/homeassistant/components/smartthings/.translations/no.json b/homeassistant/components/smartthings/.translations/no.json index 4d7df8bd65d17..fe93407b42944 100644 --- a/homeassistant/components/smartthings/.translations/no.json +++ b/homeassistant/components/smartthings/.translations/no.json @@ -7,7 +7,8 @@ "token_already_setup": "Token har allerede blitt satt opp.", "token_forbidden": "Tollet har ikke de n\u00f8dvendige OAuth m\u00e5lene.", "token_invalid_format": "Token m\u00e5 v\u00e6re i UID/GUID format", - "token_unauthorized": "Tollet er ugyldig eller ikke lenger autorisert." + "token_unauthorized": "Tollet er ugyldig eller ikke lenger autorisert.", + "webhook_error": "SmartThings kunne ikke validere endepunktet konfigurert i `base_url`. Vennligst se komponent krav." }, "step": { "user": { diff --git a/homeassistant/components/smartthings/.translations/pl.json b/homeassistant/components/smartthings/.translations/pl.json index 570f11303833c..3380399476423 100644 --- a/homeassistant/components/smartthings/.translations/pl.json +++ b/homeassistant/components/smartthings/.translations/pl.json @@ -7,7 +7,8 @@ "token_already_setup": "Token zosta\u0142 ju\u017c skonfigurowany.", "token_forbidden": "Token nie ma wymaganych zakres\u00f3w OAuth.", "token_invalid_format": "Token musi by\u0107 w formacie UID/GUID", - "token_unauthorized": "Token jest niewa\u017cny lub nie ma ju\u017c autoryzacji." + "token_unauthorized": "Token jest niewa\u017cny lub nie ma ju\u017c autoryzacji.", + "webhook_error": "SmartThings nie mo\u017ce sprawdzi\u0107 poprawno\u015bci punktu ko\u0144cowego skonfigurowanego w `base_url`. Sprawd\u017a wymagania dotycz\u0105ce komponentu." }, "step": { "user": { diff --git a/homeassistant/components/smartthings/.translations/pt.json b/homeassistant/components/smartthings/.translations/pt.json index d805cfc563da1..f49fe04ae8e93 100644 --- a/homeassistant/components/smartthings/.translations/pt.json +++ b/homeassistant/components/smartthings/.translations/pt.json @@ -1,16 +1,24 @@ { "config": { "error": { + "app_not_installed": "Por favor, instale o Home Assistant SmartApp em pelo menos um local e tente de novo.", "app_setup_error": "N\u00e3o \u00e9 poss\u00edvel configurar o SmartApp. Por favor, tente novamente.", - "token_already_setup": "O token j\u00e1 foi configurado." + "base_url_not_https": "O `base_url` para o componente` http` deve ser configurado e iniciar com `https://`.", + "token_already_setup": "O token j\u00e1 foi configurado.", + "token_forbidden": "O token n\u00e3o tem tem a cobertura OAuth necess\u00e1ria.", + "token_invalid_format": "O token deve estar no formato UID/GUID", + "token_unauthorized": "O token \u00e9 inv\u00e1lido ou ja n\u00e3o est\u00e1 autorizado." }, "step": { "user": { "data": { "access_token": "Token de Acesso" - } + }, + "description": "Por favor, insira um SmartThings [Personal Access Token]({token_url} ) que foi criado de acordo com as [instru\u00e7\u00f5es]({component_url}).", + "title": "Insira o Token de acesso pessoal" }, "wait_install": { + "description": "Por favor, instale o Home Assistant SmartApp em pelo menos um local e clique em enviar.", "title": "Instalar SmartApp" } }, diff --git a/homeassistant/components/smartthings/.translations/ru.json b/homeassistant/components/smartthings/.translations/ru.json index 334e5d8cb2310..6e34cf8a49acf 100644 --- a/homeassistant/components/smartthings/.translations/ru.json +++ b/homeassistant/components/smartthings/.translations/ru.json @@ -1,13 +1,14 @@ { "config": { "error": { - "app_not_installed": "\u0423\u0431\u0435\u0434\u0438\u0442\u0435\u0441\u044c, \u0447\u0442\u043e \u0412\u044b \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u043b\u0438 \u0438 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u043e\u0432\u0430\u043b\u0438 SmartApp Home Assistant \u0438 \u043f\u043e\u0432\u0442\u043e\u0440\u0438\u0442\u0435 \u043f\u043e\u043f\u044b\u0442\u043a\u0443.", - "app_setup_error": "\u041d\u0435 \u0443\u0434\u0430\u0435\u0442\u0441\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c SmartApp. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0435\u0449\u0435 \u0440\u0430\u0437.", + "app_not_installed": "\u0423\u0431\u0435\u0434\u0438\u0442\u0435\u0441\u044c, \u0447\u0442\u043e \u0412\u044b \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u043b\u0438 \u0438 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u043e\u0432\u0430\u043b\u0438 SmartApp 'Home Assistant' \u0438 \u043f\u043e\u0432\u0442\u043e\u0440\u0438\u0442\u0435 \u043f\u043e\u043f\u044b\u0442\u043a\u0443.", + "app_setup_error": "\u041d\u0435 \u0443\u0434\u0430\u0435\u0442\u0441\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c SmartApp. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0435\u0449\u0435 \u0440\u0430\u0437.", "base_url_not_https": "\u0412 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0435 `http` \u0434\u043e\u043b\u0436\u0435\u043d \u0431\u044b\u0442\u044c \u0443\u043a\u0430\u0437\u0430\u043d \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440 `base_url`, \u043d\u0430\u0447\u0438\u043d\u0430\u044e\u0449\u0438\u0439\u0441\u044f \u0441 `https://`.", "token_already_setup": "\u0422\u043e\u043a\u0435\u043d \u0443\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d.", "token_forbidden": "\u0422\u043e\u043a\u0435\u043d \u043d\u0435 \u043c\u043e\u0436\u0435\u0442 \u0431\u044b\u0442\u044c \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d \u0434\u043b\u044f OAuth.", "token_invalid_format": "\u0422\u043e\u043a\u0435\u043d \u0434\u043e\u043b\u0436\u0435\u043d \u0431\u044b\u0442\u044c \u0432 \u0444\u043e\u0440\u043c\u0430\u0442\u0435 UID / GUID", - "token_unauthorized": "\u0422\u043e\u043a\u0435\u043d \u043d\u0435\u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0442\u0435\u043b\u0435\u043d \u0438\u043b\u0438 \u0431\u043e\u043b\u044c\u0448\u0435 \u043d\u0435 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u043e\u0432\u0430\u043d." + "token_unauthorized": "\u0422\u043e\u043a\u0435\u043d \u043d\u0435\u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0442\u0435\u043b\u0435\u043d \u0438\u043b\u0438 \u0431\u043e\u043b\u044c\u0448\u0435 \u043d\u0435 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u043e\u0432\u0430\u043d.", + "webhook_error": "SmartThings \u043d\u0435 \u043c\u043e\u0436\u0435\u0442 \u043f\u0440\u043e\u0432\u0435\u0440\u0438\u0442\u044c \u043a\u043e\u043d\u0435\u0447\u043d\u0443\u044e \u0442\u043e\u0447\u043a\u0443, \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u043d\u0443\u044e \u0432 `base_url`. \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\u0435\u0439 \u043a \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0443." }, "step": { "user": { @@ -18,7 +19,7 @@ "title": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u043f\u0435\u0440\u0441\u043e\u043d\u0430\u043b\u044c\u043d\u044b\u0439 \u0442\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0430" }, "wait_install": { - "description": "\u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u0435 SmartApp Home Assistant \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, \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u0435 SmartApp 'Home Assistant' \u0438 \u043d\u0430\u0436\u043c\u0438\u0442\u0435 **\u041f\u041e\u0414\u0422\u0412\u0415\u0420\u0414\u0418\u0422\u042c**.", "title": "SmartThings" } }, diff --git a/homeassistant/components/smartthings/.translations/sl.json b/homeassistant/components/smartthings/.translations/sl.json new file mode 100644 index 0000000000000..e274d8c939407 --- /dev/null +++ b/homeassistant/components/smartthings/.translations/sl.json @@ -0,0 +1,27 @@ +{ + "config": { + "error": { + "app_not_installed": "Prepri\u010dajte se, da ste namestili in pooblastili Home Assistant SmartApp in poskusite znova.", + "app_setup_error": "SmartApp ni mogo\u010de nastaviti. Prosim poskusite ponovno.", + "base_url_not_https": "`Base_url` za` http 'komponento je treba konfigurirati in za\u010deti z `https: //`.", + "token_already_setup": "\u017deton je \u017ee nastavljen.", + "token_forbidden": "\u017deton nima zahtevanih OAuth obsegov.", + "token_invalid_format": "\u017deton mora biti v formatu UID / GUID", + "token_unauthorized": "\u017deton ni veljaven ali ni ve\u010d poobla\u0161\u010den." + }, + "step": { + "user": { + "data": { + "access_token": "\u017deton za dostop" + }, + "description": "Prosimo vnesite Smartthings [\u017deton za osebni dostop]({token_url}) ki je bil kreiran v skladu z [navodili]({component_url}).", + "title": "Vnesite \u017eeton za osebni dostop" + }, + "wait_install": { + "description": "Prosimo, namestite Home Assistant SmartApp v vsaj eni lokaciji in kliknite po\u0161lji.", + "title": "Namesti SmartApp" + } + }, + "title": "SmartThings" + } +} \ No newline at end of file diff --git a/homeassistant/components/smartthings/.translations/sv.json b/homeassistant/components/smartthings/.translations/sv.json new file mode 100644 index 0000000000000..6da4624fa3968 --- /dev/null +++ b/homeassistant/components/smartthings/.translations/sv.json @@ -0,0 +1,28 @@ +{ + "config": { + "error": { + "app_not_installed": "V\u00e4nligen se till att du har installerat och auktoriserad Home Assistant SmartApp och f\u00f6rs\u00f6k igen.", + "app_setup_error": "Det gick inte att installera Home Assistant SmartApp. V\u00e4nligen f\u00f6rs\u00f6k igen.", + "base_url_not_https": "Den `base_url`f\u00f6r `http` komponenten m\u00e5ste konfigureras och b\u00f6rja med `https://`.", + "token_already_setup": "Token har redan installerats.", + "token_forbidden": "Token har inte det som kr\u00e4vs inom omf\u00e5ng f\u00f6r OAuth.", + "token_invalid_format": "Token m\u00e5ste vara i UID/GUID-format", + "token_unauthorized": "Denna token \u00e4r ogiltig eller inte l\u00e4ngre auktoriserad.", + "webhook_error": "SmartThings kunde inte validera endpoint konfigurerad i \" base_url`. V\u00e4nligen granska kraven f\u00f6r komponenten." + }, + "step": { + "user": { + "data": { + "access_token": "\u00c5tkomsttoken" + }, + "description": "V\u00e4nligen ange en [personlig \u00e5tkomsttoken]({token_url}) f\u00f6r SmartThings som har skapats enligt [instruktionerna]({component_url}).", + "title": "Ange personlig \u00e5tkomsttoken" + }, + "wait_install": { + "description": "Installera Home Assistant SmartApp p\u00e5 minst en plats och klicka p\u00e5 Skicka.", + "title": "Installera SmartApp" + } + }, + "title": "SmartThings" + } +} \ No newline at end of file diff --git a/homeassistant/components/smartthings/.translations/zh-Hant.json b/homeassistant/components/smartthings/.translations/zh-Hant.json index 952eafec60c5b..10d73f8be359a 100644 --- a/homeassistant/components/smartthings/.translations/zh-Hant.json +++ b/homeassistant/components/smartthings/.translations/zh-Hant.json @@ -7,7 +7,8 @@ "token_already_setup": "\u5bc6\u9470\u5df2\u8a2d\u5b9a\u904e\u3002", "token_forbidden": "\u5bc6\u9470\u4e0d\u5177\u6240\u9700\u7684 OAuth \u7bc4\u570d\u3002", "token_invalid_format": "\u5bc6\u9470\u5fc5\u9808\u70ba UID/GUID \u683c\u5f0f", - "token_unauthorized": "\u5bc6\u9470\u7121\u6548\u6216\u4e0d\u518d\u5177\u6709\u6388\u6b0a\u3002" + "token_unauthorized": "\u5bc6\u9470\u7121\u6548\u6216\u4e0d\u518d\u5177\u6709\u6388\u6b0a\u3002", + "webhook_error": "SmartThings \u7121\u6cd5\u8a8d\u8b49\u300cbase_url\u300d\u4e2d\u8a2d\u5b9a\u4e4b\u7aef\u9ede\u3002\u8acb\u518d\u6b21\u78ba\u8a8d\u5143\u4ef6\u9700\u6c42\u3002" }, "step": { "user": { diff --git a/homeassistant/components/smartthings/__init__.py b/homeassistant/components/smartthings/__init__.py index 3cf38c358bc76..64e717cbc923f 100644 --- a/homeassistant/components/smartthings/__init__.py +++ b/homeassistant/components/smartthings/__init__.py @@ -14,16 +14,20 @@ from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send) from homeassistant.helpers.entity import Entity +from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.typing import ConfigType, HomeAssistantType from .config_flow import SmartThingsFlowHandler # noqa from .const import ( - CONF_APP_ID, CONF_INSTALLED_APP_ID, DATA_BROKERS, DATA_MANAGER, DOMAIN, - EVENT_BUTTON, SIGNAL_SMARTTHINGS_UPDATE, SUPPORTED_PLATFORMS) + CONF_APP_ID, CONF_INSTALLED_APP_ID, CONF_LOCATION_ID, CONF_OAUTH_CLIENT_ID, + CONF_OAUTH_CLIENT_SECRET, CONF_REFRESH_TOKEN, DATA_BROKERS, DATA_MANAGER, + DOMAIN, EVENT_BUTTON, SIGNAL_SMARTTHINGS_UPDATE, SUPPORTED_PLATFORMS, + TOKEN_REFRESH_INTERVAL) from .smartapp import ( - setup_smartapp, setup_smartapp_endpoint, validate_installed_app) + setup_smartapp, setup_smartapp_endpoint, smartapp_sync_subscriptions, + validate_installed_app) -REQUIREMENTS = ['pysmartapp==0.3.0', 'pysmartthings==0.6.2'] +REQUIREMENTS = ['pysmartapp==0.3.0', 'pysmartthings==0.6.3'] DEPENDENCIES = ['webhook'] _LOGGER = logging.getLogger(__name__) @@ -35,6 +39,43 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType): return True +async def async_migrate_entry(hass: HomeAssistantType, entry: ConfigEntry): + """Handle migration of a previous version config entry. + + A config entry created under a previous version must go through the + integration setup again so we can properly retrieve the needed data + elements. Force this by removing the entry and triggering a new flow. + """ + from pysmartthings import SmartThings + + # Remove the installed_app, which if already removed raises a 403 error. + api = SmartThings(async_get_clientsession(hass), + entry.data[CONF_ACCESS_TOKEN]) + installed_app_id = entry.data[CONF_INSTALLED_APP_ID] + try: + await api.delete_installed_app(installed_app_id) + except ClientResponseError as ex: + if ex.status == 403: + _LOGGER.exception("Installed app %s has already been removed", + installed_app_id) + else: + raise + _LOGGER.debug("Removed installed app %s", installed_app_id) + + # Delete the entry + hass.async_create_task( + hass.config_entries.async_remove(entry.entry_id)) + # only create new flow if there isn't a pending one for SmartThings. + flows = hass.config_entries.flow.async_progress() + if not [flow for flow in flows if flow['handler'] == DOMAIN]: + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={'source': 'import'})) + + # Return False because it could not be migrated. + return False + + async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry): """Initialize config entry which represents an installed SmartApp.""" from pysmartthings import SmartThings @@ -62,6 +103,17 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry): installed_app = await validate_installed_app( api, entry.data[CONF_INSTALLED_APP_ID]) + # Get scenes + scenes = await async_get_entry_scenes(entry, api) + + # Get SmartApp token to sync subscriptions + token = await api.generate_tokens( + entry.data[CONF_OAUTH_CLIENT_ID], + entry.data[CONF_OAUTH_CLIENT_SECRET], + entry.data[CONF_REFRESH_TOKEN]) + entry.data[CONF_REFRESH_TOKEN] = token.refresh_token + hass.config_entries.async_update_entry(entry) + # Get devices and their current status devices = await api.devices( location_ids=[installed_app.location_id]) @@ -71,18 +123,21 @@ async def retrieve_device_status(device): await device.status.refresh() except ClientResponseError: _LOGGER.debug("Unable to update status for device: %s (%s), " - "the device will be ignored", + "the device will be excluded", device.label, device.device_id, exc_info=True) devices.remove(device) await asyncio.gather(*[retrieve_device_status(d) for d in devices.copy()]) + # Sync device subscriptions + await smartapp_sync_subscriptions( + hass, token.access_token, installed_app.location_id, + installed_app.installed_app_id, devices) + # Setup device broker - broker = DeviceBroker(hass, devices, - installed_app.installed_app_id) - broker.event_handler_disconnect = \ - smart_app.connect_event(broker.event_handler) + broker = DeviceBroker(hass, entry, token, smart_app, devices, scenes) + broker.connect() hass.data[DOMAIN][DATA_BROKERS][entry.entry_id] = broker except ClientResponseError as ex: @@ -114,11 +169,25 @@ async def retrieve_device_status(device): return True +async def async_get_entry_scenes(entry: ConfigEntry, api): + """Get the scenes within an integration.""" + try: + return await api.scenes(location_id=entry.data[CONF_LOCATION_ID]) + except ClientResponseError as ex: + if ex.status == 403: + _LOGGER.exception("Unable to load scenes for config entry '%s' " + "because the access token does not have the " + "required access", entry.title) + else: + raise + return [] + + async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry): """Unload a config entry.""" broker = hass.data[DOMAIN][DATA_BROKERS].pop(entry.entry_id, None) - if broker and broker.event_handler_disconnect: - broker.event_handler_disconnect() + if broker: + broker.disconnect() tasks = [hass.config_entries.async_forward_entry_unload(entry, component) for component in SUPPORTED_PLATFORMS] @@ -128,14 +197,19 @@ async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry): class DeviceBroker: """Manages an individual SmartThings config entry.""" - def __init__(self, hass: HomeAssistantType, devices: Iterable, - installed_app_id: str): + def __init__(self, hass: HomeAssistantType, entry: ConfigEntry, + token, smart_app, devices: Iterable, scenes: Iterable): """Create a new instance of the DeviceBroker.""" self._hass = hass - self._installed_app_id = installed_app_id - self.assignments = self._assign_capabilities(devices) + self._entry = entry + self._installed_app_id = entry.data[CONF_INSTALLED_APP_ID] + self._smart_app = smart_app + self._token = token + self._event_disconnect = None + self._regenerate_token_remove = None + self._assignments = self._assign_capabilities(devices) self.devices = {device.device_id: device for device in devices} - self.event_handler_disconnect = None + self.scenes = {scene.scene_id: scene for scene in scenes} def _assign_capabilities(self, devices: Iterable): """Assign platforms to capabilities.""" @@ -146,6 +220,8 @@ def _assign_capabilities(self, devices: Iterable): for platform_name in SUPPORTED_PLATFORMS: platform = importlib.import_module( '.' + platform_name, self.__module__) + if not hasattr(platform, 'get_capabilities'): + continue assigned = platform.get_capabilities(capabilities) if not assigned: continue @@ -158,17 +234,45 @@ def _assign_capabilities(self, devices: Iterable): assignments[device.device_id] = slots return assignments + def connect(self): + """Connect handlers/listeners for device/lifecycle events.""" + # Setup interval to regenerate the refresh token on a periodic basis. + # Tokens expire in 30 days and once expired, cannot be recovered. + async def regenerate_refresh_token(now): + """Generate a new refresh token and update the config entry.""" + await self._token.refresh( + self._entry.data[CONF_OAUTH_CLIENT_ID], + self._entry.data[CONF_OAUTH_CLIENT_SECRET]) + self._entry.data[CONF_REFRESH_TOKEN] = self._token.refresh_token + self._hass.config_entries.async_update_entry(self._entry) + _LOGGER.debug('Regenerated refresh token for installed app: %s', + self._installed_app_id) + + self._regenerate_token_remove = async_track_time_interval( + self._hass, regenerate_refresh_token, TOKEN_REFRESH_INTERVAL) + + # Connect handler to incoming device events + self._event_disconnect = \ + self._smart_app.connect_event(self._event_handler) + + def disconnect(self): + """Disconnects handlers/listeners for device/lifecycle events.""" + if self._regenerate_token_remove: + self._regenerate_token_remove() + if self._event_disconnect: + self._event_disconnect() + def get_assigned(self, device_id: str, platform: str): """Get the capabilities assigned to the platform.""" - slots = self.assignments.get(device_id, {}) + slots = self._assignments.get(device_id, {}) return [key for key, value in slots.items() if value == platform] def any_assigned(self, device_id: str, platform: str): """Return True if the platform has any assigned capabilities.""" - slots = self.assignments.get(device_id, {}) + slots = self._assignments.get(device_id, {}) return any(value for value in slots.values() if value == platform) - async def event_handler(self, req, resp, app): + async def _event_handler(self, req, resp, app): """Broker for incoming events.""" from pysmartapp.event import EVENT_TYPE_DEVICE from pysmartthings import Capability, Attribute diff --git a/homeassistant/components/smartthings/climate.py b/homeassistant/components/smartthings/climate.py index f784ed101a722..f660e905274d3 100644 --- a/homeassistant/components/smartthings/climate.py +++ b/homeassistant/components/smartthings/climate.py @@ -4,11 +4,12 @@ from typing import Iterable, Optional, Sequence from homeassistant.components.climate import ( + DOMAIN as CLIMATE_DOMAIN, ClimateDevice) +from homeassistant.components.climate.const import ( ATTR_OPERATION_MODE, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, STATE_AUTO, STATE_COOL, STATE_ECO, STATE_HEAT, SUPPORT_FAN_MODE, SUPPORT_OPERATION_MODE, SUPPORT_TARGET_TEMPERATURE, - SUPPORT_TARGET_TEMPERATURE_HIGH, SUPPORT_TARGET_TEMPERATURE_LOW, - ClimateDevice, DOMAIN as CLIMATE_DOMAIN) + SUPPORT_TARGET_TEMPERATURE_HIGH, SUPPORT_TARGET_TEMPERATURE_LOW) from homeassistant.const import ( ATTR_TEMPERATURE, STATE_OFF, TEMP_CELSIUS, TEMP_FAHRENHEIT) diff --git a/homeassistant/components/smartthings/config_flow.py b/homeassistant/components/smartthings/config_flow.py index 4663222c3b440..c290f0f8e550c 100644 --- a/homeassistant/components/smartthings/config_flow.py +++ b/homeassistant/components/smartthings/config_flow.py @@ -9,7 +9,8 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import ( - CONF_APP_ID, CONF_INSTALLED_APP_ID, CONF_LOCATION_ID, DOMAIN, + APP_OAUTH_CLIENT_NAME, APP_OAUTH_SCOPES, CONF_APP_ID, CONF_INSTALLED_APPS, + CONF_LOCATION_ID, CONF_OAUTH_CLIENT_ID, CONF_OAUTH_CLIENT_SECRET, DOMAIN, VAL_UID_MATCHER) from .smartapp import ( create_app, find_app, setup_smartapp, setup_smartapp_endpoint, update_app) @@ -35,7 +36,7 @@ class SmartThingsFlowHandler(config_entries.ConfigFlow): b) Config entries setup for all installations """ - VERSION = 1 + VERSION = 2 CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_PUSH def __init__(self): @@ -43,6 +44,8 @@ def __init__(self): self.access_token = None self.app_id = None self.api = None + self.oauth_client_secret = None + self.oauth_client_id = None async def async_step_import(self, user_input=None): """Occurs when a previously entry setup fails and is re-initiated.""" @@ -50,7 +53,7 @@ async def async_step_import(self, user_input=None): async def async_step_user(self, user_input=None): """Get access token and validate it.""" - from pysmartthings import APIResponseError, SmartThings + from pysmartthings import APIResponseError, AppOAuth, SmartThings errors = {} if not self.hass.config.api.base_url.lower().startswith('https://'): @@ -83,10 +86,18 @@ async def async_step_user(self, user_input=None): if app: await app.refresh() # load all attributes await update_app(self.hass, app) + # Get oauth client id/secret by regenerating it + app_oauth = AppOAuth(app.app_id) + app_oauth.client_name = APP_OAUTH_CLIENT_NAME + app_oauth.scope.extend(APP_OAUTH_SCOPES) + client = await self.api.generate_app_oauth(app_oauth) else: - app = await create_app(self.hass, self.api) + app, client = await create_app(self.hass, self.api) setup_smartapp(self.hass, app) self.app_id = app.app_id + self.oauth_client_secret = client.client_secret + self.oauth_client_id = client.client_id + except APIResponseError as ex: if ex.is_target_error(): errors['base'] = 'webhook_error' @@ -113,19 +124,23 @@ async def async_step_user(self, user_input=None): async def async_step_wait_install(self, user_input=None): """Wait for SmartApp installation.""" - from pysmartthings import InstalledAppStatus - errors = {} if user_input is None: return self._show_step_wait_install(errors) # Find installed apps that were authorized - installed_apps = [app for app in await self.api.installed_apps( - installed_app_status=InstalledAppStatus.AUTHORIZED) - if app.app_id == self.app_id] + installed_apps = self.hass.data[DOMAIN][CONF_INSTALLED_APPS].copy() if not installed_apps: errors['base'] = 'app_not_installed' return self._show_step_wait_install(errors) + self.hass.data[DOMAIN][CONF_INSTALLED_APPS].clear() + + # Enrich the data + for installed_app in installed_apps: + installed_app[CONF_APP_ID] = self.app_id + installed_app[CONF_ACCESS_TOKEN] = self.access_token + installed_app[CONF_OAUTH_CLIENT_ID] = self.oauth_client_id + installed_app[CONF_OAUTH_CLIENT_SECRET] = self.oauth_client_secret # User may have installed the SmartApp in more than one SmartThings # location. Config flows are created for the additional installations @@ -133,21 +148,10 @@ async def async_step_wait_install(self, user_input=None): self.hass.async_create_task( self.hass.config_entries.flow.async_init( DOMAIN, context={'source': 'install'}, - data={ - CONF_APP_ID: installed_app.app_id, - CONF_INSTALLED_APP_ID: installed_app.installed_app_id, - CONF_LOCATION_ID: installed_app.location_id, - CONF_ACCESS_TOKEN: self.access_token - })) - - # return entity for the first one. - installed_app = installed_apps[0] - return await self.async_step_install({ - CONF_APP_ID: installed_app.app_id, - CONF_INSTALLED_APP_ID: installed_app.installed_app_id, - CONF_LOCATION_ID: installed_app.location_id, - CONF_ACCESS_TOKEN: self.access_token - }) + data=installed_app)) + + # Create config entity for the first one. + return await self.async_step_install(installed_apps[0]) def _show_step_user(self, errors): return self.async_show_form( diff --git a/homeassistant/components/smartthings/const.py b/homeassistant/components/smartthings/const.py index 27260b155d138..105c9760e1269 100644 --- a/homeassistant/components/smartthings/const.py +++ b/homeassistant/components/smartthings/const.py @@ -1,14 +1,20 @@ """Constants used by the SmartThings component and platforms.""" +from datetime import timedelta import re +APP_OAUTH_CLIENT_NAME = "Home Assistant" APP_OAUTH_SCOPES = [ 'r:devices:*' ] APP_NAME_PREFIX = 'homeassistant.' CONF_APP_ID = 'app_id' CONF_INSTALLED_APP_ID = 'installed_app_id' +CONF_INSTALLED_APPS = 'installed_apps' CONF_INSTANCE_ID = 'instance_id' CONF_LOCATION_ID = 'location_id' +CONF_OAUTH_CLIENT_ID = 'client_id' +CONF_OAUTH_CLIENT_SECRET = 'client_secret' +CONF_REFRESH_TOKEN = 'refresh_token' DATA_MANAGER = 'manager' DATA_BROKERS = 'brokers' DOMAIN = 'smartthings' @@ -19,16 +25,19 @@ STORAGE_KEY = DOMAIN STORAGE_VERSION = 1 # Ordered 'specific to least-specific platform' in order for capabilities -# to be drawn-down and represented by the appropriate platform. +# to be drawn-down and represented by the most appropriate platform. SUPPORTED_PLATFORMS = [ 'climate', 'fan', 'light', 'lock', + 'cover', 'switch', 'binary_sensor', - 'sensor' + 'sensor', + 'scene' ] +TOKEN_REFRESH_INTERVAL = timedelta(days=14) VAL_UID = "^(?:([0-9a-fA-F]{32})|([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]" \ "{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}))$" VAL_UID_MATCHER = re.compile(VAL_UID) diff --git a/homeassistant/components/smartthings/cover.py b/homeassistant/components/smartthings/cover.py new file mode 100644 index 0000000000000..131da75f4feba --- /dev/null +++ b/homeassistant/components/smartthings/cover.py @@ -0,0 +1,153 @@ +"""Support for covers through the SmartThings cloud API.""" +from typing import Optional, Sequence + +from homeassistant.components.cover import ( + ATTR_POSITION, DEVICE_CLASS_DOOR, DEVICE_CLASS_GARAGE, DEVICE_CLASS_SHADE, + DOMAIN as COVER_DOMAIN, STATE_CLOSED, STATE_CLOSING, STATE_OPEN, + STATE_OPENING, SUPPORT_CLOSE, SUPPORT_OPEN, SUPPORT_SET_POSITION, + CoverDevice) +from homeassistant.const import ATTR_BATTERY_LEVEL + +from . import SmartThingsEntity +from .const import DATA_BROKERS, DOMAIN + +DEPENDENCIES = ['smartthings'] + +VALUE_TO_STATE = { + 'closed': STATE_CLOSED, + 'closing': STATE_CLOSING, + 'open': STATE_OPEN, + 'opening': STATE_OPENING, + 'partially open': STATE_OPEN, + 'unknown': None +} + + +async def async_setup_platform( + hass, config, async_add_entities, discovery_info=None): + """Platform uses config entry setup.""" + pass + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Add covers for a config entry.""" + broker = hass.data[DOMAIN][DATA_BROKERS][config_entry.entry_id] + async_add_entities( + [SmartThingsCover(device) for device in broker.devices.values() + if broker.any_assigned(device.device_id, COVER_DOMAIN)], True) + + +def get_capabilities(capabilities: Sequence[str]) -> Optional[Sequence[str]]: + """Return all capabilities supported if minimum required are present.""" + from pysmartthings import Capability + + min_required = [ + Capability.door_control, + Capability.garage_door_control, + Capability.window_shade + ] + # Must have one of the min_required + if any(capability in capabilities + for capability in min_required): + # Return all capabilities supported/consumed + return min_required + [Capability.battery, Capability.switch_level] + + return None + + +class SmartThingsCover(SmartThingsEntity, CoverDevice): + """Define a SmartThings cover.""" + + def __init__(self, device): + """Initialize the cover class.""" + from pysmartthings import Capability + + super().__init__(device) + self._device_class = None + self._state = None + self._state_attrs = None + self._supported_features = SUPPORT_OPEN | SUPPORT_CLOSE + if Capability.switch_level in device.capabilities: + self._supported_features |= SUPPORT_SET_POSITION + + async def async_close_cover(self, **kwargs): + """Close cover.""" + # Same command for all 3 supported capabilities + await self._device.close(set_status=True) + # State is set optimistically in the commands above, therefore update + # the entity state ahead of receiving the confirming push updates + self.async_schedule_update_ha_state(True) + + async def async_open_cover(self, **kwargs): + """Open the cover.""" + # Same for all capability types + await self._device.open(set_status=True) + # State is set optimistically in the commands above, therefore update + # the entity state ahead of receiving the confirming push updates + self.async_schedule_update_ha_state(True) + + async def async_set_cover_position(self, **kwargs): + """Move the cover to a specific position.""" + if not self._supported_features & SUPPORT_SET_POSITION: + return + # Do not set_status=True as device will report progress. + await self._device.set_level(kwargs[ATTR_POSITION], 0) + + async def async_update(self): + """Update the attrs of the cover.""" + from pysmartthings import Attribute, Capability + + value = None + if Capability.door_control in self._device.capabilities: + self._device_class = DEVICE_CLASS_DOOR + value = self._device.status.door + elif Capability.window_shade in self._device.capabilities: + self._device_class = DEVICE_CLASS_SHADE + value = self._device.status.window_shade + elif Capability.garage_door_control in self._device.capabilities: + self._device_class = DEVICE_CLASS_GARAGE + value = self._device.status.door + + self._state = VALUE_TO_STATE.get(value) + + self._state_attrs = {} + battery = self._device.status.attributes[Attribute.battery].value + if battery is not None: + self._state_attrs[ATTR_BATTERY_LEVEL] = battery + + @property + def is_opening(self): + """Return if the cover is opening or not.""" + return self._state == STATE_OPENING + + @property + def is_closing(self): + """Return if the cover is closing or not.""" + return self._state == STATE_CLOSING + + @property + def is_closed(self): + """Return if the cover is closed or not.""" + if self._state == STATE_CLOSED: + return True + return None if self._state is None else False + + @property + def current_cover_position(self): + """Return current position of cover.""" + return self._device.status.level + + @property + def device_class(self): + """Define this cover as a garage door.""" + return self._device_class + + @property + def device_state_attributes(self): + """Get additional state attributes.""" + return self._state_attrs + + @property + def supported_features(self): + """Flag supported features.""" + return self._supported_features diff --git a/homeassistant/components/smartthings/lock.py b/homeassistant/components/smartthings/lock.py index d3f633ed0e4ac..c7ab091454cad 100644 --- a/homeassistant/components/smartthings/lock.py +++ b/homeassistant/components/smartthings/lock.py @@ -10,14 +10,17 @@ ST_STATE_LOCKED = 'locked' ST_LOCK_ATTR_MAP = { - 'method': 'method', 'codeId': 'code_id', - 'timeout': 'timeout' + 'codeName': 'code_name', + 'lockName': 'lock_name', + 'method': 'method', + 'timeout': 'timeout', + 'usedCode': 'used_code' } -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): """Platform uses config entry setup.""" pass diff --git a/homeassistant/components/smartthings/scene.py b/homeassistant/components/smartthings/scene.py new file mode 100644 index 0000000000000..9bf3211d8e36e --- /dev/null +++ b/homeassistant/components/smartthings/scene.py @@ -0,0 +1,50 @@ +"""Support for scenes through the SmartThings cloud API.""" +from homeassistant.components.scene import Scene + +from .const import DATA_BROKERS, DOMAIN + +DEPENDENCIES = ['smartthings'] + + +async def async_setup_platform( + hass, config, async_add_entities, discovery_info=None): + """Platform uses config entry setup.""" + pass + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Add switches for a config entry.""" + broker = hass.data[DOMAIN][DATA_BROKERS][config_entry.entry_id] + async_add_entities( + [SmartThingsScene(scene) for scene in broker.scenes.values()]) + + +class SmartThingsScene(Scene): + """Define a SmartThings scene.""" + + def __init__(self, scene): + """Init the scene class.""" + self._scene = scene + + async def async_activate(self): + """Activate scene.""" + await self._scene.execute() + + @property + def device_state_attributes(self): + """Get attributes about the state.""" + return { + 'icon': self._scene.icon, + 'color': self._scene.color, + 'location_id': self._scene.location_id + } + + @property + def name(self) -> str: + """Return the name of the device.""" + return self._scene.name + + @property + def unique_id(self) -> str: + """Return a unique ID.""" + return self._scene.scene_id diff --git a/homeassistant/components/smartthings/sensor.py b/homeassistant/components/smartthings/sensor.py index 32047c179b411..50beefdb5b2f4 100644 --- a/homeassistant/components/smartthings/sensor.py +++ b/homeassistant/components/smartthings/sensor.py @@ -43,8 +43,6 @@ Map('dishwasherJobState', "Dishwasher Job State", None, None), Map('completionTime', "Dishwasher Completion Time", None, DEVICE_CLASS_TIMESTAMP)], - 'doorControl': [ - Map('door', "Door", None, None)], 'dryerMode': [ Map('dryerMode', "Dryer Mode", None, None)], 'dryerOperatingState': [ @@ -62,8 +60,6 @@ 'Equivalent Carbon Dioxide Measurement', 'ppm', None)], 'formaldehydeMeasurement': [ Map('formaldehydeLevel', 'Formaldehyde Measurement', 'ppm', None)], - 'garageDoorControl': [ - Map('door', 'Garage Door', None, None)], 'illuminanceMeasurement': [ Map('illuminance', "Illuminance", 'lux', DEVICE_CLASS_ILLUMINANCE)], 'infraredLevel': [ @@ -143,9 +139,7 @@ Map('machineState', "Washer Machine State", None, None), Map('washerJobState', "Washer Job State", None, None), Map('completionTime', "Washer Completion Time", None, - DEVICE_CLASS_TIMESTAMP)], - 'windowShade': [ - Map('windowShade', 'Window Shade', None, None)] + DEVICE_CLASS_TIMESTAMP)] } UNITS = { diff --git a/homeassistant/components/smartthings/smartapp.py b/homeassistant/components/smartthings/smartapp.py index 89043d4f76c5e..5527fda54f463 100644 --- a/homeassistant/components/smartthings/smartapp.py +++ b/homeassistant/components/smartthings/smartapp.py @@ -13,15 +13,16 @@ from aiohttp import web from homeassistant.components import webhook -from homeassistant.const import CONF_ACCESS_TOKEN, CONF_WEBHOOK_ID +from homeassistant.const import CONF_WEBHOOK_ID from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send) from homeassistant.helpers.typing import HomeAssistantType from .const import ( - APP_NAME_PREFIX, APP_OAUTH_SCOPES, CONF_APP_ID, CONF_INSTALLED_APP_ID, - CONF_INSTANCE_ID, CONF_LOCATION_ID, DATA_BROKERS, DATA_MANAGER, DOMAIN, + APP_NAME_PREFIX, APP_OAUTH_CLIENT_NAME, APP_OAUTH_SCOPES, CONF_APP_ID, + CONF_INSTALLED_APP_ID, CONF_INSTALLED_APPS, CONF_INSTANCE_ID, + CONF_LOCATION_ID, CONF_REFRESH_TOKEN, DATA_BROKERS, DATA_MANAGER, DOMAIN, SETTINGS_INSTANCE_ID, SIGNAL_SMARTAPP_PREFIX, STORAGE_KEY, STORAGE_VERSION) _LOGGER = logging.getLogger(__name__) @@ -83,7 +84,7 @@ async def create_app(hass: HomeAssistantType, api): app = App() for key, value in template.items(): setattr(app, key, value) - app = (await api.create_app(app))[0] + app, client = await api.create_app(app) _LOGGER.debug("Created SmartApp '%s' (%s)", app.app_name, app.app_id) # Set unique hass id in settings @@ -97,12 +98,12 @@ async def create_app(hass: HomeAssistantType, api): # Set oauth scopes oauth = AppOAuth(app.app_id) - oauth.client_name = 'Home Assistant' + oauth.client_name = APP_OAUTH_CLIENT_NAME oauth.scope.extend(APP_OAUTH_SCOPES) await api.update_app_oauth(oauth) _LOGGER.debug("Updated App OAuth for SmartApp '%s' (%s)", app.app_name, app.app_id) - return app + return app, client async def update_app(hass: HomeAssistantType, app): @@ -185,32 +186,24 @@ async def setup_smartapp_endpoint(hass: HomeAssistantType): DATA_MANAGER: manager, CONF_INSTANCE_ID: config[CONF_INSTANCE_ID], DATA_BROKERS: {}, - CONF_WEBHOOK_ID: config[CONF_WEBHOOK_ID] + CONF_WEBHOOK_ID: config[CONF_WEBHOOK_ID], + CONF_INSTALLED_APPS: [] } async def smartapp_sync_subscriptions( hass: HomeAssistantType, auth_token: str, location_id: str, - installed_app_id: str, *, skip_delete=False): + installed_app_id: str, devices): """Synchronize subscriptions of an installed up.""" from pysmartthings import ( - CAPABILITIES, SmartThings, SourceType, Subscription) + CAPABILITIES, SmartThings, SourceType, Subscription, + SubscriptionEntity + ) api = SmartThings(async_get_clientsession(hass), auth_token) - devices = await api.devices(location_ids=[location_id]) + tasks = [] - # Build set of capabilities and prune unsupported ones - capabilities = set() - for device in devices: - capabilities.update(device.capabilities) - capabilities.intersection_update(CAPABILITIES) - - # Remove all (except for installs) - if not skip_delete: - await api.delete_subscriptions(installed_app_id) - - # Create for each capability - async def create_subscription(target): + async def create_subscription(target: str): sub = Subscription() sub.installed_app_id = installed_app_id sub.location_id = location_id @@ -224,52 +217,89 @@ async def create_subscription(target): _LOGGER.exception("Failed to create subscription for '%s' under " "app '%s'", target, installed_app_id) - tasks = [create_subscription(c) for c in capabilities] - await asyncio.gather(*tasks) + async def delete_subscription(sub: SubscriptionEntity): + try: + await api.delete_subscription( + installed_app_id, sub.subscription_id) + _LOGGER.debug("Removed subscription for '%s' under app '%s' " + "because it was no longer needed", + sub.capability, installed_app_id) + except Exception: # pylint:disable=broad-except + _LOGGER.exception("Failed to remove subscription for '%s' under " + "app '%s'", sub.capability, installed_app_id) + + # Build set of capabilities and prune unsupported ones + capabilities = set() + for device in devices: + capabilities.update(device.capabilities) + capabilities.intersection_update(CAPABILITIES) + + # Get current subscriptions and find differences + subscriptions = await api.subscriptions(installed_app_id) + for subscription in subscriptions: + if subscription.capability in capabilities: + capabilities.remove(subscription.capability) + else: + # Delete the subscription + tasks.append(delete_subscription(subscription)) + + # Remaining capabilities need subscriptions created + tasks.extend([create_subscription(c) for c in capabilities]) + + if tasks: + await asyncio.gather(*tasks) + else: + _LOGGER.debug("Subscriptions for app '%s' are up-to-date", + installed_app_id) async def smartapp_install(hass: HomeAssistantType, req, resp, app): """ Handle when a SmartApp is installed by the user into a location. - Setup subscriptions using the access token SmartThings provided in the - event. An explicit subscription is required for each 'capability' in order - to receive the related attribute updates. Finally, create a config entry - representing the installation if this is not the first installation under - the account. + Create a config entry representing the installation if this is not + the first installation under the account, otherwise store the data + for the config flow. """ - await smartapp_sync_subscriptions( - hass, req.auth_token, req.location_id, req.installed_app_id, - skip_delete=True) - - # The permanent access token is copied from another config flow with the - # same parent app_id. If one is not found, that means the user is within - # the initial config flow and the entry at the conclusion. - access_token = next(( - entry.data.get(CONF_ACCESS_TOKEN) for entry + install_data = { + CONF_INSTALLED_APP_ID: req.installed_app_id, + CONF_LOCATION_ID: req.location_id, + CONF_REFRESH_TOKEN: req.refresh_token + } + # App attributes (client id/secret, etc...) are copied from another entry + # with the same parent app_id. If one is not found, the install data is + # stored for the config flow to retrieve during the wait step. + entry = next(( + entry for entry in hass.config_entries.async_entries(DOMAIN) if entry.data[CONF_APP_ID] == app.app_id), None) - if access_token: + if entry: + data = entry.data.copy() + data.update(install_data) # Add as job not needed because the current coroutine was invoked # from the dispatcher and is not being awaited. await hass.config_entries.flow.async_init( DOMAIN, context={'source': 'install'}, - data={ - CONF_APP_ID: app.app_id, - CONF_INSTALLED_APP_ID: req.installed_app_id, - CONF_LOCATION_ID: req.location_id, - CONF_ACCESS_TOKEN: access_token - }) + data=data) + else: + # Store the data where the flow can find it + hass.data[DOMAIN][CONF_INSTALLED_APPS].append(install_data) async def smartapp_update(hass: HomeAssistantType, req, resp, app): """ Handle when a SmartApp is updated (reconfigured) by the user. - Synchronize subscriptions to ensure we're up-to-date. + Store the refresh token in the config entry. """ - await smartapp_sync_subscriptions( - hass, req.auth_token, req.location_id, req.installed_app_id) + # Update refresh token in config entry + entry = next((entry for entry in hass.config_entries.async_entries(DOMAIN) + if entry.data.get(CONF_INSTALLED_APP_ID) == + req.installed_app_id), + None) + if entry: + entry.data[CONF_REFRESH_TOKEN] = req.refresh_token + hass.config_entries.async_update_entry(entry) _LOGGER.debug("SmartApp '%s' under parent app '%s' was updated", req.installed_app_id, app.app_id) diff --git a/homeassistant/components/smartthings/switch.py b/homeassistant/components/smartthings/switch.py index 5a1224f4fc2d9..d30aa3a2303e9 100644 --- a/homeassistant/components/smartthings/switch.py +++ b/homeassistant/components/smartthings/switch.py @@ -29,7 +29,9 @@ def get_capabilities(capabilities: Sequence[str]) -> Optional[Sequence[str]]: # Must be able to be turned on/off. if Capability.switch in capabilities: - return [Capability.switch] + return [Capability.switch, + Capability.energy_meter, + Capability.power_meter] return None @@ -50,6 +52,18 @@ async def async_turn_on(self, **kwargs) -> None: # the entity state ahead of receiving the confirming push updates self.async_schedule_update_ha_state() + @property + def current_power_w(self): + """Return the current power usage in W.""" + from pysmartthings import Attribute + return self._device.status.attributes[Attribute.power].value + + @property + def today_energy_kwh(self): + """Return the today total energy usage in kWh.""" + from pysmartthings import Attribute + return self._device.status.attributes[Attribute.energy].value + @property def is_on(self) -> bool: """Return true if light is on.""" diff --git a/homeassistant/components/smhi/.translations/es-419.json b/homeassistant/components/smhi/.translations/es-419.json new file mode 100644 index 0000000000000..a3fb9ee5e27e0 --- /dev/null +++ b/homeassistant/components/smhi/.translations/es-419.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "name_exists": "El nombre ya existe", + "wrong_location": "Ubicaci\u00f3n Suecia solamente" + }, + "step": { + "user": { + "data": { + "latitude": "Latitud", + "longitude": "Longitud", + "name": "Nombre" + }, + "title": "Ubicaci\u00f3n en Suecia" + } + }, + "title": "Servicio meteorol\u00f3gico sueco (SMHI)" + } +} \ No newline at end of file diff --git a/homeassistant/components/smhi/.translations/hu.json b/homeassistant/components/smhi/.translations/hu.json index 425cf927631e9..8c79ff3bfaf7a 100644 --- a/homeassistant/components/smhi/.translations/hu.json +++ b/homeassistant/components/smhi/.translations/hu.json @@ -13,6 +13,7 @@ }, "title": "Helysz\u00edn Sv\u00e9dorsz\u00e1gban" } - } + }, + "title": "Sv\u00e9d Meteorol\u00f3giai Szolg\u00e1lat (SMHI)" } } \ No newline at end of file diff --git a/homeassistant/components/smhi/.translations/it.json b/homeassistant/components/smhi/.translations/it.json new file mode 100644 index 0000000000000..b8c228f7e9eb1 --- /dev/null +++ b/homeassistant/components/smhi/.translations/it.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "name_exists": "Il nome \u00e8 gi\u00e0 esistente", + "wrong_location": "Localit\u00e0 solamente della Svezia" + }, + "step": { + "user": { + "data": { + "latitude": "Latitudine", + "longitude": "Logitudine", + "name": "Nome" + }, + "title": "Localit\u00e0 in Svezia" + } + }, + "title": "Servizio meteo svedese (SMHI)" + } +} \ No newline at end of file diff --git a/homeassistant/components/smhi/__init__.py b/homeassistant/components/smhi/__init__.py index 6af8c14843b18..608ee9b6a6d17 100644 --- a/homeassistant/components/smhi/__init__.py +++ b/homeassistant/components/smhi/__init__.py @@ -6,7 +6,7 @@ from .config_flow import smhi_locations # noqa: F401 from .const import DOMAIN # noqa: F401 -REQUIREMENTS = ['smhi-pkg==1.0.8'] +REQUIREMENTS = ['smhi-pkg==1.0.10'] DEFAULT_NAME = 'smhi' diff --git a/homeassistant/components/smhi/weather.py b/homeassistant/components/smhi/weather.py index 75a0c51d01060..6136d093a3351 100644 --- a/homeassistant/components/smhi/weather.py +++ b/homeassistant/components/smhi/weather.py @@ -218,7 +218,7 @@ def forecast(self) -> List: ATTR_FORECAST_TEMP: forecast.temperature_max, ATTR_FORECAST_TEMP_LOW: forecast.temperature_min, ATTR_FORECAST_PRECIPITATION: - round(forecast.total_precipitation), + round(forecast.total_precipitation, 1), ATTR_FORECAST_CONDITION: condition, }) diff --git a/homeassistant/components/snips/__init__.py b/homeassistant/components/snips/__init__.py index 9a5508c8f3222..20cc7137ef880 100644 --- a/homeassistant/components/snips/__init__.py +++ b/homeassistant/components/snips/__init__.py @@ -105,10 +105,10 @@ async def message_received(topic, payload, qos): _LOGGER.error('Received invalid JSON: %s', payload) return - if (request['intent']['probability'] + if (request['intent']['confidenceScore'] < config[DOMAIN].get(CONF_PROBABILITY)): _LOGGER.warning("Intent below probaility threshold %s < %s", - request['intent']['probability'], + request['intent']['confidenceScore'], config[DOMAIN].get(CONF_PROBABILITY)) return @@ -130,7 +130,9 @@ async def message_received(topic, payload, qos): 'value': slot['rawValue']} slots['site_id'] = {'value': request.get('siteId')} slots['session_id'] = {'value': request.get('sessionId')} - slots['probability'] = {'value': request['intent']['probability']} + slots['confidenceScore'] = { + 'value': request['intent']['confidenceScore'] + } try: intent_response = await intent.async_handle( diff --git a/homeassistant/components/sonos/.translations/it.json b/homeassistant/components/sonos/.translations/it.json index e32557f1d9556..06c873b436e3a 100644 --- a/homeassistant/components/sonos/.translations/it.json +++ b/homeassistant/components/sonos/.translations/it.json @@ -6,7 +6,7 @@ }, "step": { "confirm": { - "description": "Vuoi installare Sonos", + "description": "Vuoi configurare Sonos?", "title": "Sonos" } }, diff --git a/homeassistant/components/sonos/__init__.py b/homeassistant/components/sonos/__init__.py index 69d5a9bfc3319..e9f297e4f079f 100644 --- a/homeassistant/components/sonos/__init__.py +++ b/homeassistant/components/sonos/__init__.py @@ -4,7 +4,7 @@ DOMAIN = 'sonos' -REQUIREMENTS = ['pysonos==0.0.6'] +REQUIREMENTS = ['pysonos==0.0.8'] async def async_setup(hass, config): diff --git a/homeassistant/components/sonos/media_player.py b/homeassistant/components/sonos/media_player.py index 2a7eafaf835be..e0f881f723d06 100644 --- a/homeassistant/components/sonos/media_player.py +++ b/homeassistant/components/sonos/media_player.py @@ -48,7 +48,7 @@ SERVICE_UPDATE_ALARM = 'sonos_update_alarm' SERVICE_SET_OPTION = 'sonos_set_option' -DATA_SONOS = 'sonos_devices' +DATA_SONOS = 'sonos_media_player' SOURCE_LINEIN = 'Line-in' SOURCE_TV = 'TV' @@ -114,7 +114,7 @@ class SonosData: def __init__(self): """Initialize the data.""" self.uids = set() - self.devices = [] + self.entities = [] self.topology_lock = threading.Lock() @@ -129,9 +129,9 @@ def setup_platform(hass, config, add_entities, discovery_info=None): async def async_setup_entry(hass, config_entry, async_add_entities): """Set up Sonos from a config entry.""" - def add_entities(devices, update_before_add=False): - """Sync version of async add devices.""" - hass.add_job(async_add_entities, devices, update_before_add) + def add_entities(entities, update_before_add=False): + """Sync version of async add entities.""" + hass.add_job(async_add_entities, entities, update_before_add) hass.async_add_executor_job( _setup_platform, hass, hass.data[SONOS_DOMAIN].get('media_player', {}), @@ -153,7 +153,7 @@ def _setup_platform(hass, config, add_entities, discovery_info): if discovery_info: player = pysonos.SoCo(discovery_info.get('host')) - # If device already exists by config + # If host already exists by config if player.uid in hass.data[DATA_SONOS].uids: return @@ -176,53 +176,51 @@ def _setup_platform(hass, config, add_entities, discovery_info): _LOGGER.warning("Failed to initialize '%s'", host) else: players = pysonos.discover( - interface_addr=config.get(CONF_INTERFACE_ADDR)) + interface_addr=config.get(CONF_INTERFACE_ADDR), + all_households=True) if not players: _LOGGER.warning("No Sonos speakers found") return hass.data[DATA_SONOS].uids.update(p.uid for p in players) - add_entities(SonosDevice(p) for p in players) + add_entities(SonosEntity(p) for p in players) _LOGGER.debug("Added %s Sonos speakers", len(players)) def service_handle(service): """Handle for services.""" entity_ids = service.data.get('entity_id') - devices = hass.data[DATA_SONOS].devices + entities = hass.data[DATA_SONOS].entities if entity_ids: - devices = [d for d in devices if d.entity_id in entity_ids] - - if service.service == SERVICE_JOIN: - master = [device for device in hass.data[DATA_SONOS].devices - if device.entity_id == service.data[ATTR_MASTER]] - if master: - with hass.data[DATA_SONOS].topology_lock: - master[0].join(devices) - return - - if service.service == SERVICE_UNJOIN: - with hass.data[DATA_SONOS].topology_lock: - for device in devices: - device.unjoin() - return + entities = [e for e in entities if e.entity_id in entity_ids] - for device in devices: + with hass.data[DATA_SONOS].topology_lock: if service.service == SERVICE_SNAPSHOT: - device.snapshot(service.data[ATTR_WITH_GROUP]) + SonosEntity.snapshot_multi( + entities, service.data[ATTR_WITH_GROUP]) elif service.service == SERVICE_RESTORE: - device.restore(service.data[ATTR_WITH_GROUP]) - elif service.service == SERVICE_SET_TIMER: - device.set_sleep_timer(service.data[ATTR_SLEEP_TIME]) - elif service.service == SERVICE_CLEAR_TIMER: - device.clear_sleep_timer() - elif service.service == SERVICE_UPDATE_ALARM: - device.set_alarm(**service.data) - elif service.service == SERVICE_SET_OPTION: - device.set_option(**service.data) - - device.schedule_update_ha_state(True) + SonosEntity.restore_multi( + entities, service.data[ATTR_WITH_GROUP]) + elif service.service == SERVICE_JOIN: + master = [e for e in hass.data[DATA_SONOS].entities + if e.entity_id == service.data[ATTR_MASTER]] + if master: + master[0].join(entities) + else: + for entity in entities: + if service.service == SERVICE_UNJOIN: + entity.unjoin() + elif service.service == SERVICE_SET_TIMER: + entity.set_sleep_timer(service.data[ATTR_SLEEP_TIME]) + elif service.service == SERVICE_CLEAR_TIMER: + entity.clear_sleep_timer() + elif service.service == SERVICE_UPDATE_ALARM: + entity.set_alarm(**service.data) + elif service.service == SERVICE_SET_OPTION: + entity.set_option(**service.data) + + entity.schedule_update_ha_state(True) hass.services.register( DOMAIN, SERVICE_JOIN, service_handle, @@ -270,9 +268,9 @@ def put(self, item, block=True, timeout=None): def _get_entity_from_soco_uid(hass, uid): - """Return SonosDevice from SoCo uid.""" - for entity in hass.data[DATA_SONOS].devices: - if uid == entity.soco.uid: + """Return SonosEntity from SoCo uid.""" + for entity in hass.data[DATA_SONOS].entities: + if uid == entity.unique_id: return entity return None @@ -303,11 +301,11 @@ def wrapper(*args, **kwargs): def soco_coordinator(funct): """Call function on coordinator.""" @ft.wraps(funct) - def wrapper(device, *args, **kwargs): + def wrapper(entity, *args, **kwargs): """Wrap for call to coordinator.""" - if device.is_coordinator: - return funct(device, *args, **kwargs) - return funct(device.coordinator, *args, **kwargs) + if entity.is_coordinator: + return funct(entity, *args, **kwargs) + return funct(entity.coordinator, *args, **kwargs) return wrapper @@ -329,11 +327,11 @@ def _is_radio_uri(uri): return uri.startswith(radio_schemes) -class SonosDevice(MediaPlayerDevice): - """Representation of a Sonos device.""" +class SonosEntity(MediaPlayerDevice): + """Representation of a Sonos entity.""" def __init__(self, player): - """Initialize the Sonos device.""" + """Initialize the Sonos entity.""" self._subscriptions = [] self._receives_events = False self._volume_increment = 2 @@ -345,7 +343,7 @@ def __init__(self, player): self._shuffle = None self._name = None self._coordinator = None - self._sonos_group = None + self._sonos_group = [self] self._status = None self._media_duration = None self._media_position = None @@ -361,12 +359,13 @@ def __init__(self, player): self._favorites = None self._soco_snapshot = None self._snapshot_group = None + self._restore_pending = False self._set_basic_information() async def async_added_to_hass(self): """Subscribe sonos events.""" - self.hass.data[DATA_SONOS].devices.append(self) + self.hass.data[DATA_SONOS].entities.append(self) self.hass.async_add_executor_job(self._subscribe_to_player_events) @property @@ -374,9 +373,13 @@ def unique_id(self): """Return a unique ID.""" return self._unique_id + def __hash__(self): + """Return a hash of self.""" + return hash(self.unique_id) + @property def name(self): - """Return the name of the device.""" + """Return the name of the entity.""" return self._name @property @@ -394,7 +397,7 @@ def device_info(self): @property @soco_coordinator def state(self): - """Return the state of the device.""" + """Return the state of the entity.""" if self._status in ('PAUSED_PLAYBACK', 'STOPPED'): return STATE_PAUSED if self._status in ('PLAYING', 'TRANSITIONING'): @@ -410,7 +413,7 @@ def is_coordinator(self): @property def soco(self): - """Return soco device.""" + """Return soco object.""" return self._player @property @@ -434,7 +437,7 @@ def _check_available(self): return False def _set_basic_information(self): - """Set initial device information.""" + """Set initial entity information.""" speaker_info = self.soco.get_speaker_info(True) self._name = speaker_info['zone_name'] self._model = speaker_info['model_name'] @@ -477,8 +480,8 @@ def _subscribe_to_player_events(self): self._receives_events = False # New player available, build the current group topology - for device in self.hass.data[DATA_SONOS].devices: - device.update_groups() + for entity in self.hass.data[DATA_SONOS].entities: + entity.update_groups() player = self.soco @@ -554,7 +557,7 @@ def update_media(self, event=None): self.schedule_update_ha_state() # Also update slaves - for entity in self.hass.data[DATA_SONOS].devices: + for entity in self.hass.data[DATA_SONOS].entities: coordinator = entity.coordinator if coordinator and coordinator.unique_id == self.unique_id: entity.schedule_update_ha_state() @@ -724,11 +727,14 @@ def update_groups(self, event=None): pass if self.unique_id == coordinator_uid: + if self._restore_pending: + self.restore() + sonos_group = [] for uid in (coordinator_uid, *slave_uids): entity = _get_entity_from_soco_uid(self.hass, uid) if entity: - sonos_group.append(entity.entity_id) + sonos_group.append(entity) self._coordinator = None self._sonos_group = sonos_group @@ -975,70 +981,80 @@ def unjoin(self): self._coordinator = None @soco_error() - def snapshot(self, with_group=True): - """Snapshot the player.""" + def snapshot(self, with_group): + """Snapshot the state of a player.""" from pysonos.snapshot import Snapshot self._soco_snapshot = Snapshot(self.soco) self._soco_snapshot.snapshot() - if with_group: - self._snapshot_group = self.soco.group - if self._coordinator: - self._coordinator.snapshot(False) + self._snapshot_group = self._sonos_group.copy() else: self._snapshot_group = None @soco_error() - def restore(self, with_group=True): - """Restore snapshot for the player.""" + def restore(self): + """Restore a snapshotted state to a player.""" from pysonos.exceptions import SoCoException + try: - # need catch exception if a coordinator is going to slave. - # this state will recover with group part. - self._soco_snapshot.restore(False) - except (TypeError, AttributeError, SoCoException): - _LOGGER.debug("Error on restore %s", self.entity_id) - - # restore groups - if with_group and self._snapshot_group: - old = self._snapshot_group - actual = self.soco.group - - ## - # Master have not change, update group - if old.coordinator == actual.coordinator: - if self.soco is not old.coordinator: - # restore state of the groups - self._coordinator.restore(False) - remove = actual.members - old.members - add = old.members - actual.members - - # remove new members - for soco_dev in list(remove): - soco_dev.unjoin() - - # add old members - for soco_dev in list(add): - soco_dev.join(old.coordinator) - return + # pylint: disable=protected-access + self.soco._zgs_cache.clear() + self._soco_snapshot.restore() + except (TypeError, AttributeError, SoCoException) as ex: + # Can happen if restoring a coordinator onto a current slave + _LOGGER.warning("Error on restore %s: %s", self.entity_id, ex) - ## - # old is already master, rejoin - if old.coordinator.group.coordinator == old.coordinator: - self.soco.join(old.coordinator) - return + self._soco_snapshot = None + self._snapshot_group = None + self._restore_pending = False + + @staticmethod + def snapshot_multi(entities, with_group): + """Snapshot all the entities and optionally their groups.""" + # pylint: disable=protected-access + # Find all affected players + entities = set(entities) + if with_group: + for entity in list(entities): + entities.update(entity._sonos_group) + + for entity in entities: + entity.snapshot(with_group) + + @staticmethod + def restore_multi(entities, with_group): + """Restore snapshots for all the entities.""" + # pylint: disable=protected-access + # Find all affected players + entities = set(e for e in entities if e._soco_snapshot) + if with_group: + for entity in [e for e in entities if e._snapshot_group]: + entities.update(entity._snapshot_group) - ## - # restore old master, update group - old.coordinator.unjoin() - coordinator = _get_entity_from_soco_uid( - self.hass, old.coordinator.uid) - coordinator.restore(False) + # Pause all current coordinators + for entity in (e for e in entities if e.is_coordinator): + if entity.state == STATE_PLAYING: + entity.media_pause() - for s_dev in list(old.members): - if s_dev != old.coordinator: - s_dev.join(old.coordinator) + # Bring back the original group topology + if with_group: + for entity in (e for e in entities if e._snapshot_group): + if entity._snapshot_group[0] == entity: + entity.join(entity._snapshot_group) + + # Restore slaves + for entity in (e for e in entities if not e.is_coordinator): + entity.restore() + + # Restore coordinators (or delay if moving from slave) + for entity in (e for e in entities if e.is_coordinator): + if entity._sonos_group[0] == entity: + # Was already coordinator + entity.restore() + else: + # Await coordinator role + entity._restore_pending = True @soco_error() @soco_coordinator @@ -1087,8 +1103,10 @@ def set_option(self, **data): @property def device_state_attributes(self): - """Return device specific state attributes.""" - attributes = {ATTR_SONOS_GROUP: self._sonos_group} + """Return entity specific state attributes.""" + attributes = { + ATTR_SONOS_GROUP: [e.entity_id for e in self._sonos_group], + } if self._night_sound is not None: attributes[ATTR_NIGHT_SOUND] = self._night_sound diff --git a/homeassistant/components/spider/climate.py b/homeassistant/components/spider/climate.py index 08af44ad1ad11..b3380ec8fb4b2 100644 --- a/homeassistant/components/spider/climate.py +++ b/homeassistant/components/spider/climate.py @@ -2,12 +2,13 @@ import logging -from homeassistant.components.climate import ( - ATTR_TEMPERATURE, STATE_COOL, STATE_HEAT, STATE_IDLE, +from homeassistant.components.climate import ClimateDevice +from homeassistant.components.climate.const import ( + STATE_COOL, STATE_HEAT, STATE_IDLE, SUPPORT_OPERATION_MODE, SUPPORT_TARGET_TEMPERATURE, - SUPPORT_FAN_MODE, ClimateDevice) + SUPPORT_FAN_MODE) from homeassistant.components.spider import DOMAIN as SPIDER_DOMAIN -from homeassistant.const import TEMP_CELSIUS +from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS DEPENDENCIES = ['spider'] diff --git a/homeassistant/components/switch/sony_projector.py b/homeassistant/components/switch/sony_projector.py new file mode 100644 index 0000000000000..5b3ffeed75f30 --- /dev/null +++ b/homeassistant/components/switch/sony_projector.py @@ -0,0 +1,97 @@ +"""Support for Sony projectors via SDCP network control.""" +import logging + +import voluptuous as vol + +from homeassistant.components.switch import (SwitchDevice, PLATFORM_SCHEMA) +from homeassistant.const import ( + STATE_ON, STATE_OFF, CONF_NAME, CONF_HOST) +import homeassistant.helpers.config_validation as cv + +REQUIREMENTS = ['pysdcp==1'] + +_LOGGER = logging.getLogger(__name__) + +DEFAULT_NAME = 'Sony Projector' + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string +}) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Connect to Sony projector using network.""" + import pysdcp + host = config[CONF_HOST] + name = config[CONF_NAME] + sdcp_connection = pysdcp.Projector(host) + + # Sanity check the connection + try: + sdcp_connection.get_power() + except ConnectionError: + _LOGGER.error("Failed to connect to projector '%s'", host) + return False + _LOGGER.debug("Validated projector '%s' OK", host) + add_entities([SonyProjector(sdcp_connection, name)], True) + return True + + +class SonyProjector(SwitchDevice): + """Represents a Sony Projector as a switch.""" + + def __init__(self, sdcp_connection, name): + """Init of the Sony projector.""" + self._sdcp = sdcp_connection + self._name = name + self._state = None + self._available = False + self._attributes = {} + + @property + def available(self): + """Return if projector is available.""" + return self._available + + @property + def name(self): + """Return name of the projector.""" + return self._name + + @property + def is_on(self): + """Return if the projector is turned on.""" + return self._state + + @property + def state_attributes(self): + """Return state attributes.""" + return self._attributes + + def update(self): + """Get the latest state from the projector.""" + try: + self._state = self._sdcp.get_power() + self._available = True + except ConnectionRefusedError: + _LOGGER.error("Projector connection refused") + self._available = False + + def turn_on(self, **kwargs): + """Turn the projector on.""" + _LOGGER.debug("Powering on projector '%s'...", self.name) + if self._sdcp.set_power(True): + _LOGGER.debug("Powered on successfully.") + self._state = STATE_ON + else: + _LOGGER.error("Power on command was not successful") + + def turn_off(self, **kwargs): + """Turn the projector off.""" + _LOGGER.debug("Powering off projector '%s'...", self.name) + if self._sdcp.set_power(False): + _LOGGER.debug("Powered off successfully.") + self._state = STATE_OFF + else: + _LOGGER.error("Power off command was not successful") diff --git a/homeassistant/components/switch/switchbot.py b/homeassistant/components/switch/switchbot.py index 9cd2927d83282..a85357b525a58 100644 --- a/homeassistant/components/switch/switchbot.py +++ b/homeassistant/components/switch/switchbot.py @@ -36,6 +36,7 @@ class SwitchBot(SwitchDevice): def __init__(self, mac, name) -> None: """Initialize the Switchbot.""" + # pylint: disable=import-error, no-member import switchbot self._state = False self._name = name diff --git a/homeassistant/components/switch/switchmate.py b/homeassistant/components/switch/switchmate.py index be80ef19169af..60497e0207b70 100644 --- a/homeassistant/components/switch/switchmate.py +++ b/homeassistant/components/switch/switchmate.py @@ -34,14 +34,15 @@ def setup_platform(hass, config, add_entities, discovery_info=None) -> None: name = config.get(CONF_NAME) mac_addr = config[CONF_MAC] flip_on_off = config[CONF_FLIP_ON_OFF] - add_entities([Switchmate(mac_addr, name, flip_on_off)], True) + add_entities([SwitchmateEntity(mac_addr, name, flip_on_off)], True) -class Switchmate(SwitchDevice): +class SwitchmateEntity(SwitchDevice): """Representation of a Switchmate.""" def __init__(self, mac, name, flip_on_off) -> None: """Initialize the Switchmate.""" + # pylint: disable=import-error, no-member, no-value-for-parameter import switchmate self._mac = mac self._name = name diff --git a/homeassistant/components/system_log/__init__.py b/homeassistant/components/system_log/__init__.py index 16786bdeba484..d6877c32f0dee 100644 --- a/homeassistant/components/system_log/__init__.py +++ b/homeassistant/components/system_log/__init__.py @@ -91,15 +91,15 @@ def __init__(self, record, stack, source): self.first_occured = self.timestamp = record.created self.level = record.levelname self.message = record.getMessage() + self.exception = '' + self.root_cause = None if record.exc_info: self.exception = ''.join( traceback.format_exception(*record.exc_info)) _, _, tb = record.exc_info # pylint: disable=invalid-name # Last line of traceback contains the root cause of the exception - self.root_cause = str(traceback.extract_tb(tb)[-1]) - else: - self.exception = '' - self.root_cause = None + if traceback.extract_tb(tb): + self.root_cause = str(traceback.extract_tb(tb)[-1]) self.source = source self.count = 1 diff --git a/homeassistant/components/tado/climate.py b/homeassistant/components/tado/climate.py index 1812d36b7cdb5..d5f152bbd76d5 100644 --- a/homeassistant/components/tado/climate.py +++ b/homeassistant/components/tado/climate.py @@ -2,8 +2,9 @@ import logging from homeassistant.const import (PRECISION_TENTHS, TEMP_CELSIUS) -from homeassistant.components.climate import ( - ClimateDevice, SUPPORT_TARGET_TEMPERATURE, SUPPORT_OPERATION_MODE) +from homeassistant.components.climate import ClimateDevice +from homeassistant.components.climate.const import ( + SUPPORT_TARGET_TEMPERATURE, SUPPORT_OPERATION_MODE) from homeassistant.util.temperature import convert as convert_temperature from homeassistant.const import ATTR_TEMPERATURE from homeassistant.components.tado import DATA_TADO diff --git a/homeassistant/components/tahoma/__init__.py b/homeassistant/components/tahoma/__init__.py index e76cadc7ce3da..1807667da87dc 100644 --- a/homeassistant/components/tahoma/__init__.py +++ b/homeassistant/components/tahoma/__init__.py @@ -43,6 +43,7 @@ 'io:SomfyContactIOSystemSensor': 'sensor', 'io:VerticalExteriorAwningIOComponent': 'cover', 'io:WindowOpenerVeluxIOComponent': 'cover', + 'io:GarageOpenerIOComponent': 'cover', 'rtds:RTDSContactSensor': 'sensor', 'rtds:RTDSMotionSensor': 'sensor', 'rtds:RTDSSmokeSensor': 'smoke', diff --git a/homeassistant/components/telegram_bot/__init__.py b/homeassistant/components/telegram_bot/__init__.py index 18f206541dfa6..78d45535c4847 100644 --- a/homeassistant/components/telegram_bot/__init__.py +++ b/homeassistant/components/telegram_bot/__init__.py @@ -84,6 +84,8 @@ vol.Optional(CONF_PROXY_PARAMS): dict, }) +PLATFORM_SCHEMA_BASE = cv.PLATFORM_SCHEMA_BASE.extend(PLATFORM_SCHEMA.schema) + BASE_SERVICE_SCHEMA = vol.Schema({ vol.Optional(ATTR_TARGET): vol.All(cv.ensure_list, [vol.Coerce(int)]), vol.Optional(ATTR_PARSER): cv.string, @@ -628,7 +630,7 @@ def process_message(self, data): self.hass.bus.async_fire(event, event_data) return True - elif ATTR_CALLBACK_QUERY in data: + if ATTR_CALLBACK_QUERY in data: event = EVENT_TELEGRAM_CALLBACK data = data.get(ATTR_CALLBACK_QUERY) message_ok, event_data = self._get_message_data(data) @@ -642,6 +644,6 @@ def process_message(self, data): self.hass.bus.async_fire(event, event_data) return True - else: - _LOGGER.warning("Message with unknown data received: %s", data) - return True + + _LOGGER.warning("Message with unknown data received: %s", data) + return True diff --git a/homeassistant/components/telegram_bot/polling.py b/homeassistant/components/telegram_bot/polling.py index 5bca4321a5f22..9936b69098568 100644 --- a/homeassistant/components/telegram_bot/polling.py +++ b/homeassistant/components/telegram_bot/polling.py @@ -61,7 +61,7 @@ def __init__(self): """Initialize the messages handler instance.""" super().__init__(handler) - def check_update(self, update): + def check_update(self, update): # pylint: disable=no-self-use """Check is update valid.""" return isinstance(update, Update) diff --git a/homeassistant/components/tellduslive/.translations/de.json b/homeassistant/components/tellduslive/.translations/de.json index 4e9c32a1ee5dd..a9f91f16b1195 100644 --- a/homeassistant/components/tellduslive/.translations/de.json +++ b/homeassistant/components/tellduslive/.translations/de.json @@ -2,10 +2,14 @@ "config": { "abort": { "all_configured": "TelldusLive ist bereits konfiguriert", + "already_setup": "TelldusLive ist bereits konfiguriert", "authorize_url_fail": "Unbekannter Fehler beim Erstellen der Authorisierungs-URL", "authorize_url_timeout": "Zeit\u00fcberschreitung beim Erstellen der Authorisierungs-URL.", "unknown": "Unbekannter Fehler ist aufgetreten" }, + "error": { + "auth_error": "Authentifizierungsfehler, bitte versuchen Sie es erneut" + }, "step": { "auth": { "description": "So verkn\u00fcpfen Sie Ihr TelldusLive-Konto: \n 1. Klicken Sie auf den Link unten \n 2. Melden Sie sich bei Telldus Live an \n 3. Autorisieren Sie ** {app_name} ** (klicken Sie auf ** Yes **). \n 4. Kommen Sie hierher zur\u00fcck und klicken Sie auf ** SUBMIT **. \n\n [Link TelldusLive-Konto]({auth_url})", diff --git a/homeassistant/components/tellduslive/.translations/es-419.json b/homeassistant/components/tellduslive/.translations/es-419.json new file mode 100644 index 0000000000000..bf74d1048358f --- /dev/null +++ b/homeassistant/components/tellduslive/.translations/es-419.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "all_configured": "TelldusLive ya est\u00e1 configurado", + "already_setup": "TelldusLive ya est\u00e1 configurado", + "unknown": "Se produjo un error desconocido" + }, + "error": { + "auth_error": "Error de autenticaci\u00f3n, por favor intente de nuevo" + }, + "step": { + "auth": { + "description": "Para vincular su cuenta de TelldusLive: \n 1. Haga clic en el siguiente enlace \n 2. Inicia sesi\u00f3n en Telldus Live \n 3. Autorice ** {app_name} ** (haga clic en ** S\u00ed **). \n 4. Vuelve aqu\u00ed y haz clic en ** ENVIAR **. \n\n [Enlace a la cuenta de TelldusLive] ( {auth_url} )", + "title": "Autenticar con TelldusLive" + }, + "user": { + "data": { + "host": "Host" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tellduslive/.translations/es.json b/homeassistant/components/tellduslive/.translations/es.json index 4e7de72edc408..bf1aedab17d22 100644 --- a/homeassistant/components/tellduslive/.translations/es.json +++ b/homeassistant/components/tellduslive/.translations/es.json @@ -1,7 +1,11 @@ { "config": { "abort": { - "already_setup": "TelldusLive ya est\u00e1 configurado" + "all_configured": "TelldusLive ya est\u00e1 configurado", + "already_setup": "TelldusLive ya est\u00e1 configurado", + "authorize_url_fail": "Error desconocido generando la url de autorizaci\u00f3n", + "authorize_url_timeout": "Tiempo de espera agotado generando la url de autorizaci\u00f3n", + "unknown": "Se produjo un error desconocido" }, "error": { "auth_error": "Error de autenticaci\u00f3n, por favor int\u00e9ntalo de nuevo" diff --git a/homeassistant/components/tellduslive/.translations/hu.json b/homeassistant/components/tellduslive/.translations/hu.json index ffa983db09395..6057d7b3212df 100644 --- a/homeassistant/components/tellduslive/.translations/hu.json +++ b/homeassistant/components/tellduslive/.translations/hu.json @@ -1,15 +1,24 @@ { "config": { "abort": { + "all_configured": "A TelldusLive-ot m\u00e1r be\u00e1ll\u00edtottuk.", + "already_setup": "A TelldusLive m\u00e1r be van \u00e1ll\u00edtva", + "authorize_url_fail": "Ismeretlen hiba t\u00f6rt\u00e9nt a hiteles\u00edt\u00e9si link gener\u00e1l\u00e1sa sor\u00e1n.", + "authorize_url_timeout": "Id\u0151t\u00fall\u00e9p\u00e9s az \u00e9rv\u00e9nyes\u00edt\u00e9si url gener\u00e1l\u00e1sa sor\u00e1n.", "unknown": "Ismeretlen hiba t\u00f6rt\u00e9nt" }, + "error": { + "auth_error": "Hiteles\u00edt\u00e9si hiba, pr\u00f3b\u00e1lkozz \u00fajra" + }, "step": { "user": { "data": { "host": "Kiszolg\u00e1l\u00f3" }, - "description": "\u00dcres" + "description": "\u00dcres", + "title": "V\u00e1lassz v\u00e9gpontot." } - } + }, + "title": "Telldus Live" } } \ No newline at end of file diff --git a/homeassistant/components/tellduslive/.translations/it.json b/homeassistant/components/tellduslive/.translations/it.json new file mode 100644 index 0000000000000..90f13184a67d9 --- /dev/null +++ b/homeassistant/components/tellduslive/.translations/it.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "all_configured": "TelldusLive \u00e8 gi\u00e0 configurato", + "already_setup": "TelldusLive \u00e8 gi\u00e0 configurato", + "authorize_url_fail": "Errore sconosciuto nel generare l'url di autorizzazione", + "authorize_url_timeout": "Tempo scaduto nel generare l'url di autorizzazione", + "unknown": "Si \u00e8 verificato un errore sconosciuto." + }, + "error": { + "auth_error": "Errore di autenticazione, riprovare" + }, + "step": { + "auth": { + "title": "Autenticati con TelldusLive" + }, + "user": { + "data": { + "host": "Host" + }, + "title": "Scegli l'endpoint." + } + }, + "title": "Telldus Live" + } +} \ No newline at end of file diff --git a/homeassistant/components/tellduslive/.translations/pt.json b/homeassistant/components/tellduslive/.translations/pt.json index 90da12451df2c..a13f71f75052c 100644 --- a/homeassistant/components/tellduslive/.translations/pt.json +++ b/homeassistant/components/tellduslive/.translations/pt.json @@ -2,10 +2,14 @@ "config": { "abort": { "all_configured": "TelldusLive j\u00e1 est\u00e1 configurado", + "already_setup": "TelldusLive j\u00e1 est\u00e1 configurado", "authorize_url_fail": "Erro desconhecido ao gerar um URL de autoriza\u00e7\u00e3o.", "authorize_url_timeout": "Limite temporal ultrapassado ao gerar um URL de autoriza\u00e7\u00e3o.", "unknown": "Ocorreu um erro desconhecido" }, + "error": { + "auth_error": "Erro de autentica\u00e7\u00e3o, por favor tente novamente" + }, "step": { "auth": { "description": "Para ligar \u00e0 sua conta do TelldusLive: \n 1. Clique no link abaixo \n 2. Fa\u00e7a o login no Telldus Live \n 3. Autorize **{app_name}** (clique em **Sim**). \n 4. Volte aqui e clique em **ENVIAR**. \n\n [Ligar \u00e0 TelldusLive] ( {auth_url} )", diff --git a/homeassistant/components/tellduslive/.translations/sv.json b/homeassistant/components/tellduslive/.translations/sv.json new file mode 100644 index 0000000000000..5636e13794822 --- /dev/null +++ b/homeassistant/components/tellduslive/.translations/sv.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "all_configured": "Telldus Live! \u00e4r redan konfigurerad", + "already_setup": "Telldus Live! \u00e4r redan konfigurerad", + "authorize_url_fail": "Ok\u00e4nt fel n\u00e4r genererar en url f\u00f6r att auktorisera.", + "authorize_url_timeout": "Timeout n\u00e4r genererar auktorisera url.", + "unknown": "Ok\u00e4nt fel intr\u00e4ffade" + }, + "error": { + "auth_error": "Autentiseringsfel, v\u00e4nligen f\u00f6rs\u00f6k igen" + }, + "step": { + "auth": { + "description": "F\u00f6r att l\u00e4nka ditt \"Telldus Live!\" konto: \n 1. Klicka p\u00e5 l\u00e4nken nedan \n 2. Logga in p\u00e5 Telldus Live!\n 3. Godk\u00e4nn **{app_name}** (klicka **Yes**). \n 4. Kom tillbaka hit och klicka p\u00e5 **SUBMIT**. \n\n [L\u00e4nk till Telldus Live konto]({auth_url})", + "title": "Autentisera mot Telldus Live!" + }, + "user": { + "data": { + "host": "V\u00e4rddatorn" + }, + "description": "?", + "title": "V\u00e4lj endpoint." + } + }, + "title": "Telldus Live!" + } +} \ No newline at end of file diff --git a/homeassistant/components/tellduslive/.translations/zh-Hant.json b/homeassistant/components/tellduslive/.translations/zh-Hant.json index c632b54363485..c95e96b21c900 100644 --- a/homeassistant/components/tellduslive/.translations/zh-Hant.json +++ b/homeassistant/components/tellduslive/.translations/zh-Hant.json @@ -20,7 +20,7 @@ "host": "\u4e3b\u6a5f\u7aef" }, "description": "\u7a7a\u767d", - "title": "\u9078\u64c7 endpoint\u3002" + "title": "\u9078\u64c7\u7aef\u9ede\u3002" } }, "title": "Telldus Live" diff --git a/homeassistant/components/tellduslive/config_flow.py b/homeassistant/components/tellduslive/config_flow.py index 3373e9cc2f7ca..62463bc0a9ea3 100644 --- a/homeassistant/components/tellduslive/config_flow.py +++ b/homeassistant/components/tellduslive/config_flow.py @@ -84,8 +84,7 @@ async def async_step_auth(self, user_input=None): KEY_SCAN_INTERVAL: self._scan_interval.seconds, KEY_SESSION: session, }) - else: - errors['base'] = 'auth_error' + errors['base'] = 'auth_error' try: with async_timeout.timeout(10): diff --git a/homeassistant/components/tellduslive/cover.py b/homeassistant/components/tellduslive/cover.py index 5a22311d7f096..1bd3158d100e1 100644 --- a/homeassistant/components/tellduslive/cover.py +++ b/homeassistant/components/tellduslive/cover.py @@ -44,11 +44,14 @@ def is_closed(self): def close_cover(self, **kwargs): """Close the cover.""" self.device.down() + self._update_callback() def open_cover(self, **kwargs): """Open the cover.""" self.device.up() + self._update_callback() def stop_cover(self, **kwargs): """Stop the cover.""" self.device.stop() + self._update_callback() diff --git a/homeassistant/components/tellduslive/light.py b/homeassistant/components/tellduslive/light.py index 10eaee1ad8b6c..12baf8384f636 100644 --- a/homeassistant/components/tellduslive/light.py +++ b/homeassistant/components/tellduslive/light.py @@ -45,6 +45,7 @@ def __init__(self, client, device_id): def changed(self): """Define a property of the device that might have changed.""" self._last_brightness = self.brightness + self._update_callback() @property def brightness(self): diff --git a/homeassistant/components/tellduslive/switch.py b/homeassistant/components/tellduslive/switch.py index 63d1512698ca9..bb0164b10bb21 100644 --- a/homeassistant/components/tellduslive/switch.py +++ b/homeassistant/components/tellduslive/switch.py @@ -44,7 +44,9 @@ def is_on(self): def turn_on(self, **kwargs): """Turn the switch on.""" self.device.turn_on() + self._update_callback() def turn_off(self, **kwargs): """Turn the switch off.""" self.device.turn_off() + self._update_callback() diff --git a/homeassistant/components/tesla/climate.py b/homeassistant/components/tesla/climate.py index 302c0006bcf2d..118e7204bca07 100644 --- a/homeassistant/components/tesla/climate.py +++ b/homeassistant/components/tesla/climate.py @@ -1,9 +1,9 @@ """Support for Tesla HVAC system.""" import logging -from homeassistant.components.climate import ( - ENTITY_ID_FORMAT, SUPPORT_OPERATION_MODE, SUPPORT_TARGET_TEMPERATURE, - ClimateDevice) +from homeassistant.components.climate import ClimateDevice, ENTITY_ID_FORMAT +from homeassistant.components.climate.const import ( + SUPPORT_OPERATION_MODE, SUPPORT_TARGET_TEMPERATURE) from homeassistant.components.tesla import DOMAIN as TESLA_DOMAIN from homeassistant.components.tesla import TeslaDevice from homeassistant.const import ( diff --git a/homeassistant/components/tibber/__init__.py b/homeassistant/components/tibber/__init__.py index ba9ae43f13bcf..f254774eea4cc 100644 --- a/homeassistant/components/tibber/__init__.py +++ b/homeassistant/components/tibber/__init__.py @@ -11,7 +11,7 @@ from homeassistant.helpers import discovery from homeassistant.helpers.aiohttp_client import async_get_clientsession -REQUIREMENTS = ['pyTibber==0.9.4'] +REQUIREMENTS = ['pyTibber==0.9.6'] DOMAIN = 'tibber' diff --git a/homeassistant/components/toon/.translations/ca.json b/homeassistant/components/toon/.translations/ca.json new file mode 100644 index 0000000000000..0a88b82f8296f --- /dev/null +++ b/homeassistant/components/toon/.translations/ca.json @@ -0,0 +1,34 @@ +{ + "config": { + "abort": { + "client_id": "L'identificador de client de la configuraci\u00f3 no \u00e9s v\u00e0lid.", + "client_secret": "El codi secret de client de la configuraci\u00f3 no \u00e9s v\u00e0lid.", + "no_agreements": "Aquest compte no t\u00e9 pantalles Toon.", + "no_app": "Has de configurar Toon abans de poder autenticar-t'hi. Llegeix les [instruccions](https://www.home-assistant.io/components/toon/).", + "unknown_auth_fail": "S'ha produ\u00eft un error inesperat durant l'autenticaci\u00f3." + }, + "error": { + "credentials": "Les credencials proporcionades no s\u00f3n v\u00e0lides.", + "display_exists": "La pantalla seleccionada ja est\u00e0 configurada." + }, + "step": { + "authenticate": { + "data": { + "password": "Contrasenya", + "tenant": "Tenant", + "username": "Nom d'usuari" + }, + "description": "Autentica't amb el teu compte d'Eneco Toon (no el compte de desenvolupador).", + "title": "Enlla\u00e7ar compte de Toon" + }, + "display": { + "data": { + "display": "Tria la visualitzaci\u00f3" + }, + "description": "Selecciona la pantalla Toon amb la qual vols connectar-te.", + "title": "Selecci\u00f3 de pantalla" + } + }, + "title": "Toon" + } +} \ No newline at end of file diff --git a/homeassistant/components/toon/.translations/en.json b/homeassistant/components/toon/.translations/en.json new file mode 100644 index 0000000000000..cea3146a3a557 --- /dev/null +++ b/homeassistant/components/toon/.translations/en.json @@ -0,0 +1,34 @@ +{ + "config": { + "abort": { + "client_id": "The client ID from the configuration is invalid.", + "client_secret": "The client secret from the configuration is invalid.", + "no_agreements": "This account has no Toon displays.", + "no_app": "You need to configure Toon before being able to authenticate with it. [Please read the instructions](https://www.home-assistant.io/components/toon/).", + "unknown_auth_fail": "Unexpected error occured, while authenticating." + }, + "error": { + "credentials": "The provided credentials are invalid.", + "display_exists": "The selected display is already configured." + }, + "step": { + "authenticate": { + "data": { + "password": "Password", + "tenant": "Tenant", + "username": "Username" + }, + "description": "Authenticate with your Eneco Toon account (not the developer account).", + "title": "Link your Toon account" + }, + "display": { + "data": { + "display": "Choose display" + }, + "description": "Select the Toon display to connect with.", + "title": "Select display" + } + }, + "title": "Toon" + } +} \ No newline at end of file diff --git a/homeassistant/components/toon/.translations/es-419.json b/homeassistant/components/toon/.translations/es-419.json new file mode 100644 index 0000000000000..db064def53b77 --- /dev/null +++ b/homeassistant/components/toon/.translations/es-419.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "unknown_auth_fail": "Ocurri\u00f3 un error inesperado, mientras se autenticaba." + }, + "error": { + "credentials": "Las credenciales proporcionadas no son v\u00e1lidas." + }, + "step": { + "authenticate": { + "data": { + "password": "Contrase\u00f1a", + "username": "Nombre de usuario" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/toon/.translations/ko.json b/homeassistant/components/toon/.translations/ko.json new file mode 100644 index 0000000000000..3a0698aed8eb2 --- /dev/null +++ b/homeassistant/components/toon/.translations/ko.json @@ -0,0 +1,34 @@ +{ + "config": { + "abort": { + "client_id": "\ud074\ub77c\uc774\uc5b8\ud2b8 ID \uac00 \uc720\ud6a8\ud558\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4.", + "client_secret": "\ud074\ub77c\uc774\uc5b8\ud2b8 \ube44\ubc00\ubc88\ud638\uac00 \uc720\ud6a8\ud558\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4.", + "no_agreements": "\uc774 \uacc4\uc815\uc5d0\ub294 Toon \ub514\uc2a4\ud50c\ub808\uc774\uac00 \uc5c6\uc2b5\ub2c8\ub2e4.", + "no_app": "Toon \uc744 \uc778\uc99d\ud558\uae30 \uc804\uc5d0 Toon \ub97c \uad6c\uc131\ud574\uc57c \ud569\ub2c8\ub2e4. [\uc548\ub0b4](https://www.home-assistant.io/components/toon/) \ub97c \uc77d\uc5b4\ubcf4\uc138\uc694.", + "unknown_auth_fail": "\uc778\uc99d\ud558\ub294 \ub3d9\uc548 \uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4." + }, + "error": { + "credentials": "\uc0ac\uc6a9\uc790 \uc774\ub984 \ud639\uc740 \ube44\ubc00\ubc88\ud638\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "display_exists": "\uc120\ud0dd\ub41c \ub514\uc2a4\ud50c\ub808\uc774\ub294 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4." + }, + "step": { + "authenticate": { + "data": { + "password": "\ube44\ubc00\ubc88\ud638", + "tenant": "\uac70\uc8fc\uc790", + "username": "\uc0ac\uc6a9\uc790 \uc774\ub984" + }, + "description": "Eneco Toon \uacc4\uc815\uc73c\ub85c \uc778\uc99d\ud574\uc8fc\uc138\uc694. (\uac1c\ubc1c\uc790 \uacc4\uc815 \uc544\ub2d8)", + "title": "Toon \uacc4\uc815 \uc5f0\uacb0" + }, + "display": { + "data": { + "display": "\ub514\uc2a4\ud50c\ub808\uc774 \uc120\ud0dd" + }, + "description": "\uc5f0\uacb0\ud560 Toon \ub514\uc2a4\ud50c\ub808\uc774\ub97c \uc120\ud0dd\ud574\uc8fc\uc138\uc694.", + "title": "\ub514\uc2a4\ud50c\ub808\uc774 \uc120\ud0dd" + } + }, + "title": "Toon" + } +} \ No newline at end of file diff --git a/homeassistant/components/toon/.translations/lb.json b/homeassistant/components/toon/.translations/lb.json new file mode 100644 index 0000000000000..6ea86c00057c7 --- /dev/null +++ b/homeassistant/components/toon/.translations/lb.json @@ -0,0 +1,34 @@ +{ + "config": { + "abort": { + "client_id": "Client ID vun der Konfiguratioun ass ong\u00eblteg.", + "client_secret": "Client Passwuert vun der Konfiguratioun ass ong\u00eblteg.", + "no_agreements": "D\u00ebse Kont huet keen Toon Ecran.", + "no_app": "Dir musst Toon konfigur\u00e9ieren, ier Dir d\u00ebs Authentifiz\u00e9ierung k\u00ebnnt benotzen.[Liest w.e.g. d'Instruktioune](https://www.home-assistant.io/components/toon/).", + "unknown_auth_fail": "Onerwaarte Feeler bei der Authentifikatioun." + }, + "error": { + "credentials": "Ong\u00eblteg Login Informatioune.", + "display_exists": "Den ausgewielten Ecran ass scho konfigur\u00e9iert." + }, + "step": { + "authenticate": { + "data": { + "password": "Passwuert", + "tenant": "Notzer", + "username": "Benotzernumm" + }, + "description": "Authentifikatioun mat \u00e4rem Eneco Toon Kont (net de Kont vum Entw\u00e9ckler)", + "title": "Toon Kont verbannnen" + }, + "display": { + "data": { + "display": "Ecran auswielen" + }, + "description": "Wielt den Toon Ecran aus fir sech domat ze verbannen.", + "title": "Ecran auswielen" + } + }, + "title": "Toon" + } +} \ No newline at end of file diff --git a/homeassistant/components/toon/.translations/ru.json b/homeassistant/components/toon/.translations/ru.json new file mode 100644 index 0000000000000..0cc162218e99c --- /dev/null +++ b/homeassistant/components/toon/.translations/ru.json @@ -0,0 +1,34 @@ +{ + "config": { + "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_app": "\u0412\u0430\u043c \u043d\u0443\u0436\u043d\u043e \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c Toon \u043f\u0435\u0440\u0435\u0434 \u0442\u0435\u043c, \u043a\u0430\u043a \u0432\u044b\u043f\u043e\u043b\u043d\u0438\u0442\u044c \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044e. \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", + "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": { + "authenticate": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "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" + }, + "display": { + "data": { + "display": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0434\u0438\u0441\u043f\u043b\u0435\u0439" + }, + "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0434\u0438\u0441\u043f\u043b\u0435\u0439 Toon \u0434\u043b\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f.", + "title": "Toon" + } + }, + "title": "Toon" + } +} \ No newline at end of file diff --git a/homeassistant/components/toon/.translations/zh-Hant.json b/homeassistant/components/toon/.translations/zh-Hant.json new file mode 100644 index 0000000000000..b09d921268cf1 --- /dev/null +++ b/homeassistant/components/toon/.translations/zh-Hant.json @@ -0,0 +1,34 @@ +{ + "config": { + "abort": { + "client_id": "\u8a2d\u5b9a\u5167\u7528\u6236\u7aef ID \u7121\u6548\u3002", + "client_secret": "\u8a2d\u5b9a\u5167\u5ba2\u6236\u7aef\u5bc6\u78bc\u7121\u6548\u3002", + "no_agreements": "\u6b64\u5e33\u865f\u4e26\u672a\u64c1\u6709 Toon \u88dd\u7f6e\u3002", + "no_app": "\u5fc5\u9808\u5148\u8a2d\u5b9a Toon \u65b9\u80fd\u9032\u884c\u8a8d\u8b49\u3002[\u8acb\u53c3\u95b1\u6559\u5b78\u6307\u5f15]\uff08https://www.home-assistant.io/components/toon/\uff09\u3002", + "unknown_auth_fail": "\u9a57\u8b49\u6642\u767c\u751f\u672a\u77e5\u932f\u8aa4\u3002" + }, + "error": { + "credentials": "\u6240\u63d0\u4f9b\u7684\u6191\u8b49\u7121\u6548\u3002", + "display_exists": "\u6240\u9078\u64c7\u7684\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3002" + }, + "step": { + "authenticate": { + "data": { + "password": "\u5bc6\u78bc", + "tenant": "\u79df\u7528", + "username": "\u4f7f\u7528\u8005\u540d\u7a31" + }, + "description": "\u4f7f\u7528 Eneco Toon \u5e33\u865f\uff08\u975e\u958b\u767c\u8005\u5e33\u865f\uff09\u9032\u884c\u9a57\u8b49\u3002", + "title": "\u9023\u7d50 Toon \u5e33\u865f" + }, + "display": { + "data": { + "display": "\u9078\u64c7\u88dd\u7f6e" + }, + "description": "\u9078\u64c7\u6240\u8981\u9023\u63a5\u7684 Toon display\u3002", + "title": "\u9078\u64c7\u88dd\u7f6e" + } + }, + "title": "Toon" + } +} \ No newline at end of file diff --git a/homeassistant/components/toon/__init__.py b/homeassistant/components/toon/__init__.py index 96d8b4e6d150c..0ca0a414fa569 100644 --- a/homeassistant/components/toon/__init__.py +++ b/homeassistant/components/toon/__init__.py @@ -1,142 +1,197 @@ """Support for Toon van Eneco devices.""" -from datetime import datetime, timedelta import logging +from typing import Any, Dict +from functools import partial import voluptuous as vol from homeassistant.const import CONF_PASSWORD, CONF_USERNAME -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.discovery import load_platform -from homeassistant.util import Throttle +from homeassistant.helpers import (config_validation as cv, + device_registry as dr) +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.typing import ConfigType, HomeAssistantType -REQUIREMENTS = ['toonlib==1.1.3'] +from . import config_flow # noqa pylint_disable=unused-import +from .const import ( + CONF_CLIENT_ID, CONF_CLIENT_SECRET, CONF_DISPLAY, CONF_TENANT, + DATA_TOON_CLIENT, DATA_TOON_CONFIG, DOMAIN) -_LOGGER = logging.getLogger(__name__) - -CONF_GAS = 'gas' -CONF_SOLAR = 'solar' - -DEFAULT_GAS = True -DEFAULT_SOLAR = False -DOMAIN = 'toon' - -MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=5) +REQUIREMENTS = ['toonapilib==3.2.1'] -TOON_HANDLE = 'toon_handle' +_LOGGER = logging.getLogger(__name__) # Validation of the user's configuration CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.Schema({ - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Optional(CONF_GAS, default=DEFAULT_GAS): cv.boolean, - vol.Optional(CONF_SOLAR, default=DEFAULT_SOLAR): cv.boolean, + vol.Required(CONF_CLIENT_ID): cv.string, + vol.Required(CONF_CLIENT_SECRET): cv.string, }), }, extra=vol.ALLOW_EXTRA) -def setup(hass, config): - """Set up the Toon component.""" - from toonlib import InvalidCredentials - gas = config[DOMAIN][CONF_GAS] - solar = config[DOMAIN][CONF_SOLAR] - username = config[DOMAIN][CONF_USERNAME] - password = config[DOMAIN][CONF_PASSWORD] +async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: + """Set up the Toon components.""" + if DOMAIN not in config: + return True - try: - hass.data[TOON_HANDLE] = ToonDataStore(username, password, gas, solar) - except InvalidCredentials: - return False + conf = config[DOMAIN] - for platform in ('climate', 'sensor', 'switch'): - load_platform(hass, platform, DOMAIN, {}, config) + # Store config to be used during entry setup + hass.data[DATA_TOON_CONFIG] = conf return True -class ToonDataStore: - """An object to store the Toon data.""" +async def async_setup_entry(hass: HomeAssistantType, + entry: ConfigType) -> bool: + """Set up Toon from a config entry.""" + from toonapilib import Toon + + conf = hass.data.get(DATA_TOON_CONFIG) + + toon = await hass.async_add_executor_job(partial( + Toon, entry.data[CONF_USERNAME], entry.data[CONF_PASSWORD], + conf[CONF_CLIENT_ID], conf[CONF_CLIENT_SECRET], + tenant_id=entry.data[CONF_TENANT], + display_common_name=entry.data[CONF_DISPLAY])) - def __init__( - self, username, password, gas=DEFAULT_GAS, solar=DEFAULT_SOLAR): - """Initialize Toon.""" - from toonlib import Toon + hass.data.setdefault(DATA_TOON_CLIENT, {})[entry.entry_id] = toon - toon = Toon(username, password) + # Register device for the Meter Adapter, since it will have no entities. + device_registry = await dr.async_get_registry(hass) + device_registry.async_get_or_create( + config_entry_id=entry.entry_id, + identifiers={ + (DOMAIN, toon.agreement.id, 'meter_adapter'), + }, + manufacturer='Eneco', + name="Meter Adapter", + via_hub=(DOMAIN, toon.agreement.id) + ) + for component in 'binary_sensor', 'climate', 'sensor': + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, component)) + + return True + + +class ToonEntity(Entity): + """Defines a base Toon entity.""" + + def __init__(self, toon, name: str, icon: str) -> None: + """Initialize the Toon entity.""" + self._name = name + self._state = None + self._icon = icon self.toon = toon - self.gas = gas - self.solar = solar - self.data = {} - - self.last_update = datetime.min - self.update() - - @Throttle(MIN_TIME_BETWEEN_UPDATES) - def update(self): - """Update Toon data.""" - self.last_update = datetime.now() - - self.data['power_current'] = self.toon.power.value - self.data['power_today'] = round( - (float(self.toon.power.daily_usage) + - float(self.toon.power.daily_usage_low)) / 1000, 2) - self.data['temp'] = self.toon.temperature - - if self.toon.thermostat_state: - self.data['state'] = self.toon.thermostat_state.name - else: - self.data['state'] = 'Manual' - - self.data['setpoint'] = float( - self.toon.thermostat_info.current_set_point) / 100 - self.data['gas_current'] = self.toon.gas.value - self.data['gas_today'] = round(float(self.toon.gas.daily_usage) / - 1000, 2) - - for plug in self.toon.smartplugs: - self.data[plug.name] = { - 'current_power': plug.current_usage, - 'today_energy': round(float(plug.daily_usage) / 1000, 2), - 'current_state': plug.current_state, - 'is_connected': plug.is_connected, - } - - self.data['solar_maximum'] = self.toon.solar.maximum - self.data['solar_produced'] = self.toon.solar.produced - self.data['solar_value'] = self.toon.solar.value - self.data['solar_average_produced'] = self.toon.solar.average_produced - self.data['solar_meter_reading_low_produced'] = \ - self.toon.solar.meter_reading_low_produced - self.data['solar_meter_reading_produced'] = \ - self.toon.solar.meter_reading_produced - self.data['solar_daily_cost_produced'] = \ - self.toon.solar.daily_cost_produced - - for detector in self.toon.smokedetectors: - value = '{}_smoke_detector'.format(detector.name) - self.data[value] = { - 'smoke_detector': detector.battery_level, - 'device_type': detector.device_type, - 'is_connected': detector.is_connected, - 'last_connected_change': detector.last_connected_change, - } - - def set_state(self, state): - """Push a new state to the Toon unit.""" - self.toon.thermostat_state = state - - def set_temp(self, temp): - """Push a new temperature to the Toon unit.""" - self.toon.thermostat = temp - - def get_data(self, data_id, plug_name=None): - """Get the cached data.""" - data = {'error': 'no data'} - if plug_name: - if data_id in self.data[plug_name]: - data = self.data[plug_name][data_id] - else: - if data_id in self.data: - data = self.data[data_id] - return data + + @property + def name(self) -> str: + """Return the name of the entity.""" + return self._name + + @property + def icon(self) -> str: + """Return the mdi icon of the entity.""" + return self._icon + + +class ToonDisplayDeviceEntity(ToonEntity): + """Defines a Toon display device entity.""" + + @property + def device_info(self) -> Dict[str, Any]: + """Return device information about this thermostat.""" + agreement = self.toon.agreement + model = agreement.display_hardware_version.rpartition('/')[0] + sw_version = agreement.display_software_version.rpartition('/')[-1] + return { + 'identifiers': { + (DOMAIN, agreement.id), + }, + 'name': 'Toon Display', + 'manufacturer': 'Eneco', + 'model': model, + 'sw_version': sw_version, + } + + +class ToonElectricityMeterDeviceEntity(ToonEntity): + """Defines a Electricity Meter device entity.""" + + @property + def device_info(self) -> Dict[str, Any]: + """Return device information about this entity.""" + return { + 'name': 'Electricity Meter', + 'identifiers': { + (DOMAIN, self.toon.agreement.id, 'electricity'), + }, + 'via_hub': (DOMAIN, self.toon.agreement.id, 'meter_adapter'), + } + + +class ToonGasMeterDeviceEntity(ToonEntity): + """Defines a Gas Meter device entity.""" + + @property + def device_info(self) -> Dict[str, Any]: + """Return device information about this entity.""" + via_hub = 'meter_adapter' + if self.toon.gas.is_smart: + via_hub = 'electricity' + + return { + 'name': 'Gas Meter', + 'identifiers': { + (DOMAIN, self.toon.agreement.id, 'gas'), + }, + 'via_hub': (DOMAIN, self.toon.agreement.id, via_hub), + } + + +class ToonSolarDeviceEntity(ToonEntity): + """Defines a Solar Device device entity.""" + + @property + def device_info(self) -> Dict[str, Any]: + """Return device information about this entity.""" + return { + 'name': 'Solar Panels', + 'identifiers': { + (DOMAIN, self.toon.agreement.id, 'solar'), + }, + 'via_hub': (DOMAIN, self.toon.agreement.id, 'meter_adapter'), + } + + +class ToonBoilerModuleDeviceEntity(ToonEntity): + """Defines a Boiler Module device entity.""" + + @property + def device_info(self) -> Dict[str, Any]: + """Return device information about this entity.""" + return { + 'name': 'Boiler Module', + 'manufacturer': 'Eneco', + 'identifiers': { + (DOMAIN, self.toon.agreement.id, 'boiler_module'), + }, + 'via_hub': (DOMAIN, self.toon.agreement.id), + } + + +class ToonBoilerDeviceEntity(ToonEntity): + """Defines a Boiler device entity.""" + + @property + def device_info(self) -> Dict[str, Any]: + """Return device information about this entity.""" + return { + 'name': 'Boiler', + 'identifiers': { + (DOMAIN, self.toon.agreement.id, 'boiler'), + }, + 'via_hub': (DOMAIN, self.toon.agreement.id, 'boiler_module'), + } diff --git a/homeassistant/components/toon/binary_sensor.py b/homeassistant/components/toon/binary_sensor.py new file mode 100644 index 0000000000000..a50a67085ec7c --- /dev/null +++ b/homeassistant/components/toon/binary_sensor.py @@ -0,0 +1,127 @@ +"""Support for Toon binary sensors.""" + +from datetime import timedelta +import logging +from typing import Any + +from homeassistant.components.binary_sensor import BinarySensorDevice +from homeassistant.config_entries import ConfigEntry +from homeassistant.helpers.typing import HomeAssistantType + +from . import (ToonEntity, ToonDisplayDeviceEntity, ToonBoilerDeviceEntity, + ToonBoilerModuleDeviceEntity) +from .const import DATA_TOON_CLIENT, DOMAIN + +DEPENDENCIES = ['toon'] + +_LOGGER = logging.getLogger(__name__) + +MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=5) +SCAN_INTERVAL = timedelta(seconds=30) + + +async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry, + async_add_entities) -> None: + """Set up a Toon binary sensor based on a config entry.""" + toon = hass.data[DATA_TOON_CLIENT][entry.entry_id] + + sensors = [ + ToonBoilerModuleBinarySensor(toon, 'thermostat_info', + 'boiler_connected', None, + 'Boiler Module Connection', + 'mdi:check-network-outline', + 'connectivity'), + + ToonDisplayBinarySensor(toon, 'thermostat_info', 'active_state', 4, + "Toon Holiday Mode", 'mdi:airport', None), + + ToonDisplayBinarySensor(toon, 'thermostat_info', 'next_program', None, + "Toon Program", 'mdi:calendar-clock', None), + ] + + if toon.thermostat_info.have_ot_boiler: + sensors.extend([ + ToonBoilerBinarySensor(toon, 'thermostat_info', + 'ot_communication_error', '0', + "OpenTherm Connection", + 'mdi:check-network-outline', + 'connectivity'), + ToonBoilerBinarySensor(toon, 'thermostat_info', 'error_found', 255, + "Boiler Status", 'mdi:alert', 'problem', + inverted=True), + ToonBoilerBinarySensor(toon, 'thermostat_info', 'burner_info', + None, "Boiler Burner", 'mdi:fire', None), + ToonBoilerBinarySensor(toon, 'thermostat_info', 'burner_info', '2', + "Hot Tap Water", 'mdi:water-pump', None), + ToonBoilerBinarySensor(toon, 'thermostat_info', 'burner_info', '3', + "Boiler Preheating", 'mdi:fire', None), + ]) + + async_add_entities(sensors) + + +class ToonBinarySensor(ToonEntity, BinarySensorDevice): + """Defines an Toon binary sensor.""" + + def __init__(self, toon, section: str, measurement: str, on_value: Any, + name: str, icon: str, device_class: str, + inverted: bool = False) -> None: + """Initialize the Toon sensor.""" + self._state = inverted + self._device_class = device_class + self.section = section + self.measurement = measurement + self.on_value = on_value + self.inverted = inverted + + super().__init__(toon, name, icon) + + @property + def unique_id(self) -> str: + """Return the unique ID for this binary sensor.""" + return '_'.join([DOMAIN, self.toon.agreement.id, 'binary_sensor', + self.section, self.measurement, str(self.on_value)]) + + @property + def device_class(self) -> str: + """Return the device class.""" + return self._device_class + + @property + def is_on(self) -> bool: + """Return the status of the binary sensor.""" + if self.on_value is not None: + value = self._state == self.on_value + elif self._state is None: + value = False + else: + value = bool(max(0, int(self._state))) + + if self.inverted: + return not value + + return value + + def update(self) -> None: + """Get the latest data from the binary sensor.""" + section = getattr(self.toon, self.section) + self._state = getattr(section, self.measurement) + + +class ToonBoilerBinarySensor(ToonBinarySensor, ToonBoilerDeviceEntity): + """Defines a Boiler binary sensor.""" + + pass + + +class ToonDisplayBinarySensor(ToonBinarySensor, ToonDisplayDeviceEntity): + """Defines a Toon Display binary sensor.""" + + pass + + +class ToonBoilerModuleBinarySensor(ToonBinarySensor, + ToonBoilerModuleDeviceEntity): + """Defines a Boiler module binary sensor.""" + + pass diff --git a/homeassistant/components/toon/climate.py b/homeassistant/components/toon/climate.py index 3397e3dacc2a6..13f1c1269a15d 100644 --- a/homeassistant/components/toon/climate.py +++ b/homeassistant/components/toon/climate.py @@ -1,89 +1,129 @@ -"""Support for Toon van Eneco Thermostats.""" -from homeassistant.components.climate import ( - ATTR_TEMPERATURE, STATE_COOL, STATE_ECO, STATE_HEAT, STATE_AUTO, - SUPPORT_OPERATION_MODE, SUPPORT_TARGET_TEMPERATURE, ClimateDevice) -import homeassistant.components.toon as toon_main -from homeassistant.const import TEMP_CELSIUS +"""Support for Toon thermostat.""" + +from datetime import timedelta +import logging +from typing import Any, Dict, List + +from homeassistant.components.climate import ClimateDevice +from homeassistant.components.climate.const import ( + STATE_AUTO, STATE_COOL, STATE_ECO, STATE_HEAT, SUPPORT_OPERATION_MODE, + SUPPORT_TARGET_TEMPERATURE) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS +from homeassistant.helpers.typing import HomeAssistantType + +from . import ToonDisplayDeviceEntity +from .const import DATA_TOON_CLIENT, DEFAULT_MAX_TEMP, DEFAULT_MIN_TEMP, DOMAIN + +DEPENDENCIES = ['toon'] + +_LOGGER = logging.getLogger(__name__) SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE | SUPPORT_OPERATION_MODE +MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=5) +SCAN_INTERVAL = timedelta(seconds=30) + HA_TOON = { STATE_AUTO: 'Comfort', STATE_HEAT: 'Home', STATE_ECO: 'Away', STATE_COOL: 'Sleep', } + TOON_HA = {value: key for key, value in HA_TOON.items()} -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the Toon climate device.""" - add_entities([ThermostatDevice(hass)], True) +async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry, + async_add_entities) -> None: + """Set up a Toon binary sensors based on a config entry.""" + toon = hass.data[DATA_TOON_CLIENT][entry.entry_id] + async_add_entities([ToonThermostatDevice(toon)], True) -class ThermostatDevice(ClimateDevice): +class ToonThermostatDevice(ToonDisplayDeviceEntity, ClimateDevice): """Representation of a Toon climate device.""" - def __init__(self, hass): + def __init__(self, toon) -> None: """Initialize the Toon climate device.""" - self._name = 'Toon van Eneco' - self.hass = hass - self.thermos = hass.data[toon_main.TOON_HANDLE] - self._state = None - self._temperature = None - self._setpoint = None - self._operation_list = [ - STATE_AUTO, - STATE_HEAT, - STATE_ECO, - STATE_COOL, - ] + + self._current_temperature = None + self._target_temperature = None + self._next_target_temperature = None + + self._heating_type = None + + super().__init__(toon, "Toon Thermostat", 'mdi:thermostat') @property - def supported_features(self): - """Return the list of supported features.""" - return SUPPORT_FLAGS + def unique_id(self) -> str: + """Return the unique ID for this thermostat.""" + return '_'.join([DOMAIN, self.toon.agreement.id, 'climate']) @property - def name(self): - """Return the name of this thermostat.""" - return self._name + def supported_features(self) -> int: + """Return the list of supported features.""" + return SUPPORT_FLAGS @property - def temperature_unit(self): - """Return the unit of measurement used by the platform.""" + def temperature_unit(self) -> str: + """Return the unit of measurement.""" return TEMP_CELSIUS @property - def current_operation(self): + def current_operation(self) -> str: """Return current operation i.e. comfort, home, away.""" - return TOON_HA.get(self.thermos.get_data('state')) + return TOON_HA.get(self._state) @property - def operation_list(self): + def operation_list(self) -> List[str]: """Return a list of available operation modes.""" - return self._operation_list + return list(HA_TOON.keys()) @property - def current_temperature(self): + def current_temperature(self) -> float: """Return the current temperature.""" - return self.thermos.get_data('temp') + return self._current_temperature @property - def target_temperature(self): + def target_temperature(self) -> float: """Return the temperature we try to reach.""" - return self.thermos.get_data('setpoint') + return self._target_temperature + + @property + def min_temp(self) -> float: + """Return the minimum temperature.""" + return DEFAULT_MIN_TEMP + + @property + def max_temp(self) -> float: + """Return the maximum temperature.""" + return DEFAULT_MAX_TEMP + + @property + def device_state_attributes(self) -> Dict[str, Any]: + """Return the current state of the burner.""" + return { + 'heating_type': self._heating_type, + } - def set_temperature(self, **kwargs): + def set_temperature(self, **kwargs) -> None: """Change the setpoint of the thermostat.""" - temp = kwargs.get(ATTR_TEMPERATURE) - self.thermos.set_temp(temp) + temperature = kwargs.get(ATTR_TEMPERATURE) + self.toon.thermostat = temperature - def set_operation_mode(self, operation_mode): + def set_operation_mode(self, operation_mode: str) -> None: """Set new operation mode.""" - self.thermos.set_state(HA_TOON[operation_mode]) + self.toon.thermostat_state = HA_TOON[operation_mode] - def update(self): + def update(self) -> None: """Update local state.""" - self.thermos.update() + if self.toon.thermostat_state is None: + self._state = None + else: + self._state = self.toon.thermostat_state.name + + self._current_temperature = self.toon.temperature + self._target_temperature = self.toon.thermostat + self._heating_type = self.toon.agreement.heating_type diff --git a/homeassistant/components/toon/config_flow.py b/homeassistant/components/toon/config_flow.py new file mode 100644 index 0000000000000..a09b3dd49a7ea --- /dev/null +++ b/homeassistant/components/toon/config_flow.py @@ -0,0 +1,156 @@ +"""Config flow to configure the Toon component.""" +from collections import OrderedDict +import logging +from functools import partial + +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.core import callback + +from .const import ( + CONF_CLIENT_ID, CONF_CLIENT_SECRET, CONF_DISPLAY, CONF_TENANT, + DATA_TOON_CONFIG, DOMAIN) + +_LOGGER = logging.getLogger(__name__) + + +@callback +def configured_displays(hass): + """Return a set of configured Toon displays.""" + return set( + entry.data[CONF_DISPLAY] + for entry in hass.config_entries.async_entries(DOMAIN) + ) + + +@config_entries.HANDLERS.register(DOMAIN) +class ToonFlowHandler(config_entries.ConfigFlow): + """Handle a Toon config flow.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL + + def __init__(self): + """Initialize the Toon flow.""" + self.displays = None + self.username = None + self.password = None + self.tenant = None + + async def async_step_user(self, user_input=None): + """Handle a flow initiated by the user.""" + app = self.hass.data.get(DATA_TOON_CONFIG, {}) + + if not app: + return self.async_abort(reason='no_app') + + return await self.async_step_authenticate(user_input) + + async def _show_authenticaticate_form(self, errors=None): + """Show the authentication form to the user.""" + fields = OrderedDict() + fields[vol.Required(CONF_USERNAME)] = str + fields[vol.Required(CONF_PASSWORD)] = str + fields[vol.Optional(CONF_TENANT)] = vol.In([ + 'eneco', 'electrabel', 'viesgo' + ]) + + return self.async_show_form( + step_id='authenticate', + data_schema=vol.Schema(fields), + errors=errors if errors else {}, + ) + + async def async_step_authenticate(self, user_input=None): + """Attempt to authenticate with the Toon account.""" + from toonapilib import Toon + from toonapilib.toonapilibexceptions import (InvalidConsumerSecret, + InvalidConsumerKey, + InvalidCredentials, + AgreementsRetrievalError) + + if user_input is None: + return await self._show_authenticaticate_form() + + app = self.hass.data.get(DATA_TOON_CONFIG, {}) + try: + toon = await self.hass.async_add_executor_job(partial( + Toon, user_input[CONF_USERNAME], user_input[CONF_PASSWORD], + app[CONF_CLIENT_ID], app[CONF_CLIENT_SECRET], + tenant_id=user_input[CONF_TENANT])) + + displays = toon.display_names + + except InvalidConsumerKey: + return self.async_abort(reason='client_id') + + except InvalidConsumerSecret: + return self.async_abort(reason='client_secret') + + except InvalidCredentials: + return await self._show_authenticaticate_form({ + 'base': 'credentials' + }) + + except AgreementsRetrievalError: + return self.async_abort(reason='no_agreements') + + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected error while authenticating") + return self.async_abort(reason='unknown_auth_fail') + + self.displays = displays + self.username = user_input[CONF_USERNAME] + self.password = user_input[CONF_PASSWORD] + self.tenant = user_input[CONF_TENANT] + + return await self.async_step_display() + + async def _show_display_form(self, errors=None): + """Show the select display form to the user.""" + fields = OrderedDict() + fields[vol.Required(CONF_DISPLAY)] = vol.In(self.displays) + + return self.async_show_form( + step_id='display', + data_schema=vol.Schema(fields), + errors=errors if errors else {}, + ) + + async def async_step_display(self, user_input=None): + """Select Toon display to add.""" + from toonapilib import Toon + + if not self.displays: + return self.async_abort(reason='no_displays') + + if user_input is None: + return await self._show_display_form() + + if user_input[CONF_DISPLAY] in configured_displays(self.hass): + return await self._show_display_form({ + 'base': 'display_exists' + }) + + app = self.hass.data.get(DATA_TOON_CONFIG, {}) + try: + await self.hass.async_add_executor_job(partial( + Toon, self.username, self.password, app[CONF_CLIENT_ID], + app[CONF_CLIENT_SECRET], tenant_id=self.tenant, + display_common_name=user_input[CONF_DISPLAY])) + + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected error while authenticating") + return self.async_abort(reason='unknown_auth_fail') + + return self.async_create_entry( + title=user_input[CONF_DISPLAY], + data={ + CONF_USERNAME: self.username, + CONF_PASSWORD: self.password, + CONF_TENANT: self.tenant, + CONF_DISPLAY: user_input[CONF_DISPLAY] + } + ) diff --git a/homeassistant/components/toon/const.py b/homeassistant/components/toon/const.py new file mode 100644 index 0000000000000..29b58fbfff988 --- /dev/null +++ b/homeassistant/components/toon/const.py @@ -0,0 +1,21 @@ +"""Constants for the Toon integration.""" +DOMAIN = 'toon' + +DATA_TOON = 'toon' +DATA_TOON_CONFIG = 'toon_config' +DATA_TOON_CLIENT = 'toon_client' + +CONF_CLIENT_ID = 'client_id' +CONF_CLIENT_SECRET = 'client_secret' +CONF_DISPLAY = 'display' +CONF_TENANT = 'tenant' + +DEFAULT_MAX_TEMP = 30.0 +DEFAULT_MIN_TEMP = 6.0 + +CURRENCY_EUR = 'EUR' +POWER_WATT = 'W' +POWER_KWH = 'kWh' +RATIO_PERCENT = '%' +VOLUME_CM3 = 'CM3' +VOLUME_M3 = 'M3' diff --git a/homeassistant/components/toon/sensor.py b/homeassistant/components/toon/sensor.py index ebd25e02cde3f..e263bda9fc740 100644 --- a/homeassistant/components/toon/sensor.py +++ b/homeassistant/components/toon/sensor.py @@ -1,217 +1,189 @@ -"""Support for rebranded Quby thermostat as provided by Eneco.""" +"""Support for Toon sensors.""" +from datetime import timedelta import logging -import datetime -from homeassistant.helpers.entity import Entity -import homeassistant.components.toon as toon_main - -_LOGGER = logging.getLogger(__name__) - -STATE_ATTR_DEVICE_TYPE = 'device_type' -STATE_ATTR_LAST_CONNECTED_CHANGE = 'last_connected_change' +from homeassistant.config_entries import ConfigEntry +from homeassistant.helpers.typing import HomeAssistantType +from . import (ToonEntity, ToonElectricityMeterDeviceEntity, + ToonGasMeterDeviceEntity, ToonSolarDeviceEntity, + ToonBoilerDeviceEntity) +from .const import (CURRENCY_EUR, DATA_TOON_CLIENT, DOMAIN, POWER_KWH, + POWER_WATT, VOLUME_CM3, VOLUME_M3, RATIO_PERCENT) -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the Toon sensors.""" - _toon_main = hass.data[toon_main.TOON_HANDLE] +DEPENDENCIES = ['toon'] - sensor_items = [] - sensor_items.extend([ - ToonSensor(hass, 'Power_current', 'power-plug', 'Watt'), - ToonSensor(hass, 'Power_today', 'power-plug', 'kWh'), - ]) +_LOGGER = logging.getLogger(__name__) - if _toon_main.gas: - sensor_items.extend([ - ToonSensor(hass, 'Gas_current', 'gas-cylinder', 'CM3'), - ToonSensor(hass, 'Gas_today', 'gas-cylinder', 'M3'), +MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=5) +SCAN_INTERVAL = timedelta(seconds=30) + + +async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry, + async_add_entities) -> None: + """Set up Toon sensors based on a config entry.""" + toon = hass.data[DATA_TOON_CLIENT][entry.entry_id] + + sensors = [ + ToonElectricityMeterDeviceSensor(toon, 'power', 'value', + "Current Power Usage", + 'mdi:power-plug', POWER_WATT), + ToonElectricityMeterDeviceSensor(toon, 'power', 'average', + "Average Power Usage", + 'mdi:power-plug', POWER_WATT), + ToonElectricityMeterDeviceSensor(toon, 'power', 'daily_value', + "Power Usage Today", + 'mdi:power-plug', POWER_KWH), + ToonElectricityMeterDeviceSensor(toon, 'power', 'daily_cost', + "Power Cost Today", + 'mdi:power-plug', CURRENCY_EUR), + ToonElectricityMeterDeviceSensor(toon, 'power', 'average_daily', + "Average Daily Power Usage", + 'mdi:power-plug', POWER_KWH), + ToonElectricityMeterDeviceSensor(toon, 'power', 'meter_reading', + "Power Meter Feed IN Tariff 1", + 'mdi:power-plug', POWER_KWH), + ToonElectricityMeterDeviceSensor(toon, 'power', 'meter_reading_low', + "Power Meter Feed IN Tariff 2", + 'mdi:power-plug', POWER_KWH), + ] + + if toon.gas: + sensors.extend([ + ToonGasMeterDeviceSensor(toon, 'gas', 'value', "Current Gas Usage", + 'mdi:gas-cylinder', VOLUME_CM3), + ToonGasMeterDeviceSensor(toon, 'gas', 'average', + "Average Gas Usage", 'mdi:gas-cylinder', + VOLUME_CM3), + ToonGasMeterDeviceSensor(toon, 'gas', 'daily_usage', + "Gas Usage Today", 'mdi:gas-cylinder', + VOLUME_M3), + ToonGasMeterDeviceSensor(toon, 'gas', 'average_daily', + "Average Daily Gas Usage", + 'mdi:gas-cylinder', VOLUME_M3), + ToonGasMeterDeviceSensor(toon, 'gas', 'meter_reading', "Gas Meter", + 'mdi:gas-cylinder', VOLUME_M3), + ToonGasMeterDeviceSensor(toon, 'gas', 'daily_cost', + "Gas Cost Today", 'mdi:gas-cylinder', + CURRENCY_EUR), ]) - for plug in _toon_main.toon.smartplugs: - sensor_items.extend([ - FibaroSensor(hass, '{}_current_power'.format(plug.name), - plug.name, 'power-socket-eu', 'Watt'), - FibaroSensor(hass, '{}_today_energy'.format(plug.name), - plug.name, 'power-socket-eu', 'kWh'), + if toon.solar: + sensors.extend([ + ToonSolarDeviceSensor(toon, 'solar', 'value', + "Current Solar Production", + 'mdi:solar-power', POWER_WATT), + ToonSolarDeviceSensor(toon, 'solar', 'maximum', + "Max Solar Production", 'mdi:solar-power', + POWER_WATT), + ToonSolarDeviceSensor(toon, 'solar', 'produced', + "Solar Production to Grid", + 'mdi:solar-power', POWER_WATT), + ToonSolarDeviceSensor(toon, 'solar', 'average_produced', + "Average Solar Production to Grid", + 'mdi:solar-power', POWER_WATT), + ToonElectricityMeterDeviceSensor(toon, 'solar', + 'meter_reading_produced', + "Power Meter Feed OUT Tariff 1", + 'mdi:solar-power', + POWER_KWH), + ToonElectricityMeterDeviceSensor(toon, 'solar', + 'meter_reading_low_produced', + "Power Meter Feed OUT Tariff 2", + 'mdi:solar-power', POWER_KWH), ]) - if _toon_main.toon.solar.produced or _toon_main.solar: - sensor_items.extend([ - SolarSensor(hass, 'Solar_maximum', 'kWh'), - SolarSensor(hass, 'Solar_produced', 'kWh'), - SolarSensor(hass, 'Solar_value', 'Watt'), - SolarSensor(hass, 'Solar_average_produced', 'kWh'), - SolarSensor(hass, 'Solar_meter_reading_low_produced', 'kWh'), - SolarSensor(hass, 'Solar_meter_reading_produced', 'kWh'), - SolarSensor(hass, 'Solar_daily_cost_produced', 'Euro'), + if toon.thermostat_info.have_ot_boiler: + sensors.extend([ + ToonBoilerDeviceSensor(toon, 'thermostat_info', + 'current_modulation_level', + "Boiler Modulation Level", + 'mdi:percent', + RATIO_PERCENT), ]) - for smokedetector in _toon_main.toon.smokedetectors: - sensor_items.append( - FibaroSmokeDetector( - hass, '{}_smoke_detector'.format(smokedetector.name), - smokedetector.device_uuid, 'alarm-bell', '%') - ) - - add_entities(sensor_items) + async_add_entities(sensors) -class ToonSensor(Entity): - """Representation of a Toon sensor.""" +class ToonSensor(ToonEntity): + """Defines a Toon sensor.""" - def __init__(self, hass, name, icon, unit_of_measurement): + def __init__(self, toon, section: str, measurement: str, + name: str, icon: str, unit_of_measurement: str) -> None: """Initialize the Toon sensor.""" - self._name = name self._state = None - self._icon = 'mdi:{}'.format(icon) self._unit_of_measurement = unit_of_measurement - self.thermos = hass.data[toon_main.TOON_HANDLE] + self.section = section + self.measurement = measurement - @property - def name(self): - """Return the name of the sensor.""" - return self._name + super().__init__(toon, name, icon) @property - def icon(self): - """Return the mdi icon of the sensor.""" - return self._icon + def unique_id(self) -> str: + """Return the unique ID for this sensor.""" + return '_'.join([DOMAIN, self.toon.agreement.id, 'sensor', + self.section, self.measurement]) @property def state(self): """Return the state of the sensor.""" - return self.thermos.get_data(self.name.lower()) + return self._state @property - def unit_of_measurement(self): + def unit_of_measurement(self) -> str: """Return the unit this state is expressed in.""" return self._unit_of_measurement - def update(self): + def update(self) -> None: """Get the latest data from the sensor.""" - self.thermos.update() + section = getattr(self.toon, self.section) + value = None + if self.section == 'power' and self.measurement == 'daily_value': + value = round((float(section.daily_usage) + + float(section.daily_usage_low)) / 1000.0, 2) -class FibaroSensor(Entity): - """Representation of a Fibaro sensor.""" + if value is None: + value = getattr(section, self.measurement) - def __init__(self, hass, name, plug_name, icon, unit_of_measurement): - """Initialize the Fibaro sensor.""" - self._name = name - self._plug_name = plug_name - self._state = None - self._icon = 'mdi:{}'.format(icon) - self._unit_of_measurement = unit_of_measurement - self.toon = hass.data[toon_main.TOON_HANDLE] + if self.section == 'power' and \ + self.measurement in ['meter_reading', 'meter_reading_low', + 'average_daily']: + value = round(float(value)/1000.0, 2) - @property - def name(self): - """Return the name of the sensor.""" - return self._name + if self.section == 'solar' and \ + self.measurement in ['meter_reading_produced', + 'meter_reading_low_produced']: + value = float(value)/1000.0 - @property - def icon(self): - """Return the mdi icon of the sensor.""" - return self._icon + if self.section == 'gas' and \ + self.measurement in ['average_daily', 'daily_usage', + 'meter_reading']: + value = round(float(value)/1000.0, 2) - @property - def state(self): - """Return the state of the sensor.""" - value = '_'.join(self.name.lower().split('_')[1:]) - return self.toon.get_data(value, self._plug_name) + self._state = max(0, value) - @property - def unit_of_measurement(self): - """Return the unit this state is expressed in.""" - return self._unit_of_measurement - def update(self): - """Get the latest data from the sensor.""" - self.toon.update() +class ToonElectricityMeterDeviceSensor(ToonSensor, + ToonElectricityMeterDeviceEntity): + """Defines a Eletricity Meter sensor.""" + pass -class SolarSensor(Entity): - """Representation of a Solar sensor.""" - def __init__(self, hass, name, unit_of_measurement): - """Initialize the Solar sensor.""" - self._name = name - self._state = None - self._icon = 'mdi:weather-sunny' - self._unit_of_measurement = unit_of_measurement - self.toon = hass.data[toon_main.TOON_HANDLE] +class ToonGasMeterDeviceSensor(ToonSensor, ToonGasMeterDeviceEntity): + """Defines a Gas Meter sensor.""" - @property - def name(self): - """Return the name of the sensor.""" - return self._name + pass - @property - def icon(self): - """Return the mdi icon of the sensor.""" - return self._icon - @property - def state(self): - """Return the state of the sensor.""" - return self.toon.get_data(self.name.lower()) - - @property - def unit_of_measurement(self): - """Return the unit this state is expressed in.""" - return self._unit_of_measurement - - def update(self): - """Get the latest data from the sensor.""" - self.toon.update() +class ToonSolarDeviceSensor(ToonSensor, ToonSolarDeviceEntity): + """Defines a Solar sensor.""" + pass -class FibaroSmokeDetector(Entity): - """Representation of a Fibaro smoke detector.""" - def __init__(self, hass, name, uid, icon, unit_of_measurement): - """Initialize the Fibaro smoke sensor.""" - self._name = name - self._uid = uid - self._state = None - self._icon = 'mdi:{}'.format(icon) - self._unit_of_measurement = unit_of_measurement - self.toon = hass.data[toon_main.TOON_HANDLE] - - @property - def name(self): - """Return the name of the sensor.""" - return self._name - - @property - def icon(self): - """Return the mdi icon of the sensor.""" - return self._icon +class ToonBoilerDeviceSensor(ToonSensor, ToonBoilerDeviceEntity): + """Defines a Boiler sensor.""" - @property - def state_attributes(self): - """Return the state attributes of the smoke detectors.""" - value = datetime.datetime.fromtimestamp( - int(self.toon.get_data('last_connected_change', self.name)) - ).strftime('%Y-%m-%d %H:%M:%S') - - return { - STATE_ATTR_DEVICE_TYPE: - self.toon.get_data('device_type', self.name), - STATE_ATTR_LAST_CONNECTED_CHANGE: value, - } - - @property - def state(self): - """Return the state of the sensor.""" - value = self.name.lower().split('_', 1)[1] - return self.toon.get_data(value, self.name) - - @property - def unit_of_measurement(self): - """Return the unit this state is expressed in.""" - return self._unit_of_measurement - - def update(self): - """Get the latest data from the sensor.""" - self.toon.update() + pass diff --git a/homeassistant/components/toon/strings.json b/homeassistant/components/toon/strings.json new file mode 100644 index 0000000000000..80d71d4e42162 --- /dev/null +++ b/homeassistant/components/toon/strings.json @@ -0,0 +1,34 @@ +{ + "config": { + "title": "Toon", + "step": { + "authenticate": { + "title": "Link your Toon account", + "description": "Authenticate with your Eneco Toon account (not the developer account).", + "data": { + "username": "Username", + "password": "Password", + "tenant": "Tenant" + } + }, + "display": { + "title": "Select display", + "description": "Select the Toon display to connect with.", + "data": { + "display": "Choose display" + } + } + }, + "error": { + "credentials": "The provided credentials are invalid.", + "display_exists": "The selected display is already configured." + }, + "abort": { + "client_id": "The client ID from the configuration is invalid.", + "client_secret": "The client secret from the configuration is invalid.", + "unknown_auth_fail": "Unexpected error occured, while authenticating.", + "no_agreements": "This account has no Toon displays.", + "no_app": "You need to configure Toon before being able to authenticate with it. [Please read the instructions](https://www.home-assistant.io/components/toon/)." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/toon/switch.py b/homeassistant/components/toon/switch.py deleted file mode 100644 index 08ccec588b4c9..0000000000000 --- a/homeassistant/components/toon/switch.py +++ /dev/null @@ -1,68 +0,0 @@ -"""Support for Eneco Slimmer stekkers (Smart Plugs).""" -import logging - -from homeassistant.components.switch import SwitchDevice -import homeassistant.components.toon as toon_main - -_LOGGER = logging.getLogger(__name__) - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the discovered Toon Smart Plugs.""" - _toon_main = hass.data[toon_main.TOON_HANDLE] - switch_items = [] - for plug in _toon_main.toon.smartplugs: - switch_items.append(EnecoSmartPlug(hass, plug)) - - add_entities(switch_items) - - -class EnecoSmartPlug(SwitchDevice): - """Representation of a Toon Smart Plug.""" - - def __init__(self, hass, plug): - """Initialize the Smart Plug.""" - self.smartplug = plug - self.toon_data_store = hass.data[toon_main.TOON_HANDLE] - - @property - def unique_id(self): - """Return the ID of this switch.""" - return self.smartplug.device_uuid - - @property - def name(self): - """Return the name of the switch if any.""" - return self.smartplug.name - - @property - def current_power_w(self): - """Return the current power usage in W.""" - return self.toon_data_store.get_data('current_power', self.name) - - @property - def today_energy_kwh(self): - """Return the today total energy usage in kWh.""" - return self.toon_data_store.get_data('today_energy', self.name) - - @property - def is_on(self): - """Return true if switch is on. Standby is on.""" - return self.toon_data_store.get_data('current_state', self.name) - - @property - def available(self): - """Return true if switch is available.""" - return self.smartplug.can_toggle - - def turn_on(self, **kwargs): - """Turn the switch on.""" - return self.smartplug.turn_on() - - def turn_off(self, **kwargs): - """Turn the switch off.""" - return self.smartplug.turn_off() - - def update(self): - """Update state.""" - self.toon_data_store.update() diff --git a/homeassistant/components/tplink/.translations/ca.json b/homeassistant/components/tplink/.translations/ca.json new file mode 100644 index 0000000000000..cf286f853f22c --- /dev/null +++ b/homeassistant/components/tplink/.translations/ca.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "No s'han trobat dispositius TP-Link a la xarxa.", + "single_instance_allowed": "Nom\u00e9s cal una \u00fanica configuraci\u00f3." + }, + "step": { + "confirm": { + "description": "Vols configurar dispositius intel\u00b7ligents TP-Link?", + "title": "TP-Link Smart Home" + } + }, + "title": "TP-Link Smart Home" + } +} \ No newline at end of file diff --git a/homeassistant/components/tplink/.translations/da.json b/homeassistant/components/tplink/.translations/da.json new file mode 100644 index 0000000000000..cdd953ff5c33e --- /dev/null +++ b/homeassistant/components/tplink/.translations/da.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "Ingen TP-Link enheder kunne findes p\u00e5 netv\u00e6rket.", + "single_instance_allowed": "Det er kun n\u00f8dvendigt med en ops\u00e6tning." + }, + "step": { + "confirm": { + "description": "Vil du konfigurere TP-Link smart devices?", + "title": "TP-Link Smart Home" + } + }, + "title": "TP-Link Smart Home" + } +} \ No newline at end of file diff --git a/homeassistant/components/tplink/.translations/de.json b/homeassistant/components/tplink/.translations/de.json new file mode 100644 index 0000000000000..268d8ed07172e --- /dev/null +++ b/homeassistant/components/tplink/.translations/de.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "Es wurden keine TP-Link-Ger\u00e4te im Netzwerk gefunden.", + "single_instance_allowed": "Es ist nur eine einzige Konfiguration erforderlich." + }, + "step": { + "confirm": { + "description": "M\u00f6chten Sie TP-Link Smart Devices einrichten?", + "title": "TP-Link Smart Home" + } + }, + "title": "TP-Link Smart Home" + } +} \ No newline at end of file diff --git a/homeassistant/components/tplink/.translations/en.json b/homeassistant/components/tplink/.translations/en.json new file mode 100644 index 0000000000000..ff349fe1b68f0 --- /dev/null +++ b/homeassistant/components/tplink/.translations/en.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "No TP-Link devices found on the network.", + "single_instance_allowed": "Only a single configuration is necessary." + }, + "step": { + "confirm": { + "description": "Do you want to setup TP-Link smart devices?", + "title": "TP-Link Smart Home" + } + }, + "title": "TP-Link Smart Home" + } +} \ No newline at end of file diff --git a/homeassistant/components/tplink/.translations/es-419.json b/homeassistant/components/tplink/.translations/es-419.json new file mode 100644 index 0000000000000..1d9fb41fc8ce1 --- /dev/null +++ b/homeassistant/components/tplink/.translations/es-419.json @@ -0,0 +1,10 @@ +{ + "config": { + "step": { + "confirm": { + "title": "TP-Link Smart Home" + } + }, + "title": "TP-Link Smart Home" + } +} \ No newline at end of file diff --git a/homeassistant/components/tplink/.translations/es.json b/homeassistant/components/tplink/.translations/es.json new file mode 100644 index 0000000000000..9b6e34f6c3583 --- /dev/null +++ b/homeassistant/components/tplink/.translations/es.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "No se han encontrado dispositivos TP-Link en la red.", + "single_instance_allowed": "S\u00f3lo es necesaria una configuraci\u00f3n." + }, + "step": { + "confirm": { + "description": "\u00bfDesea configurar dispositivos de TP-Link?", + "title": "TP-Link Smart Home" + } + }, + "title": "TP-Link Smart Home" + } +} \ No newline at end of file diff --git a/homeassistant/components/tplink/.translations/he.json b/homeassistant/components/tplink/.translations/he.json new file mode 100644 index 0000000000000..094174b09c136 --- /dev/null +++ b/homeassistant/components/tplink/.translations/he.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05d4\u05ea\u05e7\u05e0\u05d9 TP-Link \u05d1\u05e8\u05e9\u05ea.", + "single_instance_allowed": "\u05e0\u05d3\u05e8\u05e9\u05ea \u05ea\u05e6\u05d5\u05e8\u05d4 \u05d0\u05d7\u05ea \u05d1\u05dc\u05d1\u05d3" + }, + "step": { + "confirm": { + "description": "\u05d4\u05d0\u05dd \u05d0\u05ea\u05d4 \u05e8\u05d5\u05e6\u05d4 \u05dc\u05d4\u05d2\u05d3\u05d9\u05e8 \u05d4\u05ea\u05e7\u05e0\u05d9\u05dd \u05d7\u05db\u05de\u05d9\u05dd \u05e9\u05dc TP-Link ?", + "title": "\u05d1\u05d9\u05ea \u05d7\u05db\u05dd \u05e9\u05dc TP-Link" + } + }, + "title": "\u05d1\u05d9\u05ea \u05d7\u05db\u05dd \u05e9\u05dc TP-Link" + } +} \ No newline at end of file diff --git a/homeassistant/components/tplink/.translations/ko.json b/homeassistant/components/tplink/.translations/ko.json new file mode 100644 index 0000000000000..c31e686a76d8c --- /dev/null +++ b/homeassistant/components/tplink/.translations/ko.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "TP-Link \uc7a5\uce58\uac00 \ub124\ud2b8\uc6cc\ud06c\uc5d0\uc11c \ubc1c\uacac\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4.", + "single_instance_allowed": "\ud558\ub098\uc758 TP-Link \ub9cc \uad6c\uc131 \ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4." + }, + "step": { + "confirm": { + "description": "TP-Link \uc2a4\ub9c8\ud2b8 \uc7a5\uce58\ub97c \uc124\uc815 \ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", + "title": "TP-Link Smart Home" + } + }, + "title": "TP-Link Smart Home" + } +} \ No newline at end of file diff --git a/homeassistant/components/tplink/.translations/lb.json b/homeassistant/components/tplink/.translations/lb.json new file mode 100644 index 0000000000000..11ca7218e1167 --- /dev/null +++ b/homeassistant/components/tplink/.translations/lb.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "Keng TP-Link Apparater am Netzwierk fonnt.", + "single_instance_allowed": "N\u00ebmmen eng eenzeg Konfiguratioun ass n\u00e9ideg." + }, + "step": { + "confirm": { + "description": "Soll TP-Link Smart Home konfigur\u00e9iert ginn?", + "title": "TP-Link Smart Home" + } + }, + "title": "TP-Link Smart Home" + } +} \ No newline at end of file diff --git a/homeassistant/components/tplink/.translations/no.json b/homeassistant/components/tplink/.translations/no.json new file mode 100644 index 0000000000000..4946eb81f0295 --- /dev/null +++ b/homeassistant/components/tplink/.translations/no.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "Ingen TP-Link enheter funnet p\u00e5 nettverket.", + "single_instance_allowed": "Kun en enkelt konfigurasjon av TP-Link er n\u00f8dvendig." + }, + "step": { + "confirm": { + "description": "Vil du konfigurere TP-Link smart enheter?", + "title": "TP-Link Smart Home" + } + }, + "title": "TP-Link Smart Home" + } +} \ No newline at end of file diff --git a/homeassistant/components/tplink/.translations/pl.json b/homeassistant/components/tplink/.translations/pl.json new file mode 100644 index 0000000000000..fa90495a5bfbd --- /dev/null +++ b/homeassistant/components/tplink/.translations/pl.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "Nie znaleziono urz\u0105dze\u0144 TP-Link w sieci.", + "single_instance_allowed": "Wymagana jest tylko jedna konfiguracja." + }, + "step": { + "confirm": { + "description": "Czy chcesz skonfigurowa\u0107 urz\u0105dzenia TP-Link smart?", + "title": "TP-Link Smart Home" + } + }, + "title": "TP-Link Smart Home" + } +} \ No newline at end of file diff --git a/homeassistant/components/tplink/.translations/ru.json b/homeassistant/components/tplink/.translations/ru.json new file mode 100644 index 0000000000000..b7d767932458d --- /dev/null +++ b/homeassistant/components/tplink/.translations/ru.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 TP-Link \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 TP-Link Smart Home?", + "title": "TP-Link Smart Home" + } + }, + "title": "TP-Link Smart Home" + } +} \ No newline at end of file diff --git a/homeassistant/components/tplink/.translations/sv.json b/homeassistant/components/tplink/.translations/sv.json new file mode 100644 index 0000000000000..14b6417d59346 --- /dev/null +++ b/homeassistant/components/tplink/.translations/sv.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "Inga TP-Link enheter hittades p\u00e5 n\u00e4tverket.", + "single_instance_allowed": "Endast en enda konfiguration \u00e4r n\u00f6dv\u00e4ndig." + }, + "step": { + "confirm": { + "description": "Vill du konfigurera TP-Link smart enheter?", + "title": "TP-Link Smart Home" + } + }, + "title": "TP-Link Smart Home" + } +} \ No newline at end of file diff --git a/homeassistant/components/tplink/.translations/zh-Hant.json b/homeassistant/components/tplink/.translations/zh-Hant.json new file mode 100644 index 0000000000000..d44faf195e55e --- /dev/null +++ b/homeassistant/components/tplink/.translations/zh-Hant.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "\u5728\u7db2\u8def\u4e0a\u627e\u4e0d\u5230 TP-Link \u88dd\u7f6e\u3002", + "single_instance_allowed": "\u50c5\u9700\u8a2d\u5b9a\u4e00\u6b21\u5373\u53ef\u3002" + }, + "step": { + "confirm": { + "description": "\u662f\u5426\u8981\u8a2d\u5b9a TP-Link \u667a\u80fd\u88dd\u7f6e\uff1f", + "title": "TP-Link Smart Home" + } + }, + "title": "TP-Link Smart Home" + } +} \ No newline at end of file diff --git a/homeassistant/components/tplink/__init__.py b/homeassistant/components/tplink/__init__.py new file mode 100644 index 0000000000000..bc2851508907a --- /dev/null +++ b/homeassistant/components/tplink/__init__.py @@ -0,0 +1,154 @@ +"""Component to embed TP-Link smart home devices.""" +import logging + +import voluptuous as vol + +from homeassistant.const import CONF_HOST +from homeassistant import config_entries +from homeassistant.helpers import config_entry_flow +import homeassistant.helpers.config_validation as cv + + +_LOGGER = logging.getLogger(__name__) + +DOMAIN = 'tplink' + +TPLINK_HOST_SCHEMA = vol.Schema({ + vol.Required(CONF_HOST): cv.string +}) + +CONF_LIGHT = 'light' +CONF_SWITCH = 'switch' +CONF_DISCOVERY = 'discovery' + +ATTR_CONFIG = 'config' + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Optional('light', default=[]): vol.All(cv.ensure_list, + [TPLINK_HOST_SCHEMA]), + vol.Optional('switch', default=[]): vol.All(cv.ensure_list, + [TPLINK_HOST_SCHEMA]), + vol.Optional('discovery', default=True): cv.boolean, + }), +}, extra=vol.ALLOW_EXTRA) + +REQUIREMENTS = ['pyHS100==0.3.4'] + + +async def _async_has_devices(hass): + """Return if there are devices that can be discovered.""" + from pyHS100 import Discover + + def discover(): + devs = Discover.discover() + return devs + return await hass.async_add_executor_job(discover) + + +async def async_setup(hass, config): + """Set up the TP-Link component.""" + conf = config.get(DOMAIN) + + hass.data[DOMAIN] = {} + hass.data[DOMAIN][ATTR_CONFIG] = conf + + if conf is not None: + 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, config_entry): + """Set up TPLink from a config entry.""" + from pyHS100 import SmartBulb, SmartPlug, SmartDeviceException + + devices = {} + + config_data = hass.data[DOMAIN].get(ATTR_CONFIG) + + # These will contain the initialized devices + lights = hass.data[DOMAIN][CONF_LIGHT] = [] + switches = hass.data[DOMAIN][CONF_SWITCH] = [] + + # If discovery is defined and not disabled, discover devices + # If initialized from configure integrations, there's no config + # so we default here to True + if config_data is None or config_data[CONF_DISCOVERY]: + devs = await _async_has_devices(hass) + _LOGGER.info("Discovered %s TP-Link smart home device(s)", len(devs)) + devices.update(devs) + + def _device_for_type(host, type_): + dev = None + if type_ == CONF_LIGHT: + dev = SmartBulb(host) + elif type_ == CONF_SWITCH: + dev = SmartPlug(host) + + return dev + + # When arriving from configure integrations, we have no config data. + if config_data is not None: + for type_ in [CONF_LIGHT, CONF_SWITCH]: + for entry in config_data[type_]: + try: + host = entry['host'] + dev = _device_for_type(host, type_) + devices[host] = dev + _LOGGER.debug("Succesfully added %s %s: %s", + type_, host, dev) + except SmartDeviceException as ex: + _LOGGER.error("Unable to initialize %s %s: %s", + type_, host, ex) + + # This is necessary to avoid I/O blocking on is_dimmable + def _fill_device_lists(): + for dev in devices.values(): + if isinstance(dev, SmartPlug): + if dev.is_dimmable: # Dimmers act as lights + lights.append(dev) + else: + switches.append(dev) + elif isinstance(dev, SmartBulb): + lights.append(dev) + else: + _LOGGER.error("Unknown smart device type: %s", type(dev)) + + # Avoid blocking on is_dimmable + await hass.async_add_executor_job(_fill_device_lists) + + forward_setup = hass.config_entries.async_forward_entry_setup + if lights: + _LOGGER.debug("Got %s lights: %s", len(lights), lights) + hass.async_create_task(forward_setup(config_entry, 'light')) + if switches: + _LOGGER.debug("Got %s switches: %s", len(switches), switches) + hass.async_create_task(forward_setup(config_entry, 'switch')) + + return True + + +async def async_unload_entry(hass, entry): + """Unload a config entry.""" + forward_unload = hass.config_entries.async_forward_entry_unload + remove_lights = remove_switches = False + if hass.data[DOMAIN][CONF_LIGHT]: + remove_lights = await forward_unload(entry, 'light') + if hass.data[DOMAIN][CONF_SWITCH]: + remove_switches = await forward_unload(entry, 'switch') + + if remove_lights or remove_switches: + hass.data[DOMAIN].clear() + return True + + # We were not able to unload the platforms, either because there + # were none or one of the forward_unloads failed. + return False + + +config_entry_flow.register_discovery_flow(DOMAIN, + 'TP-Link Smart Home', + _async_has_devices, + config_entries.CONN_CLASS_LOCAL_POLL) diff --git a/homeassistant/components/light/tplink.py b/homeassistant/components/tplink/light.py similarity index 70% rename from homeassistant/components/light/tplink.py rename to homeassistant/components/tplink/light.py index bd1621a0b358f..de1a943c33a00 100644 --- a/homeassistant/components/light/tplink.py +++ b/homeassistant/components/tplink/light.py @@ -7,19 +7,20 @@ import logging import time -import voluptuous as vol - -from homeassistant.const import (CONF_HOST, CONF_NAME) from homeassistant.components.light import ( Light, ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_HS_COLOR, SUPPORT_BRIGHTNESS, - SUPPORT_COLOR_TEMP, SUPPORT_COLOR, PLATFORM_SCHEMA) -import homeassistant.helpers.config_validation as cv + SUPPORT_COLOR_TEMP, SUPPORT_COLOR) from homeassistant.util.color import \ color_temperature_mired_to_kelvin as mired_to_kelvin from homeassistant.util.color import ( color_temperature_kelvin_to_mired as kelvin_to_mired) +import homeassistant.helpers.device_registry as dr +from homeassistant.components.tplink import (DOMAIN as TPLINK_DOMAIN, + CONF_LIGHT) + +DEPENDENCIES = ['tplink'] -REQUIREMENTS = ['pyHS100==0.3.4'] +PARALLEL_UPDATES = 0 _LOGGER = logging.getLogger(__name__) @@ -27,20 +28,25 @@ ATTR_DAILY_ENERGY_KWH = 'daily_energy_kwh' ATTR_MONTHLY_ENERGY_KWH = 'monthly_energy_kwh' -DEFAULT_NAME = 'TP-Link Light' -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_HOST): cv.string, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string -}) +def async_setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the platform. + + Deprecated. + """ + _LOGGER.warning('Loading as a platform is no longer supported, ' + 'convert to use the tplink component.') + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up discovered switches.""" + devs = [] + for dev in hass.data[TPLINK_DOMAIN][CONF_LIGHT]: + devs.append(TPLinkSmartBulb(dev)) -def setup_platform(hass, config, add_entities, discovery_info=None): - """Initialise pyLB100 SmartBulb.""" - from pyHS100 import SmartBulb - host = config.get(CONF_HOST) - name = config.get(CONF_NAME) - add_entities([TPLinkSmartBulb(SmartBulb(host), name)], True) + async_add_entities(devs, True) + + return True def brightness_to_percentage(byt): @@ -56,25 +62,42 @@ def brightness_from_percentage(percent): class TPLinkSmartBulb(Light): """Representation of a TPLink Smart Bulb.""" - # F821: https://github.com/PyCQA/pyflakes/issues/373 - def __init__(self, smartbulb: 'SmartBulb', name) -> None: # noqa: F821 + def __init__(self, smartbulb) -> None: """Initialize the bulb.""" self.smartbulb = smartbulb - self._name = name + self._sysinfo = None self._state = None - self._available = True + self._available = False self._color_temp = None self._brightness = None self._hs = None - self._supported_features = 0 + self._supported_features = None self._min_mireds = None self._max_mireds = None self._emeter_params = {} + @property + def unique_id(self): + """Return a unique ID.""" + return self._sysinfo["mac"] + @property def name(self): - """Return the name of the Smart Bulb, if any.""" - return self._name + """Return the name of the Smart Bulb.""" + return self._sysinfo["alias"] + + @property + def device_info(self): + """Return information about the device.""" + return { + "name": self.name, + "model": self._sysinfo["model"], + "manufacturer": 'TP-Link', + "connections": { + (dr.CONNECTION_NETWORK_MAC, self._sysinfo["mac"]) + }, + "sw_version": self._sysinfo["sw_ver"], + } @property def available(self) -> bool: @@ -88,7 +111,8 @@ def device_state_attributes(self): def turn_on(self, **kwargs): """Turn the light on.""" - self.smartbulb.state = self.smartbulb.BULB_STATE_ON + from pyHS100 import SmartBulb + self.smartbulb.state = SmartBulb.BULB_STATE_ON if ATTR_COLOR_TEMP in kwargs: self.smartbulb.color_temp = \ @@ -105,7 +129,8 @@ def turn_on(self, **kwargs): def turn_off(self, **kwargs): """Turn the light off.""" - self.smartbulb.state = self.smartbulb.BULB_STATE_OFF + from pyHS100 import SmartBulb + self.smartbulb.state = SmartBulb.BULB_STATE_OFF @property def min_mireds(self): @@ -139,17 +164,13 @@ def is_on(self): def update(self): """Update the TP-Link Bulb's state.""" - from pyHS100 import SmartDeviceException + from pyHS100 import SmartDeviceException, SmartBulb try: - if self._supported_features == 0: + if self._supported_features is None: self.get_features() self._state = ( - self.smartbulb.state == self.smartbulb.BULB_STATE_ON) - - # Pull the name from the device if a name was not specified - if self._name == DEFAULT_NAME: - self._name = self.smartbulb.alias + self.smartbulb.state == SmartBulb.BULB_STATE_ON) if self._supported_features & SUPPORT_BRIGHTNESS: self._brightness = brightness_from_percentage( @@ -185,9 +206,9 @@ def update(self): except (SmartDeviceException, OSError) as ex: if self._available: - _LOGGER.warning( - "Could not read state for %s: %s", self._name, ex) - self._available = False + _LOGGER.warning("Could not read state for %s: %s", + self.smartbulb.host, ex) + self._available = False @property def supported_features(self): @@ -196,13 +217,16 @@ def supported_features(self): def get_features(self): """Determine all supported features in one go.""" + self._sysinfo = self.smartbulb.sys_info + self._supported_features = 0 + if self.smartbulb.is_dimmable: self._supported_features += SUPPORT_BRIGHTNESS - if self.smartbulb.is_variable_color_temp: + if getattr(self.smartbulb, 'is_variable_color_temp', False): self._supported_features += SUPPORT_COLOR_TEMP self._min_mireds = kelvin_to_mired( self.smartbulb.valid_temperature_range[1]) self._max_mireds = kelvin_to_mired( self.smartbulb.valid_temperature_range[0]) - if self.smartbulb.is_color: + if getattr(self.smartbulb, 'is_color', False): self._supported_features += SUPPORT_COLOR diff --git a/homeassistant/components/tplink/strings.json b/homeassistant/components/tplink/strings.json new file mode 100644 index 0000000000000..e353c1363abf3 --- /dev/null +++ b/homeassistant/components/tplink/strings.json @@ -0,0 +1,15 @@ +{ + "config": { + "title": "TP-Link Smart Home", + "step": { + "confirm": { + "title": "TP-Link Smart Home", + "description": "Do you want to setup TP-Link smart devices?" + } + }, + "abort": { + "single_instance_allowed": "Only a single configuration is necessary.", + "no_devices_found": "No TP-Link devices found on the network." + } + } +} diff --git a/homeassistant/components/switch/tplink.py b/homeassistant/components/tplink/switch.py similarity index 59% rename from homeassistant/components/switch/tplink.py rename to homeassistant/components/tplink/switch.py index 67c8094a1f208..65b884169c791 100644 --- a/homeassistant/components/switch/tplink.py +++ b/homeassistant/components/tplink/switch.py @@ -7,58 +7,77 @@ import logging import time -import voluptuous as vol - from homeassistant.components.switch import ( - SwitchDevice, PLATFORM_SCHEMA, ATTR_CURRENT_POWER_W, ATTR_TODAY_ENERGY_KWH) -from homeassistant.const import (CONF_HOST, CONF_NAME, ATTR_VOLTAGE) -import homeassistant.helpers.config_validation as cv + SwitchDevice, ATTR_CURRENT_POWER_W, ATTR_TODAY_ENERGY_KWH) +from homeassistant.components.tplink import (DOMAIN as TPLINK_DOMAIN, + CONF_SWITCH) +from homeassistant.const import ATTR_VOLTAGE +import homeassistant.helpers.device_registry as dr + +DEPENDENCIES = ['tplink'] -REQUIREMENTS = ['pyHS100==0.3.4'] +PARALLEL_UPDATES = 0 _LOGGER = logging.getLogger(__name__) ATTR_TOTAL_ENERGY_KWH = 'total_energy_kwh' ATTR_CURRENT_A = 'current_a' -CONF_LEDS = 'enable_leds' -DEFAULT_NAME = 'TP-Link Switch' +def async_setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the platform. + + Deprecated. + """ + _LOGGER.warning('Loading as a platform is no longer supported, ' + 'convert to use the tplink component.') -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_HOST): cv.string, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_LEDS): cv.boolean, -}) +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up discovered switches.""" + devs = [] + for dev in hass.data[TPLINK_DOMAIN][CONF_SWITCH]: + devs.append(SmartPlugSwitch(dev)) -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the TPLink switch platform.""" - from pyHS100 import SmartPlug - host = config.get(CONF_HOST) - name = config.get(CONF_NAME) - leds_on = config.get(CONF_LEDS) + async_add_entities(devs, True) - add_entities([SmartPlugSwitch(SmartPlug(host), name, leds_on)], True) + return True class SmartPlugSwitch(SwitchDevice): """Representation of a TPLink Smart Plug switch.""" - def __init__(self, smartplug, name, leds_on): + def __init__(self, smartplug): """Initialize the switch.""" self.smartplug = smartplug - self._name = name - self._leds_on = leds_on + self._sysinfo = None self._state = None - self._available = True + self._available = False # Set up emeter cache self._emeter_params = {} + @property + def unique_id(self): + """Return a unique ID.""" + return self._sysinfo["mac"] + @property def name(self): - """Return the name of the Smart Plug, if any.""" - return self._name + """Return the name of the Smart Plug.""" + return self._sysinfo["alias"] + + @property + def device_info(self): + """Return information about the device.""" + return { + "name": self.name, + "model": self._sysinfo["model"], + "manufacturer": 'TP-Link', + "connections": { + (dr.CONNECTION_NETWORK_MAC, self._sysinfo["mac"]) + }, + "sw_version": self._sysinfo["sw_ver"], + } @property def available(self) -> bool: @@ -87,17 +106,12 @@ def update(self): """Update the TP-Link switch's state.""" from pyHS100 import SmartDeviceException try: + if not self._sysinfo: + self._sysinfo = self.smartplug.sys_info + self._state = self.smartplug.state == \ self.smartplug.SWITCH_STATE_ON - if self._leds_on is not None: - self.smartplug.led = self._leds_on - self._leds_on = None - - # Pull the name from the device if a name was not specified - if self._name == DEFAULT_NAME: - self._name = self.smartplug.alias - if self.smartplug.has_emeter: emeter_readings = self.smartplug.get_emeter_realtime() @@ -123,6 +137,6 @@ def update(self): except (SmartDeviceException, OSError) as ex: if self._available: - _LOGGER.warning( - "Could not read state for %s: %s", self.name, ex) - self._available = False + _LOGGER.warning("Could not read state for %s: %s", + self.smartplug.host, ex) + self._available = False diff --git a/homeassistant/components/tradfri/.translations/es-419.json b/homeassistant/components/tradfri/.translations/es-419.json new file mode 100644 index 0000000000000..55016606e2dbe --- /dev/null +++ b/homeassistant/components/tradfri/.translations/es-419.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "El Bridge ya est\u00e1 configurado" + }, + "error": { + "invalid_key": "Error al registrarse con la clave proporcionada. Si esto sigue sucediendo, intente reiniciar el gateway.", + "timeout": "Tiempo de espera para validar el c\u00f3digo." + }, + "step": { + "auth": { + "data": { + "host": "Host", + "security_code": "C\u00f3digo de seguridad" + }, + "description": "Puede encontrar el c\u00f3digo de seguridad en la parte posterior de su puerta de enlace.", + "title": "Ingrese el c\u00f3digo de seguridad" + } + }, + "title": "IKEA TR\u00c5DFRI" + } +} \ No newline at end of file diff --git a/homeassistant/components/tradfri/.translations/it.json b/homeassistant/components/tradfri/.translations/it.json index 3d5101bbce84f..4c11449233649 100644 --- a/homeassistant/components/tradfri/.translations/it.json +++ b/homeassistant/components/tradfri/.translations/it.json @@ -1,5 +1,23 @@ { "config": { + "abort": { + "already_configured": "Il bridge \u00e8 gi\u00e0 configurato" + }, + "error": { + "cannot_connect": "Impossibile connettersi al gateway.", + "invalid_key": "Impossibile registrarsi con la chiave fornita. Se questo continua a succedere, prova a riavviare il gateway.", + "timeout": "Tempo scaduto per la validazione del codice." + }, + "step": { + "auth": { + "data": { + "host": "Host", + "security_code": "Codice di sicurezza" + }, + "description": "Puoi trovare il codice di sicurezza sul retro del tuo gateway.", + "title": "Inserisci il codice di sicurezza" + } + }, "title": "IKEA TR\u00c5DFRI" } } \ No newline at end of file diff --git a/homeassistant/components/tuya/climate.py b/homeassistant/components/tuya/climate.py index 97ff18ba911b4..06714760a024f 100644 --- a/homeassistant/components/tuya/climate.py +++ b/homeassistant/components/tuya/climate.py @@ -1,13 +1,14 @@ """Support for the Tuya climate devices.""" -from homeassistant.components.climate import ( - ATTR_TEMPERATURE, ENTITY_ID_FORMAT, STATE_AUTO, STATE_COOL, STATE_ECO, +from homeassistant.components.climate import ClimateDevice, ENTITY_ID_FORMAT +from homeassistant.components.climate.const import ( + STATE_AUTO, STATE_COOL, STATE_ECO, STATE_FAN_ONLY, STATE_HEAT, SUPPORT_FAN_MODE, SUPPORT_ON_OFF, - SUPPORT_OPERATION_MODE, SUPPORT_TARGET_TEMPERATURE, ClimateDevice) + SUPPORT_OPERATION_MODE, SUPPORT_TARGET_TEMPERATURE) from homeassistant.components.fan import SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH from homeassistant.components.tuya import DATA_TUYA, TuyaDevice from homeassistant.const import ( - PRECISION_WHOLE, TEMP_CELSIUS, TEMP_FAHRENHEIT) + ATTR_TEMPERATURE, PRECISION_WHOLE, TEMP_CELSIUS, TEMP_FAHRENHEIT) DEPENDENCIES = ['tuya'] DEVICE_TYPE = 'climate' diff --git a/homeassistant/components/twilio/.translations/es-419.json b/homeassistant/components/twilio/.translations/es-419.json new file mode 100644 index 0000000000000..a5fd83abef4fd --- /dev/null +++ b/homeassistant/components/twilio/.translations/es-419.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "Su instancia de Home Assistant debe ser accesible desde Internet para recibir los mensajes de Twilio.", + "one_instance_allowed": "Solo una instancia es necesaria." + }, + "create_entry": { + "default": "Para enviar eventos a Home Assistant, deber\u00e1 configurar [Webhooks with Twilio] ( {twilio_url} ). \n\n Complete la siguiente informaci\u00f3n: \n\n - URL: ` {webhook_url} ` \n - M\u00e9todo: POST \n - Tipo de contenido: application / x-www-form-urlencoded \n\n Consulte [la documentaci\u00f3n] ( {docs_url} ) sobre c\u00f3mo configurar las automatizaciones para manejar los datos entrantes." + }, + "step": { + "user": { + "description": "\u00bfEst\u00e1s seguro de que quieres configurar Twilio?", + "title": "Configurar el Webhook Twilio" + } + }, + "title": "Twilio" + } +} \ No newline at end of file diff --git a/homeassistant/components/twilio/.translations/hu.json b/homeassistant/components/twilio/.translations/hu.json index 257dd24f08232..ae96d08976de2 100644 --- a/homeassistant/components/twilio/.translations/hu.json +++ b/homeassistant/components/twilio/.translations/hu.json @@ -1,7 +1,15 @@ { "config": { "abort": { + "not_internet_accessible": "A Home Assistant rendszerednek el\u00e9rhet\u0151nek kell lennie az internetr\u0151l a Twilio \u00fczenetek fogad\u00e1s\u00e1hoz.", "one_instance_allowed": "Csak egyetlen konfigur\u00e1ci\u00f3 sz\u00fcks\u00e9ges." - } + }, + "step": { + "user": { + "description": "Biztosan be szeretn\u00e9d \u00e1ll\u00edtani a Twilio-t?", + "title": "A Twilio Webhook be\u00e1ll\u00edt\u00e1sa" + } + }, + "title": "Twilio" } } \ No newline at end of file diff --git a/homeassistant/components/twilio/.translations/it.json b/homeassistant/components/twilio/.translations/it.json new file mode 100644 index 0000000000000..4f8926c23e5f0 --- /dev/null +++ b/homeassistant/components/twilio/.translations/it.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "La tua istanza di Home Assistant deve essere accessibile da Internet per ricevere messaggi da Twilio.", + "one_instance_allowed": "\u00c8 necessaria una sola istanza." + }, + "create_entry": { + "default": "Per inviare eventi a Home Assistant, dovrai configurare [Webhooks con Twilio]({twilio_url})\n\n Compila le seguenti informazioni: \n\n - URL: ` {webhook_url} ` \n - Method: POST \n - Content Type: application/x-www-form-urlencoded\n\n Vedi [la documentazione]({docs_url}) su come configurare le automazioni per gestire i dati in arrivo." + }, + "step": { + "user": { + "description": "Sei sicuro di voler configurare Twilio?", + "title": "Configura il webhook di Twilio" + } + }, + "title": "Twilio" + } +} \ No newline at end of file diff --git a/homeassistant/components/twilio/.translations/ru.json b/homeassistant/components/twilio/.translations/ru.json index c195392be2233..b8d6f11f7efba 100644 --- a/homeassistant/components/twilio/.translations/ru.json +++ b/homeassistant/components/twilio/.translations/ru.json @@ -5,7 +5,7 @@ "one_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430." }, "create_entry": { - "default": "\u0414\u043b\u044f \u043e\u0442\u043f\u0440\u0430\u0432\u043a\u0438 \u0441\u043e\u0431\u044b\u0442\u0438\u0439 \u0432 Home Assistant \u0432\u044b \u0434\u043e\u043b\u0436\u043d\u044b \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c [Webhooks \u0434\u043b\u044f Twilio]({twilio_url}).\n\n\u0417\u0430\u043f\u043e\u043b\u043d\u0438\u0442\u0435 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0443\u044e \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e:\n\n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/x-www-form-urlencoded\n\n\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0435\u0439]({docs_url}) \u0434\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0437\u0430\u0446\u0438\u0439 \u043f\u043e \u043e\u0431\u0440\u0430\u0431\u043e\u0442\u043a\u0435 \u043f\u043e\u0441\u0442\u0443\u043f\u0430\u044e\u0449\u0438\u0445 \u0434\u0430\u043d\u043d\u044b\u0445." + "default": "\u0414\u043b\u044f \u043e\u0442\u043f\u0440\u0430\u0432\u043a\u0438 \u0441\u043e\u0431\u044b\u0442\u0438\u0439 \u0432 Home Assistant \u0412\u044b \u0434\u043e\u043b\u0436\u043d\u044b \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c [Webhooks \u0434\u043b\u044f Twilio]({twilio_url}).\n\n\u0417\u0430\u043f\u043e\u043b\u043d\u0438\u0442\u0435 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0443\u044e \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e:\n\n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/x-www-form-urlencoded\n\n\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0435\u0439]({docs_url}) \u0434\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0437\u0430\u0446\u0438\u0439 \u043f\u043e \u043e\u0431\u0440\u0430\u0431\u043e\u0442\u043a\u0435 \u043f\u043e\u0441\u0442\u0443\u043f\u0430\u044e\u0449\u0438\u0445 \u0434\u0430\u043d\u043d\u044b\u0445." }, "step": { "user": { diff --git a/homeassistant/components/twilio/.translations/sv.json b/homeassistant/components/twilio/.translations/sv.json new file mode 100644 index 0000000000000..673997d5aa985 --- /dev/null +++ b/homeassistant/components/twilio/.translations/sv.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "Din Home Assistant instans m\u00e5ste vara tillg\u00e4nglig fr\u00e5n internet f\u00f6r att ta emot Twilio meddelanden.", + "one_instance_allowed": "Endast en enda instans \u00e4r n\u00f6dv\u00e4ndig." + }, + "create_entry": { + "default": "F\u00f6r att skicka h\u00e4ndelser till Home Assistant m\u00e5ste du konfigurera [Webhooks med Twilio]({twilio_url}).\n\n Fyll i f\u00f6ljande information:\n \n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/x-www-form-urlencoded\n\n Se [dokumentationen]({docs_url}) om hur du konfigurerar automatiseringar f\u00f6r att hantera inkommande data." + }, + "step": { + "user": { + "description": "\u00c4r du s\u00e4ker p\u00e5 att du vill konfigurera Twilio?", + "title": "Konfigurera Twilio Webhook" + } + }, + "title": "Twilio" + } +} \ No newline at end of file diff --git a/homeassistant/components/unifi/.translations/es-419.json b/homeassistant/components/unifi/.translations/es-419.json new file mode 100644 index 0000000000000..9b729e4c4abaa --- /dev/null +++ b/homeassistant/components/unifi/.translations/es-419.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "El sitio del controlador ya est\u00e1 configurado", + "user_privilege": "El usuario necesita ser administrador" + }, + "error": { + "faulty_credentials": "Credenciales de usuario incorrectas", + "service_unavailable": "No hay servicio disponible" + }, + "step": { + "user": { + "data": { + "host": "Host", + "password": "Contrase\u00f1a", + "port": "Puerto", + "site": "ID del sitio", + "username": "Nombre de usuario", + "verify_ssl": "Controlador usando el certificado apropiado" + }, + "title": "Configurar el controlador UniFi" + } + }, + "title": "Controlador UniFi" + } +} \ No newline at end of file diff --git a/homeassistant/components/unifi/.translations/hu.json b/homeassistant/components/unifi/.translations/hu.json index 4a664a40c74d0..6f78beaffd631 100644 --- a/homeassistant/components/unifi/.translations/hu.json +++ b/homeassistant/components/unifi/.translations/hu.json @@ -14,9 +14,12 @@ "password": "Jelsz\u00f3", "port": "Port", "site": "Site azonos\u00edt\u00f3", - "username": "Felhaszn\u00e1l\u00f3n\u00e9v" - } + "username": "Felhaszn\u00e1l\u00f3n\u00e9v", + "verify_ssl": "Vez\u00e9rl\u0151 megfelel\u0151 tan\u00fas\u00edtv\u00e1nnyal" + }, + "title": "UniFi vez\u00e9rl\u0151 be\u00e1ll\u00edt\u00e1sa" } - } + }, + "title": "UniFi Vez\u00e9rl\u0151" } } \ No newline at end of file diff --git a/homeassistant/components/unifi/.translations/it.json b/homeassistant/components/unifi/.translations/it.json new file mode 100644 index 0000000000000..407371bf89f19 --- /dev/null +++ b/homeassistant/components/unifi/.translations/it.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "Il sito del Controller \u00e8 gi\u00e0 configurato", + "user_privilege": "L'utente deve essere amministratore" + }, + "error": { + "faulty_credentials": "Credenziali utente non valide", + "service_unavailable": "Servizio non disponibile" + }, + "step": { + "user": { + "data": { + "host": "Host", + "password": "Password", + "port": "Porta", + "site": "ID del sito", + "username": "Nome utente", + "verify_ssl": "Il Controller sta utilizzando il certificato corretto" + }, + "title": "Configura l'UniFi Controller" + } + }, + "title": "UniFi Controller" + } +} \ No newline at end of file diff --git a/homeassistant/components/unifi/.translations/sv.json b/homeassistant/components/unifi/.translations/sv.json new file mode 100644 index 0000000000000..864c887d6fe8e --- /dev/null +++ b/homeassistant/components/unifi/.translations/sv.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "Controller-platsen \u00e4r redan konfigurerad", + "user_privilege": "Anv\u00e4ndaren m\u00e5ste vara administrat\u00f6r" + }, + "error": { + "faulty_credentials": "Felaktiga anv\u00e4ndaruppgifter", + "service_unavailable": "Ingen tj\u00e4nst tillg\u00e4nglig" + }, + "step": { + "user": { + "data": { + "host": "V\u00e4rddatorn", + "password": "L\u00f6senord", + "port": "Port", + "site": "Plats-ID", + "username": "Anv\u00e4ndarnamn", + "verify_ssl": "Controller med korrekt certifikat" + }, + "title": "Konfigurera UniFi Controller" + } + }, + "title": "UniFi Controller" + } +} \ No newline at end of file diff --git a/homeassistant/components/upnp/.translations/es-419.json b/homeassistant/components/upnp/.translations/es-419.json new file mode 100644 index 0000000000000..bd95b48359ee1 --- /dev/null +++ b/homeassistant/components/upnp/.translations/es-419.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "already_configured": "UPnP/IGD ya est\u00e1 configurado", + "incomplete_device": "Ignorar un dispositivo UPnP incompleto", + "no_devices_discovered": "No se han descubierto UPnP/IGDs", + "no_devices_found": "No se encuentran dispositivos UPnP/IGD en la red.", + "no_sensors_or_port_mapping": "Habilitar al menos sensores o mapeo de puertos", + "single_instance_allowed": "S\u00f3lo se necesita una \u00fanica configuraci\u00f3n de UPnP/IGD." + }, + "step": { + "confirm": { + "description": "\u00bfDesea configurar UPnP/IGD?", + "title": "UPnP/IGD" + }, + "init": { + "title": "UPnP/IGD" + }, + "user": { + "data": { + "enable_port_mapping": "Habilitar la asignaci\u00f3n de puertos para Home Assistant", + "enable_sensors": "A\u00f1adir sensores de tr\u00e1fico", + "igd": "UPnP/IGD" + }, + "title": "Opciones de configuraci\u00f3n para UPnP/IGD" + } + }, + "title": "UPnP/IGD" + } +} \ No newline at end of file diff --git a/homeassistant/components/upnp/.translations/es.json b/homeassistant/components/upnp/.translations/es.json index 6afdeca804738..fa299cc379f61 100644 --- a/homeassistant/components/upnp/.translations/es.json +++ b/homeassistant/components/upnp/.translations/es.json @@ -4,13 +4,19 @@ "already_configured": "UPnP / IGD ya est\u00e1 configurado", "incomplete_device": "Ignorando el dispositivo UPnP incompleto", "no_devices_discovered": "No se descubrieron UPnP / IGDs", - "no_sensors_or_port_mapping": "Habilitar al menos sensores o mapeo de puertos" + "no_devices_found": "No se encuentran dispositivos UPnP/IGD en la red.", + "no_sensors_or_port_mapping": "Habilitar al menos sensores o mapeo de puertos", + "single_instance_allowed": "S\u00f3lo se necesita una configuraci\u00f3n de UPnP/IGD." }, "error": { "one": "UNO", "other": "OTRO" }, "step": { + "confirm": { + "description": "\u00bfDesea configurar UPnP/IGD?", + "title": "UPnP/IGD" + }, "init": { "title": "UPnP / IGD" }, diff --git a/homeassistant/components/upnp/.translations/hu.json b/homeassistant/components/upnp/.translations/hu.json index f2fd380b1e3c5..7d3827e76da6e 100644 --- a/homeassistant/components/upnp/.translations/hu.json +++ b/homeassistant/components/upnp/.translations/hu.json @@ -1,7 +1,11 @@ { "config": { "abort": { - "no_devices_found": "Nincsenek UPnPIGD eszk\u00f6z\u00f6k a h\u00e1l\u00f3zaton." + "already_configured": "Az UPnP / IGD m\u00e1r konfigur\u00e1l\u00e1sra ker\u00fclt", + "incomplete_device": "A hi\u00e1nyos UPnP-eszk\u00f6z figyelmen k\u00edv\u00fcl hagy\u00e1sa", + "no_devices_discovered": "Nem tal\u00e1ltam UPnP / IGD-ket", + "no_devices_found": "Nincsenek UPnPIGD eszk\u00f6z\u00f6k a h\u00e1l\u00f3zaton.", + "single_instance_allowed": "Csak egy UPnP / IGD konfigur\u00e1ci\u00f3 sz\u00fcks\u00e9ges." }, "error": { "one": "hiba", @@ -17,6 +21,7 @@ }, "user": { "data": { + "enable_port_mapping": "Enged\u00e9lyezd a port mappinget a Home Assistant sz\u00e1m\u00e1ra", "enable_sensors": "Forgalom \u00e9rz\u00e9kel\u0151k hozz\u00e1ad\u00e1sa", "igd": "UPnP/IGD" }, diff --git a/homeassistant/components/upnp/.translations/it.json b/homeassistant/components/upnp/.translations/it.json new file mode 100644 index 0000000000000..798f657809395 --- /dev/null +++ b/homeassistant/components/upnp/.translations/it.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "already_configured": "UPnP/IGD \u00e8 gi\u00e0 configurato", + "incomplete_device": "Ignorare il dispositivo UPnP incompleto", + "no_devices_discovered": "Nessun UPnP/IGD trovato", + "no_devices_found": "Nessun dispositivo UPnP/IGD trovato in rete.", + "no_sensors_or_port_mapping": "Abilita almeno i sensori o la mappatura delle porte", + "single_instance_allowed": "\u00c8 necessaria una sola configurazione di UPnP/IGD." + }, + "step": { + "confirm": { + "description": "Vuoi configurare UPnP/IGD?", + "title": "UPnP/IGD" + }, + "init": { + "title": "UPnP/IGD" + }, + "user": { + "data": { + "enable_port_mapping": "Abilita il port mapping per Home Assistant", + "enable_sensors": "Aggiungi sensori di traffico", + "igd": "UPnP/IGD" + }, + "title": "Opzioni di configurazione per UPnP/IGD" + } + }, + "title": "UPnP/IGD" + } +} \ No newline at end of file diff --git a/homeassistant/components/upnp/.translations/ko.json b/homeassistant/components/upnp/.translations/ko.json index d38d5be58ba7e..9fa37e1236dd3 100644 --- a/homeassistant/components/upnp/.translations/ko.json +++ b/homeassistant/components/upnp/.translations/ko.json @@ -8,9 +8,6 @@ "no_sensors_or_port_mapping": "\ucd5c\uc18c\ud55c \uc13c\uc11c \ud639\uc740 \ud3ec\ud2b8 \ub9e4\ud551\uc744 \ud65c\uc131\ud654 \ud574\uc57c \ud569\ub2c8\ub2e4", "single_instance_allowed": "\ud558\ub098\uc758 UPnP/IGD \ub9cc \uad6c\uc131 \ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4." }, - "error": { - "other": "\ub2e4\ub978" - }, "step": { "confirm": { "description": "UPnP/IGD \ub97c \uc124\uc815 \ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", diff --git a/homeassistant/components/upnp/.translations/sv.json b/homeassistant/components/upnp/.translations/sv.json index 63c63781845cc..e3864aee4da20 100644 --- a/homeassistant/components/upnp/.translations/sv.json +++ b/homeassistant/components/upnp/.translations/sv.json @@ -2,14 +2,21 @@ "config": { "abort": { "already_configured": "UPnP/IGD \u00e4r redan konfigurerad", + "incomplete_device": "Ignorera ofullst\u00e4ndig UPnP-enhet", "no_devices_discovered": "Inga UPnP/IGDs uppt\u00e4cktes", - "no_sensors_or_port_mapping": "Aktivera minst sensorer eller portmappning" + "no_devices_found": "Inga UPnP/IGD-enheter hittades p\u00e5 n\u00e4tverket.", + "no_sensors_or_port_mapping": "Aktivera minst sensorer eller portmappning", + "single_instance_allowed": "Endast en enda konfiguration av UPnP/IGD \u00e4r n\u00f6dv\u00e4ndig." }, "error": { "one": "En", "other": "Andra" }, "step": { + "confirm": { + "description": "Vill du konfigurera UPnP/IGD?", + "title": "UPnP/IGD" + }, "init": { "title": "UPnP/IGD" }, diff --git a/homeassistant/components/utility_meter/__init__.py b/homeassistant/components/utility_meter/__init__.py index 3cf1b2fea6190..2f062851ee6df 100644 --- a/homeassistant/components/utility_meter/__init__.py +++ b/homeassistant/components/utility_meter/__init__.py @@ -12,9 +12,9 @@ from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from .const import ( DOMAIN, SIGNAL_RESET_METER, METER_TYPES, CONF_SOURCE_SENSOR, - CONF_METER_TYPE, CONF_METER_OFFSET, CONF_TARIFF_ENTITY, CONF_TARIFF, - CONF_TARIFFS, CONF_METER, DATA_UTILITY, SERVICE_RESET, - SERVICE_SELECT_TARIFF, SERVICE_SELECT_NEXT_TARIFF, + CONF_METER_TYPE, CONF_METER_OFFSET, CONF_METER_NET_CONSUMPTION, + CONF_TARIFF_ENTITY, CONF_TARIFF, CONF_TARIFFS, CONF_METER, DATA_UTILITY, + SERVICE_RESET, SERVICE_SELECT_TARIFF, SERVICE_SELECT_NEXT_TARIFF, ATTR_TARIFF) _LOGGER = logging.getLogger(__name__) @@ -36,6 +36,7 @@ vol.Optional(CONF_NAME): cv.string, vol.Optional(CONF_METER_TYPE): vol.In(METER_TYPES), vol.Optional(CONF_METER_OFFSET, default=0): cv.positive_int, + vol.Optional(CONF_METER_NET_CONSUMPTION, default=False): cv.boolean, vol.Optional(CONF_TARIFFS, default=[]): vol.All( cv.ensure_list, [cv.string]), }) diff --git a/homeassistant/components/utility_meter/const.py b/homeassistant/components/utility_meter/const.py index 4d2df0372b5c6..c5cb6b8aa33ee 100644 --- a/homeassistant/components/utility_meter/const.py +++ b/homeassistant/components/utility_meter/const.py @@ -15,6 +15,7 @@ CONF_SOURCE_SENSOR = 'source' CONF_METER_TYPE = 'cycle' CONF_METER_OFFSET = 'offset' +CONF_METER_NET_CONSUMPTION = 'net_consumption' CONF_PAUSED = 'paused' CONF_TARIFFS = 'tariffs' CONF_TARIFF = 'tariff' diff --git a/homeassistant/components/utility_meter/sensor.py b/homeassistant/components/utility_meter/sensor.py index a01c53b20e345..21dc1099442af 100644 --- a/homeassistant/components/utility_meter/sensor.py +++ b/homeassistant/components/utility_meter/sensor.py @@ -17,7 +17,7 @@ DATA_UTILITY, SIGNAL_RESET_METER, HOURLY, DAILY, WEEKLY, MONTHLY, YEARLY, CONF_SOURCE_SENSOR, CONF_METER_TYPE, CONF_METER_OFFSET, - CONF_TARIFF, CONF_TARIFF_ENTITY, CONF_METER) + CONF_METER_NET_CONSUMPTION, CONF_TARIFF, CONF_TARIFF_ENTITY, CONF_METER) _LOGGER = logging.getLogger(__name__) @@ -48,13 +48,15 @@ async def async_setup_platform( conf_meter_source = hass.data[DATA_UTILITY][meter][CONF_SOURCE_SENSOR] conf_meter_type = hass.data[DATA_UTILITY][meter].get(CONF_METER_TYPE) conf_meter_offset = hass.data[DATA_UTILITY][meter][CONF_METER_OFFSET] + conf_meter_net_consumption =\ + hass.data[DATA_UTILITY][meter][CONF_METER_NET_CONSUMPTION] conf_meter_tariff_entity = hass.data[DATA_UTILITY][meter].get( CONF_TARIFF_ENTITY) meters.append(UtilityMeterSensor( conf_meter_source, conf.get(CONF_NAME), conf_meter_type, - conf_meter_offset, conf.get(CONF_TARIFF), - conf_meter_tariff_entity)) + conf_meter_offset, conf_meter_net_consumption, + conf.get(CONF_TARIFF), conf_meter_tariff_entity)) async_add_entities(meters) @@ -62,8 +64,8 @@ async def async_setup_platform( class UtilityMeterSensor(RestoreEntity): """Representation of an utility meter sensor.""" - def __init__(self, source_entity, name, meter_type, meter_offset=0, - tariff=None, tariff_entity=None): + def __init__(self, source_entity, name, meter_type, meter_offset, + net_consumption, tariff=None, tariff_entity=None): """Initialize the Utility Meter sensor.""" self._sensor_source_id = source_entity self._state = 0 @@ -77,6 +79,7 @@ def __init__(self, source_entity, name, meter_type, meter_offset=0, self._unit_of_measurement = None self._period = meter_type self._period_offset = meter_offset + self._sensor_net_consumption = net_consumption self._tariff = tariff self._tariff_entity = tariff_entity @@ -96,7 +99,7 @@ def async_reading(self, entity, old_state, new_state): try: diff = Decimal(new_state.state) - Decimal(old_state.state) - if diff < 0: + if (not self._sensor_net_consumption) and diff < 0: # Source sensor just rolled over for unknow reasons, return self._state += diff diff --git a/homeassistant/components/vacuum/services.yaml b/homeassistant/components/vacuum/services.yaml index 6e40b3d67fc8e..fe5bb77cefea4 100644 --- a/homeassistant/components/vacuum/services.yaml +++ b/homeassistant/components/vacuum/services.yaml @@ -131,3 +131,35 @@ xiaomi_remote_control_move_step: 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/velbus/climate.py b/homeassistant/components/velbus/climate.py index ae7a282849215..1f45408a666c9 100644 --- a/homeassistant/components/velbus/climate.py +++ b/homeassistant/components/velbus/climate.py @@ -1,8 +1,9 @@ """Support for Velbus thermostat.""" import logging -from homeassistant.components.climate import ( - STATE_HEAT, SUPPORT_TARGET_TEMPERATURE, ClimateDevice) +from homeassistant.components.climate import ClimateDevice +from homeassistant.components.climate.const import ( + STATE_HEAT, SUPPORT_TARGET_TEMPERATURE) from homeassistant.components.velbus import ( DOMAIN as VELBUS_DOMAIN, VelbusEntity) from homeassistant.const import ( diff --git a/homeassistant/components/velux/__init__.py b/homeassistant/components/velux/__init__.py index 1018f72fdbc5b..6ea50ae6c0d21 100644 --- a/homeassistant/components/velux/__init__.py +++ b/homeassistant/components/velux/__init__.py @@ -12,7 +12,7 @@ SUPPORTED_DOMAINS = ['cover', 'scene'] _LOGGER = logging.getLogger(__name__) -REQUIREMENTS = ['pyvlx==0.2.8'] +REQUIREMENTS = ['pyvlx==0.2.9'] CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.Schema({ diff --git a/homeassistant/components/vera/climate.py b/homeassistant/components/vera/climate.py index 7cd3129bc1485..9c812da9208ee 100644 --- a/homeassistant/components/vera/climate.py +++ b/homeassistant/components/vera/climate.py @@ -2,9 +2,10 @@ import logging from homeassistant.util import convert -from homeassistant.components.climate import ( - ClimateDevice, STATE_AUTO, STATE_COOL, - STATE_HEAT, ENTITY_ID_FORMAT, SUPPORT_TARGET_TEMPERATURE, +from homeassistant.components.climate import ClimateDevice, ENTITY_ID_FORMAT +from homeassistant.components.climate.const import ( + STATE_AUTO, STATE_COOL, + STATE_HEAT, SUPPORT_TARGET_TEMPERATURE, SUPPORT_OPERATION_MODE, SUPPORT_FAN_MODE) from homeassistant.const import ( STATE_ON, diff --git a/homeassistant/components/water_heater/demo.py b/homeassistant/components/water_heater/demo.py index a0220927f1647..b551993aca5d8 100644 --- a/homeassistant/components/water_heater/demo.py +++ b/homeassistant/components/water_heater/demo.py @@ -1,4 +1,4 @@ -"""Demo platform that offers a fake water_heater device.""" +"""Demo platform that offers a fake water heater device.""" from homeassistant.components.water_heater import ( WaterHeaterDevice, SUPPORT_TARGET_TEMPERATURE, diff --git a/homeassistant/components/water_heater/econet.py b/homeassistant/components/water_heater/econet.py index 93ae98ed94b9f..69fde44bdd244 100644 --- a/homeassistant/components/water_heater/econet.py +++ b/homeassistant/components/water_heater/econet.py @@ -13,7 +13,7 @@ TEMP_FAHRENHEIT) import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['pyeconet==0.0.6'] +REQUIREMENTS = ['pyeconet==0.0.8'] _LOGGER = logging.getLogger(__name__) @@ -43,18 +43,18 @@ ECONET_DATA = 'econet' -HA_STATE_TO_ECONET = { - STATE_ECO: 'Energy Saver', - STATE_ELECTRIC: 'Electric', - STATE_HEAT_PUMP: 'Heat Pump', - STATE_GAS: 'gas', - STATE_HIGH_DEMAND: 'High Demand', - STATE_OFF: 'Off', - STATE_PERFORMANCE: 'Performance' +ECONET_STATE_TO_HA = { + 'Energy Saver': STATE_ECO, + 'gas': STATE_GAS, + 'High Demand': STATE_HIGH_DEMAND, + 'Off': STATE_OFF, + 'Performance': STATE_PERFORMANCE, + 'Heat Pump Only': STATE_HEAT_PUMP, + 'Electric-Only': STATE_ELECTRIC, + 'Electric': STATE_ELECTRIC, + 'Heat Pump': STATE_HEAT_PUMP } -ECONET_STATE_TO_HA = {value: key for key, value in HA_STATE_TO_ECONET.items()} - PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_PASSWORD): cv.string, @@ -110,6 +110,19 @@ class EcoNetWaterHeater(WaterHeaterDevice): def __init__(self, water_heater): """Initialize the water heater.""" self.water_heater = water_heater + self.supported_modes = self.water_heater.supported_modes + self.econet_state_to_ha = {} + self.ha_state_to_econet = {} + for mode in ECONET_STATE_TO_HA: + if mode in self.supported_modes: + self.econet_state_to_ha[mode] = ECONET_STATE_TO_HA.get(mode) + for key, value in self.econet_state_to_ha.items(): + self.ha_state_to_econet[value] = key + for mode in self.supported_modes: + if mode not in ECONET_STATE_TO_HA: + error = "Invalid operation mode mapping. " + mode + \ + " doesn't map. Please report this." + _LOGGER.error(error) @property def name(self): @@ -149,22 +162,17 @@ def current_operation(self): ["eco", "heat_pump", "high_demand", "electric_only"] """ - current_op = ECONET_STATE_TO_HA.get(self.water_heater.mode) + current_op = self.econet_state_to_ha.get(self.water_heater.mode) return current_op @property def operation_list(self): """List of available operation modes.""" op_list = [] - modes = self.water_heater.supported_modes - for mode in modes: - ha_mode = ECONET_STATE_TO_HA.get(mode) + for mode in self.supported_modes: + ha_mode = self.econet_state_to_ha.get(mode) if ha_mode is not None: op_list.append(ha_mode) - else: - error = "Invalid operation mode mapping. " + mode + \ - " doesn't map. Please report this." - _LOGGER.error(error) return op_list @property @@ -182,7 +190,7 @@ def set_temperature(self, **kwargs): def set_operation_mode(self, operation_mode): """Set operation mode.""" - op_mode_to_set = HA_STATE_TO_ECONET.get(operation_mode) + op_mode_to_set = self.ha_state_to_econet.get(operation_mode) if op_mode_to_set is not None: self.water_heater.set_mode(op_mode_to_set) else: diff --git a/homeassistant/components/weather/__init__.py b/homeassistant/components/weather/__init__.py index d479725657bcf..34cd86347f29d 100644 --- a/homeassistant/components/weather/__init__.py +++ b/homeassistant/components/weather/__init__.py @@ -1,18 +1,13 @@ -""" -Weather component that handles meteorological data for your location. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/weather/ -""" +"""Weather component that handles meteorological data for your location.""" from datetime import timedelta import logging -from homeassistant.helpers.entity_component import EntityComponent -from homeassistant.helpers.temperature import display_temp as show_temp -from homeassistant.const import PRECISION_WHOLE, PRECISION_TENTHS, TEMP_CELSIUS +from homeassistant.const import PRECISION_TENTHS, PRECISION_WHOLE, TEMP_CELSIUS from homeassistant.helpers.config_validation import ( # noqa PLATFORM_SCHEMA, PLATFORM_SCHEMA_BASE) from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity_component import EntityComponent +from homeassistant.helpers.temperature import display_temp as show_temp _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/weather/bom.py b/homeassistant/components/weather/bom.py index 1ed54496c6f53..b05d5fc594da1 100644 --- a/homeassistant/components/weather/bom.py +++ b/homeassistant/components/weather/bom.py @@ -1,20 +1,15 @@ -""" -Support for Australian BOM (Bureau of Meteorology) weather service. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/weather.bom/ -""" +"""Support for Australian BOM (Bureau of Meteorology) weather service.""" import logging import voluptuous as vol -from homeassistant.components.weather import WeatherEntity, PLATFORM_SCHEMA -from homeassistant.const import \ - CONF_NAME, TEMP_CELSIUS, CONF_LATITUDE, CONF_LONGITUDE -from homeassistant.helpers import config_validation as cv # Reuse data and API logic from the sensor implementation -from homeassistant.components.sensor.bom import \ - BOMCurrentData, closest_station, CONF_STATION, validate_station +from homeassistant.components.sensor.bom import ( + CONF_STATION, BOMCurrentData, closest_station, validate_station) +from homeassistant.components.weather import PLATFORM_SCHEMA, WeatherEntity +from homeassistant.const import ( + CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME, TEMP_CELSIUS) +from homeassistant.helpers import config_validation as cv _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/weather/buienradar.py b/homeassistant/components/weather/buienradar.py index 1ec3fc513e906..31f51824146c7 100644 --- a/homeassistant/components/weather/buienradar.py +++ b/homeassistant/components/weather/buienradar.py @@ -1,22 +1,16 @@ -""" -Support for Buienradar.nl weather service. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/weather.buienradar/ -""" +"""Support for Buienradar.nl weather service.""" import logging import voluptuous as vol +# Reuse data and API logic from the sensor implementation +from homeassistant.components.sensor.buienradar import BrData from homeassistant.components.weather import ( - WeatherEntity, PLATFORM_SCHEMA, ATTR_FORECAST_CONDITION, - ATTR_FORECAST_TEMP, ATTR_FORECAST_TEMP_LOW, ATTR_FORECAST_TIME) -from homeassistant.const import \ - CONF_NAME, TEMP_CELSIUS, CONF_LATITUDE, CONF_LONGITUDE + ATTR_FORECAST_CONDITION, ATTR_FORECAST_TEMP, ATTR_FORECAST_TEMP_LOW, + ATTR_FORECAST_TIME, PLATFORM_SCHEMA, WeatherEntity) +from homeassistant.const import ( + CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME, TEMP_CELSIUS) from homeassistant.helpers import config_validation as cv -# Reuse data and API logic from the sensor implementation -from homeassistant.components.sensor.buienradar import ( - BrData) REQUIREMENTS = ['buienradar==0.91'] diff --git a/homeassistant/components/weather/darksky.py b/homeassistant/components/weather/darksky.py index 4ac3d2a1d221a..17e3cbbcf14eb 100644 --- a/homeassistant/components/weather/darksky.py +++ b/homeassistant/components/weather/darksky.py @@ -1,9 +1,4 @@ -""" -Platform for retrieving meteorological data from Dark Sky. - -For more details about this platform, please refer to the documentation -https://home-assistant.io/components/weather.darksky/ -""" +"""Support for retrieving meteorological data from Dark Sky.""" from datetime import datetime, timedelta import logging @@ -12,13 +7,12 @@ import voluptuous as vol from homeassistant.components.weather import ( - ATTR_FORECAST_TEMP, ATTR_FORECAST_TIME, ATTR_FORECAST_CONDITION, - ATTR_FORECAST_WIND_SPEED, ATTR_FORECAST_WIND_BEARING, - ATTR_FORECAST_TEMP_LOW, ATTR_FORECAST_PRECIPITATION, - PLATFORM_SCHEMA, WeatherEntity) + ATTR_FORECAST_CONDITION, ATTR_FORECAST_PRECIPITATION, ATTR_FORECAST_TEMP, + ATTR_FORECAST_TEMP_LOW, ATTR_FORECAST_TIME, ATTR_FORECAST_WIND_BEARING, + ATTR_FORECAST_WIND_SPEED, PLATFORM_SCHEMA, WeatherEntity) from homeassistant.const import ( - CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME, TEMP_CELSIUS, - CONF_MODE, TEMP_FAHRENHEIT) + CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_MODE, CONF_NAME, + TEMP_CELSIUS, TEMP_FAHRENHEIT) import homeassistant.helpers.config_validation as cv from homeassistant.util import Throttle diff --git a/homeassistant/components/weather/demo.py b/homeassistant/components/weather/demo.py index 6bcb89185042b..d20e91b1f9378 100644 --- a/homeassistant/components/weather/demo.py +++ b/homeassistant/components/weather/demo.py @@ -1,15 +1,10 @@ -""" -Demo platform that offers fake meteorological data. - -For more details about this platform, please refer to the documentation -https://home-assistant.io/components/demo/ -""" +"""Demo platform that offers fake meteorological data.""" from datetime import datetime, timedelta from homeassistant.components.weather import ( - WeatherEntity, ATTR_FORECAST_CONDITION, ATTR_FORECAST_PRECIPITATION, - ATTR_FORECAST_TEMP, ATTR_FORECAST_TEMP_LOW, ATTR_FORECAST_TIME) -from homeassistant.const import (TEMP_CELSIUS, TEMP_FAHRENHEIT) + ATTR_FORECAST_CONDITION, ATTR_FORECAST_PRECIPITATION, ATTR_FORECAST_TEMP, + ATTR_FORECAST_TEMP_LOW, ATTR_FORECAST_TIME, WeatherEntity) +from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT CONDITION_CLASSES = { 'cloudy': [], diff --git a/homeassistant/components/weather/met.py b/homeassistant/components/weather/met.py index c905e6b6ce341..6c9613ac5d283 100644 --- a/homeassistant/components/weather/met.py +++ b/homeassistant/components/weather/met.py @@ -1,29 +1,24 @@ -""" -Support for Met.no weather service. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/weather.met/ -""" +"""Support for Met.no weather service.""" import logging from random import randrange import voluptuous as vol from homeassistant.components.weather import PLATFORM_SCHEMA, WeatherEntity -from homeassistant.const import (CONF_ELEVATION, CONF_LATITUDE, CONF_LONGITUDE, - CONF_NAME, TEMP_CELSIUS) +from homeassistant.const import ( + CONF_ELEVATION, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME, TEMP_CELSIUS) from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.helpers.event import (async_track_utc_time_change, - async_call_later) +from homeassistant.helpers.event import ( + async_call_later, async_track_utc_time_change) import homeassistant.util.dt as dt_util -REQUIREMENTS = ['pyMetno==0.4.5'] +REQUIREMENTS = ['pyMetno==0.4.6'] _LOGGER = logging.getLogger(__name__) -CONF_ATTRIBUTION = "Weather forecast from met.no, delivered " \ - "by the Norwegian Meteorological Institute." +ATTRIBUTION = "Weather forecast from met.no, delivered by the Norwegian " \ + "Meteorological Institute." DEFAULT_NAME = "Met.no" URL = 'https://aa015h6buqvih86i1.api.met.no/weatherapi/locationforecast/1.9/' @@ -55,8 +50,8 @@ async def async_setup_platform(hass, config, async_add_entities, 'msl': str(elevation), } - async_add_entities([MetWeather(name, coordinates, - async_get_clientsession(hass))]) + async_add_entities([MetWeather( + name, coordinates, async_get_clientsession(hass))]) class MetWeather(WeatherEntity): @@ -66,18 +61,16 @@ def __init__(self, name, coordinates, clientsession): """Initialise the platform with a data instance and site.""" import metno self._name = name - self._weather_data = metno.MetWeatherData(coordinates, - clientsession, - URL - ) + self._weather_data = metno.MetWeatherData( + coordinates, clientsession, URL) self._current_weather_data = {} self._forecast_data = None async def async_added_to_hass(self): """Start fetching data.""" await self._fetch_data() - async_track_utc_time_change(self.hass, self._update, - minute=31, second=0) + async_track_utc_time_change( + self.hass, self._update, minute=31, second=0) async def _fetch_data(self, *_): """Get the latest data from met.no.""" @@ -146,7 +139,7 @@ def wind_bearing(self): @property def attribution(self): """Return the attribution.""" - return CONF_ATTRIBUTION + return ATTRIBUTION @property def forecast(self): diff --git a/homeassistant/components/weather/metoffice.py b/homeassistant/components/weather/metoffice.py index 7382319e7a4fe..3b52eebcff6e8 100644 --- a/homeassistant/components/weather/metoffice.py +++ b/homeassistant/components/weather/metoffice.py @@ -1,15 +1,10 @@ -""" -Support for UK Met Office weather service. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/weather.metoffice/ -""" +"""Support for UK Met Office weather service.""" import logging import voluptuous as vol from homeassistant.components.sensor.metoffice import ( - CONDITION_CLASSES, CONF_ATTRIBUTION, MetOfficeCurrentData) + CONDITION_CLASSES, ATTRIBUTION, MetOfficeCurrentData) from homeassistant.components.weather import PLATFORM_SCHEMA, WeatherEntity from homeassistant.const import ( CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME, TEMP_CELSIUS) @@ -123,4 +118,4 @@ def wind_bearing(self): @property def attribution(self): """Return the attribution.""" - return CONF_ATTRIBUTION + return ATTRIBUTION diff --git a/homeassistant/components/weather/openweathermap.py b/homeassistant/components/weather/openweathermap.py index 2b86359361a28..58016dd3e2ccb 100644 --- a/homeassistant/components/weather/openweathermap.py +++ b/homeassistant/components/weather/openweathermap.py @@ -1,9 +1,4 @@ -""" -Support for the OpenWeatherMap (OWM) service. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/weather.openweathermap/ -""" +"""Support for the OpenWeatherMap (OWM) service.""" from datetime import timedelta import logging @@ -11,12 +6,11 @@ from homeassistant.components.weather import ( ATTR_FORECAST_CONDITION, ATTR_FORECAST_PRECIPITATION, ATTR_FORECAST_TEMP, - ATTR_FORECAST_TEMP_LOW, ATTR_FORECAST_TIME, ATTR_FORECAST_WIND_SPEED, - ATTR_FORECAST_WIND_BEARING, - PLATFORM_SCHEMA, WeatherEntity) + ATTR_FORECAST_TEMP_LOW, ATTR_FORECAST_TIME, ATTR_FORECAST_WIND_BEARING, + ATTR_FORECAST_WIND_SPEED, PLATFORM_SCHEMA, WeatherEntity) from homeassistant.const import ( - CONF_API_KEY, TEMP_CELSIUS, CONF_LATITUDE, CONF_LONGITUDE, CONF_MODE, - CONF_NAME, STATE_UNKNOWN) + CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_MODE, CONF_NAME, + STATE_UNKNOWN, TEMP_CELSIUS) import homeassistant.helpers.config_validation as cv from homeassistant.util import Throttle @@ -26,7 +20,7 @@ ATTRIBUTION = 'Data provided by OpenWeatherMap' -FORECAST_MODE = ['hourly', 'daily'] +FORECAST_MODE = ['hourly', 'daily', 'freedaily'] DEFAULT_NAME = 'OpenWeatherMap' @@ -158,7 +152,12 @@ def calc_precipitation(rain, snow): return None return round(rain_value + snow_value, 1) - for entry in self.forecast_data.get_weathers(): + if self._mode == 'freedaily': + weather = self.forecast_data.get_weathers()[::8] + else: + weather = self.forecast_data.get_weathers() + + for entry in weather: if self._mode == 'daily': data.append({ ATTR_FORECAST_TIME: diff --git a/homeassistant/components/weather/yweather.py b/homeassistant/components/weather/yweather.py index 567b8e235a818..e4eb34a039ac6 100644 --- a/homeassistant/components/weather/yweather.py +++ b/homeassistant/components/weather/yweather.py @@ -1,9 +1,4 @@ -""" -Support for the Yahoo! Weather service. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/weather.yweather/ -""" +"""Support for the Yahoo! Weather service.""" from datetime import timedelta import logging diff --git a/homeassistant/components/weather/zamg.py b/homeassistant/components/weather/zamg.py index f76b733ef0bb1..60707fa5e3079 100644 --- a/homeassistant/components/weather/zamg.py +++ b/homeassistant/components/weather/zamg.py @@ -1,23 +1,18 @@ -""" -Sensor for data from Austrian "Zentralanstalt für Meteorologie und Geodynamik". - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/weather.zamg/ -""" +"""Sensor for data from Austrian Zentralanstalt für Meteorologie.""" import logging import voluptuous as vol +# Reuse data and API logic from the sensor implementation +from homeassistant.components.sensor.zamg import ( + ATTRIBUTION, CONF_STATION_ID, ZamgData, closest_station, zamg_stations) from homeassistant.components.weather import ( - WeatherEntity, ATTR_WEATHER_HUMIDITY, ATTR_WEATHER_PRESSURE, - ATTR_WEATHER_TEMPERATURE, ATTR_WEATHER_WIND_BEARING, - ATTR_WEATHER_WIND_SPEED, PLATFORM_SCHEMA) + ATTR_WEATHER_HUMIDITY, ATTR_WEATHER_PRESSURE, ATTR_WEATHER_TEMPERATURE, + ATTR_WEATHER_WIND_BEARING, ATTR_WEATHER_WIND_SPEED, PLATFORM_SCHEMA, + WeatherEntity) from homeassistant.const import ( - CONF_NAME, TEMP_CELSIUS, CONF_LATITUDE, CONF_LONGITUDE) + CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME, TEMP_CELSIUS) from homeassistant.helpers import config_validation as cv -# Reuse data and API logic from the sensor implementation -from homeassistant.components.sensor.zamg import ( - ATTRIBUTION, closest_station, CONF_STATION_ID, zamg_stations, ZamgData) _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/websocket_api/commands.py b/homeassistant/components/websocket_api/commands.py index 34bb04cb394c7..3313971e79e64 100644 --- a/homeassistant/components/websocket_api/commands.py +++ b/homeassistant/components/websocket_api/commands.py @@ -3,7 +3,8 @@ from homeassistant.const import MATCH_ALL, EVENT_TIME_CHANGED from homeassistant.core import callback, DOMAIN as HASS_DOMAIN -from homeassistant.exceptions import Unauthorized, ServiceNotFound +from homeassistant.exceptions import Unauthorized, ServiceNotFound, \ + HomeAssistantError from homeassistant.helpers import config_validation as cv from homeassistant.helpers.service import async_get_all_descriptions @@ -149,6 +150,14 @@ async def handle_call_service(hass, connection, msg): except ServiceNotFound: connection.send_message(messages.error_message( msg['id'], const.ERR_NOT_FOUND, 'Service not found.')) + except HomeAssistantError as err: + connection.logger.exception(err) + connection.send_message(messages.error_message( + msg['id'], const.ERR_HOME_ASSISTANT_ERROR, '{}'.format(err))) + except Exception as err: # pylint: disable=broad-except + connection.logger.exception(err) + connection.send_message(messages.error_message( + msg['id'], const.ERR_UNKNOWN_ERROR, '{}'.format(err))) @callback diff --git a/homeassistant/components/websocket_api/const.py b/homeassistant/components/websocket_api/const.py index fd8f7eb7b08a0..01145275b3161 100644 --- a/homeassistant/components/websocket_api/const.py +++ b/homeassistant/components/websocket_api/const.py @@ -9,6 +9,7 @@ ERR_ID_REUSE = 'id_reuse' ERR_INVALID_FORMAT = 'invalid_format' ERR_NOT_FOUND = 'not_found' +ERR_HOME_ASSISTANT_ERROR = 'home_assistant_error' ERR_UNKNOWN_COMMAND = 'unknown_command' ERR_UNKNOWN_ERROR = 'unknown_error' ERR_UNAUTHORIZED = 'unauthorized' diff --git a/homeassistant/components/websocket_api/http.py b/homeassistant/components/websocket_api/http.py index 42c2c0a5751e3..1ab2b09d7fa6f 100644 --- a/homeassistant/components/websocket_api/http.py +++ b/homeassistant/components/websocket_api/http.py @@ -130,7 +130,7 @@ def handle_hass_stop(event): if msg.type in (WSMsgType.CLOSE, WSMsgType.CLOSING): raise Disconnect - elif msg.type != WSMsgType.TEXT: + if msg.type != WSMsgType.TEXT: disconnect_warn = 'Received non-Text message.' raise Disconnect diff --git a/homeassistant/components/wink/climate.py b/homeassistant/components/wink/climate.py index 8d946bf03dfe1..efd8eecf5afcd 100644 --- a/homeassistant/components/wink/climate.py +++ b/homeassistant/components/wink/climate.py @@ -1,17 +1,18 @@ """Support for Wink thermostats and Air Conditioners.""" import logging -from homeassistant.components.climate import ( +from homeassistant.components.climate import ClimateDevice +from homeassistant.components.climate.const import ( ATTR_CURRENT_HUMIDITY, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, - ATTR_TEMPERATURE, STATE_AUTO, STATE_COOL, STATE_ECO, + STATE_AUTO, STATE_COOL, STATE_ECO, STATE_FAN_ONLY, STATE_HEAT, SUPPORT_AUX_HEAT, SUPPORT_AWAY_MODE, SUPPORT_FAN_MODE, SUPPORT_OPERATION_MODE, SUPPORT_TARGET_TEMPERATURE, - SUPPORT_TARGET_TEMPERATURE_HIGH, SUPPORT_TARGET_TEMPERATURE_LOW, - ClimateDevice) + SUPPORT_TARGET_TEMPERATURE_HIGH, SUPPORT_TARGET_TEMPERATURE_LOW) from homeassistant.components.wink import DOMAIN, WinkDevice from homeassistant.const import ( - PRECISION_TENTHS, STATE_OFF, STATE_ON, STATE_UNKNOWN, TEMP_CELSIUS) + ATTR_TEMPERATURE, PRECISION_TENTHS, STATE_OFF, STATE_ON, STATE_UNKNOWN, + TEMP_CELSIUS) from homeassistant.helpers.temperature import display_temp as show_temp _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/xiaomi_aqara/__init__.py b/homeassistant/components/xiaomi_aqara/__init__.py index 5e47adc47f91b..19d7aaaa30d2a 100644 --- a/homeassistant/components/xiaomi_aqara/__init__.py +++ b/homeassistant/components/xiaomi_aqara/__init__.py @@ -16,7 +16,7 @@ from homeassistant.helpers.event import async_track_point_in_utc_time from homeassistant.util.dt import utcnow -REQUIREMENTS = ['PyXiaomiGateway==0.11.2'] +REQUIREMENTS = ['PyXiaomiGateway==0.12.0'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/xiaomi_miio/vacuum.py b/homeassistant/components/xiaomi_miio/vacuum.py index 82a92dd317cc8..361961586008b 100644 --- a/homeassistant/components/xiaomi_miio/vacuum.py +++ b/homeassistant/components/xiaomi_miio/vacuum.py @@ -33,6 +33,7 @@ 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, @@ -58,6 +59,8 @@ ATTR_RC_ROTATION = 'rotation' ATTR_RC_VELOCITY = 'velocity' ATTR_STATUS = 'status' +ATTR_ZONE_ARRAY = 'zone' +ATTR_ZONE_REPEATER = 'repeats' SERVICE_SCHEMA_REMOTE_CONTROL = VACUUM_SERVICE_SCHEMA.extend({ vol.Optional(ATTR_RC_VELOCITY): @@ -67,6 +70,24 @@ vol.Optional(ATTR_RC_DURATION): cv.positive_int, }) +SERVICE_SCHEMA_CLEAN_ZONE = VACUUM_SERVICE_SCHEMA.extend({ + vol.Required(ATTR_ZONE_ARRAY): + vol.All(list, [vol.ExactSequence( + [vol.Coerce(int), vol.Coerce(int), + vol.Coerce(int), vol.Coerce(int)])]), + vol.Required(ATTR_ZONE_REPEATER): + vol.All(vol.Coerce(int), vol.Clamp(min=1, max=3)), +}) + +SERVICE_SCHEMA_CLEAN_ZONE = VACUUM_SERVICE_SCHEMA.extend({ + vol.Required(ATTR_ZONE_ARRAY): + vol.All(list, [vol.ExactSequence( + [vol.Coerce(int), vol.Coerce(int), + vol.Coerce(int), vol.Coerce(int)])]), + vol.Required(ATTR_ZONE_REPEATER): + vol.All(vol.Coerce(int), vol.Clamp(min=1, max=3)), +}) + SERVICE_TO_METHOD = { SERVICE_START_REMOTE_CONTROL: {'method': 'async_remote_control_start'}, SERVICE_STOP_REMOTE_CONTROL: {'method': 'async_remote_control_stop'}, @@ -76,6 +97,9 @@ SERVICE_MOVE_REMOTE_CONTROL_STEP: { 'method': 'async_remote_control_move_step', 'schema': SERVICE_SCHEMA_REMOTE_CONTROL}, + SERVICE_CLEAN_ZONE: { + 'method': 'async_clean_zone', + 'schema': SERVICE_SCHEMA_CLEAN_ZONE}, } SUPPORT_XIAOMI = SUPPORT_STATE | SUPPORT_PAUSE | \ @@ -127,6 +151,7 @@ async def async_service_handler(service): params = {key: value for key, value in service.data.items() if key != ATTR_ENTITY_ID} entity_ids = service.data.get(ATTR_ENTITY_ID) + if entity_ids: target_vacuums = [vac for vac in hass.data[DATA_KEY].values() if vac.entity_id in entity_ids] @@ -377,3 +402,19 @@ def update(self): _LOGGER.error("Got OSError while fetching the state: %s", exc) except DeviceException as exc: _LOGGER.warning("Got exception while fetching the state: %s", exc) + + async def async_clean_zone(self, + zone, + repeats=1): + """Clean selected area for the number of repeats indicated.""" + from miio import DeviceException + for _zone in zone: + _zone.append(repeats) + _LOGGER.debug("Zone with repeats: %s", zone) + try: + await self.hass.async_add_executor_job( + self._vacuum.zoned_clean, zone) + except (OSError, DeviceException) as exc: + _LOGGER.error( + "Unable to send zoned_clean command to the vacuum: %s", + exc) diff --git a/homeassistant/components/xs1/climate.py b/homeassistant/components/xs1/climate.py index 92a2c75895ce2..e579761474bbb 100644 --- a/homeassistant/components/xs1/climate.py +++ b/homeassistant/components/xs1/climate.py @@ -2,10 +2,12 @@ from functools import partial import logging -from homeassistant.components.climate import ( - ATTR_TEMPERATURE, ClimateDevice, SUPPORT_TARGET_TEMPERATURE) +from homeassistant.components.climate import ClimateDevice +from homeassistant.components.climate.const import ( + SUPPORT_TARGET_TEMPERATURE) from homeassistant.components.xs1 import ( ACTUATORS, DOMAIN as COMPONENT_DOMAIN, SENSORS, XS1DeviceEntity) +from homeassistant.const import ATTR_TEMPERATURE DEPENDENCIES = ['xs1'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/zha/.translations/de.json b/homeassistant/components/zha/.translations/de.json index 280c941b427f4..686c1f35a98d2 100644 --- a/homeassistant/components/zha/.translations/de.json +++ b/homeassistant/components/zha/.translations/de.json @@ -12,6 +12,7 @@ "radio_type": "Radio-Type", "usb_path": "USB-Ger\u00e4te-Pfad" }, + "description": "Leer", "title": "ZHA" } }, diff --git a/homeassistant/components/zha/.translations/es-419.json b/homeassistant/components/zha/.translations/es-419.json new file mode 100644 index 0000000000000..0047c762a9de0 --- /dev/null +++ b/homeassistant/components/zha/.translations/es-419.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "S\u00f3lo se permite una \u00fanica configuraci\u00f3n de ZHA." + }, + "error": { + "cannot_connect": "No se puede conectar al dispositivo ZHA." + }, + "step": { + "user": { + "data": { + "radio_type": "Tipo de radio", + "usb_path": "Ruta del dispositivo USB" + }, + "title": "ZHA" + } + }, + "title": "ZHA" + } +} \ No newline at end of file diff --git a/homeassistant/components/zha/.translations/es.json b/homeassistant/components/zha/.translations/es.json new file mode 100644 index 0000000000000..9984a31688497 --- /dev/null +++ b/homeassistant/components/zha/.translations/es.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "S\u00f3lo se permite una \u00fanica configuraci\u00f3n de ZHA." + }, + "error": { + "cannot_connect": "No se puede conectar al dispositivo ZHA." + }, + "step": { + "user": { + "data": { + "radio_type": "Tipo de radio", + "usb_path": "Ruta del dispositivo USB" + }, + "description": "Vac\u00edo", + "title": "ZHA" + } + }, + "title": "ZHA" + } +} \ No newline at end of file diff --git a/homeassistant/components/zha/.translations/hu.json b/homeassistant/components/zha/.translations/hu.json index 11b2a9fc83356..39c00a4dee300 100644 --- a/homeassistant/components/zha/.translations/hu.json +++ b/homeassistant/components/zha/.translations/hu.json @@ -12,6 +12,7 @@ "radio_type": "R\u00e1di\u00f3 t\u00edpusa", "usb_path": "USB eszk\u00f6z el\u00e9r\u00e9si \u00fat" }, + "description": "\u00dcres", "title": "ZHA" } }, diff --git a/homeassistant/components/zha/.translations/it.json b/homeassistant/components/zha/.translations/it.json new file mode 100644 index 0000000000000..e4b87c9d7b6df --- /dev/null +++ b/homeassistant/components/zha/.translations/it.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "\u00c8 consentita solo una singola configurazione di ZHA." + }, + "error": { + "cannot_connect": "Impossibile connettersi al dispositivo ZHA." + }, + "step": { + "user": { + "data": { + "radio_type": "Tipo di Radio", + "usb_path": "Percorso del dispositivo USB" + }, + "title": "ZHA" + } + }, + "title": "ZHA" + } +} \ No newline at end of file diff --git a/homeassistant/components/zha/.translations/sv.json b/homeassistant/components/zha/.translations/sv.json new file mode 100644 index 0000000000000..029f03916571f --- /dev/null +++ b/homeassistant/components/zha/.translations/sv.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Endast en enda konfiguration av ZHA \u00e4r till\u00e5ten." + }, + "error": { + "cannot_connect": "Det gick inte att ansluta till ZHA enhet." + }, + "step": { + "user": { + "data": { + "radio_type": "Typ av radio", + "usb_path": "USB-enhetens s\u00f6kv\u00e4g" + }, + "description": "?", + "title": "ZHA" + } + }, + "title": "ZHA" + } +} \ No newline at end of file diff --git a/homeassistant/components/zha/__init__.py b/homeassistant/components/zha/__init__.py index 6c7e83689ad4c..cafbae1342153 100644 --- a/homeassistant/components/zha/__init__.py +++ b/homeassistant/components/zha/__init__.py @@ -29,11 +29,11 @@ from .core.channels.registry import populate_channel_registry REQUIREMENTS = [ - 'bellows==0.7.0', - 'zigpy==0.2.0', - 'zigpy-xbee==0.1.1', + 'bellows-homeassistant==0.7.1', + 'zigpy-homeassistant==0.3.0', + 'zigpy-xbee-homeassistant==0.1.2', 'zha-quirks==0.0.6', - 'zigpy-deconz==0.0.1' + 'zigpy-deconz==0.1.2' ] DEVICE_CONFIG_SCHEMA_ENTRY = vol.Schema({ @@ -154,11 +154,9 @@ def handle_message(sender, is_reply, profile, cluster, """Handle message from a device.""" if not sender.initializing and sender.ieee in zha_gateway.devices and \ not zha_gateway.devices[sender.ieee].available: - hass.async_create_task( - zha_gateway.async_device_became_available( - sender, is_reply, profile, cluster, src_ep, dst_ep, tsn, - command_id, args - ) + zha_gateway.async_device_became_available( + sender, is_reply, profile, cluster, src_ep, dst_ep, tsn, + command_id, args ) return sender.handle_message( is_reply, profile, cluster, src_ep, dst_ep, tsn, command_id, args) diff --git a/homeassistant/components/zha/api.py b/homeassistant/components/zha/api.py index 0dd6dd7840042..f0739f9a073c2 100644 --- a/homeassistant/components/zha/api.py +++ b/homeassistant/components/zha/api.py @@ -251,7 +251,7 @@ async def websocket_device_clusters(hass, connection, msg): zha_device = zha_gateway.get_device(ieee) response_clusters = [] if zha_device is not None: - clusters_by_endpoint = await zha_device.get_clusters() + clusters_by_endpoint = zha_device.async_get_clusters() for ep_id, clusters in clusters_by_endpoint.items(): for c_id, cluster in clusters[IN].items(): response_clusters.append({ @@ -289,7 +289,7 @@ async def websocket_device_cluster_attributes(hass, connection, msg): zha_device = zha_gateway.get_device(ieee) attributes = None if zha_device is not None: - attributes = await zha_device.get_cluster_attributes( + attributes = zha_device.async_get_cluster_attributes( endpoint_id, cluster_id, cluster_type) @@ -329,7 +329,7 @@ async def websocket_device_cluster_commands(hass, connection, msg): cluster_commands = [] commands = None if zha_device is not None: - commands = await zha_device.get_cluster_commands( + commands = zha_device.async_get_cluster_commands( endpoint_id, cluster_id, cluster_type) @@ -380,7 +380,7 @@ async def websocket_read_zigbee_cluster_attributes(hass, connection, msg): zha_device = zha_gateway.get_device(ieee) success = failure = None if zha_device is not None: - cluster = await zha_device.get_cluster( + cluster = zha_device.async_get_cluster( endpoint_id, cluster_id, cluster_type=cluster_type) success, failure = await cluster.read_attributes( [attribute], diff --git a/homeassistant/components/zha/core/channels/__init__.py b/homeassistant/components/zha/core/channels/__init__.py index 0c0e1ed217323..a070343b775df 100644 --- a/homeassistant/components/zha/core/channels/__init__.py +++ b/homeassistant/components/zha/core/channels/__init__.py @@ -5,6 +5,7 @@ https://home-assistant.io/components/zha/ """ import asyncio +from concurrent.futures import TimeoutError as Timeout from enum import Enum from functools import wraps import logging @@ -55,9 +56,13 @@ async def wrapper(*args, **kwds): if isinstance(result, bool): return result return result[1] is Status.SUCCESS - except DeliveryError: - _LOGGER.debug("%s: command failed: %s", channel.unique_id, - command.__name__) + except (DeliveryError, Timeout) as ex: + _LOGGER.debug( + "%s: command failed: %s exception: %s", + channel.unique_id, + command.__name__, + str(ex) + ) return False return wrapper diff --git a/homeassistant/components/zha/core/channels/lighting.py b/homeassistant/components/zha/core/channels/lighting.py index ee88a30e8288d..9c904a7a001ba 100644 --- a/homeassistant/components/zha/core/channels/lighting.py +++ b/homeassistant/components/zha/core/channels/lighting.py @@ -28,8 +28,16 @@ def get_color_capabilities(self): """Return the color capabilities.""" return self._color_capabilities + async def async_configure(self): + """Configure channel.""" + await self.fetch_color_capabilities(False) + async def async_initialize(self, from_cache): """Initialize channel.""" + await self.fetch_color_capabilities(True) + + async def fetch_color_capabilities(self, from_cache): + """Get the color configuration.""" capabilities = await self.get_attribute_value( 'color_capabilities', from_cache=from_cache) diff --git a/homeassistant/components/zha/core/device.py b/homeassistant/components/zha/core/device.py index 1ee800d8559f5..102c9bed2d370 100644 --- a/homeassistant/components/zha/core/device.py +++ b/homeassistant/components/zha/core/device.py @@ -8,6 +8,7 @@ from enum import Enum import logging +from homeassistant.core import callback from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send ) @@ -188,13 +189,14 @@ async def async_initialize(self, from_cache=False): """Initialize channels.""" _LOGGER.debug('%s: started initialization', self.name) await self._execute_channel_tasks('async_initialize', from_cache) - self.power_source = self.cluster_channels.get( - BASIC_CHANNEL).get_power_source() - _LOGGER.debug( - '%s: power source: %s', - self.name, - BasicChannel.POWER_SOURCES.get(self.power_source) - ) + if BASIC_CHANNEL in self.cluster_channels: + self.power_source = self.cluster_channels.get( + BASIC_CHANNEL).get_power_source() + _LOGGER.debug( + '%s: power source: %s', + self.name, + BasicChannel.POWER_SOURCES.get(self.power_source) + ) self.status = DeviceStatus.INITIALIZED _LOGGER.debug('%s: completed initialization', self.name) @@ -229,7 +231,8 @@ async def async_unsub_dispatcher(self): if self._unsub: self._unsub() - async def get_clusters(self): + @callback + def async_get_clusters(self): """Get all clusters for this device.""" return { ep_id: { @@ -239,25 +242,27 @@ async def get_clusters(self): if ep_id != 0 } - async def get_cluster(self, endpoint_id, cluster_id, cluster_type=IN): + @callback + def async_get_cluster(self, endpoint_id, cluster_id, cluster_type=IN): """Get zigbee cluster from this entity.""" - clusters = await self.get_clusters() + clusters = self.async_get_clusters() return clusters[endpoint_id][cluster_type][cluster_id] - async def get_cluster_attributes(self, endpoint_id, cluster_id, + @callback + def async_get_cluster_attributes(self, endpoint_id, cluster_id, cluster_type=IN): """Get zigbee attributes for specified cluster.""" - cluster = await self.get_cluster(endpoint_id, cluster_id, + cluster = self.async_get_cluster(endpoint_id, cluster_id, cluster_type) if cluster is None: return None return cluster.attributes - async def get_cluster_commands(self, endpoint_id, cluster_id, + @callback + def async_get_cluster_commands(self, endpoint_id, cluster_id, cluster_type=IN): """Get zigbee commands for specified cluster.""" - cluster = await self.get_cluster(endpoint_id, cluster_id, - cluster_type) + cluster = self.async_get_cluster(endpoint_id, cluster_id, cluster_type) if cluster is None: return None return { @@ -269,8 +274,7 @@ async def write_zigbee_attribute(self, endpoint_id, cluster_id, attribute, value, cluster_type=IN, manufacturer=None): """Write a value to a zigbee attribute for a cluster in this entity.""" - cluster = await self.get_cluster( - endpoint_id, cluster_id, cluster_type) + cluster = self.async_get_cluster(endpoint_id, cluster_id, cluster_type) if cluster is None: return None @@ -304,8 +308,7 @@ async def issue_cluster_command(self, endpoint_id, cluster_id, command, command_type, args, cluster_type=IN, manufacturer=None): """Issue a command against specified zigbee cluster on this entity.""" - cluster = await self.get_cluster( - endpoint_id, cluster_id, cluster_type) + cluster = self.async_get_cluster(endpoint_id, cluster_id, cluster_type) if cluster is None: return None response = None diff --git a/homeassistant/components/zha/core/gateway.py b/homeassistant/components/zha/core/gateway.py index cb5e5bf77749f..563543fa4bd8a 100644 --- a/homeassistant/components/zha/core/gateway.py +++ b/homeassistant/components/zha/core/gateway.py @@ -5,11 +5,11 @@ https://home-assistant.io/components/zha/ """ -import asyncio import collections import itertools import logging from homeassistant import const as ha_const +from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.entity_component import EntityComponent from . import const as zha_const @@ -122,7 +122,8 @@ def register_entity_reference( ) ) - async def _get_or_create_device(self, zigpy_device): + @callback + def _async_get_or_create_device(self, zigpy_device): """Get or create a ZHA device.""" zha_device = self._devices.get(zigpy_device.ieee) if zha_device is None: @@ -130,12 +131,14 @@ async def _get_or_create_device(self, zigpy_device): self._devices[zigpy_device.ieee] = zha_device return zha_device - async def async_device_became_available( + @callback + def async_device_became_available( self, sender, is_reply, profile, cluster, src_ep, dst_ep, tsn, command_id, args): """Handle tasks when a device becomes available.""" self.async_update_device(sender) + @callback def async_update_device(self, sender): """Update device that has just become available.""" if sender.ieee in self.devices: @@ -146,34 +149,17 @@ def async_update_device(self, sender): async def async_device_initialized(self, device, is_new_join): """Handle device joined and basic information discovered (async).""" - zha_device = await self._get_or_create_device(device) + zha_device = self._async_get_or_create_device(device) discovery_infos = [] - endpoint_tasks = [] for endpoint_id, endpoint in device.endpoints.items(): - endpoint_tasks.append(self._async_process_endpoint( + self._async_process_endpoint( endpoint_id, endpoint, discovery_infos, device, zha_device, is_new_join - )) - await asyncio.gather(*endpoint_tasks) - - await zha_device.async_initialize(from_cache=(not is_new_join)) - - discovery_tasks = [] - for discovery_info in discovery_infos: - discovery_tasks.append(_dispatch_discovery_info( - self._hass, - is_new_join, - discovery_info - )) - await asyncio.gather(*discovery_tasks) - - device_entity = _create_device_entity(zha_device) - await self._component.async_add_entities([device_entity]) + ) if is_new_join: - # because it's a new join we can immediately mark the device as - # available and we already loaded fresh state above - zha_device.update_available(True) + # configure the device + await zha_device.async_configure() elif not zha_device.available and zha_device.power_source is not None\ and zha_device.power_source != BasicChannel.BATTERY\ and zha_device.power_source != BasicChannel.UNKNOWN: @@ -187,15 +173,33 @@ async def async_device_initialized(self, device, is_new_join): ) ) await zha_device.async_initialize(from_cache=False) + else: + await zha_device.async_initialize(from_cache=True) + + for discovery_info in discovery_infos: + _async_dispatch_discovery_info( + self._hass, + is_new_join, + discovery_info + ) - async def _async_process_endpoint( + device_entity = _async_create_device_entity(zha_device) + await self._component.async_add_entities([device_entity]) + + if is_new_join: + # because it's a new join we can immediately mark the device as + # available. We do it here because the entities didn't exist above + zha_device.update_available(True) + + @callback + def _async_process_endpoint( self, endpoint_id, endpoint, discovery_infos, device, zha_device, is_new_join): """Process an endpoint on a zigpy device.""" import zigpy.profiles if endpoint_id == 0: # ZDO - await _create_cluster_channel( + _async_create_cluster_channel( endpoint, zha_device, is_new_join, @@ -226,12 +230,12 @@ async def _async_process_endpoint( profile_clusters = zha_const.COMPONENT_CLUSTERS[component] if component and component in COMPONENTS: - profile_match = await _handle_profile_match( + profile_match = _async_handle_profile_match( self._hass, endpoint, profile_clusters, zha_device, component, device_key, is_new_join) discovery_infos.append(profile_match) - discovery_infos.extend(await _handle_single_cluster_matches( + discovery_infos.extend(_async_handle_single_cluster_matches( self._hass, endpoint, zha_device, @@ -241,21 +245,21 @@ async def _async_process_endpoint( )) -async def _create_cluster_channel(cluster, zha_device, is_new_join, +@callback +def _async_create_cluster_channel(cluster, zha_device, is_new_join, channels=None, channel_class=None): """Create a cluster channel and attach it to a device.""" if channel_class is None: channel_class = ZIGBEE_CHANNEL_REGISTRY.get(cluster.cluster_id, AttributeListeningChannel) channel = channel_class(cluster, zha_device) - if is_new_join: - await channel.async_configure() zha_device.add_cluster_channel(channel) if channels is not None: channels.append(channel) -async def _dispatch_discovery_info(hass, is_new_join, discovery_info): +@callback +def _async_dispatch_discovery_info(hass, is_new_join, discovery_info): """Dispatch or store discovery information.""" if not discovery_info['channels']: _LOGGER.warning( @@ -273,7 +277,8 @@ async def _dispatch_discovery_info(hass, is_new_join, discovery_info): discovery_info -async def _handle_profile_match(hass, endpoint, profile_clusters, zha_device, +@callback +def _async_handle_profile_match(hass, endpoint, profile_clusters, zha_device, component, device_key, is_new_join): """Dispatch a profile match to the appropriate HA component.""" in_clusters = [endpoint.in_clusters[c] @@ -284,17 +289,14 @@ async def _handle_profile_match(hass, endpoint, profile_clusters, zha_device, if c in endpoint.out_clusters] channels = [] - cluster_tasks = [] for cluster in in_clusters: - cluster_tasks.append(_create_cluster_channel( - cluster, zha_device, is_new_join, channels=channels)) + _async_create_cluster_channel( + cluster, zha_device, is_new_join, channels=channels) for cluster in out_clusters: - cluster_tasks.append(_create_cluster_channel( - cluster, zha_device, is_new_join, channels=channels)) - - await asyncio.gather(*cluster_tasks) + _async_create_cluster_channel( + cluster, zha_device, is_new_join, channels=channels) discovery_info = { 'unique_id': device_key, @@ -319,24 +321,25 @@ async def _handle_profile_match(hass, endpoint, profile_clusters, zha_device, return discovery_info -async def _handle_single_cluster_matches(hass, endpoint, zha_device, +@callback +def _async_handle_single_cluster_matches(hass, endpoint, zha_device, profile_clusters, device_key, is_new_join): """Dispatch single cluster matches to HA components.""" cluster_matches = [] - cluster_match_tasks = [] - event_channel_tasks = [] + cluster_match_results = [] for cluster in endpoint.in_clusters.values(): # don't let profiles prevent these channels from being created if cluster.cluster_id in NO_SENSOR_CLUSTERS: - cluster_match_tasks.append(_handle_channel_only_cluster_match( - zha_device, - cluster, - is_new_join, - )) + cluster_match_results.append( + _async_handle_channel_only_cluster_match( + zha_device, + cluster, + is_new_join, + )) if cluster.cluster_id not in profile_clusters[0]: - cluster_match_tasks.append(_handle_single_cluster_match( + cluster_match_results.append(_async_handle_single_cluster_match( hass, zha_device, cluster, @@ -347,7 +350,7 @@ async def _handle_single_cluster_matches(hass, endpoint, zha_device, for cluster in endpoint.out_clusters.values(): if cluster.cluster_id not in profile_clusters[1]: - cluster_match_tasks.append(_handle_single_cluster_match( + cluster_match_results.append(_async_handle_single_cluster_match( hass, zha_device, cluster, @@ -357,27 +360,28 @@ async def _handle_single_cluster_matches(hass, endpoint, zha_device, )) if cluster.cluster_id in EVENT_RELAY_CLUSTERS: - event_channel_tasks.append(_create_cluster_channel( + _async_create_cluster_channel( cluster, zha_device, is_new_join, channel_class=EventRelayChannel - )) - await asyncio.gather(*event_channel_tasks) - cluster_match_results = await asyncio.gather(*cluster_match_tasks) + ) + for cluster_match in cluster_match_results: if cluster_match is not None: cluster_matches.append(cluster_match) return cluster_matches -async def _handle_channel_only_cluster_match( +@callback +def _async_handle_channel_only_cluster_match( zha_device, cluster, is_new_join): """Handle a channel only cluster match.""" - await _create_cluster_channel(cluster, zha_device, is_new_join) + _async_create_cluster_channel(cluster, zha_device, is_new_join) -async def _handle_single_cluster_match(hass, zha_device, cluster, device_key, +@callback +def _async_handle_single_cluster_match(hass, zha_device, cluster, device_key, device_classes, is_new_join): """Dispatch a single cluster match to a HA component.""" component = None # sub_component = None @@ -392,7 +396,7 @@ async def _handle_single_cluster_match(hass, zha_device, cluster, device_key, if component is None or component not in COMPONENTS: return channels = [] - await _create_cluster_channel(cluster, zha_device, is_new_join, + _async_create_cluster_channel(cluster, zha_device, is_new_join, channels=channels) cluster_key = "{}-{}".format(device_key, cluster.cluster_id) @@ -416,7 +420,8 @@ async def _handle_single_cluster_match(hass, zha_device, cluster, device_key, return discovery_info -def _create_device_entity(zha_device): +@callback +def _async_create_device_entity(zha_device): """Create ZHADeviceEntity.""" device_entity_channels = [] if POWER_CONFIGURATION_CHANNEL in zha_device.cluster_channels: diff --git a/homeassistant/components/zha/light.py b/homeassistant/components/zha/light.py index efa6f679ae8fa..740d67db1bd28 100644 --- a/homeassistant/components/zha/light.py +++ b/homeassistant/components/zha/light.py @@ -20,7 +20,7 @@ DEPENDENCIES = ['zha'] -DEFAULT_DURATION = 0.5 +DEFAULT_DURATION = 5 CAPABILITIES_COLOR_XY = 0x08 CAPABILITIES_COLOR_TEMP = 0x10 @@ -110,8 +110,13 @@ def device_state_attributes(self): return self.state_attributes def set_level(self, value): - """Set the brightness of this light between 0..255.""" - value = max(0, min(255, value)) + """Set the brightness of this light between 0..254. + + brightness level 255 is a special value instructing the device to come + on at `on_level` Zigbee attribute value, regardless of the last set + level + """ + value = max(0, min(254, value)) self._brightness = value self.async_schedule_update_ha_state() @@ -146,8 +151,31 @@ async def async_added_to_hass(self): async def async_turn_on(self, **kwargs): """Turn the entity on.""" - duration = kwargs.get(light.ATTR_TRANSITION, DEFAULT_DURATION) - duration = duration * 10 # tenths of s + transition = kwargs.get(light.ATTR_TRANSITION) + duration = transition * 10 if transition else DEFAULT_DURATION + brightness = kwargs.get(light.ATTR_BRIGHTNESS) + + if (brightness is not None or transition) and \ + self._supported_features & light.SUPPORT_BRIGHTNESS: + if brightness is not None: + level = min(254, brightness) + else: + level = self._brightness or 254 + success = await self._level_channel.move_to_level_with_on_off( + level, + duration + ) + if not success: + return + self._state = bool(level) + if level: + self._brightness = level + + if brightness is None or brightness: + success = await self._on_off_channel.on() + if not success: + return + self._state = True if light.ATTR_COLOR_TEMP in kwargs and \ self.supported_features & light.SUPPORT_COLOR_TEMP: @@ -171,32 +199,12 @@ async def async_turn_on(self, **kwargs): return self._hs_color = hs_color - if self._brightness is not None: - brightness = kwargs.get( - light.ATTR_BRIGHTNESS, self._brightness or 255) - success = await self._level_channel.move_to_level_with_on_off( - brightness, - duration - ) - if not success: - return - self._state = True - self._brightness = brightness - self.async_schedule_update_ha_state() - return - - success = await self._on_off_channel.on() - if not success: - return - - self._state = True self.async_schedule_update_ha_state() async def async_turn_off(self, **kwargs): """Turn the entity off.""" duration = kwargs.get(light.ATTR_TRANSITION) supports_level = self.supported_features & light.SUPPORT_BRIGHTNESS - success = None if duration and supports_level: success = await self._level_channel.move_to_level_with_on_off( 0, diff --git a/homeassistant/components/zwave/.translations/es-419.json b/homeassistant/components/zwave/.translations/es-419.json new file mode 100644 index 0000000000000..2e246fb9931a7 --- /dev/null +++ b/homeassistant/components/zwave/.translations/es-419.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Z-Wave ya est\u00e1 configurado", + "one_instance_only": "El componente solo admite una instancia de Z-Wave" + }, + "step": { + "user": { + "data": { + "network_key": "Clave de red (dejar en blanco para auto-generar)", + "usb_path": "Ruta USB" + }, + "description": "Consulte https://www.home-assistant.io/docs/z-wave/installation/ para obtener informaci\u00f3n sobre las variables de configuraci\u00f3n", + "title": "Configurar Z-Wave" + } + }, + "title": "Z-Wave" + } +} \ No newline at end of file diff --git a/homeassistant/components/zwave/.translations/hu.json b/homeassistant/components/zwave/.translations/hu.json index e326c5152a617..2842c535984a0 100644 --- a/homeassistant/components/zwave/.translations/hu.json +++ b/homeassistant/components/zwave/.translations/hu.json @@ -1,7 +1,11 @@ { "config": { "abort": { - "already_configured": "A Z-Wave m\u00e1r konfigur\u00e1lva van" + "already_configured": "A Z-Wave m\u00e1r konfigur\u00e1lva van", + "one_instance_only": "Az \u00f6sszetev\u0151 csak egy Z-Wave p\u00e9ld\u00e1nyt t\u00e1mogat" + }, + "error": { + "option_error": "A Z-Wave \u00e9rv\u00e9nyes\u00edt\u00e9s sikertelen. Az USB-meghajt\u00f3 el\u00e9r\u00e9si \u00fatj\u00e1t helyesen adtad meg?" }, "step": { "user": { diff --git a/homeassistant/components/zwave/.translations/it.json b/homeassistant/components/zwave/.translations/it.json index 86a6130781499..c380d8e5625eb 100644 --- a/homeassistant/components/zwave/.translations/it.json +++ b/homeassistant/components/zwave/.translations/it.json @@ -1,5 +1,12 @@ { "config": { + "abort": { + "already_configured": "Z-Wave \u00e8 gi\u00e0 configurato", + "one_instance_only": "Il componente supporta solo un'istanza di Z-Wave" + }, + "error": { + "option_error": "Convalida Z-Wave fallita. Il percorso della chiavetta USB \u00e8 corretto?" + }, "step": { "user": { "data": { @@ -7,7 +14,7 @@ "usb_path": "Percorso USB" }, "description": "Vai su https://www.home-assistant.io/docs/z-wave/installation/ per le informazioni sulle variabili di configurazione", - "title": "Imposta Z-Wave" + "title": "Configura Z-Wave" } }, "title": "Z-Wave" diff --git a/homeassistant/components/zwave/climate.py b/homeassistant/components/zwave/climate.py index bf7b64549acec..b0ab273e86af3 100644 --- a/homeassistant/components/zwave/climate.py +++ b/homeassistant/components/zwave/climate.py @@ -2,8 +2,9 @@ # Because we do not compile openzwave on CI import logging from homeassistant.core import callback -from homeassistant.components.climate import ( - DOMAIN, ClimateDevice, STATE_AUTO, STATE_COOL, STATE_HEAT, +from homeassistant.components.climate import ClimateDevice +from homeassistant.components.climate.const import ( + DOMAIN, STATE_AUTO, STATE_COOL, STATE_HEAT, SUPPORT_TARGET_TEMPERATURE, SUPPORT_FAN_MODE, SUPPORT_OPERATION_MODE, SUPPORT_SWING_MODE) from homeassistant.components.zwave import ZWaveDeviceEntity diff --git a/homeassistant/config.py b/homeassistant/config.py index 3310cd3e160d9..492db240eeecd 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -429,7 +429,7 @@ def _format_config_error(ex: vol.Invalid, domain: str, config: Dict) -> str: async def async_process_ha_core_config( hass: HomeAssistant, config: Dict, has_api_password: bool = False, - has_trusted_networks: bool = False) -> None: + trusted_networks: Optional[Any] = None) -> None: """Process the [homeassistant] section from the configuration. This method is a coroutine. @@ -446,8 +446,11 @@ async def async_process_ha_core_config( ] if has_api_password: auth_conf.append({'type': 'legacy_api_password'}) - if has_trusted_networks: - auth_conf.append({'type': 'trusted_networks'}) + if trusted_networks: + auth_conf.append({ + 'type': 'trusted_networks', + 'trusted_networks': trusted_networks, + }) mfa_conf = config.get(CONF_AUTH_MFA_MODULES, [ {'type': 'totp', 'id': 'totp', 'name': 'Authenticator app'}, diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index c7dfc0c889b9e..7b22c2e197c04 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -7,7 +7,11 @@ During startup, Home Assistant will setup the entries during the normal setup of a component. It will first call the normal setup and then call the method `async_setup_entry(hass, entry)` for each entry. The same method is called when -Home Assistant is running while a config entry is created. +Home Assistant is running while a config entry is created. If the version of +the config entry does not match that of the flow handler, setup will +call the method `async_migrate_entry(hass, entry)` with the expectation that +the entry be brought to the current version. Return `True` to indicate +migration was successful, otherwise `False`. ## Config Flows @@ -116,8 +120,10 @@ async def async_step_discovery(info): the flow from the config panel. """ import logging +import functools import uuid -from typing import Set, Optional, List, Dict # noqa pylint: disable=unused-import +from typing import Callable, Dict, List, Optional, Set # noqa pylint: disable=unused-import +import weakref from homeassistant import data_entry_flow from homeassistant.core import callback, HomeAssistant @@ -125,7 +131,6 @@ async def async_step_discovery(info): from homeassistant.setup import async_setup_component, async_process_deps_reqs from homeassistant.util.decorator import Registry - _LOGGER = logging.getLogger(__name__) _UNDEF = object() @@ -160,19 +165,22 @@ async def async_step_discovery(info): 'openuv', 'owntracks', 'point', + 'ps4', 'rainmachine', 'simplisafe', 'smartthings', 'smhi', 'sonos', 'tellduslive', + 'toon', + 'tplink', 'tradfri', 'twilio', 'unifi', 'upnp', 'zha', 'zone', - 'zwave' + 'zwave', ] @@ -188,6 +196,8 @@ async def async_step_discovery(info): ENTRY_STATE_LOADED = 'loaded' # There was an error while trying to set up this config entry ENTRY_STATE_SETUP_ERROR = 'setup_error' +# There was an error while trying to migrate the config entry to a new version +ENTRY_STATE_MIGRATION_ERROR = 'migration_error' # The config entry was not ready to be set up yet, but might be later ENTRY_STATE_SETUP_RETRY = 'setup_retry' # The config entry has not been loaded @@ -214,12 +224,13 @@ async def async_step_discovery(info): class ConfigEntry: """Hold a configuration entry.""" - __slots__ = ('entry_id', 'version', 'domain', 'title', 'data', 'source', - 'connection_class', 'state', '_setup_lock', - '_async_cancel_retry_setup') + __slots__ = ('entry_id', 'version', 'domain', 'title', 'data', 'options', + 'source', 'connection_class', 'state', '_setup_lock', + 'update_listeners', '_async_cancel_retry_setup') def __init__(self, version: str, domain: str, title: str, data: dict, source: str, connection_class: str, + options: Optional[dict] = None, entry_id: Optional[str] = None, state: str = ENTRY_STATE_NOT_LOADED) -> None: """Initialize a config entry.""" @@ -238,6 +249,9 @@ def __init__(self, version: str, domain: str, title: str, data: dict, # Config data self.data = data + # Entry options + self.options = options or {} + # Source of the configuration (user, discovery, cloud) self.source = source @@ -247,6 +261,9 @@ def __init__(self, version: str, domain: str, title: str, data: dict, # State of the entry (LOADED, NOT_LOADED) self.state = state + # Listeners to call on update + self.update_listeners = [] # type: list + # Function to cancel a scheduled retry self._async_cancel_retry_setup = None @@ -256,6 +273,12 @@ async def async_setup( if component is None: component = getattr(hass.components, self.domain) + # Perform migration + if component.DOMAIN == self.domain: + if not await self.async_migrate(hass): + self.state = ENTRY_STATE_MIGRATION_ERROR + return + try: result = await component.async_setup_entry(hass, self) @@ -332,6 +355,57 @@ async def async_unload(self, hass, *, component=None): self.state = ENTRY_STATE_FAILED_UNLOAD return False + async def async_migrate(self, hass: HomeAssistant) -> bool: + """Migrate an entry. + + Returns True if config entry is up-to-date or has been migrated. + """ + handler = HANDLERS.get(self.domain) + if handler is None: + _LOGGER.error("Flow handler not found for entry %s for %s", + self.title, self.domain) + return False + # Handler may be a partial + while isinstance(handler, functools.partial): + handler = handler.func + + if self.version == handler.VERSION: + return True + + component = getattr(hass.components, self.domain) + supports_migrate = hasattr(component, 'async_migrate_entry') + if not supports_migrate: + _LOGGER.error("Migration handler not found for entry %s for %s", + self.title, self.domain) + return False + + try: + result = await component.async_migrate_entry(hass, self) + if not isinstance(result, bool): + _LOGGER.error('%s.async_migrate_entry did not return boolean', + self.domain) + return False + if result: + # pylint: disable=protected-access + hass.config_entries._async_schedule_save() # type: ignore + return result + except Exception: # pylint: disable=broad-except + _LOGGER.exception('Error migrating entry %s for %s', + self.title, component.DOMAIN) + return False + + def add_update_listener(self, listener: Callable) -> Callable: + """Listen for when entry is updated. + + Listener: Callback function(hass, entry) + + Returns function to unlisten. + """ + weak_listener = weakref.ref(listener) + self.update_listeners.append(weak_listener) + + return lambda: self.update_listeners.remove(weak_listener) + def as_dict(self): """Return dictionary version of this entry.""" return { @@ -340,6 +414,7 @@ def as_dict(self): 'domain': self.domain, 'title': self.title, 'data': self.data, + 'options': self.options, 'source': self.source, 'connection_class': self.connection_class, } @@ -364,6 +439,7 @@ def __init__(self, hass: HomeAssistant, hass_config: dict) -> None: self.hass = hass self.flow = data_entry_flow.FlowManager( hass, self._async_create_flow, self._async_finish_flow) + self.options = OptionsFlowManager(hass) self._hass_config = hass_config self._entries = [] # type: List[ConfigEntry] self._store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY) @@ -381,6 +457,14 @@ def async_domains(self) -> List[str]: return result + @callback + def async_get_entry(self, entry_id: str) -> Optional[ConfigEntry]: + """Return entry with matching entry_id.""" + for entry in self._entries: + if entry_id == entry.entry_id: + return entry + return None + @callback def async_entries(self, domain: Optional[str] = None) -> List[ConfigEntry]: """Return all entries or entries for a specific domain.""" @@ -438,14 +522,25 @@ async def async_load(self) -> None: title=entry['title'], # New in 0.79 connection_class=entry.get('connection_class', - CONN_CLASS_UNKNOWN)) + CONN_CLASS_UNKNOWN), + # New in 0.89 + options=entry.get('options')) for entry in config['entries']] @callback - def async_update_entry(self, entry, *, data=_UNDEF): + def async_update_entry(self, entry, *, data=_UNDEF, options=_UNDEF): """Update a config entry.""" if data is not _UNDEF: entry.data = data + + if options is not _UNDEF: + entry.options = options + + if data is not _UNDEF or options is not _UNDEF: + for listener_ref in entry.update_listeners: + listener = listener_ref() + self.hass.async_create_task(listener(self.hass, entry)) + self._async_schedule_save() async def async_forward_entry_setup(self, entry, component): @@ -495,6 +590,7 @@ async def _async_finish_flow(self, flow, result): domain=result['handler'], title=result['title'], data=result['data'], + options={}, source=flow.context['source'], connection_class=flow.CONNECTION_CLASS, ) @@ -544,7 +640,7 @@ async def _async_create_flow(self, handler_key, *, context, data): flow.init_step = source return flow - def _async_schedule_save(self): + def _async_schedule_save(self) -> None: """Save the entity registry to a file.""" self._store.async_delay_save(self._data_to_save, SAVE_DELAY) @@ -577,3 +673,39 @@ def _async_in_progress(self): return [flw for flw in self.hass.config_entries.flow.async_progress() if flw['handler'] == self.handler and flw['flow_id'] != self.flow_id] + + +class OptionsFlowManager: + """Flow to set options for a configuration entry.""" + + def __init__(self, hass: HomeAssistant) -> None: + """Initialize the options manager.""" + self.hass = hass + self.flow = data_entry_flow.FlowManager( + hass, self._async_create_flow, self._async_finish_flow) + + async def _async_create_flow(self, entry_id, *, context, data): + """Create an options flow for a config entry. + + Entry_id and flow.handler is the same thing to map entry with flow. + """ + entry = self.hass.config_entries.async_get_entry(entry_id) + if entry is None: + return + flow = HANDLERS[entry.domain].async_get_options_flow( + entry.data, entry.options) + return flow + + async def _async_finish_flow(self, flow, result): + """Finish an options flow and update options for configuration entry. + + Flow.handler and entry_id is the same thing to map flow with entry. + """ + entry = self.hass.config_entries.async_get_entry(flow.handler) + if entry is None: + return + self.hass.config_entries.async_update_entry( + entry, options=result['data']) + + result['result'] = True + return result diff --git a/homeassistant/const.py b/homeassistant/const.py index e2329a2de26a2..5b943ddb3cfe1 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,8 +1,8 @@ # coding: utf-8 """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 -MINOR_VERSION = 88 -PATCH_VERSION = '2' +MINOR_VERSION = 89 +PATCH_VERSION = '0' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 5, 3) diff --git a/homeassistant/core.py b/homeassistant/core.py index e7f654f518486..48ef4f462729f 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -1120,7 +1120,7 @@ async def async_call(self, domain: str, service: str, ATTR_DOMAIN: domain.lower(), ATTR_SERVICE: service.lower(), ATTR_SERVICE_DATA: service_data, - }) + }, context=context) if not blocking: self._hass.async_create_task( diff --git a/homeassistant/helpers/area_registry.py b/homeassistant/helpers/area_registry.py index bc8c05ed0a67b..3fa820f835074 100644 --- a/homeassistant/helpers/area_registry.py +++ b/homeassistant/helpers/area_registry.py @@ -75,8 +75,8 @@ def async_update(self, area_id: str, name: str) -> AreaEntry: if self._async_is_registered(name): raise ValueError('Name is already in use') - else: - changes['name'] = name + + changes['name'] = name new = self.areas[area_id] = attr.evolve(old, **changes) self.async_schedule_save() diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index b5716431217f7..4bba80aa15436 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -293,7 +293,7 @@ def time_period_str(value: str) -> timedelta: """Validate and transform time offset.""" if isinstance(value, int): raise vol.Invalid('Make sure you wrap time values in quotes') - elif not isinstance(value, str): + if not isinstance(value, str): raise vol.Invalid(TIME_PERIOD_ERROR.format(value)) negative_offset = False @@ -440,7 +440,7 @@ def template(value): """Validate a jinja2 template.""" if value is None: raise vol.Invalid('template value is None') - elif isinstance(value, (list, dict, template_helper.Template)): + if isinstance(value, (list, dict, template_helper.Template)): raise vol.Invalid('template value should be a string') value = template_helper.Template(str(value)) diff --git a/homeassistant/helpers/device_registry.py b/homeassistant/helpers/device_registry.py index 83827cca2359f..21c3b0d0209d0 100644 --- a/homeassistant/helpers/device_registry.py +++ b/homeassistant/helpers/device_registry.py @@ -37,6 +37,7 @@ class DeviceEntry: sw_version = attr.ib(type=str, default=None) hub_device_id = attr.ib(type=str, default=None) area_id = attr.ib(type=str, default=None) + name_by_user = attr.ib(type=str, default=None) id = attr.ib(type=str, default=attr.Factory(lambda: uuid.uuid4().hex)) @@ -124,9 +125,11 @@ def async_get_or_create(self, *, config_entry_id, connections=None, ) @callback - def async_update_device(self, device_id, *, area_id=_UNDEF): + def async_update_device( + self, device_id, *, area_id=_UNDEF, name_by_user=_UNDEF): """Update properties of a device.""" - return self._async_update_device(device_id, area_id=area_id) + return self._async_update_device( + device_id, area_id=area_id, name_by_user=name_by_user) @callback def _async_update_device(self, device_id, *, add_config_entry_id=_UNDEF, @@ -138,7 +141,8 @@ def _async_update_device(self, device_id, *, add_config_entry_id=_UNDEF, name=_UNDEF, sw_version=_UNDEF, hub_device_id=_UNDEF, - area_id=_UNDEF): + area_id=_UNDEF, + name_by_user=_UNDEF): """Update device attributes.""" old = self.devices[device_id] @@ -179,6 +183,10 @@ def _async_update_device(self, device_id, *, add_config_entry_id=_UNDEF, if (area_id is not _UNDEF and area_id != old.area_id): changes['area_id'] = area_id + if (name_by_user is not _UNDEF and + name_by_user != old.name_by_user): + changes['name_by_user'] = name_by_user + if not changes: return old @@ -208,7 +216,8 @@ async def async_load(self): # Introduced in 0.79 hub_device_id=device.get('hub_device_id'), # Introduced in 0.87 - area_id=device.get('area_id') + area_id=device.get('area_id'), + name_by_user=device.get('name_by_user') ) self.devices = devices @@ -234,7 +243,8 @@ def _data_to_save(self): 'sw_version': entry.sw_version, 'id': entry.id, 'hub_device_id': entry.hub_device_id, - 'area_id': entry.area_id + 'area_id': entry.area_id, + 'name_by_user': entry.name_by_user } for entry in self.devices.values() ] diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index c13ebe7cfab1e..dd9677f651556 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -28,11 +28,10 @@ def generate_entity_id(entity_id_format: str, name: Optional[str], if current_ids is None: if hass is None: raise ValueError("Missing required parameter currentids or hass") - else: - return run_callback_threadsafe( - hass.loop, async_generate_entity_id, entity_id_format, name, - current_ids, hass - ).result() + return run_callback_threadsafe( + hass.loop, async_generate_entity_id, entity_id_format, name, + current_ids, hass + ).result() name = (slugify(name) or slugify(DEVICE_DEFAULT_NAME)).lower() diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py index 9c76d244138c8..87cc4d4fd90ed 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -334,9 +334,9 @@ async def _async_add_entity(self, entity, update_before_add, if not valid_entity_id(entity.entity_id): raise HomeAssistantError( 'Invalid entity id: {}'.format(entity.entity_id)) - elif (entity.entity_id in self.entities or - entity.entity_id in self.hass.states.async_entity_ids( - self.domain)): + if (entity.entity_id in self.entities or + entity.entity_id in self.hass.states.async_entity_ids( + self.domain)): msg = 'Entity id already exists: {}'.format(entity.entity_id) if entity.unique_id is not None: msg += '. Platform {} does not generate unique IDs'.format( diff --git a/homeassistant/helpers/event.py b/homeassistant/helpers/event.py index c1dae00bed58e..5e262a47565c6 100644 --- a/homeassistant/helpers/event.py +++ b/homeassistant/helpers/event.py @@ -370,7 +370,7 @@ def pattern_time_change_listener(event): last_now = now if next_time <= now: - hass.async_run_job(action, event.data[ATTR_NOW]) + hass.async_run_job(action, dt_util.as_local(now) if local else now) calculate_next(now + timedelta(seconds=1)) # We can't use async_track_point_in_utc_time here because it would diff --git a/homeassistant/helpers/service.py b/homeassistant/helpers/service.py index d2211d031f55a..22138d7c2aa91 100644 --- a/homeassistant/helpers/service.py +++ b/homeassistant/helpers/service.py @@ -272,7 +272,10 @@ async def entity_service_call(hass, platforms, func, call, service_name=''): ] if tasks: - await asyncio.wait(tasks) + done, pending = await asyncio.wait(tasks) + assert not pending + for future in done: + future.result() # pop exception if have async def _handle_service_platform_call(func, data, entities, context): @@ -294,4 +297,7 @@ async def _handle_service_platform_call(func, data, entities, context): tasks.append(entity.async_update_ha_state(True)) if tasks: - await asyncio.wait(tasks) + done, pending = await asyncio.wait(tasks) + assert not pending + for future in done: + future.result() # pop exception if have diff --git a/homeassistant/helpers/state.py b/homeassistant/helpers/state.py index 7d69defed480e..bbed1ffbbcd90 100644 --- a/homeassistant/helpers/state.py +++ b/homeassistant/helpers/state.py @@ -218,7 +218,7 @@ def state_as_number(state: State) -> float: Raises ValueError if this is not possible. """ - from homeassistant.components.climate import ( + from homeassistant.components.climate.const import ( STATE_HEAT, STATE_COOL, STATE_IDLE) if state.state in (STATE_ON, STATE_LOCKED, STATE_ABOVE_HORIZON, diff --git a/homeassistant/loader.py b/homeassistant/loader.py index 962b168aa9765..e36ad5451c18a 100644 --- a/homeassistant/loader.py +++ b/homeassistant/loader.py @@ -15,7 +15,7 @@ import logging import sys from types import ModuleType -from typing import Optional, Set, TYPE_CHECKING, Callable, Any, TypeVar # noqa pylint: disable=unused-import +from typing import Optional, Set, TYPE_CHECKING, Callable, Any, TypeVar, List # noqa pylint: disable=unused-import from homeassistant.const import PLATFORM_FORMAT @@ -34,8 +34,9 @@ DATA_KEY = 'components' -PATH_CUSTOM_COMPONENTS = 'custom_components' -PACKAGE_COMPONENTS = 'homeassistant.components' +PACKAGE_CUSTOM_COMPONENTS = 'custom_components' +PACKAGE_BUILTIN = 'homeassistant.components' +LOOKUP_PATHS = [PACKAGE_CUSTOM_COMPONENTS, PACKAGE_BUILTIN] class LoaderError(Exception): @@ -76,23 +77,43 @@ def get_platform(hass, # type: HomeAssistant domain: str, platform_name: str) -> Optional[ModuleType]: """Try to load specified platform. + Example invocation: get_platform(hass, 'light', 'hue') + Async friendly. """ - platform = _load_file(hass, PLATFORM_FORMAT.format( - domain=domain, platform=platform_name)) + # If the platform has a component, we will limit the platform loading path + # to be the same source (custom/built-in). + component = _load_file(hass, platform_name, LOOKUP_PATHS) + + # Until we have moved all platforms under their component/own folder, it + # can be that the component is None. + if component is not None: + base_paths = [component.__name__.rsplit('.', 1)[0]] + else: + base_paths = LOOKUP_PATHS + + platform = _load_file( + hass, PLATFORM_FORMAT.format(domain=domain, platform=platform_name), + base_paths) if platform is not None: return platform # Legacy platform check: light/hue.py - platform = _load_file(hass, PLATFORM_FORMAT.format( - domain=platform_name, platform=domain)) + platform = _load_file( + hass, PLATFORM_FORMAT.format(domain=platform_name, platform=domain), + base_paths) if platform is None: - _LOGGER.error("Unable to find platform %s", platform_name) + if component is None: + extra = "" + else: + extra = " Search path was limited to path of component: {}".format( + base_paths[0]) + _LOGGER.error("Unable to find platform %s.%s", platform_name, extra) return None - if platform.__name__.startswith(PATH_CUSTOM_COMPONENTS): + if platform.__name__.startswith(PACKAGE_CUSTOM_COMPONENTS): _LOGGER.warning( "Integrations need to be in their own folder. Change %s/%s.py to " "%s/%s.py. This will stop working soon.", @@ -107,7 +128,7 @@ def get_component(hass, # type: HomeAssistant Async friendly. """ - comp = _load_file(hass, comp_or_platform) + comp = _load_file(hass, comp_or_platform, LOOKUP_PATHS) if comp is None: _LOGGER.error("Unable to find component %s", comp_or_platform) @@ -116,7 +137,8 @@ def get_component(hass, # type: HomeAssistant def _load_file(hass, # type: HomeAssistant - comp_or_platform: str) -> Optional[ModuleType]: + comp_or_platform: str, + base_paths: List[str]) -> Optional[ModuleType]: """Try to load specified file. Looks in config dir first, then built-in components. @@ -138,11 +160,8 @@ def _load_file(hass, # type: HomeAssistant sys.path.insert(0, hass.config.config_dir) cache = hass.data[DATA_KEY] = {} - # First check custom, then built-in - potential_paths = ['custom_components.{}'.format(comp_or_platform), - 'homeassistant.components.{}'.format(comp_or_platform)] - - for index, path in enumerate(potential_paths): + for path in ('{}.{}'.format(base, comp_or_platform) + for base in base_paths): try: module = importlib.import_module(path) @@ -162,7 +181,7 @@ def _load_file(hass, # type: HomeAssistant cache[comp_or_platform] = module - if index == 0: + if module.__name__.startswith(PACKAGE_CUSTOM_COMPONENTS): _LOGGER.warning( 'You are using a custom component for %s which has not ' 'been tested by Home Assistant. This component might ' diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 838c3f31bc56e..fa1fe5a959df0 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -1,8 +1,8 @@ aiohttp==3.5.4 -astral==1.9.2 +astral==1.10.1 async_timeout==3.0.1 attrs==18.2.0 -bcrypt==3.1.5 +bcrypt==3.1.6 certifi>=2018.04.16 jinja2>=2.10 PyJWT==1.6.4 @@ -14,7 +14,7 @@ pyyaml>=3.13,<4 requests==2.21.0 ruamel.yaml==0.15.88 voluptuous==0.11.5 -voluptuous-serialize==2.0.0 +voluptuous-serialize==2.1.0 pycryptodome>=3.6.6 diff --git a/homeassistant/scripts/__init__.py b/homeassistant/scripts/__init__.py index 02cc0bff36224..3050379a496c7 100644 --- a/homeassistant/scripts/__init__.py +++ b/homeassistant/scripts/__init__.py @@ -9,7 +9,8 @@ from homeassistant.bootstrap import async_mount_local_lib_path from homeassistant.config import get_default_config_dir -from homeassistant import requirements +from homeassistant.core import HomeAssistant +from homeassistant.requirements import pip_kwargs, PackageLoadable from homeassistant.util.package import install_package, is_virtual_env @@ -39,16 +40,25 @@ def run(args: List) -> int: config_dir = extract_config_dir() + loop = asyncio.get_event_loop() + if not is_virtual_env(): - asyncio.get_event_loop().run_until_complete( - async_mount_local_lib_path(config_dir)) + loop.run_until_complete(async_mount_local_lib_path(config_dir)) - pip_kwargs = requirements.pip_kwargs(config_dir) + _pip_kwargs = pip_kwargs(config_dir) logging.basicConfig(stream=sys.stdout, level=logging.INFO) + hass = HomeAssistant(loop) + pkgload = PackageLoadable(hass) for req in getattr(script, 'REQUIREMENTS', []): - returncode = install_package(req, **pip_kwargs) + try: + loop.run_until_complete(pkgload.loadable(req)) + continue + except ImportError: + pass + + returncode = install_package(req, **_pip_kwargs) if not returncode: print('Aborting script, could not install dependency', req) diff --git a/homeassistant/scripts/check_config.py b/homeassistant/scripts/check_config.py index 67bc97da9924d..1b8c67193950f 100644 --- a/homeassistant/scripts/check_config.py +++ b/homeassistant/scripts/check_config.py @@ -5,7 +5,6 @@ import os from collections import OrderedDict, namedtuple from glob import glob -from platform import system from typing import Dict, List, Sequence from unittest.mock import patch @@ -22,8 +21,6 @@ from homeassistant.exceptions import HomeAssistantError REQUIREMENTS = ('colorlog==4.0.2',) -if system() == 'Windows': # Ensure colorama installed for colorlog on Windows - REQUIREMENTS += ('colorama<=1',) _LOGGER = logging.getLogger(__name__) # pylint: disable=protected-access diff --git a/requirements_all.txt b/requirements_all.txt index 629dca5cccf5a..72185c594cd5c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1,9 +1,9 @@ # Home Assistant core aiohttp==3.5.4 -astral==1.9.2 +astral==1.10.1 async_timeout==3.0.1 attrs==18.2.0 -bcrypt==3.1.5 +bcrypt==3.1.6 certifi>=2018.04.16 jinja2>=2.10 PyJWT==1.6.4 @@ -15,7 +15,7 @@ pyyaml>=3.13,<4 requests==2.21.0 ruamel.yaml==0.15.88 voluptuous==0.11.5 -voluptuous-serialize==2.0.0 +voluptuous-serialize==2.1.0 # homeassistant.components.nuimo_controller --only-binary=all nuimo==0.1.0 @@ -50,6 +50,10 @@ PyMVGLive==1.1.4 # homeassistant.components.arduino PyMata==2.14 +# homeassistant.components.mobile_app +# homeassistant.components.owntracks +PyNaCl==1.3.0 + # homeassistant.auth.mfa_modules.totp PyQRCode==1.2.1 @@ -57,13 +61,13 @@ PyQRCode==1.2.1 PyRMVtransport==0.1.3 # homeassistant.components.switch.switchbot -PySwitchbot==0.5 +# PySwitchbot==0.5 # homeassistant.components.sensor.transport_nsw PyTransportNSW==0.1.1 # homeassistant.components.xiaomi_aqara -PyXiaomiGateway==0.11.2 +PyXiaomiGateway==0.12.0 # homeassistant.components.rpi_gpio # RPi.GPIO==0.6.5 @@ -78,7 +82,7 @@ TravisPy==0.3.5 TwitterAPI==2.5.9 # homeassistant.components.sensor.waze_travel_time -WazeRouteCalculator==0.6 +WazeRouteCalculator==0.9 # homeassistant.components.notify.yessssms YesssSMS==0.2.3 @@ -90,7 +94,7 @@ abodepy==0.15.0 afsapi==0.0.4 # homeassistant.components.ambient_station -aioambient==0.1.2 +aioambient==0.1.3 # homeassistant.components.asuswrt aioasuswrt==1.1.20 @@ -102,7 +106,7 @@ aioautomatic==0.6.5 aiodns==1.1.1 # homeassistant.components.esphome -aioesphomeapi==1.5.0 +aioesphomeapi==1.6.0 # homeassistant.components.freebox aiofreepybox==0.0.6 @@ -118,7 +122,7 @@ aioharmony==0.1.8 aiohttp_cors==0.7.0 # homeassistant.components.hue -aiohue==1.9.0 +aiohue==1.9.1 # homeassistant.components.sensor.iliad_italy aioiliad==0.1.1 @@ -154,7 +158,7 @@ amcrest==1.2.3 anel_pwrctrl-homeassistant==0.0.1.dev2 # homeassistant.components.media_player.anthemav -anthemav==1.1.8 +anthemav==1.1.9 # homeassistant.components.apcupsd apcaccess==0.0.13 @@ -196,7 +200,7 @@ batinfo==0.4.2 beautifulsoup4==4.7.1 # homeassistant.components.zha -bellows==0.7.0 +bellows-homeassistant==0.7.1 # homeassistant.components.bmw_connected_drive bimmer_connected==0.5.3 @@ -292,7 +296,7 @@ construct==2.9.45 # credstash==1.15.0 # homeassistant.components.sensor.crimereports -crimereports==1.0.0 +crimereports==1.0.1 # homeassistant.components.datadog datadog==0.15.0 @@ -420,7 +424,7 @@ fiblary3==0.1.7 fints==1.0.1 # homeassistant.components.media_player.firetv -firetv==1.0.7 +firetv==1.0.9 # homeassistant.components.sensor.fitbit fitbit==0.3.0 @@ -535,7 +539,7 @@ hole==0.3.0 holidays==0.9.9 # homeassistant.components.frontend -home-assistant-frontend==20190220.0 +home-assistant-frontend==20190305.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.2 @@ -568,7 +572,7 @@ ibmiotf==0.3.4 iglo==1.2.7 # homeassistant.components.ihc -ihcsdk==2.2.0 +ihcsdk==2.3.0 # homeassistant.components.influxdb # homeassistant.components.sensor.influxdb @@ -577,7 +581,7 @@ influxdb==5.2.0 # homeassistant.components.insteon insteonplm==0.15.2 -# homeassistant.components.sensor.iperf3 +# homeassistant.components.iperf3 iperf3==0.1.10 # homeassistant.components.route53 @@ -606,10 +610,7 @@ kiwiki-client==0.1.1 konnected==0.1.4 # homeassistant.components.eufy -lakeside==0.11 - -# homeassistant.components.owntracks -libnacl==1.6.1 +lakeside==0.12 # homeassistant.components.dyson libpurecoollink==0.4.2 @@ -681,8 +682,8 @@ mbddns==0.1.2 # homeassistant.components.notify.message_bird messagebird==1.2.0 -# homeassistant.components.sensor.meteo_france -meteofrance==0.2.7 +# homeassistant.components.meteo_france +meteofrance==0.3.4 # homeassistant.components.sensor.mfi # homeassistant.components.switch.mfi @@ -722,7 +723,7 @@ nanoleaf==0.4.1 ndms2_client==0.0.6 # homeassistant.components.ness_alarm -nessclient==0.9.9 +nessclient==0.9.13 # homeassistant.components.sensor.netdata netdata==0.1.2 @@ -752,7 +753,7 @@ nuheat==0.3.0 # homeassistant.components.image_processing.opencv # homeassistant.components.image_processing.tensorflow # homeassistant.components.sensor.pollen -numpy==1.16.0 +numpy==1.16.1 # homeassistant.components.google oauth2client==4.0.0 @@ -773,7 +774,10 @@ openevsewifi==0.4 openhomedevice==0.4.2 # homeassistant.components.air_quality.opensensemap -opensensemap-api==0.1.3 +opensensemap-api==0.1.4 + +# homeassistant.components.device_tracker.luci +openwrt-luci-rpc==1.0.5 # homeassistant.components.switch.orvibo orvibo==1.1.1 @@ -837,6 +841,9 @@ pocketcasts==0.1 # homeassistant.components.sensor.postnl postnl_api==1.0.2 +# homeassistant.components.reddit.sensor +praw==6.1.1 + # homeassistant.components.sensor.islamic_prayer_times prayer_times_calculator==0.0.3 @@ -853,7 +860,7 @@ prometheus_client==0.2.0 protobuf==3.6.1 # homeassistant.components.sensor.systemmonitor -psutil==5.5.0 +psutil==5.5.1 # homeassistant.components.wink pubnubsub-handler==1.0.3 @@ -889,22 +896,21 @@ py17track==2.1.1 # homeassistant.components.hdmi_cec pyCEC==0.4.13 -# homeassistant.components.light.tplink -# homeassistant.components.switch.tplink +# homeassistant.components.tplink pyHS100==0.3.4 # homeassistant.components.air_quality.norway_air # homeassistant.components.weather.met -pyMetno==0.4.5 +pyMetno==0.4.6 # homeassistant.components.rfxtrx pyRFXtrx==0.23 # homeassistant.components.switch.switchmate -pySwitchmate==0.4.5 +# pySwitchmate==0.4.5 # homeassistant.components.tibber -pyTibber==0.9.4 +pyTibber==0.9.6 # homeassistant.components.switch.dlink pyW215==0.6.0 @@ -922,7 +928,7 @@ pyads==3.0.7 pyaftership==0.1.2 # homeassistant.components.sensor.airvisual -pyairvisual==2.0.1 +pyairvisual==3.0.1 # homeassistant.components.alarm_control_panel.alarmdotcom pyalarmdotcom==0.3.2 @@ -949,6 +955,9 @@ pyblackbird==0.5 # homeassistant.components.neato pybotvac==0.0.13 +# homeassistant.components.nissan_leaf +pycarwings2==2.8 + # homeassistant.components.cloudflare pycfdns==0.0.1 @@ -977,10 +986,10 @@ pycsspeechtts==1.0.2 pydaikin==0.9 # homeassistant.components.danfoss_air -pydanfossair==0.0.6 +pydanfossair==0.0.7 # homeassistant.components.deconz -pydeconz==47 +pydeconz==52 # homeassistant.components.zwave pydispatcher==2.0.5 @@ -995,7 +1004,7 @@ pydukeenergy==0.0.6 pyebox==1.1.4 # homeassistant.components.water_heater.econet -pyeconet==0.0.6 +pyeconet==0.0.8 # homeassistant.components.switch.edimax pyedimax==0.1 @@ -1053,13 +1062,13 @@ pygtt==1.1.2 pyhaversion==2.0.3 # homeassistant.components.binary_sensor.hikvision -pyhik==0.1.9 +pyhik==0.2.2 # homeassistant.components.hive pyhiveapi==0.2.17 # homeassistant.components.homematic -pyhomematic==0.1.55 +pyhomematic==0.1.56 # homeassistant.components.homeworks pyhomeworks==0.0.6 @@ -1108,7 +1117,7 @@ pylgnetcast-homeassistant==0.2.0.dev0 pylgtv==0.1.9 # homeassistant.components.sensor.linky -pylinky==0.1.8 +pylinky==0.3.0 # homeassistant.components.litejet pylitejet==0.1 @@ -1185,6 +1194,9 @@ pyotgw==0.4b1 # homeassistant.components.sensor.otp pyotp==2.2.6 +# homeassistant.components.owlet +pyowlet==1.0.2 + # homeassistant.components.sensor.openweathermap # homeassistant.components.weather.openweathermap pyowm==2.10.0 @@ -1196,11 +1208,14 @@ pypck==0.5.9 pypjlink2==1.2.0 # homeassistant.components.point -pypoint==1.0.8 +pypoint==1.1.1 # homeassistant.components.sensor.pollen pypollencom==2.2.2 +# homeassistant.components.ps4 +pyps4-homeassistant==0.3.0 + # homeassistant.components.qwikswitch pyqwikswitch==0.8 @@ -1219,6 +1234,9 @@ pyruter==1.1.0 # homeassistant.components.sabnzbd pysabnzbd==1.1.0 +# homeassistant.components.switch.sony_projector +pysdcp==1 + # homeassistant.components.climate.sensibo pysensibo==1.0.3 @@ -1241,7 +1259,7 @@ pysma==0.3.1 pysmartapp==0.3.0 # homeassistant.components.smartthings -pysmartthings==0.6.2 +pysmartthings==0.6.3 # homeassistant.components.device_tracker.snmp # homeassistant.components.sensor.snmp @@ -1249,7 +1267,7 @@ pysmartthings==0.6.2 pysnmp==4.4.8 # homeassistant.components.sonos -pysonos==0.0.6 +pysonos==0.0.8 # homeassistant.components.spc pyspcwebgw==0.4.0 @@ -1394,7 +1412,7 @@ pytile==2.0.5 pytouchline==0.7 # homeassistant.components.device_tracker.traccar -pytraccar==0.2.1 +pytraccar==0.3.0 # homeassistant.components.device_tracker.trackr pytrackr==0.0.5 @@ -1405,6 +1423,9 @@ pytradfri[async]==6.0.1 # homeassistant.components.sensor.trafikverket_weatherstation pytrafikverket==0.1.5.8 +# homeassistant.components.device_tracker.ubee +pyubee==0.2 + # homeassistant.components.device_tracker.unifi pyunifi==2.16 @@ -1424,7 +1445,7 @@ pyvesync==0.1.1 pyvizio==0.0.4 # homeassistant.components.velux -pyvlx==0.2.8 +pyvlx==0.2.9 # homeassistant.components.notify.html5 pywebpush==1.6.0 @@ -1493,7 +1514,7 @@ rocketchat-API==0.6.1 roombapy==1.3.1 # homeassistant.components.sensor.rova -rova==0.0.2 +rova==0.1.0 # homeassistant.components.switch.rpi_rf # rpi-rf==0.9.7 @@ -1533,13 +1554,13 @@ sense_energy==0.6.0 sharp_aquos_rc==0.3.2 # homeassistant.components.sensor.shodan -shodan==1.10.4 +shodan==1.11.1 # homeassistant.components.notify.simplepush simplepush==1.1.4 # homeassistant.components.simplisafe -simplisafe-python==3.1.14 +simplisafe-python==3.4.1 # homeassistant.components.sisyphus sisyphus-control==2.1 @@ -1568,7 +1589,7 @@ smappy==0.2.16 # smbus-cffi==0.5.1 # homeassistant.components.smhi -smhi-pkg==1.0.8 +smhi-pkg==1.0.10 # homeassistant.components.media_player.snapcast snapcast==2.0.9 @@ -1596,13 +1617,13 @@ spotipy-homeassistant==2.4.4.dev1 # homeassistant.components.recorder # homeassistant.components.sensor.sql -sqlalchemy==1.2.17 +sqlalchemy==1.2.18 # homeassistant.components.sensor.srp_energy srpenergy==1.0.5 # homeassistant.components.sensor.starlingbank -starlingbank==1.2 +starlingbank==3.0 # homeassistant.components.statsd statsd==3.2.1 @@ -1626,7 +1647,7 @@ suds-py3==1.3.3.0 swisshydrodata==0.0.3 # homeassistant.components.device_tracker.synology_srm -synology-srm==0.0.4 +synology-srm==0.0.6 # homeassistant.components.tahoma tahoma-api==0.0.14 @@ -1668,7 +1689,7 @@ tikteck==0.4 todoist-python==7.0.17 # homeassistant.components.toon -toonlib==1.1.3 +toonapilib==3.2.1 # homeassistant.components.alarm_control_panel.totalconnect total_connect_client==0.22 @@ -1784,7 +1805,7 @@ yeelight==0.4.3 yeelightsunflower==0.0.10 # homeassistant.components.media_extractor -youtube_dl==2019.02.08 +youtube_dl==2019.02.18 # homeassistant.components.light.zengge zengge==0.2 @@ -1802,13 +1823,13 @@ zhong_hong_hvac==1.0.9 ziggo-mediabox-xl==1.1.0 # homeassistant.components.zha -zigpy-deconz==0.0.1 +zigpy-deconz==0.1.2 # homeassistant.components.zha -zigpy-xbee==0.1.1 +zigpy-homeassistant==0.3.0 # homeassistant.components.zha -zigpy==0.2.0 +zigpy-xbee-homeassistant==0.1.2 # homeassistant.components.zoneminder zm-py==0.3.3 diff --git a/requirements_test.txt b/requirements_test.txt index b9da9890c611c..531fb0b78f65e 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -4,14 +4,14 @@ asynctest==0.12.2 coveralls==1.2.0 flake8-docstrings==1.3.0 -flake8==3.7.5 +flake8==3.7.7 mock-open==1.3.1 -mypy==0.660 +mypy==0.670 pydocstyle==3.0.0 -pylint==2.2.2 +pylint==2.3.0 pytest-aiohttp==0.3.0 pytest-cov==2.6.1 pytest-sugar==0.9.2 pytest-timeout==1.3.3 -pytest==4.1.1 +pytest==4.3.0 requests_mock==1.5.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1ee8060715006..33a88ac4391b5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -5,22 +5,26 @@ asynctest==0.12.2 coveralls==1.2.0 flake8-docstrings==1.3.0 -flake8==3.7.5 +flake8==3.7.7 mock-open==1.3.1 -mypy==0.660 +mypy==0.670 pydocstyle==3.0.0 -pylint==2.2.2 +pylint==2.3.0 pytest-aiohttp==0.3.0 pytest-cov==2.6.1 pytest-sugar==0.9.2 pytest-timeout==1.3.3 -pytest==4.1.1 +pytest==4.3.0 requests_mock==1.5.2 # homeassistant.components.homekit HAP-python==2.4.2 +# homeassistant.components.mobile_app +# homeassistant.components.owntracks +PyNaCl==1.3.0 + # homeassistant.components.sensor.rmvtransport PyRMVtransport==0.1.3 @@ -31,7 +35,7 @@ PyTransportNSW==0.1.1 YesssSMS==0.2.3 # homeassistant.components.ambient_station -aioambient==0.1.2 +aioambient==0.1.3 # homeassistant.components.device_tracker.automatic aioautomatic==0.6.5 @@ -41,7 +45,7 @@ aioautomatic==0.6.5 aiohttp_cors==0.7.0 # homeassistant.components.hue -aiohue==1.9.0 +aiohue==1.9.1 # homeassistant.components.unifi aiounifi==4 @@ -50,7 +54,7 @@ aiounifi==4 apns2==0.3.0 # homeassistant.components.zha -bellows==0.7.0 +bellows-homeassistant==0.7.1 # homeassistant.components.calendar.caldav caldav==0.5.0 @@ -116,7 +120,7 @@ hdate==0.8.7 holidays==0.9.9 # homeassistant.components.frontend -home-assistant-frontend==20190220.0 +home-assistant-frontend==20190305.0 # homeassistant.components.homekit_controller homekit==0.12.2 @@ -151,7 +155,7 @@ mficlient==0.3.0 # homeassistant.components.image_processing.opencv # homeassistant.components.image_processing.tensorflow # homeassistant.components.sensor.pollen -numpy==1.16.0 +numpy==1.16.1 # homeassistant.components.mqtt # homeassistant.components.shiftr @@ -180,17 +184,20 @@ pushbullet.py==0.11.0 # homeassistant.components.canary py-canary==0.5.0 +# homeassistant.components.tplink +pyHS100==0.3.4 + # homeassistant.components.media_player.blackbird pyblackbird==0.5 # homeassistant.components.deconz -pydeconz==47 +pydeconz==52 # homeassistant.components.zwave pydispatcher==2.0.5 # homeassistant.components.homematic -pyhomematic==0.1.55 +pyhomematic==0.1.56 # homeassistant.components.litejet pylitejet==0.1 @@ -210,6 +217,9 @@ pyopenuv==1.0.4 # homeassistant.components.sensor.otp pyotp==2.2.6 +# homeassistant.components.ps4 +pyps4-homeassistant==0.3.0 + # homeassistant.components.qwikswitch pyqwikswitch==0.8 @@ -217,10 +227,10 @@ pyqwikswitch==0.8 pysmartapp==0.3.0 # homeassistant.components.smartthings -pysmartthings==0.6.2 +pysmartthings==0.6.3 # homeassistant.components.sonos -pysonos==0.0.6 +pysonos==0.0.8 # homeassistant.components.spc pyspcwebgw==0.4.0 @@ -263,20 +273,20 @@ ring_doorbell==0.2.2 rxv==0.6.0 # homeassistant.components.simplisafe -simplisafe-python==3.1.14 +simplisafe-python==3.4.1 # homeassistant.components.sleepiq sleepyq==0.6 # homeassistant.components.smhi -smhi-pkg==1.0.8 +smhi-pkg==1.0.10 # homeassistant.components.climate.honeywell somecomfort==0.5.2 # homeassistant.components.recorder # homeassistant.components.sensor.sql -sqlalchemy==1.2.17 +sqlalchemy==1.2.18 # homeassistant.components.sensor.srp_energy srpenergy==1.0.5 @@ -284,6 +294,9 @@ srpenergy==1.0.5 # homeassistant.components.statsd statsd==3.2.1 +# homeassistant.components.toon +toonapilib==3.2.1 + # homeassistant.components.camera.uvc uvcclient==0.11.0 @@ -303,4 +316,4 @@ wakeonlan==1.1.6 warrant==0.6.1 # homeassistant.components.zha -zigpy==0.2.0 +zigpy-homeassistant==0.3.0 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 926aadfc3a557..7db76b1361bea 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -8,31 +8,33 @@ import fnmatch COMMENT_REQUIREMENTS = ( - 'RPi.GPIO', - 'raspihats', - 'rpi-rf', 'Adafruit-DHT', 'Adafruit_BBIO', - 'fritzconnection', - 'pybluez', + 'avion', 'beacontools', + 'blinkt', 'bluepy', - 'opencv-python', - 'python-lirc', - 'pyuserinput', - 'evdev', - 'pycups', - 'python-eq3bt', - 'avion', + 'bme680', + 'credstash', 'decora', - 'face_recognition', - 'blinkt', - 'smbus-cffi', 'envirophat', + 'evdev', + 'face_recognition', + 'fritzconnection', 'i2csense', - 'credstash', - 'bme680', + 'opencv-python', 'py_noaa', + 'pybluez', + 'pycups', + 'PySwitchbot', + 'pySwitchmate', + 'python-eq3bt', + 'python-lirc', + 'pyuserinput', + 'raspihats', + 'rpi-rf', + 'RPi.GPIO', + 'smbus-cffi', ) TEST_REQUIREMENTS = ( @@ -90,6 +92,7 @@ 'pynx584', 'pyopenuv', 'pyotp', + 'pyps4-homeassistant', 'pysmartapp', 'pysmartthings', 'pysonos', @@ -104,6 +107,8 @@ 'pyunifi', 'pyupnp-async', 'pywebpush', + 'pyHS100', + 'PyNaCl', 'regenmaschine', 'restrictedpython', 'rflink', @@ -116,6 +121,7 @@ 'sqlalchemy', 'srpenergy', 'statsd', + 'toonapilib', 'uvcclient', 'vsure', 'warrant', @@ -124,8 +130,8 @@ 'vultr', 'YesssSMS', 'ruamel.yaml', - 'zigpy', - 'bellows', + 'zigpy-homeassistant', + 'bellows-homeassistant', ) IGNORE_PACKAGES = ( diff --git a/setup.py b/setup.py index 52be310574a6c..c4c9d0e53edae 100755 --- a/setup.py +++ b/setup.py @@ -33,10 +33,10 @@ REQUIRES = [ 'aiohttp==3.5.4', - 'astral==1.9.2', + 'astral==1.10.1', 'async_timeout==3.0.1', 'attrs==18.2.0', - 'bcrypt==3.1.5', + 'bcrypt==3.1.6', 'certifi>=2018.04.16', 'jinja2>=2.10', 'PyJWT==1.6.4', @@ -49,7 +49,7 @@ 'requests==2.21.0', 'ruamel.yaml==0.15.88', 'voluptuous==0.11.5', - 'voluptuous-serialize==2.0.0', + 'voluptuous-serialize==2.1.0', ] MIN_PY_VERSION = '.'.join(map(str, hass_const.REQUIRED_PYTHON_VER)) diff --git a/tests/auth/mfa_modules/test_notify.py b/tests/auth/mfa_modules/test_notify.py index 748b550782468..c0680024daeb8 100644 --- a/tests/auth/mfa_modules/test_notify.py +++ b/tests/auth/mfa_modules/test_notify.py @@ -1,4 +1,5 @@ """Test the HMAC-based One Time Password (MFA) auth module.""" +import asyncio from unittest.mock import patch from homeassistant import data_entry_flow @@ -395,3 +396,26 @@ async def test_not_raise_exception_when_service_not_exist(hass): # wait service call finished await hass.async_block_till_done() + + +async def test_race_condition_in_data_loading(hass): + """Test race condition in the data loading.""" + counter = 0 + + async def mock_load(_): + """Mock homeassistant.helpers.storage.Store.async_load.""" + nonlocal counter + counter += 1 + await asyncio.sleep(0) + + notify_auth_module = await auth_mfa_module_from_config(hass, { + 'type': 'notify' + }) + with patch('homeassistant.helpers.storage.Store.async_load', + new=mock_load): + task1 = notify_auth_module.async_validate('user', {'code': 'value'}) + task2 = notify_auth_module.async_validate('user', {'code': 'value'}) + results = await asyncio.gather(task1, task2, return_exceptions=True) + assert counter == 1 + assert results[0] is False + assert results[1] is False diff --git a/tests/auth/mfa_modules/test_totp.py b/tests/auth/mfa_modules/test_totp.py index d400fe80672d8..35ab21ae6def3 100644 --- a/tests/auth/mfa_modules/test_totp.py +++ b/tests/auth/mfa_modules/test_totp.py @@ -1,4 +1,5 @@ """Test the Time-based One Time Password (MFA) auth module.""" +import asyncio from unittest.mock import patch from homeassistant import data_entry_flow @@ -128,3 +129,26 @@ async def test_login_flow_validates_mfa(hass): result['flow_id'], {'code': MOCK_CODE}) assert result['type'] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert result['data'].id == 'mock-user' + + +async def test_race_condition_in_data_loading(hass): + """Test race condition in the data loading.""" + counter = 0 + + async def mock_load(_): + """Mock of homeassistant.helpers.storage.Store.async_load.""" + nonlocal counter + counter += 1 + await asyncio.sleep(0) + + totp_auth_module = await auth_mfa_module_from_config(hass, { + 'type': 'totp' + }) + with patch('homeassistant.helpers.storage.Store.async_load', + new=mock_load): + task1 = totp_auth_module.async_validate('user', {'code': 'value'}) + task2 = totp_auth_module.async_validate('user', {'code': 'value'}) + results = await asyncio.gather(task1, task2, return_exceptions=True) + assert counter == 1 + assert results[0] is False + assert results[1] is False diff --git a/tests/auth/providers/test_homeassistant.py b/tests/auth/providers/test_homeassistant.py index ffc4d67f21d1d..c466a1fa42bb5 100644 --- a/tests/auth/providers/test_homeassistant.py +++ b/tests/auth/providers/test_homeassistant.py @@ -1,4 +1,5 @@ """Test the Home Assistant local auth provider.""" +import asyncio from unittest.mock import Mock, patch import pytest @@ -288,3 +289,29 @@ async def test_legacy_get_or_create_credentials(hass, legacy_data): 'username': 'hello ' }) assert credentials1 is not credentials3 + + +async def test_race_condition_in_data_loading(hass): + """Test race condition in the hass_auth.Data loading. + + Ref issue: https://github.com/home-assistant/home-assistant/issues/21569 + """ + counter = 0 + + async def mock_load(_): + """Mock of homeassistant.helpers.storage.Store.async_load.""" + nonlocal counter + counter += 1 + await asyncio.sleep(0) + + provider = hass_auth.HassAuthProvider(hass, auth_store.AuthStore(hass), + {'type': 'homeassistant'}) + with patch('homeassistant.helpers.storage.Store.async_load', + new=mock_load): + task1 = provider.async_validate_login('user', 'pass') + task2 = provider.async_validate_login('user', 'pass') + results = await asyncio.gather(task1, task2, return_exceptions=True) + assert counter == 1 + assert isinstance(results[0], hass_auth.InvalidAuth) + # results[1] will be a TypeError if race condition occurred + assert isinstance(results[1], hass_auth.InvalidAuth) diff --git a/tests/auth/providers/test_trusted_networks.py b/tests/auth/providers/test_trusted_networks.py index 0ca302f827305..57e74e750d562 100644 --- a/tests/auth/providers/test_trusted_networks.py +++ b/tests/auth/providers/test_trusted_networks.py @@ -1,5 +1,5 @@ """Test the Trusted Networks auth provider.""" -from unittest.mock import Mock +from ipaddress import ip_address import pytest import voluptuous as vol @@ -18,9 +18,17 @@ def store(hass): @pytest.fixture def provider(hass, store): """Mock provider.""" - return tn_auth.TrustedNetworksAuthProvider(hass, store, { - 'type': 'trusted_networks' - }) + return tn_auth.TrustedNetworksAuthProvider( + hass, store, tn_auth.CONFIG_SCHEMA({ + 'type': 'trusted_networks', + 'trusted_networks': [ + '192.168.0.1', + '192.168.128.0/24', + '::1', + 'fd00::/8' + ] + }) + ) @pytest.fixture @@ -56,14 +64,17 @@ async def test_trusted_networks_credentials(manager, provider): async def test_validate_access(provider): """Test validate access from trusted networks.""" - with pytest.raises(tn_auth.InvalidAuthError): - provider.async_validate_access('192.168.0.1') - - provider.hass.http = Mock(trusted_networks=['192.168.0.1']) - provider.async_validate_access('192.168.0.1') + provider.async_validate_access(ip_address('192.168.0.1')) + provider.async_validate_access(ip_address('192.168.128.10')) + provider.async_validate_access(ip_address('::1')) + provider.async_validate_access(ip_address('fd01:db8::ff00:42:8329')) with pytest.raises(tn_auth.InvalidAuthError): - provider.async_validate_access('127.0.0.1') + provider.async_validate_access(ip_address('192.168.0.2')) + with pytest.raises(tn_auth.InvalidAuthError): + provider.async_validate_access(ip_address('127.0.0.1')) + with pytest.raises(tn_auth.InvalidAuthError): + provider.async_validate_access(ip_address('2001:db8::ff00:42:8329')) async def test_login_flow(manager, provider): @@ -71,22 +82,16 @@ async def test_login_flow(manager, provider): owner = await manager.async_create_user("test-owner") user = await manager.async_create_user("test-user") - # trusted network didn't loaded - flow = await provider.async_login_flow({'ip_address': '127.0.0.1'}) - step = await flow.async_step_init() - assert step['type'] == 'abort' - assert step['reason'] == 'not_whitelisted' - - provider.hass.http = Mock(trusted_networks=['192.168.0.1']) - # not from trusted network - flow = await provider.async_login_flow({'ip_address': '127.0.0.1'}) + flow = await provider.async_login_flow( + {'ip_address': ip_address('127.0.0.1')}) step = await flow.async_step_init() assert step['type'] == 'abort' assert step['reason'] == 'not_whitelisted' # from trusted network, list users - flow = await provider.async_login_flow({'ip_address': '192.168.0.1'}) + flow = await provider.async_login_flow( + {'ip_address': ip_address('192.168.0.1')}) step = await flow.async_step_init() assert step['step_id'] == 'init' diff --git a/tests/common.py b/tests/common.py index 409b020f7286b..a55546da73b4d 100644 --- a/tests/common.py +++ b/tests/common.py @@ -451,8 +451,10 @@ class MockModule: def __init__(self, domain=None, dependencies=None, setup=None, requirements=None, config_schema=None, platform_schema=None, platform_schema_base=None, async_setup=None, - async_setup_entry=None, async_unload_entry=None): + async_setup_entry=None, async_unload_entry=None, + async_migrate_entry=None): """Initialize the mock module.""" + self.__name__ = 'homeassistant.components.{}'.format(domain) self.DOMAIN = domain self.DEPENDENCIES = dependencies or [] self.REQUIREMENTS = requirements or [] @@ -482,6 +484,9 @@ def __init__(self, domain=None, dependencies=None, setup=None, if async_unload_entry is not None: self.async_unload_entry = async_unload_entry + if async_migrate_entry is not None: + self.async_migrate_entry = async_migrate_entry + class MockPlatform: """Provide a fake platform.""" @@ -602,15 +607,16 @@ def last_call(self, method=None): class MockConfigEntry(config_entries.ConfigEntry): """Helper for creating config entries that adds some defaults.""" - def __init__(self, *, domain='test', data=None, version=0, entry_id=None, + def __init__(self, *, domain='test', data=None, version=1, entry_id=None, source=config_entries.SOURCE_USER, title='Mock Title', - state=None, + state=None, options={}, connection_class=config_entries.CONN_CLASS_UNKNOWN): """Initialize a mock config entry.""" kwargs = { 'entry_id': entry_id or 'mock-id', 'domain': domain, 'data': data or {}, + 'options': options, 'version': version, 'title': title, 'connection_class': connection_class, diff --git a/tests/components/binary_sensor/test_tod.py b/tests/components/binary_sensor/test_tod.py new file mode 100644 index 0000000000000..3c08314196287 --- /dev/null +++ b/tests/components/binary_sensor/test_tod.py @@ -0,0 +1,839 @@ +"""Test Times of the Day Binary Sensor.""" +import unittest +from unittest.mock import patch +from datetime import timedelta, datetime +import pytz + +from homeassistant import setup +import homeassistant.core as ha +from homeassistant.const import STATE_OFF, STATE_ON +import homeassistant.util.dt as dt_util +from homeassistant.setup import setup_component +from tests.common import ( + get_test_home_assistant, assert_setup_component) +from homeassistant.helpers.sun import ( + get_astral_event_date, get_astral_event_next) + + +class TestBinarySensorTod(unittest.TestCase): + """Test for Binary sensor tod platform.""" + + hass = None + # pylint: disable=invalid-name + + def setup_method(self, method): + """Set up things to be run when tests are started.""" + self.hass = get_test_home_assistant() + self.hass.config.latitute = 50.27583 + self.hass.config.longitude = 18.98583 + + def teardown_method(self, method): + """Stop everything that was started.""" + self.hass.stop() + + def test_setup(self): + """Test the setup.""" + config = { + 'binary_sensor': [ + { + 'platform': 'tod', + 'name': 'Early Morning', + 'after': 'sunrise', + 'after_offset': '-02:00', + 'before': '7:00', + 'before_offset': '1:00' + }, + { + 'platform': 'tod', + 'name': 'Morning', + 'after': 'sunrise', + 'before': '12:00' + } + ], + } + with assert_setup_component(2): + assert setup.setup_component( + self.hass, 'binary_sensor', config) + + def test_setup_no_sensors(self): + """Test setup with no sensors.""" + with assert_setup_component(0): + assert setup.setup_component(self.hass, 'binary_sensor', { + 'binary_sensor': { + 'platform': 'tod' + } + }) + + def test_in_period_on_start(self): + """Test simple setting.""" + test_time = datetime( + 2019, 1, 10, 18, 43, 0, tzinfo=self.hass.config.time_zone) + config = { + 'binary_sensor': [ + { + 'platform': 'tod', + 'name': 'Evening', + 'after': '18:00', + 'before': '22:00' + } + ] + } + with patch('homeassistant.components.binary_sensor.tod.dt_util.utcnow', + return_value=test_time): + setup_component(self.hass, 'binary_sensor', config) + + self.hass.block_till_done() + state = self.hass.states.get('binary_sensor.evening') + assert state.state == STATE_ON + + def test_midnight_turnover_before_midnight_inside_period(self): + """Test midnight turnover setting before midnight inside period .""" + test_time = datetime( + 2019, 1, 10, 22, 30, 0, tzinfo=self.hass.config.time_zone) + config = { + 'binary_sensor': [ + { + 'platform': 'tod', + 'name': 'Night', + 'after': '22:00', + 'before': '5:00' + }, + ] + } + with patch('homeassistant.components.binary_sensor.tod.dt_util.utcnow', + return_value=test_time): + setup_component(self.hass, 'binary_sensor', config) + + self.hass.block_till_done() + state = self.hass.states.get('binary_sensor.night') + assert state.state == STATE_ON + + def test_midnight_turnover_after_midnight_inside_period(self): + """Test midnight turnover setting before midnight inside period .""" + test_time = datetime( + 2019, 1, 10, 21, 00, 0, tzinfo=self.hass.config.time_zone) + config = { + 'binary_sensor': [ + { + 'platform': 'tod', + 'name': 'Night', + 'after': '22:00', + 'before': '5:00' + }, + ] + } + with patch('homeassistant.components.binary_sensor.tod.dt_util.utcnow', + return_value=test_time): + setup_component(self.hass, 'binary_sensor', config) + + state = self.hass.states.get('binary_sensor.night') + assert state.state == STATE_OFF + + self.hass.block_till_done() + + with patch('homeassistant.components.binary_sensor.tod.dt_util.utcnow', + return_value=test_time + timedelta(hours=1)): + + self.hass.bus.fire(ha.EVENT_TIME_CHANGED, { + ha.ATTR_NOW: test_time + timedelta(hours=1)}) + + self.hass.block_till_done() + state = self.hass.states.get('binary_sensor.night') + assert state.state == STATE_ON + + def test_midnight_turnover_before_midnight_outside_period(self): + """Test midnight turnover setting before midnight outside period.""" + test_time = datetime( + 2019, 1, 10, 20, 30, 0, tzinfo=self.hass.config.time_zone) + config = { + 'binary_sensor': [ + { + 'platform': 'tod', + 'name': 'Night', + 'after': '22:00', + 'before': '5:00' + } + ] + } + with patch('homeassistant.components.binary_sensor.tod.dt_util.utcnow', + return_value=test_time): + setup_component(self.hass, 'binary_sensor', config) + + self.hass.block_till_done() + state = self.hass.states.get('binary_sensor.night') + assert state.state == STATE_OFF + + def test_midnight_turnover_after_midnight_outside_period(self): + """Test midnight turnover setting before midnight inside period .""" + test_time = datetime( + 2019, 1, 10, 20, 0, 0, tzinfo=self.hass.config.time_zone) + config = { + 'binary_sensor': [ + { + 'platform': 'tod', + 'name': 'Night', + 'after': '22:00', + 'before': '5:00' + } + ] + } + with patch('homeassistant.components.binary_sensor.tod.dt_util.utcnow', + return_value=test_time): + setup_component(self.hass, 'binary_sensor', config) + + self.hass.block_till_done() + state = self.hass.states.get('binary_sensor.night') + assert state.state == STATE_OFF + + switchover_time = datetime( + 2019, 1, 11, 4, 59, 0, tzinfo=self.hass.config.time_zone) + with patch('homeassistant.components.binary_sensor.tod.dt_util.utcnow', + return_value=switchover_time): + + self.hass.bus.fire(ha.EVENT_TIME_CHANGED, { + ha.ATTR_NOW: switchover_time}) + self.hass.block_till_done() + state = self.hass.states.get('binary_sensor.night') + assert state.state == STATE_ON + + with patch('homeassistant.components.binary_sensor.tod.dt_util.utcnow', + return_value=switchover_time + timedelta( + minutes=1, seconds=1)): + + self.hass.bus.fire(ha.EVENT_TIME_CHANGED, { + ha.ATTR_NOW: switchover_time + timedelta( + minutes=1, seconds=1)}) + + self.hass.block_till_done() + state = self.hass.states.get('binary_sensor.night') + assert state.state == STATE_OFF + + def test_from_sunrise_to_sunset(self): + """Test period from sunrise to sunset.""" + test_time = datetime( + 2019, 1, 12, tzinfo=self.hass.config.time_zone) + sunrise = dt_util.as_local(get_astral_event_date( + self.hass, 'sunrise', dt_util.as_utc(test_time))) + sunset = dt_util.as_local(get_astral_event_date( + self.hass, 'sunset', dt_util.as_utc(test_time))) + config = { + 'binary_sensor': [ + { + 'platform': 'tod', + 'name': 'Day', + 'after': 'sunrise', + 'before': 'sunset' + } + ] + } + entity_id = 'binary_sensor.day' + testtime = sunrise + timedelta(seconds=-1) + with patch('homeassistant.components.binary_sensor.tod.dt_util.utcnow', + return_value=testtime): + setup_component(self.hass, 'binary_sensor', config) + + self.hass.block_till_done() + state = self.hass.states.get(entity_id) + assert state.state == STATE_OFF + + testtime = sunrise + with patch('homeassistant.components.binary_sensor.tod.dt_util.utcnow', + return_value=testtime): + + self.hass.bus.fire(ha.EVENT_TIME_CHANGED, { + ha.ATTR_NOW: testtime}) + self.hass.block_till_done() + + state = self.hass.states.get(entity_id) + assert state.state == STATE_ON + + testtime = sunrise + timedelta(seconds=1) + with patch('homeassistant.components.binary_sensor.tod.dt_util.utcnow', + return_value=testtime): + + self.hass.bus.fire(ha.EVENT_TIME_CHANGED, { + ha.ATTR_NOW: testtime}) + self.hass.block_till_done() + + state = self.hass.states.get(entity_id) + assert state.state == STATE_ON + + self.hass.block_till_done() + + testtime = sunset + timedelta(seconds=-1) + with patch('homeassistant.components.binary_sensor.tod.dt_util.utcnow', + return_value=testtime): + + self.hass.bus.fire(ha.EVENT_TIME_CHANGED, { + ha.ATTR_NOW: testtime}) + self.hass.block_till_done() + + state = self.hass.states.get(entity_id) + assert state.state == STATE_ON + + self.hass.block_till_done() + + testtime = sunset + with patch('homeassistant.components.binary_sensor.tod.dt_util.utcnow', + return_value=testtime): + + self.hass.bus.fire(ha.EVENT_TIME_CHANGED, { + ha.ATTR_NOW: testtime}) + self.hass.block_till_done() + + state = self.hass.states.get(entity_id) + assert state.state == STATE_OFF + + self.hass.block_till_done() + + testtime = sunset + timedelta(seconds=1) + with patch('homeassistant.components.binary_sensor.tod.dt_util.utcnow', + return_value=testtime): + + self.hass.bus.fire(ha.EVENT_TIME_CHANGED, { + ha.ATTR_NOW: testtime}) + self.hass.block_till_done() + + state = self.hass.states.get(entity_id) + assert state.state == STATE_OFF + + def test_from_sunset_to_sunrise(self): + """Test period from sunset to sunrise.""" + test_time = datetime( + 2019, 1, 12, tzinfo=self.hass.config.time_zone) + sunset = dt_util.as_local(get_astral_event_date( + self.hass, 'sunset', test_time)) + sunrise = dt_util.as_local(get_astral_event_next( + self.hass, 'sunrise', sunset)) + # assert sunset == sunrise + config = { + 'binary_sensor': [ + { + 'platform': 'tod', + 'name': 'Night', + 'after': 'sunset', + 'before': 'sunrise' + } + ] + } + entity_id = 'binary_sensor.night' + testtime = sunset + timedelta(minutes=-1) + with patch('homeassistant.components.binary_sensor.tod.dt_util.utcnow', + return_value=testtime): + setup_component(self.hass, 'binary_sensor', config) + + self.hass.block_till_done() + state = self.hass.states.get(entity_id) + assert state.state == STATE_OFF + + testtime = sunset + with patch('homeassistant.components.binary_sensor.tod.dt_util.utcnow', + return_value=testtime): + + self.hass.bus.fire(ha.EVENT_TIME_CHANGED, { + ha.ATTR_NOW: testtime}) + self.hass.block_till_done() + + state = self.hass.states.get(entity_id) + assert state.state == STATE_ON + + testtime = sunset + timedelta(minutes=1) + with patch('homeassistant.components.binary_sensor.tod.dt_util.utcnow', + return_value=testtime): + + self.hass.bus.fire(ha.EVENT_TIME_CHANGED, { + ha.ATTR_NOW: testtime}) + self.hass.block_till_done() + + state = self.hass.states.get(entity_id) + assert state.state == STATE_ON + + testtime = sunrise + timedelta(minutes=-1) + with patch('homeassistant.components.binary_sensor.tod.dt_util.utcnow', + return_value=testtime): + + self.hass.bus.fire(ha.EVENT_TIME_CHANGED, { + ha.ATTR_NOW: testtime}) + self.hass.block_till_done() + + state = self.hass.states.get(entity_id) + assert state.state == STATE_ON + + testtime = sunrise + with patch('homeassistant.components.binary_sensor.tod.dt_util.utcnow', + return_value=testtime): + + self.hass.bus.fire(ha.EVENT_TIME_CHANGED, { + ha.ATTR_NOW: testtime}) + self.hass.block_till_done() + + state = self.hass.states.get(entity_id) + self.hass.block_till_done() + # assert state == "dupa" + assert state.state == STATE_OFF + + testtime = sunrise + timedelta(minutes=1) + with patch('homeassistant.components.binary_sensor.tod.dt_util.utcnow', + return_value=testtime): + + self.hass.bus.fire(ha.EVENT_TIME_CHANGED, { + ha.ATTR_NOW: testtime}) + self.hass.block_till_done() + + state = self.hass.states.get(entity_id) + assert state.state == STATE_OFF + + def test_offset(self): + """Test offset.""" + after = datetime( + 2019, 1, 10, 18, 0, 0, + tzinfo=self.hass.config.time_zone) + \ + timedelta(hours=1, minutes=34) + before = datetime( + 2019, 1, 10, 22, 0, 0, + tzinfo=self.hass.config.time_zone) + \ + timedelta(hours=1, minutes=45) + entity_id = 'binary_sensor.evening' + config = { + 'binary_sensor': [ + { + 'platform': 'tod', + 'name': 'Evening', + 'after': '18:00', + 'after_offset': '1:34', + 'before': '22:00', + 'before_offset': '1:45' + } + ] + } + testtime = after + timedelta(seconds=-1) + with patch('homeassistant.components.binary_sensor.tod.dt_util.utcnow', + return_value=testtime): + setup_component(self.hass, 'binary_sensor', config) + + self.hass.block_till_done() + state = self.hass.states.get(entity_id) + assert state.state == STATE_OFF + + testtime = after + with patch('homeassistant.components.binary_sensor.tod.dt_util.utcnow', + return_value=testtime): + self.hass.bus.fire(ha.EVENT_TIME_CHANGED, { + ha.ATTR_NOW: testtime}) + self.hass.block_till_done() + + state = self.hass.states.get(entity_id) + assert state.state == STATE_ON + + testtime = before + timedelta(seconds=-1) + with patch('homeassistant.components.binary_sensor.tod.dt_util.utcnow', + return_value=testtime): + self.hass.bus.fire(ha.EVENT_TIME_CHANGED, { + ha.ATTR_NOW: testtime}) + self.hass.block_till_done() + + state = self.hass.states.get(entity_id) + assert state.state == STATE_ON + + testtime = before + with patch('homeassistant.components.binary_sensor.tod.dt_util.utcnow', + return_value=testtime): + self.hass.bus.fire(ha.EVENT_TIME_CHANGED, { + ha.ATTR_NOW: testtime}) + self.hass.block_till_done() + + state = self.hass.states.get(entity_id) + assert state.state == STATE_OFF + + testtime = before + timedelta(seconds=1) + with patch('homeassistant.components.binary_sensor.tod.dt_util.utcnow', + return_value=testtime): + self.hass.bus.fire(ha.EVENT_TIME_CHANGED, { + ha.ATTR_NOW: testtime}) + self.hass.block_till_done() + + state = self.hass.states.get(entity_id) + assert state.state == STATE_OFF + + def test_offset_overnight(self): + """Test offset overnight.""" + after = datetime( + 2019, 1, 10, 18, 0, 0, + tzinfo=self.hass.config.time_zone) + \ + timedelta(hours=1, minutes=34) + entity_id = 'binary_sensor.evening' + config = { + 'binary_sensor': [ + { + 'platform': 'tod', + 'name': 'Evening', + 'after': '18:00', + 'after_offset': '1:34', + 'before': '22:00', + 'before_offset': '3:00' + } + ] + } + testtime = after + timedelta(seconds=-1) + with patch('homeassistant.components.binary_sensor.tod.dt_util.utcnow', + return_value=testtime): + setup_component(self.hass, 'binary_sensor', config) + + self.hass.block_till_done() + state = self.hass.states.get(entity_id) + assert state.state == STATE_OFF + + testtime = after + with patch('homeassistant.components.binary_sensor.tod.dt_util.utcnow', + return_value=testtime): + self.hass.bus.fire(ha.EVENT_TIME_CHANGED, { + ha.ATTR_NOW: testtime}) + self.hass.block_till_done() + + state = self.hass.states.get(entity_id) + assert state.state == STATE_ON + + def test_norwegian_case_winter(self): + """Test location in Norway where the sun doesn't set in summer.""" + self.hass.config.latitude = 69.6 + self.hass.config.longitude = 18.8 + + test_time = datetime(2010, 1, 1, tzinfo=self.hass.config.time_zone) + sunrise = dt_util.as_local(get_astral_event_next( + self.hass, 'sunrise', dt_util.as_utc(test_time))) + sunset = dt_util.as_local(get_astral_event_next( + self.hass, 'sunset', dt_util.as_utc(test_time))) + config = { + 'binary_sensor': [ + { + 'platform': 'tod', + 'name': 'Day', + 'after': 'sunrise', + 'before': 'sunset' + } + ] + } + entity_id = 'binary_sensor.day' + testtime = test_time + with patch('homeassistant.components.binary_sensor.tod.dt_util.utcnow', + return_value=testtime): + setup_component(self.hass, 'binary_sensor', config) + + self.hass.block_till_done() + state = self.hass.states.get(entity_id) + assert state.state == STATE_OFF + + testtime = sunrise + timedelta(seconds=-1) + with patch('homeassistant.components.binary_sensor.tod.dt_util.utcnow', + return_value=testtime): + + self.hass.bus.fire(ha.EVENT_TIME_CHANGED, { + ha.ATTR_NOW: testtime}) + self.hass.block_till_done() + + state = self.hass.states.get(entity_id) + assert state.state == STATE_OFF + + testtime = sunrise + with patch('homeassistant.components.binary_sensor.tod.dt_util.utcnow', + return_value=testtime): + + self.hass.bus.fire(ha.EVENT_TIME_CHANGED, { + ha.ATTR_NOW: testtime}) + self.hass.block_till_done() + + state = self.hass.states.get(entity_id) + assert state.state == STATE_ON + + testtime = sunrise + timedelta(seconds=1) + with patch('homeassistant.components.binary_sensor.tod.dt_util.utcnow', + return_value=testtime): + + self.hass.bus.fire(ha.EVENT_TIME_CHANGED, { + ha.ATTR_NOW: testtime}) + self.hass.block_till_done() + + state = self.hass.states.get(entity_id) + assert state.state == STATE_ON + + self.hass.block_till_done() + + testtime = sunset + timedelta(seconds=-1) + with patch('homeassistant.components.binary_sensor.tod.dt_util.utcnow', + return_value=testtime): + + self.hass.bus.fire(ha.EVENT_TIME_CHANGED, { + ha.ATTR_NOW: testtime}) + self.hass.block_till_done() + + state = self.hass.states.get(entity_id) + assert state.state == STATE_ON + + self.hass.block_till_done() + + testtime = sunset + with patch('homeassistant.components.binary_sensor.tod.dt_util.utcnow', + return_value=testtime): + + self.hass.bus.fire(ha.EVENT_TIME_CHANGED, { + ha.ATTR_NOW: testtime}) + self.hass.block_till_done() + + state = self.hass.states.get(entity_id) + assert state.state == STATE_OFF + + self.hass.block_till_done() + + testtime = sunset + timedelta(seconds=1) + with patch('homeassistant.components.binary_sensor.tod.dt_util.utcnow', + return_value=testtime): + + self.hass.bus.fire(ha.EVENT_TIME_CHANGED, { + ha.ATTR_NOW: testtime}) + self.hass.block_till_done() + + state = self.hass.states.get(entity_id) + assert state.state == STATE_OFF + + def test_norwegian_case_summer(self): + """Test location in Norway where the sun doesn't set in summer.""" + self.hass.config.latitude = 69.6 + self.hass.config.longitude = 18.8 + + test_time = datetime(2010, 6, 1, tzinfo=self.hass.config.time_zone) + sunrise = dt_util.as_local(get_astral_event_next( + self.hass, 'sunrise', dt_util.as_utc(test_time))) + sunset = dt_util.as_local(get_astral_event_next( + self.hass, 'sunset', dt_util.as_utc(test_time))) + print(sunrise) + print(sunset) + config = { + 'binary_sensor': [ + { + 'platform': 'tod', + 'name': 'Day', + 'after': 'sunrise', + 'before': 'sunset' + } + ] + } + entity_id = 'binary_sensor.day' + testtime = test_time + with patch('homeassistant.components.binary_sensor.tod.dt_util.utcnow', + return_value=testtime): + setup_component(self.hass, 'binary_sensor', config) + + self.hass.block_till_done() + state = self.hass.states.get(entity_id) + assert state.state == STATE_OFF + + testtime = sunrise + timedelta(seconds=-1) + with patch('homeassistant.components.binary_sensor.tod.dt_util.utcnow', + return_value=testtime): + + self.hass.bus.fire(ha.EVENT_TIME_CHANGED, { + ha.ATTR_NOW: testtime}) + self.hass.block_till_done() + + state = self.hass.states.get(entity_id) + assert state.state == STATE_OFF + + testtime = sunrise + with patch('homeassistant.components.binary_sensor.tod.dt_util.utcnow', + return_value=testtime): + + self.hass.bus.fire(ha.EVENT_TIME_CHANGED, { + ha.ATTR_NOW: testtime}) + self.hass.block_till_done() + + state = self.hass.states.get(entity_id) + assert state.state == STATE_ON + + testtime = sunrise + timedelta(seconds=1) + with patch('homeassistant.components.binary_sensor.tod.dt_util.utcnow', + return_value=testtime): + + self.hass.bus.fire(ha.EVENT_TIME_CHANGED, { + ha.ATTR_NOW: testtime}) + self.hass.block_till_done() + + state = self.hass.states.get(entity_id) + assert state.state == STATE_ON + + self.hass.block_till_done() + + testtime = sunset + timedelta(seconds=-1) + with patch('homeassistant.components.binary_sensor.tod.dt_util.utcnow', + return_value=testtime): + + self.hass.bus.fire(ha.EVENT_TIME_CHANGED, { + ha.ATTR_NOW: testtime}) + self.hass.block_till_done() + + state = self.hass.states.get(entity_id) + assert state.state == STATE_ON + + self.hass.block_till_done() + + testtime = sunset + with patch('homeassistant.components.binary_sensor.tod.dt_util.utcnow', + return_value=testtime): + + self.hass.bus.fire(ha.EVENT_TIME_CHANGED, { + ha.ATTR_NOW: testtime}) + self.hass.block_till_done() + + state = self.hass.states.get(entity_id) + assert state.state == STATE_OFF + + self.hass.block_till_done() + + testtime = sunset + timedelta(seconds=1) + with patch('homeassistant.components.binary_sensor.tod.dt_util.utcnow', + return_value=testtime): + + self.hass.bus.fire(ha.EVENT_TIME_CHANGED, { + ha.ATTR_NOW: testtime}) + self.hass.block_till_done() + + state = self.hass.states.get(entity_id) + assert state.state == STATE_OFF + + def test_sun_offset(self): + """Test sun event with offset.""" + test_time = datetime( + 2019, 1, 12, tzinfo=self.hass.config.time_zone) + sunrise = dt_util.as_local(get_astral_event_date( + self.hass, 'sunrise', dt_util.as_utc(test_time)) + + timedelta(hours=-1, minutes=-30)) + sunset = dt_util.as_local(get_astral_event_date( + self.hass, 'sunset', dt_util.as_utc(test_time)) + + timedelta(hours=1, minutes=30)) + config = { + 'binary_sensor': [ + { + 'platform': 'tod', + 'name': 'Day', + 'after': 'sunrise', + 'after_offset': '-1:30', + 'before': 'sunset', + 'before_offset': '1:30' + } + ] + } + entity_id = 'binary_sensor.day' + testtime = sunrise + timedelta(seconds=-1) + with patch('homeassistant.components.binary_sensor.tod.dt_util.utcnow', + return_value=testtime): + setup_component(self.hass, 'binary_sensor', config) + + self.hass.block_till_done() + state = self.hass.states.get(entity_id) + assert state.state == STATE_OFF + + testtime = sunrise + with patch('homeassistant.components.binary_sensor.tod.dt_util.utcnow', + return_value=testtime): + + self.hass.bus.fire(ha.EVENT_TIME_CHANGED, { + ha.ATTR_NOW: testtime}) + self.hass.block_till_done() + + state = self.hass.states.get(entity_id) + assert state.state == STATE_ON + + testtime = sunrise + timedelta(seconds=1) + with patch('homeassistant.components.binary_sensor.tod.dt_util.utcnow', + return_value=testtime): + + self.hass.bus.fire(ha.EVENT_TIME_CHANGED, { + ha.ATTR_NOW: testtime}) + self.hass.block_till_done() + + state = self.hass.states.get(entity_id) + assert state.state == STATE_ON + + self.hass.block_till_done() + + testtime = sunset + timedelta(seconds=-1) + with patch('homeassistant.components.binary_sensor.tod.dt_util.utcnow', + return_value=testtime): + + self.hass.bus.fire(ha.EVENT_TIME_CHANGED, { + ha.ATTR_NOW: testtime}) + self.hass.block_till_done() + + state = self.hass.states.get(entity_id) + assert state.state == STATE_ON + + self.hass.block_till_done() + + testtime = sunset + with patch('homeassistant.components.binary_sensor.tod.dt_util.utcnow', + return_value=testtime): + + self.hass.bus.fire(ha.EVENT_TIME_CHANGED, { + ha.ATTR_NOW: testtime}) + self.hass.block_till_done() + + state = self.hass.states.get(entity_id) + assert state.state == STATE_OFF + + self.hass.block_till_done() + + testtime = sunset + timedelta(seconds=1) + with patch('homeassistant.components.binary_sensor.tod.dt_util.utcnow', + return_value=testtime): + + self.hass.bus.fire(ha.EVENT_TIME_CHANGED, { + ha.ATTR_NOW: testtime}) + self.hass.block_till_done() + + state = self.hass.states.get(entity_id) + assert state.state == STATE_OFF + + test_time = test_time + timedelta(days=1) + sunrise = dt_util.as_local(get_astral_event_date( + self.hass, 'sunrise', dt_util.as_utc(test_time)) + + timedelta(hours=-1, minutes=-30)) + testtime = sunrise + with patch('homeassistant.components.binary_sensor.tod.dt_util.utcnow', + return_value=testtime): + + self.hass.bus.fire(ha.EVENT_TIME_CHANGED, { + ha.ATTR_NOW: testtime}) + self.hass.block_till_done() + + state = self.hass.states.get(entity_id) + assert state.state == STATE_ON + + def test_dst(self): + """Test sun event with offset.""" + self.hass.config.time_zone = pytz.timezone('CET') + test_time = datetime( + 2019, 3, 30, 3, 0, 0, tzinfo=self.hass.config.time_zone) + config = { + 'binary_sensor': [ + { + 'platform': 'tod', + 'name': 'Day', + 'after': '2:30', + 'before': '2:40' + } + ] + } + # after 2019-03-30 03:00 CET the next update should ge scheduled + # at 3:30 not 2:30 local time + # Internally the + entity_id = 'binary_sensor.day' + testtime = test_time + with patch('homeassistant.components.binary_sensor.tod.dt_util.utcnow', + return_value=testtime): + setup_component(self.hass, 'binary_sensor', config) + + self.hass.block_till_done() + state = self.hass.states.get(entity_id) + state.attributes['after'] == '2019-03-31T03:30:00+02:00' + state.attributes['before'] == '2019-03-31T03:40:00+02:00' + state.attributes['next_update'] == '2019-03-31T03:30:00+02:00' + assert state.state == STATE_OFF diff --git a/tests/components/climate/common.py b/tests/components/climate/common.py index d1626e1f23540..b5b6137a0a8d7 100644 --- a/tests/components/climate/common.py +++ b/tests/components/climate/common.py @@ -3,8 +3,9 @@ All containing methods are legacy helpers that should not be used by new components. Instead call the service directly. """ -from homeassistant.components.climate import ( - _LOGGER, ATTR_AUX_HEAT, ATTR_AWAY_MODE, ATTR_FAN_MODE, ATTR_HOLD_MODE, +from homeassistant.components.climate import _LOGGER +from homeassistant.components.climate.const import ( + ATTR_AUX_HEAT, ATTR_AWAY_MODE, ATTR_FAN_MODE, ATTR_HOLD_MODE, ATTR_HUMIDITY, ATTR_OPERATION_MODE, ATTR_SWING_MODE, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, DOMAIN, SERVICE_SET_AWAY_MODE, SERVICE_SET_HOLD_MODE, SERVICE_SET_AUX_HEAT, SERVICE_SET_TEMPERATURE, SERVICE_SET_HUMIDITY, diff --git a/tests/components/climate/test_demo.py b/tests/components/climate/test_demo.py index 3a023916741c6..3166b2d315864 100644 --- a/tests/components/climate/test_demo.py +++ b/tests/components/climate/test_demo.py @@ -8,7 +8,9 @@ METRIC_SYSTEM ) from homeassistant.setup import setup_component -from homeassistant.components import climate +from homeassistant.components.climate import ( + DOMAIN, SERVICE_TURN_OFF, SERVICE_TURN_ON) +from homeassistant.const import (ATTR_ENTITY_ID) from tests.common import get_test_home_assistant from tests.components.climate import common @@ -26,7 +28,7 @@ def setUp(self): # pylint: disable=invalid-name """Set up things to be run when tests are started.""" self.hass = get_test_home_assistant() self.hass.config.units = METRIC_SYSTEM - assert setup_component(self.hass, climate.DOMAIN, { + assert setup_component(self.hass, DOMAIN, { 'climate': { 'platform': 'demo', }}) @@ -267,14 +269,14 @@ def test_set_on_off(self): state = self.hass.states.get(ENTITY_ECOBEE) assert 'auto' == state.state - self.hass.services.call(climate.DOMAIN, climate.SERVICE_TURN_OFF, - {climate.ATTR_ENTITY_ID: ENTITY_ECOBEE}) + self.hass.services.call(DOMAIN, SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: ENTITY_ECOBEE}) self.hass.block_till_done() state = self.hass.states.get(ENTITY_ECOBEE) assert 'off' == state.state - self.hass.services.call(climate.DOMAIN, climate.SERVICE_TURN_ON, - {climate.ATTR_ENTITY_ID: ENTITY_ECOBEE}) + self.hass.services.call(DOMAIN, SERVICE_TURN_ON, + {ATTR_ENTITY_ID: ENTITY_ECOBEE}) self.hass.block_till_done() state = self.hass.states.get(ENTITY_ECOBEE) assert 'auto' == state.state diff --git a/tests/components/climate/test_generic_thermostat.py b/tests/components/climate/test_generic_thermostat.py index 8d2346260d932..1d532f4757cff 100644 --- a/tests/components/climate/test_generic_thermostat.py +++ b/tests/components/climate/test_generic_thermostat.py @@ -20,8 +20,9 @@ ) from homeassistant import loader from homeassistant.util.unit_system import METRIC_SYSTEM -from homeassistant.components import climate, input_boolean, switch -from homeassistant.components.climate import STATE_HEAT, STATE_COOL +from homeassistant.components import input_boolean, switch +from homeassistant.components.climate.const import ( + ATTR_OPERATION_MODE, STATE_HEAT, STATE_COOL, DOMAIN) import homeassistant.components as comps from tests.common import assert_setup_component, mock_restore_cache from tests.components.climate import common @@ -77,7 +78,7 @@ async def test_heater_input_boolean(hass, setup_comp_1): assert await async_setup_component(hass, input_boolean.DOMAIN, { 'input_boolean': {'test': None}}) - assert await async_setup_component(hass, climate.DOMAIN, {'climate': { + assert await async_setup_component(hass, DOMAIN, {'climate': { 'platform': 'generic_thermostat', 'name': 'test', 'heater': heater_switch, @@ -105,7 +106,7 @@ async def test_heater_switch(hass, setup_comp_1): 'platform': 'test'}}) heater_switch = switch_1.entity_id - assert await async_setup_component(hass, climate.DOMAIN, {'climate': { + assert await async_setup_component(hass, DOMAIN, {'climate': { 'platform': 'generic_thermostat', 'name': 'test', 'heater': heater_switch, @@ -134,7 +135,7 @@ def setup_comp_2(hass): """Initialize components.""" hass.config.units = METRIC_SYSTEM assert hass.loop.run_until_complete(async_setup_component( - hass, climate.DOMAIN, {'climate': { + hass, DOMAIN, {'climate': { 'platform': 'generic_thermostat', 'name': 'test', 'cold_tolerance': 2, @@ -162,7 +163,7 @@ async def test_get_operation_modes(hass, setup_comp_2): """Test that the operation list returns the correct modes.""" state = hass.states.get(ENTITY) modes = state.attributes.get('operation_list') - assert [climate.STATE_HEAT, STATE_OFF] == modes + assert [STATE_HEAT, STATE_OFF] == modes async def test_set_target_temp(hass, setup_comp_2): @@ -355,7 +356,7 @@ async def test_operating_mode_heat(hass, setup_comp_2): _setup_sensor(hass, 25) await hass.async_block_till_done() calls = _setup_switch(hass, False) - common.async_set_operation_mode(hass, climate.STATE_HEAT) + common.async_set_operation_mode(hass, STATE_HEAT) await hass.async_block_till_done() assert 1 == len(calls) call = calls[0] @@ -385,7 +386,7 @@ def setup_comp_3(hass): """Initialize components.""" hass.config.temperature_unit = TEMP_CELSIUS assert hass.loop.run_until_complete(async_setup_component( - hass, climate.DOMAIN, {'climate': { + hass, DOMAIN, {'climate': { 'platform': 'generic_thermostat', 'name': 'test', 'cold_tolerance': 2, @@ -433,7 +434,7 @@ async def test_operating_mode_cool(hass, setup_comp_3): _setup_sensor(hass, 30) await hass.async_block_till_done() calls = _setup_switch(hass, False) - common.async_set_operation_mode(hass, climate.STATE_COOL) + common.async_set_operation_mode(hass, STATE_COOL) await hass.async_block_till_done() assert 1 == len(calls) call = calls[0] @@ -535,7 +536,7 @@ def setup_comp_4(hass): """Initialize components.""" hass.config.temperature_unit = TEMP_CELSIUS assert hass.loop.run_until_complete(async_setup_component( - hass, climate.DOMAIN, {'climate': { + hass, DOMAIN, {'climate': { 'platform': 'generic_thermostat', 'name': 'test', 'cold_tolerance': 0.3, @@ -611,7 +612,7 @@ async def test_mode_change_ac_trigger_off_not_long_enough(hass, setup_comp_4): _setup_sensor(hass, 25) await hass.async_block_till_done() assert 0 == len(calls) - common.async_set_operation_mode(hass, climate.STATE_OFF) + common.async_set_operation_mode(hass, STATE_OFF) await hass.async_block_till_done() assert 1 == len(calls) call = calls[0] @@ -628,7 +629,7 @@ async def test_mode_change_ac_trigger_on_not_long_enough(hass, setup_comp_4): _setup_sensor(hass, 30) await hass.async_block_till_done() assert 0 == len(calls) - common.async_set_operation_mode(hass, climate.STATE_HEAT) + common.async_set_operation_mode(hass, STATE_HEAT) await hass.async_block_till_done() assert 1 == len(calls) call = calls[0] @@ -642,7 +643,7 @@ def setup_comp_5(hass): """Initialize components.""" hass.config.temperature_unit = TEMP_CELSIUS assert hass.loop.run_until_complete(async_setup_component( - hass, climate.DOMAIN, {'climate': { + hass, DOMAIN, {'climate': { 'platform': 'generic_thermostat', 'name': 'test', 'cold_tolerance': 0.3, @@ -720,7 +721,7 @@ async def test_mode_change_ac_trigger_off_not_long_enough_2( _setup_sensor(hass, 25) await hass.async_block_till_done() assert 0 == len(calls) - common.async_set_operation_mode(hass, climate.STATE_OFF) + common.async_set_operation_mode(hass, STATE_OFF) await hass.async_block_till_done() assert 1 == len(calls) call = calls[0] @@ -737,7 +738,7 @@ async def test_mode_change_ac_trigger_on_not_long_enough_2(hass, setup_comp_5): _setup_sensor(hass, 30) await hass.async_block_till_done() assert 0 == len(calls) - common.async_set_operation_mode(hass, climate.STATE_HEAT) + common.async_set_operation_mode(hass, STATE_HEAT) await hass.async_block_till_done() assert 1 == len(calls) call = calls[0] @@ -751,7 +752,7 @@ def setup_comp_6(hass): """Initialize components.""" hass.config.temperature_unit = TEMP_CELSIUS assert hass.loop.run_until_complete(async_setup_component( - hass, climate.DOMAIN, {'climate': { + hass, DOMAIN, {'climate': { 'platform': 'generic_thermostat', 'name': 'test', 'cold_tolerance': 0.3, @@ -829,7 +830,7 @@ async def test_mode_change_heater_trigger_off_not_long_enough( _setup_sensor(hass, 30) await hass.async_block_till_done() assert 0 == len(calls) - common.async_set_operation_mode(hass, climate.STATE_OFF) + common.async_set_operation_mode(hass, STATE_OFF) await hass.async_block_till_done() assert 1 == len(calls) call = calls[0] @@ -847,7 +848,7 @@ async def test_mode_change_heater_trigger_on_not_long_enough( _setup_sensor(hass, 25) await hass.async_block_till_done() assert 0 == len(calls) - common.async_set_operation_mode(hass, climate.STATE_HEAT) + common.async_set_operation_mode(hass, STATE_HEAT) await hass.async_block_till_done() assert 1 == len(calls) call = calls[0] @@ -861,7 +862,7 @@ def setup_comp_7(hass): """Initialize components.""" hass.config.temperature_unit = TEMP_CELSIUS assert hass.loop.run_until_complete(async_setup_component( - hass, climate.DOMAIN, {'climate': { + hass, DOMAIN, {'climate': { 'platform': 'generic_thermostat', 'name': 'test', 'cold_tolerance': 0.3, @@ -933,7 +934,7 @@ def setup_comp_8(hass): """Initialize components.""" hass.config.temperature_unit = TEMP_CELSIUS assert hass.loop.run_until_complete(async_setup_component( - hass, climate.DOMAIN, {'climate': { + hass, DOMAIN, {'climate': { 'platform': 'generic_thermostat', 'name': 'test', 'cold_tolerance': 0.3, @@ -1000,7 +1001,7 @@ def setup_comp_9(hass): """Initialize components.""" hass.config.temperature_unit = TEMP_CELSIUS assert hass.loop.run_until_complete(async_setup_component( - hass, climate.DOMAIN, {'climate': [ + hass, DOMAIN, {'climate': [ { 'platform': 'generic_thermostat', 'name': 'test_heat', @@ -1080,7 +1081,7 @@ def setup_comp_10(hass): """Initialize components.""" hass.config.temperature_unit = TEMP_FAHRENHEIT assert hass.loop.run_until_complete(async_setup_component( - hass, climate.DOMAIN, {'climate': { + hass, DOMAIN, {'climate': { 'platform': 'generic_thermostat', 'name': 'test', 'cold_tolerance': 0.3, @@ -1109,7 +1110,7 @@ async def test_precision(hass, setup_comp_10): async def test_custom_setup_params(hass): """Test the setup with custom parameters.""" result = await async_setup_component( - hass, climate.DOMAIN, {'climate': { + hass, DOMAIN, {'climate': { 'platform': 'generic_thermostat', 'name': 'test', 'heater': ENT_SWITCH, @@ -1129,13 +1130,14 @@ async def test_restore_state(hass): """Ensure states are restored on startup.""" mock_restore_cache(hass, ( State('climate.test_thermostat', '0', {ATTR_TEMPERATURE: "20", - climate.ATTR_OPERATION_MODE: "off", ATTR_AWAY_MODE: "on"}), + ATTR_OPERATION_MODE: "off", + ATTR_AWAY_MODE: "on"}), )) hass.state = CoreState.starting await async_setup_component( - hass, climate.DOMAIN, {'climate': { + hass, DOMAIN, {'climate': { 'platform': 'generic_thermostat', 'name': 'test_thermostat', 'heater': ENT_SWITCH, @@ -1144,7 +1146,7 @@ async def test_restore_state(hass): state = hass.states.get('climate.test_thermostat') assert(state.attributes[ATTR_TEMPERATURE] == 20) - assert(state.attributes[climate.ATTR_OPERATION_MODE] == "off") + assert(state.attributes[ATTR_OPERATION_MODE] == "off") assert(state.state == STATE_OFF) @@ -1155,13 +1157,14 @@ async def test_no_restore_state(hass): """ mock_restore_cache(hass, ( State('climate.test_thermostat', '0', {ATTR_TEMPERATURE: "20", - climate.ATTR_OPERATION_MODE: "off", ATTR_AWAY_MODE: "on"}), + ATTR_OPERATION_MODE: "off", + ATTR_AWAY_MODE: "on"}), )) hass.state = CoreState.starting await async_setup_component( - hass, climate.DOMAIN, {'climate': { + hass, DOMAIN, {'climate': { 'platform': 'generic_thermostat', 'name': 'test_thermostat', 'heater': ENT_SWITCH, @@ -1191,7 +1194,7 @@ async def test_restore_state_uncoherence_case(hass): state = hass.states.get(ENTITY) assert 20 == state.attributes[ATTR_TEMPERATURE] assert STATE_OFF == \ - state.attributes[climate.ATTR_OPERATION_MODE] + state.attributes[ATTR_OPERATION_MODE] assert STATE_OFF == state.state assert 0 == len(calls) @@ -1199,12 +1202,12 @@ async def test_restore_state_uncoherence_case(hass): await hass.async_block_till_done() state = hass.states.get(ENTITY) assert STATE_OFF == \ - state.attributes[climate.ATTR_OPERATION_MODE] + state.attributes[ATTR_OPERATION_MODE] assert STATE_OFF == state.state async def _setup_climate(hass): - assert await async_setup_component(hass, climate.DOMAIN, {'climate': { + assert await async_setup_component(hass, DOMAIN, {'climate': { 'platform': 'generic_thermostat', 'name': 'test', 'cold_tolerance': 2, @@ -1220,6 +1223,6 @@ def _mock_restore_cache(hass, temperature=20, operation_mode=STATE_OFF): mock_restore_cache(hass, ( State(ENTITY, '0', { ATTR_TEMPERATURE: str(temperature), - climate.ATTR_OPERATION_MODE: operation_mode, + ATTR_OPERATION_MODE: operation_mode, ATTR_AWAY_MODE: "on"}), )) diff --git a/tests/components/climate/test_honeywell.py b/tests/components/climate/test_honeywell.py index 7daed2ff4a971..b01b5b35c35cd 100644 --- a/tests/components/climate/test_honeywell.py +++ b/tests/components/climate/test_honeywell.py @@ -8,7 +8,7 @@ from homeassistant.const import ( CONF_USERNAME, CONF_PASSWORD, TEMP_CELSIUS, TEMP_FAHRENHEIT) -from homeassistant.components.climate import ( +from homeassistant.components.climate.const import ( ATTR_FAN_MODE, ATTR_OPERATION_MODE, ATTR_FAN_LIST, ATTR_OPERATION_LIST) import homeassistant.components.climate.honeywell as honeywell diff --git a/tests/components/climate/test_melissa.py b/tests/components/climate/test_melissa.py index 2c135bfc09d8d..6b77981a9140a 100644 --- a/tests/components/climate/test_melissa.py +++ b/tests/components/climate/test_melissa.py @@ -4,8 +4,9 @@ from homeassistant.components.climate.melissa import MelissaClimate -from homeassistant.components.climate import ( - melissa, SUPPORT_TARGET_TEMPERATURE, SUPPORT_OPERATION_MODE, +from homeassistant.components.climate import melissa +from homeassistant.components.climate.const import ( + SUPPORT_TARGET_TEMPERATURE, SUPPORT_OPERATION_MODE, SUPPORT_ON_OFF, SUPPORT_FAN_MODE, STATE_HEAT, STATE_FAN_ONLY, STATE_DRY, STATE_COOL, STATE_AUTO ) diff --git a/tests/components/climate/test_nuheat.py b/tests/components/climate/test_nuheat.py index 40b0732f661ee..19919d2595440 100644 --- a/tests/components/climate/test_nuheat.py +++ b/tests/components/climate/test_nuheat.py @@ -3,7 +3,7 @@ from unittest.mock import Mock, patch from tests.common import get_test_home_assistant -from homeassistant.components.climate import ( +from homeassistant.components.climate.const import ( SUPPORT_HOLD_MODE, SUPPORT_OPERATION_MODE, SUPPORT_TARGET_TEMPERATURE, diff --git a/tests/components/climate/test_reproduce_state.py b/tests/components/climate/test_reproduce_state.py index c16151320b447..8ec8e7b142901 100644 --- a/tests/components/climate/test_reproduce_state.py +++ b/tests/components/climate/test_reproduce_state.py @@ -2,13 +2,13 @@ import pytest -from homeassistant.components.climate import STATE_HEAT, async_reproduce_states +from homeassistant.components.climate import async_reproduce_states from homeassistant.components.climate.const import ( ATTR_AUX_HEAT, ATTR_AWAY_MODE, ATTR_HOLD_MODE, ATTR_HUMIDITY, ATTR_OPERATION_MODE, ATTR_SWING_MODE, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, DOMAIN, SERVICE_SET_AUX_HEAT, SERVICE_SET_AWAY_MODE, SERVICE_SET_HOLD_MODE, SERVICE_SET_HUMIDITY, SERVICE_SET_OPERATION_MODE, - SERVICE_SET_SWING_MODE, SERVICE_SET_TEMPERATURE) + SERVICE_SET_SWING_MODE, SERVICE_SET_TEMPERATURE, STATE_HEAT) from homeassistant.const import ( ATTR_TEMPERATURE, SERVICE_TURN_OFF, SERVICE_TURN_ON, STATE_OFF, STATE_ON) from homeassistant.core import Context, State diff --git a/tests/components/config/test_config_entries.py b/tests/components/config/test_config_entries.py index be73906c1bfbe..87ed83d9a7ef3 100644 --- a/tests/components/config/test_config_entries.py +++ b/tests/components/config/test_config_entries.py @@ -7,8 +7,9 @@ import pytest import voluptuous as vol -from homeassistant import config_entries as core_ce +from homeassistant import config_entries as core_ce, data_entry_flow from homeassistant.config_entries import HANDLERS +from homeassistant.core import callback from homeassistant.setup import async_setup_component from homeassistant.components.config import config_entries from homeassistant.loader import set_component @@ -30,25 +31,37 @@ def client(hass, hass_client): yield hass.loop.run_until_complete(hass_client()) -@asyncio.coroutine -def test_get_entries(hass, client): +async def test_get_entries(hass, client): """Test get entries.""" MockConfigEntry( - domain='comp', - title='Test 1', - source='bla', - connection_class=core_ce.CONN_CLASS_LOCAL_POLL, + domain='comp', + title='Test 1', + source='bla', + connection_class=core_ce.CONN_CLASS_LOCAL_POLL, ).add_to_hass(hass) MockConfigEntry( - domain='comp2', - title='Test 2', - source='bla2', - state=core_ce.ENTRY_STATE_LOADED, - connection_class=core_ce.CONN_CLASS_ASSUMED, + domain='comp2', + title='Test 2', + source='bla2', + state=core_ce.ENTRY_STATE_LOADED, + connection_class=core_ce.CONN_CLASS_ASSUMED, ).add_to_hass(hass) - resp = yield from client.get('/api/config/config_entries/entry') + + class CompConfigFlow: + @staticmethod + @callback + def async_get_options_flow(config, options): + pass + HANDLERS['comp'] = CompConfigFlow() + + class Comp2ConfigFlow: + def __init__(self): + pass + HANDLERS['comp2'] = Comp2ConfigFlow() + + resp = await client.get('/api/config/config_entries/entry') assert resp.status == 200 - data = yield from resp.json() + data = await resp.json() for entry in data: entry.pop('entry_id') assert data == [ @@ -58,6 +71,7 @@ def test_get_entries(hass, client): 'source': 'bla', 'state': 'not_loaded', 'connection_class': 'local_poll', + 'supports_options': True, }, { 'domain': 'comp2', @@ -65,6 +79,7 @@ def test_get_entries(hass, client): 'source': 'bla2', 'state': 'loaded', 'connection_class': 'assumed', + 'supports_options': False, }, ] @@ -467,3 +482,136 @@ async def async_step_user(self, user_input=None): '/api/config/config_entries/flow/{}'.format(data['flow_id'])) assert resp2.status == 401 + + +async def test_options_flow(hass, client): + """Test we can change options.""" + class TestFlow(core_ce.ConfigFlow): + @staticmethod + @callback + def async_get_options_flow(config, options): + class OptionsFlowHandler(data_entry_flow.FlowHandler): + def __init__(self, config, options): + self.config = config + self.options = options + + async def async_step_init(self, user_input=None): + schema = OrderedDict() + schema[vol.Required('enabled')] = bool + return self.async_show_form( + step_id='user', + data_schema=schema, + description_placeholders={ + 'enabled': 'Set to true to be true', + } + ) + return OptionsFlowHandler(config, options) + + MockConfigEntry( + domain='test', + entry_id='test1', + source='bla', + connection_class=core_ce.CONN_CLASS_LOCAL_POLL, + ).add_to_hass(hass) + entry = hass.config_entries._entries[0] + + with patch.dict(HANDLERS, {'test': TestFlow}): + url = '/api/config/config_entries/entry/option/flow' + resp = await client.post(url, json={'handler': entry.entry_id}) + + assert resp.status == 200 + data = await resp.json() + + data.pop('flow_id') + assert data == { + 'type': 'form', + 'handler': 'test1', + 'step_id': 'user', + 'data_schema': [ + { + 'name': 'enabled', + 'required': True, + 'type': 'boolean' + }, + ], + 'description_placeholders': { + 'enabled': 'Set to true to be true', + }, + 'errors': None + } + + +async def test_two_step_options_flow(hass, client): + """Test we can finish a two step options flow.""" + set_component( + hass, 'test', + MockModule('test', async_setup_entry=mock_coro_func(True))) + + class TestFlow(core_ce.ConfigFlow): + @staticmethod + @callback + def async_get_options_flow(config, options): + class OptionsFlowHandler(data_entry_flow.FlowHandler): + def __init__(self, config, options): + self.config = config + self.options = options + + async def async_step_init(self, user_input=None): + return self.async_show_form( + step_id='finish', + data_schema=vol.Schema({ + 'enabled': bool + }) + ) + + async def async_step_finish(self, user_input=None): + return self.async_create_entry( + title='Enable disable', + data=user_input + ) + return OptionsFlowHandler(config, options) + + MockConfigEntry( + domain='test', + entry_id='test1', + source='bla', + connection_class=core_ce.CONN_CLASS_LOCAL_POLL, + ).add_to_hass(hass) + entry = hass.config_entries._entries[0] + + with patch.dict(HANDLERS, {'test': TestFlow}): + url = '/api/config/config_entries/entry/option/flow' + resp = await client.post(url, json={'handler': entry.entry_id}) + + assert resp.status == 200 + data = await resp.json() + flow_id = data.pop('flow_id') + assert data == { + 'type': 'form', + 'handler': 'test1', + 'step_id': 'finish', + 'data_schema': [ + { + 'name': 'enabled', + 'type': 'boolean' + } + ], + 'description_placeholders': None, + 'errors': None + } + + with patch.dict(HANDLERS, {'test': TestFlow}): + resp = await client.post( + '/api/config/config_entries/options/flow/{}'.format(flow_id), + json={'enabled': True}) + assert resp.status == 200 + data = await resp.json() + data.pop('flow_id') + assert data == { + 'handler': 'test1', + 'type': 'create_entry', + 'title': 'Enable disable', + 'version': 1, + 'description': None, + 'description_placeholders': None, + } diff --git a/tests/components/config/test_device_registry.py b/tests/components/config/test_device_registry.py index aa1b9e4e2d4b7..de603707ae207 100644 --- a/tests/components/config/test_device_registry.py +++ b/tests/components/config/test_device_registry.py @@ -49,6 +49,7 @@ async def test_list_devices(hass, client, registry): 'sw_version': None, 'hub_device_id': None, 'area_id': None, + 'name_by_user': None, }, { 'config_entries': ['1234'], @@ -59,6 +60,7 @@ async def test_list_devices(hass, client, registry): 'sw_version': None, 'hub_device_id': dev1, 'area_id': None, + 'name_by_user': None, } ] @@ -72,11 +74,13 @@ async def test_update_device(hass, client, registry): manufacturer='manufacturer', model='model') assert not device.area_id + assert not device.name_by_user await client.send_json({ 'id': 1, 'device_id': device.id, 'area_id': '12345A', + 'name_by_user': 'Test Friendly Name', 'type': 'config/device_registry/update', }) @@ -84,4 +88,5 @@ async def test_update_device(hass, client, registry): assert msg['result']['id'] == device.id assert msg['result']['area_id'] == '12345A' + assert msg['result']['name_by_user'] == 'Test Friendly Name' assert len(registry.devices) == 1 diff --git a/tests/components/deconz/test_climate.py b/tests/components/deconz/test_climate.py new file mode 100644 index 0000000000000..fa274f1d676ca --- /dev/null +++ b/tests/components/deconz/test_climate.py @@ -0,0 +1,189 @@ +"""deCONZ climate platform tests.""" +from unittest.mock import Mock, patch + +import asynctest + +from homeassistant import config_entries +from homeassistant.components import deconz +from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.setup import async_setup_component + +import homeassistant.components.climate as climate + +from tests.common import mock_coro + + +SENSOR = { + "1": { + "id": "Climate 1 id", + "name": "Climate 1 name", + "type": "ZHAThermostat", + "state": {"on": True, "temperature": 2260}, + "config": {"battery": 100, "heatsetpoint": 2200, "mode": "auto", + "offset": 10, "reachable": True, "valve": 30}, + "uniqueid": "00:00:00:00:00:00:00:00-00" + }, + "2": { + "id": "Sensor 2 id", + "name": "Sensor 2 name", + "type": "ZHAPresence", + "state": {"presence": False}, + "config": {} + } +} + +ENTRY_CONFIG = { + deconz.const.CONF_ALLOW_CLIP_SENSOR: True, + deconz.const.CONF_ALLOW_DECONZ_GROUPS: True, + deconz.config_flow.CONF_API_KEY: "ABCDEF", + deconz.config_flow.CONF_BRIDGEID: "0123456789", + deconz.config_flow.CONF_HOST: "1.2.3.4", + deconz.config_flow.CONF_PORT: 80 +} + + +async def setup_gateway(hass, data, allow_clip_sensor=True): + """Load the deCONZ sensor platform.""" + from pydeconz import DeconzSession + + session = Mock(put=asynctest.CoroutineMock( + return_value=Mock(status=200, + json=asynctest.CoroutineMock(), + text=asynctest.CoroutineMock(), + ) + ) + ) + + ENTRY_CONFIG[deconz.const.CONF_ALLOW_CLIP_SENSOR] = allow_clip_sensor + + config_entry = config_entries.ConfigEntry( + 1, deconz.DOMAIN, 'Mock Title', ENTRY_CONFIG, 'test', + config_entries.CONN_CLASS_LOCAL_PUSH) + gateway = deconz.DeconzGateway(hass, config_entry) + gateway.api = DeconzSession(hass.loop, session, **config_entry.data) + gateway.api.config = Mock() + hass.data[deconz.DOMAIN] = gateway + + with patch('pydeconz.DeconzSession.async_get_state', + return_value=mock_coro(data)): + await gateway.api.async_load_parameters() + + await hass.config_entries.async_forward_entry_setup( + config_entry, 'climate') + # To flush out the service call to update the group + await hass.async_block_till_done() + + +async def test_platform_manually_configured(hass): + """Test that we do not discover anything or try to set up a gateway.""" + assert await async_setup_component(hass, climate.DOMAIN, { + 'climate': { + 'platform': deconz.DOMAIN + } + }) is True + assert deconz.DOMAIN not in hass.data + + +async def test_no_sensors(hass): + """Test that no sensors in deconz results in no climate entities.""" + await setup_gateway(hass, {}) + assert not hass.data[deconz.DOMAIN].deconz_ids + assert not hass.states.async_all() + + +async def test_climate_devices(hass): + """Test successful creation of sensor entities.""" + await setup_gateway(hass, {"sensors": SENSOR}) + assert "climate.climate_1_name" in hass.data[deconz.DOMAIN].deconz_ids + assert "sensor.sensor_2_name" not in hass.data[deconz.DOMAIN].deconz_ids + assert len(hass.states.async_all()) == 1 + + hass.data[deconz.DOMAIN].api.sensors['1'].async_update( + {'state': {'on': False}}) + + await hass.services.async_call( + 'climate', 'turn_on', {'entity_id': 'climate.climate_1_name'}, + blocking=True + ) + hass.data[deconz.DOMAIN].api.session.put.assert_called_with( + 'http://1.2.3.4:80/api/ABCDEF/sensors/1/config', + data='{"mode": "auto"}' + ) + + await hass.services.async_call( + 'climate', 'turn_off', {'entity_id': 'climate.climate_1_name'}, + blocking=True + ) + hass.data[deconz.DOMAIN].api.session.put.assert_called_with( + 'http://1.2.3.4:80/api/ABCDEF/sensors/1/config', + data='{"mode": "off"}' + ) + + await hass.services.async_call( + 'climate', 'set_temperature', + {'entity_id': 'climate.climate_1_name', 'temperature': 20}, + blocking=True + ) + hass.data[deconz.DOMAIN].api.session.put.assert_called_with( + 'http://1.2.3.4:80/api/ABCDEF/sensors/1/config', + data='{"heatsetpoint": 2000.0}' + ) + + assert len(hass.data[deconz.DOMAIN].api.session.put.mock_calls) == 3 + + +async def test_verify_state_update(hass): + """Test that state update properly.""" + await setup_gateway(hass, {"sensors": SENSOR}) + assert "climate.climate_1_name" in hass.data[deconz.DOMAIN].deconz_ids + + thermostat = hass.states.get('climate.climate_1_name') + assert thermostat.state == 'on' + + state_update = { + "t": "event", + "e": "changed", + "r": "sensors", + "id": "1", + "config": {"on": False} + } + hass.data[deconz.DOMAIN].api.async_event_handler(state_update) + + await hass.async_block_till_done() + assert len(hass.states.async_all()) == 1 + + thermostat = hass.states.get('climate.climate_1_name') + assert thermostat.state == 'off' + + +async def test_add_new_climate_device(hass): + """Test successful creation of climate entities.""" + await setup_gateway(hass, {}) + sensor = Mock() + sensor.name = 'name' + sensor.type = 'ZHAThermostat' + sensor.register_async_callback = Mock() + async_dispatcher_send(hass, 'deconz_new_sensor', [sensor]) + await hass.async_block_till_done() + assert "climate.name" in hass.data[deconz.DOMAIN].deconz_ids + + +async def test_do_not_allow_clipsensor(hass): + """Test that clip sensors can be ignored.""" + await setup_gateway(hass, {}, allow_clip_sensor=False) + sensor = Mock() + sensor.name = 'name' + sensor.type = 'CLIPThermostat' + sensor.register_async_callback = Mock() + async_dispatcher_send(hass, 'deconz_new_sensor', [sensor]) + await hass.async_block_till_done() + assert len(hass.data[deconz.DOMAIN].deconz_ids) == 0 + + +async def test_unload_sensor(hass): + """Test that it works to unload sensor entities.""" + await setup_gateway(hass, {"sensors": SENSOR}) + + await hass.data[deconz.DOMAIN].async_reset() + + assert len(hass.states.async_all()) == 0 diff --git a/tests/components/deconz/test_gateway.py b/tests/components/deconz/test_gateway.py index dbc45c955b564..d73f225b2acfb 100644 --- a/tests/components/deconz/test_gateway.py +++ b/tests/components/deconz/test_gateway.py @@ -35,18 +35,20 @@ async def test_gateway_setup(): assert await deconz_gateway.async_setup() is True assert deconz_gateway.api is api - assert len(hass.config_entries.async_forward_entry_setup.mock_calls) == 6 + assert len(hass.config_entries.async_forward_entry_setup.mock_calls) == 7 assert hass.config_entries.async_forward_entry_setup.mock_calls[0][1] == \ (entry, 'binary_sensor') assert hass.config_entries.async_forward_entry_setup.mock_calls[1][1] == \ - (entry, 'cover') + (entry, 'climate') assert hass.config_entries.async_forward_entry_setup.mock_calls[2][1] == \ - (entry, 'light') + (entry, 'cover') assert hass.config_entries.async_forward_entry_setup.mock_calls[3][1] == \ - (entry, 'scene') + (entry, 'light') assert hass.config_entries.async_forward_entry_setup.mock_calls[4][1] == \ - (entry, 'sensor') + (entry, 'scene') assert hass.config_entries.async_forward_entry_setup.mock_calls[5][1] == \ + (entry, 'sensor') + assert hass.config_entries.async_forward_entry_setup.mock_calls[6][1] == \ (entry, 'switch') assert len(api.start.mock_calls) == 1 @@ -150,7 +152,7 @@ async def test_reset_after_successful_setup(): mock_coro(True) assert await deconz_gateway.async_reset() is True - assert len(hass.config_entries.async_forward_entry_unload.mock_calls) == 6 + assert len(hass.config_entries.async_forward_entry_unload.mock_calls) == 7 assert len(listener.mock_calls) == 1 assert len(deconz_gateway.listeners) == 0 diff --git a/tests/components/deconz/test_light.py b/tests/components/deconz/test_light.py index 081fd61ec4e38..49c3f280d8a34 100644 --- a/tests/components/deconz/test_light.py +++ b/tests/components/deconz/test_light.py @@ -41,14 +41,15 @@ "lights": [ "1", "2" - ] + ], }, "2": { "id": "Group 2 id", "name": "Group 2 name", "state": {}, "action": {}, - "scenes": [] + "scenes": [], + "lights": [], }, } diff --git a/tests/components/deconz/test_scene.py b/tests/components/deconz/test_scene.py index 788c6dc1c3e78..963f1064b3565 100644 --- a/tests/components/deconz/test_scene.py +++ b/tests/components/deconz/test_scene.py @@ -20,6 +20,7 @@ "id": "1", "name": "Scene 1" }], + "lights": [], } } diff --git a/tests/components/device_tracker/test_owntracks.py b/tests/components/device_tracker/test_owntracks.py index 5e65e0a75c77b..8e86829670394 100644 --- a/tests/components/device_tracker/test_owntracks.py +++ b/tests/components/device_tracker/test_owntracks.py @@ -1,14 +1,16 @@ """The tests for the Owntracks device tracker.""" import json + from asynctest import patch import pytest -from tests.common import ( - async_fire_mqtt_message, mock_coro, mock_component, - async_mock_mqtt_component, MockConfigEntry) from homeassistant.components import owntracks -from homeassistant.setup import async_setup_component from homeassistant.const import STATE_NOT_HOME +from homeassistant.setup import async_setup_component + +from tests.common import ( + MockConfigEntry, async_fire_mqtt_message, async_mock_mqtt_component, + mock_coro) USER = 'greg' DEVICE = 'phone' @@ -45,8 +47,8 @@ # Home Assistant Zones INNER_ZONE = { 'name': 'zone', - 'latitude': TEST_ZONE_LAT+0.1, - 'longitude': TEST_ZONE_LON+0.1, + 'latitude': TEST_ZONE_LAT + 0.1, + 'longitude': TEST_ZONE_LON + 0.1, 'radius': 50 } @@ -271,12 +273,14 @@ def build_message(test_params, default_params): BAD_JSON_PREFIX = '--$this is bad json#--' BAD_JSON_SUFFIX = '** and it ends here ^^' +# pylint: disable=invalid-name, len-as-condition, redefined-outer-name + @pytest.fixture -def setup_comp(hass): +def setup_comp(hass, mock_device_tracker_conf): """Initialize components.""" - mock_component(hass, 'group') - mock_component(hass, 'zone') + assert hass.loop.run_until_complete(async_setup_component( + hass, 'persistent_notification', {})) hass.loop.run_until_complete(async_setup_component( hass, 'device_tracker', {})) hass.loop.run_until_complete(async_mock_mqtt_component(hass)) @@ -289,48 +293,42 @@ def setup_comp(hass): hass.states.async_set( 'zone.outer', 'zoning', OUTER_ZONE) + yield async def setup_owntracks(hass, config, ctx_cls=owntracks.OwnTracksContext): """Set up OwnTracks.""" - await async_mock_mqtt_component(hass) - MockConfigEntry(domain='owntracks', data={ 'webhook_id': 'owntracks_test', 'secret': 'abcd', }).add_to_hass(hass) - with patch('homeassistant.components.device_tracker.async_load_config', - return_value=mock_coro([])), \ - patch('homeassistant.components.device_tracker.' - 'load_yaml_config_file', return_value=mock_coro({})), \ - patch.object(owntracks, 'OwnTracksContext', ctx_cls): + with patch.object(owntracks, 'OwnTracksContext', ctx_cls): assert await async_setup_component( hass, 'owntracks', {'owntracks': config}) + await hass.async_block_till_done() @pytest.fixture def context(hass, setup_comp): """Set up the mocked context.""" - patcher = patch('homeassistant.components.device_tracker.' - 'DeviceTracker.async_update_config') - patcher.start() - orig_context = owntracks.OwnTracksContext - context = None + # pylint: disable=no-value-for-parameter + def store_context(*args): + """Store the context.""" nonlocal context context = orig_context(*args) return context hass.loop.run_until_complete(setup_owntracks(hass, { - CONF_MAX_GPS_ACCURACY: 200, - CONF_WAYPOINT_IMPORT: True, - CONF_WAYPOINT_WHITELIST: ['jon', 'greg'] - }, store_context)) + CONF_MAX_GPS_ACCURACY: 200, + CONF_WAYPOINT_IMPORT: True, + CONF_WAYPOINT_WHITELIST: ['jon', 'greg'] + }, store_context)) def get_context(): """Get the current context.""" @@ -338,8 +336,6 @@ def get_context(): yield get_context - patcher.stop() - async def send_message(hass, topic, message, corrupt=False): """Test the sending of a message.""" @@ -851,7 +847,7 @@ async def test_event_beacon_unknown_zone_no_location(hass, context): # that will be tracked at my current location. Except # in this case my Device hasn't had a location message # yet so it's in an odd state where it has state.state - # None and no GPS coords so set the beacon to. + # None and no GPS coords to set the beacon to. hass.states.async_set(DEVICE_TRACKER_STATE, None) message = build_message( @@ -993,8 +989,7 @@ async def test_mobile_multiple_async_enter_exit(hass, context): await hass.async_block_till_done() await send_message(hass, EVENT_TOPIC, MOBILE_BEACON_LEAVE_EVENT_MESSAGE) - assert len(context().mobile_beacons_active['greg_phone']) == \ - 0 + assert len(context().mobile_beacons_active['greg_phone']) == 0 async def test_mobile_multiple_enter_exit(hass, context): @@ -1003,8 +998,7 @@ async def test_mobile_multiple_enter_exit(hass, context): await send_message(hass, EVENT_TOPIC, MOBILE_BEACON_ENTER_EVENT_MESSAGE) await send_message(hass, EVENT_TOPIC, MOBILE_BEACON_LEAVE_EVENT_MESSAGE) - assert len(context().mobile_beacons_active['greg_phone']) == \ - 0 + assert len(context().mobile_beacons_active['greg_phone']) == 0 async def test_complex_movement(hass, context): @@ -1153,38 +1147,46 @@ async def test_complex_movement_sticky_keys_beacon(hass, context): # leave keys await send_message(hass, LOCATION_TOPIC, location_message) await send_message(hass, EVENT_TOPIC, MOBILE_BEACON_LEAVE_EVENT_MESSAGE) + assert_location_latitude(hass, INNER_ZONE['latitude']) assert_location_state(hass, 'inner') assert_mobile_tracker_state(hass, 'inner') + assert_mobile_tracker_latitude(hass, INNER_ZONE['latitude']) # leave inner region beacon await send_message(hass, EVENT_TOPIC, REGION_BEACON_LEAVE_MESSAGE) await send_message(hass, LOCATION_TOPIC, location_message) assert_location_state(hass, 'inner') assert_mobile_tracker_state(hass, 'inner') + assert_mobile_tracker_latitude(hass, INNER_ZONE['latitude']) # enter inner region beacon await send_message(hass, EVENT_TOPIC, REGION_BEACON_ENTER_MESSAGE) await send_message(hass, LOCATION_TOPIC, location_message) assert_location_latitude(hass, INNER_ZONE['latitude']) assert_location_state(hass, 'inner') + assert_mobile_tracker_state(hass, 'inner') + assert_mobile_tracker_latitude(hass, INNER_ZONE['latitude']) # enter keys await send_message(hass, EVENT_TOPIC, MOBILE_BEACON_ENTER_EVENT_MESSAGE) await send_message(hass, LOCATION_TOPIC, location_message) assert_location_state(hass, 'inner') assert_mobile_tracker_state(hass, 'inner') + assert_mobile_tracker_latitude(hass, INNER_ZONE['latitude']) # leave keys await send_message(hass, LOCATION_TOPIC, location_message) await send_message(hass, EVENT_TOPIC, MOBILE_BEACON_LEAVE_EVENT_MESSAGE) assert_location_state(hass, 'inner') assert_mobile_tracker_state(hass, 'inner') + assert_mobile_tracker_latitude(hass, INNER_ZONE['latitude']) # leave inner region beacon await send_message(hass, EVENT_TOPIC, REGION_BEACON_LEAVE_MESSAGE) await send_message(hass, LOCATION_TOPIC, location_message) assert_location_state(hass, 'inner') assert_mobile_tracker_state(hass, 'inner') + assert_mobile_tracker_latitude(hass, INNER_ZONE['latitude']) # GPS leave inner region, I'm in the 'outer' region now # but on GPS coords @@ -1222,7 +1224,7 @@ async def test_waypoint_import_blacklist(hass, context): assert wayp is None -async def test_waypoint_import_no_whitelist(hass, config_context): +async def test_waypoint_import_no_whitelist(hass, setup_comp): """Test import of list of waypoints with no whitelist set.""" await setup_owntracks(hass, { CONF_MAX_GPS_ACCURACY: 200, @@ -1293,20 +1295,25 @@ async def test_unsupported_message(hass, context): def generate_ciphers(secret): """Generate test ciphers for the DEFAULT_LOCATION_MESSAGE.""" - # libnacl ciphertext generation will fail if the module + # PyNaCl ciphertext generation will fail if the module # cannot be imported. However, the test for decryption # also relies on this library and won't be run without it. - import json import pickle import base64 try: - from libnacl import crypto_secretbox_KEYBYTES as KEYLEN - from libnacl.secret import SecretBox - key = secret.encode("utf-8")[:KEYLEN].ljust(KEYLEN, b'\0') - ctxt = base64.b64encode(SecretBox(key).encrypt( - json.dumps(DEFAULT_LOCATION_MESSAGE).encode("utf-8")) - ).decode("utf-8") + from nacl.secret import SecretBox + from nacl.encoding import Base64Encoder + + keylen = SecretBox.KEY_SIZE + key = secret.encode("utf-8") + key = key[:keylen] + key = key.ljust(keylen, b'\0') + + msg = json.dumps(DEFAULT_LOCATION_MESSAGE).encode("utf-8") + + ctxt = SecretBox(key).encrypt(msg, + encoder=Base64Encoder).decode("utf-8") except (ImportError, OSError): ctxt = '' @@ -1341,7 +1348,8 @@ def mock_cipher(): def mock_decrypt(ciphertext, key): """Decrypt/unpickle.""" import pickle - (mkey, plaintext) = pickle.loads(ciphertext) + import base64 + (mkey, plaintext) = pickle.loads(base64.b64decode(ciphertext)) if key != mkey: raise ValueError() return plaintext @@ -1368,7 +1376,7 @@ def config_context(hass, setup_comp): @patch('homeassistant.components.owntracks.device_tracker.get_cipher', mock_cipher) -async def test_encrypted_payload(hass, config_context): +async def test_encrypted_payload(hass, setup_comp): """Test encrypted payload.""" await setup_owntracks(hass, { CONF_SECRET: TEST_SECRET_KEY, @@ -1379,7 +1387,7 @@ async def test_encrypted_payload(hass, config_context): @patch('homeassistant.components.owntracks.device_tracker.get_cipher', mock_cipher) -async def test_encrypted_payload_topic_key(hass, config_context): +async def test_encrypted_payload_topic_key(hass, setup_comp): """Test encrypted payload with a topic key.""" await setup_owntracks(hass, { CONF_SECRET: { @@ -1392,7 +1400,7 @@ async def test_encrypted_payload_topic_key(hass, config_context): @patch('homeassistant.components.owntracks.device_tracker.get_cipher', mock_cipher) -async def test_encrypted_payload_no_key(hass, config_context): +async def test_encrypted_payload_no_key(hass, setup_comp): """Test encrypted payload with no key, .""" assert hass.states.get(DEVICE_TRACKER_STATE) is None await setup_owntracks(hass, { @@ -1405,7 +1413,7 @@ async def test_encrypted_payload_no_key(hass, config_context): @patch('homeassistant.components.owntracks.device_tracker.get_cipher', mock_cipher) -async def test_encrypted_payload_wrong_key(hass, config_context): +async def test_encrypted_payload_wrong_key(hass, setup_comp): """Test encrypted payload with wrong key.""" await setup_owntracks(hass, { CONF_SECRET: 'wrong key', @@ -1416,7 +1424,7 @@ async def test_encrypted_payload_wrong_key(hass, config_context): @patch('homeassistant.components.owntracks.device_tracker.get_cipher', mock_cipher) -async def test_encrypted_payload_wrong_topic_key(hass, config_context): +async def test_encrypted_payload_wrong_topic_key(hass, setup_comp): """Test encrypted payload with wrong topic key.""" await setup_owntracks(hass, { CONF_SECRET: { @@ -1429,7 +1437,7 @@ async def test_encrypted_payload_wrong_topic_key(hass, config_context): @patch('homeassistant.components.owntracks.device_tracker.get_cipher', mock_cipher) -async def test_encrypted_payload_no_topic_key(hass, config_context): +async def test_encrypted_payload_no_topic_key(hass, setup_comp): """Test encrypted payload with no topic key.""" await setup_owntracks(hass, { CONF_SECRET: { @@ -1439,12 +1447,13 @@ async def test_encrypted_payload_no_topic_key(hass, config_context): assert hass.states.get(DEVICE_TRACKER_STATE) is None -async def test_encrypted_payload_libsodium(hass, config_context): +async def test_encrypted_payload_libsodium(hass, setup_comp): """Test sending encrypted message payload.""" try: - import libnacl # noqa: F401 + # pylint: disable=unused-import + import nacl # noqa: F401 except (ImportError, OSError): - pytest.skip("libnacl/libsodium is not installed") + pytest.skip("PyNaCl/libsodium is not installed") return await setup_owntracks(hass, { @@ -1455,7 +1464,7 @@ async def test_encrypted_payload_libsodium(hass, config_context): assert_location_latitude(hass, LOCATION_MESSAGE['lat']) -async def test_customized_mqtt_topic(hass, config_context): +async def test_customized_mqtt_topic(hass, setup_comp): """Test subscribing to a custom mqtt topic.""" await setup_owntracks(hass, { CONF_MQTT_TOPIC: 'mytracks/#', @@ -1467,7 +1476,7 @@ async def test_customized_mqtt_topic(hass, config_context): assert_location_latitude(hass, LOCATION_MESSAGE['lat']) -async def test_region_mapping(hass, config_context): +async def test_region_mapping(hass, setup_comp): """Test region to zone mapping.""" await setup_owntracks(hass, { CONF_REGION_MAPPING: { diff --git a/tests/components/device_tracker/test_upc_connect.py b/tests/components/device_tracker/test_upc_connect.py index 6b38edc3ce94d..677a6d1f31017 100644 --- a/tests/components/device_tracker/test_upc_connect.py +++ b/tests/components/device_tracker/test_upc_connect.py @@ -1,273 +1,230 @@ """The tests for the UPC ConnextBox device tracker platform.""" import asyncio -from unittest.mock import patch -import logging +from asynctest import patch import pytest -from homeassistant.setup import setup_component -from homeassistant.const import ( - CONF_PLATFORM, CONF_HOST) from homeassistant.components.device_tracker import DOMAIN import homeassistant.components.device_tracker.upc_connect as platform -from homeassistant.util.async_ import run_coroutine_threadsafe +from homeassistant.const import CONF_HOST, CONF_PLATFORM +from homeassistant.setup import async_setup_component -from tests.common import ( - get_test_home_assistant, assert_setup_component, load_fixture, - mock_component, mock_coro) +from tests.common import assert_setup_component, load_fixture, mock_component -_LOGGER = logging.getLogger(__name__) +HOST = "127.0.0.1" -@asyncio.coroutine -def async_scan_devices_mock(scanner): +async def async_scan_devices_mock(scanner): """Mock async_scan_devices.""" return [] @pytest.fixture(autouse=True) -def mock_load_config(): - """Mock device tracker loading config.""" - with patch('homeassistant.components.device_tracker.async_load_config', - return_value=mock_coro([])): - yield - - -class TestUPCConnect: - """Tests for the Ddwrt device tracker platform.""" - - def setup_method(self): - """Set up things to be run when tests are started.""" - self.hass = get_test_home_assistant() - mock_component(self.hass, 'zone') - mock_component(self.hass, 'group') - - self.host = "127.0.0.1" - - def teardown_method(self): - """Stop everything that was started.""" - self.hass.stop() - - @patch('homeassistant.components.device_tracker.upc_connect.' - 'UPCDeviceScanner.async_scan_devices', - return_value=async_scan_devices_mock) - def test_setup_platform(self, scan_mock, aioclient_mock): - """Set up a platform.""" - aioclient_mock.get( - "http://{}/common_page/login.html".format(self.host), - cookies={'sessionToken': '654321'} - ) - aioclient_mock.post( - "http://{}/xml/getter.xml".format(self.host), - content=b'successful' - ) - - with assert_setup_component(1, DOMAIN): - assert setup_component( - self.hass, DOMAIN, {DOMAIN: { - CONF_PLATFORM: 'upc_connect', - CONF_HOST: self.host - }}) - - assert len(aioclient_mock.mock_calls) == 1 - - @patch('homeassistant.components.device_tracker._LOGGER.error') - def test_setup_platform_timeout_webservice(self, mock_error, - aioclient_mock): - """Set up a platform with api timeout.""" - aioclient_mock.get( - "http://{}/common_page/login.html".format(self.host), - cookies={'sessionToken': '654321'}, - content=b'successful', - exc=asyncio.TimeoutError() - ) - - with assert_setup_component(1, DOMAIN): - assert setup_component( - self.hass, DOMAIN, {DOMAIN: { - CONF_PLATFORM: 'upc_connect', - CONF_HOST: self.host - }}) - - assert len(aioclient_mock.mock_calls) == 1 - - assert 'Error setting up platform' in \ - str(mock_error.call_args_list[-1]) - - @patch('homeassistant.components.device_tracker._LOGGER.error') - def test_setup_platform_timeout_loginpage(self, mock_error, - aioclient_mock): - """Set up a platform with timeout on loginpage.""" - aioclient_mock.get( - "http://{}/common_page/login.html".format(self.host), - exc=asyncio.TimeoutError() - ) - aioclient_mock.post( - "http://{}/xml/getter.xml".format(self.host), - content=b'successful', - ) - - with assert_setup_component(1, DOMAIN): - assert setup_component( - self.hass, DOMAIN, {DOMAIN: { - CONF_PLATFORM: 'upc_connect', - CONF_HOST: self.host - }}) - - assert len(aioclient_mock.mock_calls) == 1 - - assert 'Error setting up platform' in \ - str(mock_error.call_args_list[-1]) - - def test_scan_devices(self, aioclient_mock): - """Set up a upc platform and scan device.""" - aioclient_mock.get( - "http://{}/common_page/login.html".format(self.host), - cookies={'sessionToken': '654321'} - ) - aioclient_mock.post( - "http://{}/xml/getter.xml".format(self.host), - content=b'successful', - cookies={'sessionToken': '654321'} - ) - - scanner = run_coroutine_threadsafe(platform.async_get_scanner( - self.hass, {DOMAIN: { - CONF_PLATFORM: 'upc_connect', - CONF_HOST: self.host - }} - ), self.hass.loop).result() - - assert len(aioclient_mock.mock_calls) == 1 - - aioclient_mock.clear_requests() - aioclient_mock.post( - "http://{}/xml/getter.xml".format(self.host), - text=load_fixture('upc_connect.xml'), - cookies={'sessionToken': '1235678'} - ) - - mac_list = run_coroutine_threadsafe( - scanner.async_scan_devices(), self.hass.loop).result() - - assert len(aioclient_mock.mock_calls) == 1 - assert aioclient_mock.mock_calls[0][2] == 'token=654321&fun=123' - assert mac_list == ['30:D3:2D:0:69:21', '5C:AA:FD:25:32:02', - '70:EE:50:27:A1:38'] - - def test_scan_devices_without_session(self, aioclient_mock): - """Set up a upc platform and scan device with no token.""" - aioclient_mock.get( - "http://{}/common_page/login.html".format(self.host), - cookies={'sessionToken': '654321'} - ) - aioclient_mock.post( - "http://{}/xml/getter.xml".format(self.host), - content=b'successful', - cookies={'sessionToken': '654321'} - ) - - scanner = run_coroutine_threadsafe(platform.async_get_scanner( - self.hass, {DOMAIN: { - CONF_PLATFORM: 'upc_connect', - CONF_HOST: self.host - }} - ), self.hass.loop).result() - - assert len(aioclient_mock.mock_calls) == 1 - - aioclient_mock.clear_requests() - aioclient_mock.get( - "http://{}/common_page/login.html".format(self.host), - cookies={'sessionToken': '654321'} - ) - aioclient_mock.post( - "http://{}/xml/getter.xml".format(self.host), - text=load_fixture('upc_connect.xml'), - cookies={'sessionToken': '1235678'} - ) - - scanner.token = None - mac_list = run_coroutine_threadsafe( - scanner.async_scan_devices(), self.hass.loop).result() - - assert len(aioclient_mock.mock_calls) == 2 - assert aioclient_mock.mock_calls[1][2] == 'token=654321&fun=123' - assert mac_list == ['30:D3:2D:0:69:21', '5C:AA:FD:25:32:02', - '70:EE:50:27:A1:38'] - - def test_scan_devices_without_session_wrong_re(self, aioclient_mock): - """Set up a upc platform and scan device with no token and wrong.""" - aioclient_mock.get( - "http://{}/common_page/login.html".format(self.host), - cookies={'sessionToken': '654321'} - ) - aioclient_mock.post( - "http://{}/xml/getter.xml".format(self.host), - content=b'successful', - cookies={'sessionToken': '654321'} - ) - - scanner = run_coroutine_threadsafe(platform.async_get_scanner( - self.hass, {DOMAIN: { - CONF_PLATFORM: 'upc_connect', - CONF_HOST: self.host - }} - ), self.hass.loop).result() - - assert len(aioclient_mock.mock_calls) == 1 - - aioclient_mock.clear_requests() - aioclient_mock.get( - "http://{}/common_page/login.html".format(self.host), - cookies={'sessionToken': '654321'} - ) - aioclient_mock.post( - "http://{}/xml/getter.xml".format(self.host), - status=400, - cookies={'sessionToken': '1235678'} - ) - - scanner.token = None - mac_list = run_coroutine_threadsafe( - scanner.async_scan_devices(), self.hass.loop).result() - - assert len(aioclient_mock.mock_calls) == 2 - assert aioclient_mock.mock_calls[1][2] == 'token=654321&fun=123' - assert mac_list == [] - - def test_scan_devices_parse_error(self, aioclient_mock): - """Set up a upc platform and scan device with parse error.""" - aioclient_mock.get( - "http://{}/common_page/login.html".format(self.host), - cookies={'sessionToken': '654321'} - ) - aioclient_mock.post( - "http://{}/xml/getter.xml".format(self.host), - content=b'successful', - cookies={'sessionToken': '654321'} - ) - - scanner = run_coroutine_threadsafe(platform.async_get_scanner( - self.hass, {DOMAIN: { - CONF_PLATFORM: 'upc_connect', - CONF_HOST: self.host - }} - ), self.hass.loop).result() - - assert len(aioclient_mock.mock_calls) == 1 - - aioclient_mock.clear_requests() - aioclient_mock.post( - "http://{}/xml/getter.xml".format(self.host), - text="Blablebla blabalble", - cookies={'sessionToken': '1235678'} - ) - - mac_list = run_coroutine_threadsafe( - scanner.async_scan_devices(), self.hass.loop).result() - - assert len(aioclient_mock.mock_calls) == 1 - assert aioclient_mock.mock_calls[0][2] == 'token=654321&fun=123' - assert scanner.token is None - assert mac_list == [] +def setup_comp_deps(hass, mock_device_tracker_conf): + """Set up component dependencies.""" + mock_component(hass, 'zone') + mock_component(hass, 'group') + yield + + +async def test_setup_platform_timeout_loginpage(hass, caplog, aioclient_mock): + """Set up a platform with timeout on loginpage.""" + aioclient_mock.get( + "http://{}/common_page/login.html".format(HOST), + exc=asyncio.TimeoutError() + ) + aioclient_mock.post( + "http://{}/xml/getter.xml".format(HOST), + content=b'successful', + ) + + assert await async_setup_component( + hass, DOMAIN, { + DOMAIN: {CONF_PLATFORM: 'upc_connect', CONF_HOST: HOST}}) + + assert len(aioclient_mock.mock_calls) == 1 + + assert 'Error setting up platform' in caplog.text + + +async def test_setup_platform_timeout_webservice(hass, caplog, aioclient_mock): + """Set up a platform with api timeout.""" + aioclient_mock.get( + "http://{}/common_page/login.html".format(HOST), + cookies={'sessionToken': '654321'}, + content=b'successful', + exc=asyncio.TimeoutError() + ) + + assert await async_setup_component( + hass, DOMAIN, { + DOMAIN: {CONF_PLATFORM: 'upc_connect', CONF_HOST: HOST}}) + + assert len(aioclient_mock.mock_calls) == 1 + + assert 'Error setting up platform' in caplog.text + + +@patch('homeassistant.components.device_tracker.upc_connect.' + 'UPCDeviceScanner.async_scan_devices', + return_value=async_scan_devices_mock) +async def test_setup_platform(scan_mock, hass, aioclient_mock): + """Set up a platform.""" + aioclient_mock.get( + "http://{}/common_page/login.html".format(HOST), + cookies={'sessionToken': '654321'} + ) + aioclient_mock.post( + "http://{}/xml/getter.xml".format(HOST), + content=b'successful' + ) + + with assert_setup_component(1, DOMAIN): + assert await async_setup_component( + hass, DOMAIN, {DOMAIN: { + CONF_PLATFORM: 'upc_connect', + CONF_HOST: HOST + }}) + + assert len(aioclient_mock.mock_calls) == 1 + + +async def test_scan_devices(hass, aioclient_mock): + """Set up a upc platform and scan device.""" + aioclient_mock.get( + "http://{}/common_page/login.html".format(HOST), + cookies={'sessionToken': '654321'} + ) + aioclient_mock.post( + "http://{}/xml/getter.xml".format(HOST), + content=b'successful', + cookies={'sessionToken': '654321'} + ) + + scanner = await platform.async_get_scanner( + hass, { + DOMAIN: {CONF_PLATFORM: 'upc_connect', CONF_HOST: HOST}}) + + assert len(aioclient_mock.mock_calls) == 1 + + aioclient_mock.clear_requests() + aioclient_mock.post( + "http://{}/xml/getter.xml".format(HOST), + text=load_fixture('upc_connect.xml'), + cookies={'sessionToken': '1235678'} + ) + + mac_list = await scanner.async_scan_devices() + + assert len(aioclient_mock.mock_calls) == 1 + assert aioclient_mock.mock_calls[0][2] == 'token=654321&fun=123' + assert mac_list == ['30:D3:2D:0:69:21', '5C:AA:FD:25:32:02', + '70:EE:50:27:A1:38'] + + +async def test_scan_devices_without_session(hass, aioclient_mock): + """Set up a upc platform and scan device with no token.""" + aioclient_mock.get( + "http://{}/common_page/login.html".format(HOST), + cookies={'sessionToken': '654321'} + ) + aioclient_mock.post( + "http://{}/xml/getter.xml".format(HOST), + content=b'successful', + cookies={'sessionToken': '654321'} + ) + + scanner = await platform.async_get_scanner( + hass, { + DOMAIN: {CONF_PLATFORM: 'upc_connect', CONF_HOST: HOST}}) + + assert len(aioclient_mock.mock_calls) == 1 + + aioclient_mock.clear_requests() + aioclient_mock.get( + "http://{}/common_page/login.html".format(HOST), + cookies={'sessionToken': '654321'} + ) + aioclient_mock.post( + "http://{}/xml/getter.xml".format(HOST), + text=load_fixture('upc_connect.xml'), + cookies={'sessionToken': '1235678'} + ) + + scanner.token = None + mac_list = await scanner.async_scan_devices() + + assert len(aioclient_mock.mock_calls) == 2 + assert aioclient_mock.mock_calls[1][2] == 'token=654321&fun=123' + assert mac_list == ['30:D3:2D:0:69:21', '5C:AA:FD:25:32:02', + '70:EE:50:27:A1:38'] + + +async def test_scan_devices_without_session_wrong_re(hass, aioclient_mock): + """Set up a upc platform and scan device with no token and wrong.""" + aioclient_mock.get( + "http://{}/common_page/login.html".format(HOST), + cookies={'sessionToken': '654321'} + ) + aioclient_mock.post( + "http://{}/xml/getter.xml".format(HOST), + content=b'successful', + cookies={'sessionToken': '654321'} + ) + + scanner = await platform.async_get_scanner( + hass, { + DOMAIN: {CONF_PLATFORM: 'upc_connect', CONF_HOST: HOST}}) + + assert len(aioclient_mock.mock_calls) == 1 + + aioclient_mock.clear_requests() + aioclient_mock.get( + "http://{}/common_page/login.html".format(HOST), + cookies={'sessionToken': '654321'} + ) + aioclient_mock.post( + "http://{}/xml/getter.xml".format(HOST), + status=400, + cookies={'sessionToken': '1235678'} + ) + + scanner.token = None + mac_list = await scanner.async_scan_devices() + + assert len(aioclient_mock.mock_calls) == 2 + assert aioclient_mock.mock_calls[1][2] == 'token=654321&fun=123' + assert mac_list == [] + + +async def test_scan_devices_parse_error(hass, aioclient_mock): + """Set up a upc platform and scan device with parse error.""" + aioclient_mock.get( + "http://{}/common_page/login.html".format(HOST), + cookies={'sessionToken': '654321'} + ) + aioclient_mock.post( + "http://{}/xml/getter.xml".format(HOST), + content=b'successful', + cookies={'sessionToken': '654321'} + ) + + scanner = await platform.async_get_scanner( + hass, { + DOMAIN: {CONF_PLATFORM: 'upc_connect', CONF_HOST: HOST}}) + + assert len(aioclient_mock.mock_calls) == 1 + + aioclient_mock.clear_requests() + aioclient_mock.post( + "http://{}/xml/getter.xml".format(HOST), + text="Blablebla blabalble", + cookies={'sessionToken': '1235678'} + ) + + mac_list = await scanner.async_scan_devices() + + assert len(aioclient_mock.mock_calls) == 1 + assert aioclient_mock.mock_calls[0][2] == 'token=654321&fun=123' + assert scanner.token is None + assert mac_list == [] diff --git a/tests/components/ecobee/test_climate.py b/tests/components/ecobee/test_climate.py index 965fb37dcb814..f4412b5d564e3 100644 --- a/tests/components/ecobee/test_climate.py +++ b/tests/components/ecobee/test_climate.py @@ -3,7 +3,7 @@ from unittest import mock import homeassistant.const as const from homeassistant.components.ecobee import climate as ecobee -from homeassistant.components.climate import STATE_OFF +from homeassistant.const import STATE_OFF class TestEcobee(unittest.TestCase): diff --git a/tests/components/esphome/test_config_flow.py b/tests/components/esphome/test_config_flow.py index 8c870c6ad735f..076ec0066a6ea 100644 --- a/tests/components/esphome/test_config_flow.py +++ b/tests/components/esphome/test_config_flow.py @@ -203,6 +203,11 @@ async def test_discovery_initiation(hass, mock_client): MockDeviceInfo(False, "test8266")) result = await flow.async_step_discovery(user_input=service_info) + assert result['type'] == 'form' + assert result['step_id'] == 'discovery_confirm' + assert result['description_placeholders']['name'] == 'test8266' + + result = await flow.async_step_discovery_confirm(user_input={}) assert result['type'] == 'create_entry' assert result['title'] == 'test8266' assert result['data']['host'] == 'test8266.local' diff --git a/tests/components/geofency/test_init.py b/tests/components/geofency/test_init.py index 41c232a51c35d..dd87a6d950359 100644 --- a/tests/components/geofency/test_init.py +++ b/tests/components/geofency/test_init.py @@ -10,11 +10,10 @@ CONF_MOBILE_BEACONS, DOMAIN, TRACKER_UPDATE) from homeassistant.const import ( HTTP_OK, HTTP_UNPROCESSABLE_ENTITY, STATE_HOME, - STATE_NOT_HOME, CONF_WEBHOOK_ID) + STATE_NOT_HOME) from homeassistant.helpers.dispatcher import DATA_DISPATCHER from homeassistant.setup import async_setup_component from homeassistant.util import slugify -from tests.common import MockConfigEntry HOME_LATITUDE = 37.239622 HOME_LONGITUDE = -115.815811 @@ -288,16 +287,22 @@ async def test_beacon_enter_and_exit_car(hass, geofency_client, webhook_id): @pytest.mark.xfail( reason='The device_tracker component does not support unloading yet.' ) -async def test_load_unload_entry(hass, geofency_client): +async def test_load_unload_entry(hass, geofency_client, webhook_id): """Test that the appropriate dispatch signals are added and removed.""" - entry = MockConfigEntry(domain=DOMAIN, data={ - CONF_WEBHOOK_ID: 'geofency_test' - }) + url = '/api/webhook/{}'.format(webhook_id) - await geofency.async_setup_entry(hass, entry) + # Enter the Home zone + req = await geofency_client.post(url, data=GPS_ENTER_HOME) await hass.async_block_till_done() - assert 1 == len(hass.data[DATA_DISPATCHER][TRACKER_UPDATE]) + assert req.status == HTTP_OK + device_name = slugify(GPS_ENTER_HOME['device']) + state_name = hass.states.get('{}.{}'.format( + 'device_tracker', device_name)).state + assert STATE_HOME == state_name + assert len(hass.data[DATA_DISPATCHER][TRACKER_UPDATE]) == 1 + + entry = hass.config_entries.async_entries(DOMAIN)[0] - await geofency.async_unload_entry(hass, entry) + assert await geofency.async_unload_entry(hass, entry) await hass.async_block_till_done() - assert 0 == len(hass.data[DATA_DISPATCHER][TRACKER_UPDATE]) + assert not hass.data[DATA_DISPATCHER][TRACKER_UPDATE] diff --git a/tests/components/google_assistant/test_google_assistant.py b/tests/components/google_assistant/test_google_assistant.py index 89e9090da98a7..18f99c8268524 100644 --- a/tests/components/google_assistant/test_google_assistant.py +++ b/tests/components/google_assistant/test_google_assistant.py @@ -8,7 +8,8 @@ from homeassistant import core, const, setup from homeassistant.components import ( - fan, cover, light, switch, climate, lock, async_setup, media_player) + fan, cover, light, switch, lock, async_setup, media_player) +from homeassistant.components.climate import const as climate from homeassistant.const import CLOUD_NEVER_EXPOSED_ENTITIES from homeassistant.components import google_assistant as ga diff --git a/tests/components/google_assistant/test_smart_home.py b/tests/components/google_assistant/test_smart_home.py index 36971224f92ed..d1ec80844b61d 100644 --- a/tests/components/google_assistant/test_smart_home.py +++ b/tests/components/google_assistant/test_smart_home.py @@ -3,9 +3,12 @@ from homeassistant.const import ( ATTR_SUPPORTED_FEATURES, ATTR_UNIT_OF_MEASUREMENT, TEMP_CELSIUS) from homeassistant.setup import async_setup_component -from homeassistant.components import climate +from homeassistant.components.climate.const import ( + ATTR_MIN_TEMP, ATTR_MAX_TEMP, STATE_HEAT, SUPPORT_OPERATION_MODE +) from homeassistant.components.google_assistant import ( - const, trait, helpers, smart_home as sh) + const, trait, helpers, smart_home as sh, + EVENT_COMMAND_RECEIVED, EVENT_QUERY_RECEIVED, EVENT_SYNC_RECEIVED) from homeassistant.components.light.demo import DemoLight @@ -46,6 +49,9 @@ async def test_sync_message(hass): } ) + events = [] + hass.bus.async_listen(EVENT_SYNC_RECEIVED, events.append) + result = await sh.async_handle_message(hass, config, { "requestId": REQ_ID, "inputs": [{ @@ -83,6 +89,13 @@ async def test_sync_message(hass): }] } } + 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, + } async def test_query_message(hass): @@ -107,6 +120,9 @@ async def test_query_message(hass): light2.entity_id = 'light.another_light' await light2.async_update_ha_state() + events = [] + hass.bus.async_listen(EVENT_QUERY_RECEIVED, events.append) + result = await sh.async_handle_message(hass, BASIC_CONFIG, { "requestId": REQ_ID, "inputs": [{ @@ -147,12 +163,33 @@ async def test_query_message(hass): } } + assert len(events) == 3 + assert events[0].event_type == EVENT_QUERY_RECEIVED + assert events[0].data == { + 'request_id': REQ_ID, + 'entity_id': 'light.demo_light' + } + assert events[1].event_type == EVENT_QUERY_RECEIVED + assert events[1].data == { + 'request_id': REQ_ID, + 'entity_id': 'light.another_light' + } + assert events[2].event_type == EVENT_QUERY_RECEIVED + assert events[2].data == { + 'request_id': REQ_ID, + 'entity_id': 'light.non_existing' + } + async def test_execute(hass): """Test an execute command.""" await async_setup_component(hass, 'light', { 'light': {'platform': 'demo'} }) + + events = [] + hass.bus.async_listen(EVENT_COMMAND_RECEIVED, events.append) + await hass.services.async_call( 'light', 'turn_off', {'entity_id': 'light.ceiling_lights'}, blocking=True) @@ -207,15 +244,66 @@ async def test_execute(hass): } } + assert len(events) == 4 + assert events[0].event_type == EVENT_COMMAND_RECEIVED + assert events[0].data == { + 'request_id': REQ_ID, + 'entity_id': 'light.non_existing', + 'execution': { + 'command': 'action.devices.commands.OnOff', + 'params': { + 'on': True + } + } + } + assert events[1].event_type == EVENT_COMMAND_RECEIVED + assert events[1].data == { + 'request_id': REQ_ID, + 'entity_id': 'light.non_existing', + 'execution': { + 'command': 'action.devices.commands.BrightnessAbsolute', + 'params': { + 'brightness': 20 + } + } + } + assert events[2].event_type == EVENT_COMMAND_RECEIVED + assert events[2].data == { + 'request_id': REQ_ID, + 'entity_id': 'light.ceiling_lights', + 'execution': { + 'command': 'action.devices.commands.OnOff', + 'params': { + 'on': True + } + } + } + assert events[3].event_type == EVENT_COMMAND_RECEIVED + assert events[3].data == { + 'request_id': REQ_ID, + 'entity_id': 'light.ceiling_lights', + 'execution': { + 'command': 'action.devices.commands.BrightnessAbsolute', + 'params': { + 'brightness': 20 + } + } + } + async def test_raising_error_trait(hass): """Test raising an error while executing a trait command.""" - hass.states.async_set('climate.bla', climate.STATE_HEAT, { - climate.ATTR_MIN_TEMP: 15, - climate.ATTR_MAX_TEMP: 30, - ATTR_SUPPORTED_FEATURES: climate.SUPPORT_OPERATION_MODE, + hass.states.async_set('climate.bla', STATE_HEAT, { + ATTR_MIN_TEMP: 15, + ATTR_MAX_TEMP: 30, + ATTR_SUPPORTED_FEATURES: SUPPORT_OPERATION_MODE, ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, }) + + events = [] + hass.bus.async_listen(EVENT_COMMAND_RECEIVED, events.append) + await hass.async_block_till_done() + result = await sh.async_handle_message(hass, BASIC_CONFIG, { "requestId": REQ_ID, "inputs": [{ @@ -248,6 +336,19 @@ async def test_raising_error_trait(hass): } } + assert len(events) == 1 + assert events[0].event_type == EVENT_COMMAND_RECEIVED + assert events[0].data == { + 'request_id': REQ_ID, + 'entity_id': 'climate.bla', + 'execution': { + 'command': 'action.devices.commands.ThermostatTemperatureSetpoint', + 'params': { + 'thermostatTemperatureSetpoint': 10 + } + } + } + def test_serialize_input_boolean(): """Test serializing an input boolean entity.""" @@ -314,3 +415,15 @@ async def test_empty_name_doesnt_sync(hass): 'devices': [] } } + + +async def test_query_disconnect(hass): + """Test a disconnect message.""" + result = await sh.async_handle_message(hass, BASIC_CONFIG, { + 'inputs': [ + {'intent': 'action.devices.DISCONNECT'} + ], + 'requestId': REQ_ID + }) + + assert result is None diff --git a/tests/components/google_assistant/test_trait.py b/tests/components/google_assistant/test_trait.py index e9169c9bbbe16..e051a5de4da46 100644 --- a/tests/components/google_assistant/test_trait.py +++ b/tests/components/google_assistant/test_trait.py @@ -2,7 +2,6 @@ import pytest from homeassistant.components import ( - climate, cover, fan, input_boolean, @@ -15,10 +14,11 @@ vacuum, group, ) +from homeassistant.components.climate import const as climate from homeassistant.components.google_assistant import trait, helpers, const from homeassistant.const import ( STATE_ON, STATE_OFF, ATTR_ENTITY_ID, SERVICE_TURN_ON, SERVICE_TURN_OFF, - TEMP_CELSIUS, TEMP_FAHRENHEIT, ATTR_SUPPORTED_FEATURES) + TEMP_CELSIUS, TEMP_FAHRENHEIT, ATTR_SUPPORTED_FEATURES, ATTR_TEMPERATURE) from homeassistant.core import State, DOMAIN as HA_DOMAIN from homeassistant.util import color from tests.common import async_mock_service @@ -668,7 +668,7 @@ async def test_temperature_setting_climate_range(hass): climate.ATTR_CURRENT_HUMIDITY: 25, climate.ATTR_OPERATION_MODE: climate.STATE_AUTO, climate.ATTR_OPERATION_LIST: [ - climate.STATE_OFF, + STATE_OFF, climate.STATE_COOL, climate.STATE_HEAT, climate.STATE_AUTO, @@ -737,12 +737,12 @@ async def test_temperature_setting_climate_setpoint(hass): 'climate.bla', climate.STATE_AUTO, { climate.ATTR_OPERATION_MODE: climate.STATE_COOL, climate.ATTR_OPERATION_LIST: [ - climate.STATE_OFF, + STATE_OFF, climate.STATE_COOL, ], climate.ATTR_MIN_TEMP: 10, climate.ATTR_MAX_TEMP: 30, - climate.ATTR_TEMPERATURE: 18, + ATTR_TEMPERATURE: 18, climate.ATTR_CURRENT_TEMPERATURE: 20 }), BASIC_CONFIG) assert trt.sync_attributes() == { @@ -772,7 +772,7 @@ async def test_temperature_setting_climate_setpoint(hass): assert len(calls) == 1 assert calls[0].data == { ATTR_ENTITY_ID: 'climate.bla', - climate.ATTR_TEMPERATURE: 19 + ATTR_TEMPERATURE: 19 } diff --git a/tests/components/homekit/test_type_thermostats.py b/tests/components/homekit/test_type_thermostats.py index f645cddf73000..ce6774796d33f 100644 --- a/tests/components/homekit/test_type_thermostats.py +++ b/tests/components/homekit/test_type_thermostats.py @@ -4,7 +4,7 @@ import pytest -from homeassistant.components.climate import ( +from homeassistant.components.climate.const import ( ATTR_CURRENT_TEMPERATURE, ATTR_MAX_TEMP, ATTR_MIN_TEMP, ATTR_TARGET_TEMP_LOW, ATTR_TARGET_TEMP_HIGH, ATTR_OPERATION_MODE, ATTR_OPERATION_LIST, DEFAULT_MAX_TEMP, DEFAULT_MIN_TEMP, diff --git a/tests/components/homekit_controller/common.py b/tests/components/homekit_controller/common.py index d3c1f9ab07b84..d543cf517496c 100644 --- a/tests/components/homekit_controller/common.py +++ b/tests/components/homekit_controller/common.py @@ -134,8 +134,11 @@ def add_characteristic(self, name): return char -async def setup_test_component(hass, services): - """Load a fake homekit accessory based on a homekit accessory model.""" +async def setup_test_component(hass, services, capitalize=False): + """Load a fake homekit accessory based on a homekit accessory model. + + If capitalize is True, property names will be in upper case. + """ domain = None for service in services: service_name = ServicesTypes.get_short(service.type) @@ -162,9 +165,9 @@ async def setup_test_component(hass, services): 'host': '127.0.0.1', 'port': 8080, 'properties': { - 'md': 'TestDevice', - 'id': '00:00:00:00:00:00', - 'c#': 1, + ('MD' if capitalize else 'md'): 'TestDevice', + ('ID' if capitalize else 'id'): '00:00:00:00:00:00', + ('C#' if capitalize else 'c#'): 1, } } diff --git a/tests/components/homekit_controller/test_climate.py b/tests/components/homekit_controller/test_climate.py index 9f5cc9d876423..b04a57fa967e0 100644 --- a/tests/components/homekit_controller/test_climate.py +++ b/tests/components/homekit_controller/test_climate.py @@ -1,5 +1,5 @@ """Basic checks for HomeKitclimate.""" -from homeassistant.components.climate import ( +from homeassistant.components.climate.const import ( DOMAIN, SERVICE_SET_OPERATION_MODE, SERVICE_SET_TEMPERATURE) from tests.components.homekit_controller.common import ( setup_test_component) diff --git a/tests/components/homekit_controller/test_cover.py b/tests/components/homekit_controller/test_cover.py index 062ecc540419d..62fce4325c752 100644 --- a/tests/components/homekit_controller/test_cover.py +++ b/tests/components/homekit_controller/test_cover.py @@ -70,6 +70,20 @@ def create_window_covering_service_with_v_tilt(): return service +async def test_accept_capitalized_property_names(hass, utcnow): + """Test that we can handle a device with capitalized property names.""" + window_cover = create_window_covering_service() + helper = await setup_test_component(hass, [window_cover], capitalize=True) + + # The specific interaction we do here doesn't matter; we just need + # to do *something* to ensure that discovery properly dealt with the + # capitalized property names. + await hass.services.async_call('cover', 'open_cover', { + 'entity_id': helper.entity_id, + }, blocking=True) + assert helper.characteristics[POSITION_TARGET].value == 100 + + async def test_change_window_cover_state(hass, utcnow): """Test that we can turn a HomeKit alarm on and off again.""" window_cover = create_window_covering_service() diff --git a/tests/components/homematicip_cloud/test_hap.py b/tests/components/homematicip_cloud/test_hap.py index c39e7d4e26bd0..61ca3300d601b 100644 --- a/tests/components/homematicip_cloud/test_hap.py +++ b/tests/components/homematicip_cloud/test_hap.py @@ -44,7 +44,7 @@ async def test_auth_auth_check_and_register(hass): hap = hmipc.HomematicipAuth(hass, config) hap.auth = Mock() with patch.object(hap.auth, 'isRequestAcknowledged', - return_value=mock_coro()), \ + return_value=mock_coro(True)), \ patch.object(hap.auth, 'requestAuthToken', return_value=mock_coro('ABC')), \ patch.object(hap.auth, 'confirmAuthToken', diff --git a/tests/components/homematicip_cloud/test_init.py b/tests/components/homematicip_cloud/test_init.py index 185372272471b..8b02b36de20d9 100644 --- a/tests/components/homematicip_cloud/test_init.py +++ b/tests/components/homematicip_cloud/test_init.py @@ -21,7 +21,7 @@ async def test_config_with_accesspoint_passed_to_config_entry(hass): }) is True # Flow started for the access point - assert len(mock_config_entries.flow.mock_calls) == 2 + assert len(mock_config_entries.flow.mock_calls) >= 2 async def test_config_already_registered_not_passed_to_config_entry(hass): @@ -58,7 +58,7 @@ async def test_setup_entry_successful(hass): } }) is True - assert len(mock_hap.mock_calls) == 2 + assert len(mock_hap.mock_calls) >= 2 async def test_setup_defined_accesspoint(hass): @@ -95,7 +95,7 @@ async def test_unload_entry(hass): mock_hap.return_value.async_setup.return_value = mock_coro(True) assert await async_setup_component(hass, hmipc.DOMAIN, {}) is True - assert len(mock_hap.return_value.mock_calls) == 1 + assert len(mock_hap.return_value.mock_calls) >= 1 mock_hap.return_value.async_reset.return_value = mock_coro(True) assert await hmipc.async_unload_entry(hass, entry) diff --git a/tests/components/hue/test_init.py b/tests/components/hue/test_init.py index 6c89995a1a108..cdad8e02d25f3 100644 --- a/tests/components/hue/test_init.py +++ b/tests/components/hue/test_init.py @@ -97,10 +97,8 @@ async def test_config_passed_to_config_entry(hass): mock_bridge.return_value.api.config = Mock( mac='mock-mac', bridgeid='mock-bridgeid', - raw={ - 'modelid': 'mock-modelid', - 'swversion': 'mock-swversion', - } + modelid='mock-modelid', + swversion='mock-swversion' ) # Can't set name via kwargs mock_bridge.return_value.api.config.name = 'mock-name' diff --git a/tests/components/locative/test_init.py b/tests/components/locative/test_init.py index 877d25d04bdfb..f757080eadc72 100644 --- a/tests/components/locative/test_init.py +++ b/tests/components/locative/test_init.py @@ -8,11 +8,11 @@ from homeassistant.components.device_tracker import \ DOMAIN as DEVICE_TRACKER_DOMAIN from homeassistant.components.locative import DOMAIN, TRACKER_UPDATE -from homeassistant.const import HTTP_OK, HTTP_UNPROCESSABLE_ENTITY, \ - CONF_WEBHOOK_ID +from homeassistant.const import HTTP_OK, HTTP_UNPROCESSABLE_ENTITY from homeassistant.helpers.dispatcher import DATA_DISPATCHER from homeassistant.setup import async_setup_component -from tests.common import MockConfigEntry + +# pylint: disable=redefined-outer-name @pytest.fixture(autouse=True) @@ -127,7 +127,7 @@ async def test_enter_and_exit(hass, locative_client, webhook_id): assert req.status == HTTP_OK state_name = hass.states.get('{}.{}'.format(DEVICE_TRACKER_DOMAIN, data['device'])).state - assert 'home' == state_name + assert state_name == 'home' data['id'] = 'HOME' data['trigger'] = 'exit' @@ -138,7 +138,7 @@ async def test_enter_and_exit(hass, locative_client, webhook_id): assert req.status == HTTP_OK state_name = hass.states.get('{}.{}'.format(DEVICE_TRACKER_DOMAIN, data['device'])).state - assert 'not_home' == state_name + assert state_name == 'not_home' data['id'] = 'hOmE' data['trigger'] = 'enter' @@ -149,7 +149,7 @@ async def test_enter_and_exit(hass, locative_client, webhook_id): assert req.status == HTTP_OK state_name = hass.states.get('{}.{}'.format(DEVICE_TRACKER_DOMAIN, data['device'])).state - assert 'home' == state_name + assert state_name == 'home' data['trigger'] = 'exit' @@ -159,7 +159,7 @@ async def test_enter_and_exit(hass, locative_client, webhook_id): assert req.status == HTTP_OK state_name = hass.states.get('{}.{}'.format(DEVICE_TRACKER_DOMAIN, data['device'])).state - assert 'not_home' == state_name + assert state_name == 'not_home' data['id'] = 'work' data['trigger'] = 'enter' @@ -170,7 +170,7 @@ async def test_enter_and_exit(hass, locative_client, webhook_id): assert req.status == HTTP_OK state_name = hass.states.get('{}.{}'.format(DEVICE_TRACKER_DOMAIN, data['device'])).state - assert 'work' == state_name + assert state_name == 'work' async def test_exit_after_enter(hass, locative_client, webhook_id): @@ -243,16 +243,30 @@ async def test_exit_first(hass, locative_client, webhook_id): @pytest.mark.xfail( reason='The device_tracker component does not support unloading yet.' ) -async def test_load_unload_entry(hass): +async def test_load_unload_entry(hass, locative_client, webhook_id): """Test that the appropriate dispatch signals are added and removed.""" - entry = MockConfigEntry(domain=DOMAIN, data={ - CONF_WEBHOOK_ID: 'locative_test' - }) + url = '/api/webhook/{}'.format(webhook_id) - await locative.async_setup_entry(hass, entry) + data = { + 'latitude': 40.7855, + 'longitude': -111.7367, + 'device': 'new_device', + 'id': 'Home', + 'trigger': 'exit' + } + + # Exit Home + req = await locative_client.post(url, data=data) await hass.async_block_till_done() - assert 1 == len(hass.data[DATA_DISPATCHER][TRACKER_UPDATE]) + assert req.status == HTTP_OK + + state = hass.states.get('{}.{}'.format(DEVICE_TRACKER_DOMAIN, + data['device'])) + assert state.state == 'not_home' + assert len(hass.data[DATA_DISPATCHER][TRACKER_UPDATE]) == 1 + + entry = hass.config_entries.async_entries(DOMAIN)[0] await locative.async_unload_entry(hass, entry) await hass.async_block_till_done() - assert 0 == len(hass.data[DATA_DISPATCHER][TRACKER_UPDATE]) + assert not hass.data[DATA_DISPATCHER][TRACKER_UPDATE] diff --git a/tests/components/media_player/test_demo.py b/tests/components/media_player/test_demo.py index b213cf0b5c195..8cbe0a594d294 100644 --- a/tests/components/media_player/test_demo.py +++ b/tests/components/media_player/test_demo.py @@ -6,22 +6,13 @@ import pytest import voluptuous as vol -from homeassistant.setup import setup_component -from homeassistant.const import HTTP_HEADER_HA_AUTH +from homeassistant.setup import setup_component, async_setup_component import homeassistant.components.media_player as mp -import homeassistant.components.http as http from homeassistant.helpers.aiohttp_client import DATA_CLIENTSESSION -import requests - -from tests.common import get_test_home_assistant, get_test_instance_port +from tests.common import get_test_home_assistant from tests.components.media_player import common -SERVER_PORT = get_test_instance_port() -HTTP_BASE_URL = 'http://127.0.0.1:{}'.format(SERVER_PORT) -API_PASSWORD = "test1234" -HA_HEADERS = {HTTP_HEADER_HA_AUTH: API_PASSWORD} - entity_id = 'media_player.walkman' @@ -231,61 +222,41 @@ def test_play_media(self, mock_seek): assert mock_seek.called -class TestMediaPlayerWeb(unittest.TestCase): - """Test the media player web views sensor.""" - - def setUp(self): - """Set up things to be run when tests are started.""" - self.hass = get_test_home_assistant() - - assert setup_component(self.hass, http.DOMAIN, { - http.DOMAIN: { - http.CONF_SERVER_PORT: SERVER_PORT, - http.CONF_API_PASSWORD: API_PASSWORD, - }, - }) - - assert setup_component( - self.hass, mp.DOMAIN, - {'media_player': {'platform': 'demo'}}) - - self.hass.start() - - def tearDown(self): - """Stop everything that was started.""" - self.hass.stop() +async def test_media_image_proxy(hass, hass_client): + """Test the media server image proxy server .""" + assert await async_setup_component( + hass, mp.DOMAIN, + {'media_player': {'platform': 'demo'}}) - def test_media_image_proxy(self): - """Test the media server image proxy server .""" - fake_picture_data = 'test.test' + fake_picture_data = 'test.test' - class MockResponse(): - def __init__(self): - self.status = 200 - self.headers = {'Content-Type': 'sometype'} + class MockResponse(): + def __init__(self): + self.status = 200 + self.headers = {'Content-Type': 'sometype'} - @asyncio.coroutine - def read(self): - return fake_picture_data.encode('ascii') + @asyncio.coroutine + def read(self): + return fake_picture_data.encode('ascii') - @asyncio.coroutine - def release(self): - pass + @asyncio.coroutine + def release(self): + pass - class MockWebsession(): + class MockWebsession(): - @asyncio.coroutine - def get(self, url): - return MockResponse() + @asyncio.coroutine + def get(self, url): + return MockResponse() - def detach(self): - pass + def detach(self): + pass - self.hass.data[DATA_CLIENTSESSION] = MockWebsession() + hass.data[DATA_CLIENTSESSION] = MockWebsession() - assert self.hass.states.is_state(entity_id, 'playing') - state = self.hass.states.get(entity_id) - req = requests.get(HTTP_BASE_URL + - state.attributes.get('entity_picture')) - assert req.status_code == 200 - assert req.text == fake_picture_data + assert hass.states.is_state(entity_id, 'playing') + state = hass.states.get(entity_id) + client = await hass_client() + req = await client.get(state.attributes.get('entity_picture')) + assert req.status == 200 + assert await req.text() == fake_picture_data diff --git a/tests/components/mobile_app/__init__.py b/tests/components/mobile_app/__init__.py new file mode 100644 index 0000000000000..becdc2841f3c0 --- /dev/null +++ b/tests/components/mobile_app/__init__.py @@ -0,0 +1 @@ +"""Tests for mobile_app component.""" diff --git a/tests/components/mobile_app/test_init.py b/tests/components/mobile_app/test_init.py new file mode 100644 index 0000000000000..d0c1ae02c6c5b --- /dev/null +++ b/tests/components/mobile_app/test_init.py @@ -0,0 +1,275 @@ +"""Test the mobile_app_http platform.""" +import pytest + +from homeassistant.setup import async_setup_component + +from homeassistant.const import CONF_WEBHOOK_ID +from homeassistant.components.mobile_app import (DOMAIN, STORAGE_KEY, + STORAGE_VERSION, + CONF_SECRET, CONF_USER_ID) +from homeassistant.core import callback + +from tests.common import async_mock_service + +FIRE_EVENT = { + 'type': 'fire_event', + 'data': { + 'event_type': 'test_event', + 'event_data': { + 'hello': 'yo world' + } + } +} + +RENDER_TEMPLATE = { + 'type': 'render_template', + 'data': { + 'template': 'Hello world' + } +} + +CALL_SERVICE = { + 'type': 'call_service', + 'data': { + 'domain': 'test', + 'service': 'mobile_app', + 'service_data': { + 'foo': 'bar' + } + } +} + +REGISTER = { + 'app_data': {'foo': 'bar'}, + 'app_id': 'io.homeassistant.mobile_app_test', + 'app_name': 'Mobile App Tests', + 'app_version': '1.0.0', + 'device_name': 'Test 1', + 'manufacturer': 'mobile_app', + 'model': 'Test', + 'os_version': '1.0', + 'supports_encryption': True +} + +UPDATE = { + 'app_data': {'foo': 'bar'}, + 'app_version': '2.0.0', + 'device_name': 'Test 1', + 'manufacturer': 'mobile_app', + 'model': 'Test', + 'os_version': '1.0' +} + +# pylint: disable=redefined-outer-name + + +@pytest.fixture +def mobile_app_client(hass, aiohttp_client, hass_storage, hass_admin_user): + """mobile_app mock client.""" + hass_storage[STORAGE_KEY] = { + 'version': STORAGE_VERSION, + 'data': { + 'mobile_app_test': { + CONF_SECRET: '58eb127991594dad934d1584bdee5f27', + 'supports_encryption': True, + CONF_WEBHOOK_ID: 'mobile_app_test', + 'device_name': 'Test Device', + CONF_USER_ID: hass_admin_user.id, + } + } + } + + assert hass.loop.run_until_complete(async_setup_component( + hass, DOMAIN, { + DOMAIN: {} + })) + + return hass.loop.run_until_complete(aiohttp_client(hass.http.app)) + + +@pytest.fixture +async def mock_api_client(hass, hass_client): + """Provide an authenticated client for mobile_app to use.""" + await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) + return await hass_client() + + +async def test_handle_render_template(mobile_app_client): + """Test that we render templates properly.""" + resp = await mobile_app_client.post( + '/api/webhook/mobile_app_test', + json=RENDER_TEMPLATE + ) + + assert resp.status == 200 + + json = await resp.json() + assert json == {'rendered': 'Hello world'} + + +async def test_handle_call_services(hass, mobile_app_client): + """Test that we call services properly.""" + calls = async_mock_service(hass, 'test', 'mobile_app') + + resp = await mobile_app_client.post( + '/api/webhook/mobile_app_test', + json=CALL_SERVICE + ) + + assert resp.status == 200 + + assert len(calls) == 1 + + +async def test_handle_fire_event(hass, mobile_app_client): + """Test that we can fire events.""" + events = [] + + @callback + def store_event(event): + """Helepr to store events.""" + events.append(event) + + hass.bus.async_listen('test_event', store_event) + + resp = await mobile_app_client.post( + '/api/webhook/mobile_app_test', + json=FIRE_EVENT + ) + + assert resp.status == 200 + text = await resp.text() + assert text == "" + + assert len(events) == 1 + assert events[0].data['hello'] == 'yo world' + + +async def test_update_registration(mobile_app_client, hass_client): + """Test that a we can update an existing registration via webhook.""" + mock_api_client = await hass_client() + register_resp = await mock_api_client.post( + '/api/mobile_app/devices', json=REGISTER + ) + + assert register_resp.status == 201 + register_json = await register_resp.json() + + webhook_id = register_json[CONF_WEBHOOK_ID] + + update_container = { + 'type': 'update_registration', + 'data': UPDATE + } + + update_resp = await mobile_app_client.post( + '/api/webhook/{}'.format(webhook_id), json=update_container + ) + + assert update_resp.status == 200 + update_json = await update_resp.json() + assert update_json['app_version'] == '2.0.0' + assert CONF_WEBHOOK_ID not in update_json + assert CONF_SECRET not in update_json + + +async def test_returns_error_incorrect_json(mobile_app_client, caplog): + """Test that an error is returned when JSON is invalid.""" + resp = await mobile_app_client.post( + '/api/webhook/mobile_app_test', + data='not json' + ) + + assert resp.status == 400 + json = await resp.json() + assert json == [] + assert 'invalid JSON' in caplog.text + + +async def test_handle_decryption(mobile_app_client): + """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 + except (ImportError, OSError): + pytest.skip("libnacl/libsodium is not installed") + return + + import json + + keylen = SecretBox.KEY_SIZE + key = "58eb127991594dad934d1584bdee5f27".encode("utf-8") + key = key[:keylen] + key = key.ljust(keylen, b'\0') + + payload = json.dumps({'template': 'Hello world'}).encode("utf-8") + + data = SecretBox(key).encrypt(payload, + encoder=Base64Encoder).decode("utf-8") + + container = { + 'type': 'render_template', + 'encrypted': True, + 'encrypted_data': data, + } + + resp = await mobile_app_client.post( + '/api/webhook/mobile_app_test', + json=container + ) + + assert resp.status == 200 + + json = await resp.json() + assert json == {'rendered': 'Hello world'} + + +async def test_register_device(hass_client, mock_api_client): + """Test that a device can be registered.""" + try: + # pylint: disable=unused-import + from nacl.secret import SecretBox # noqa: F401 + from nacl.encoding import Base64Encoder # noqa: F401 + except (ImportError, OSError): + pytest.skip("libnacl/libsodium is not installed") + return + + import json + + resp = await mock_api_client.post( + '/api/mobile_app/devices', json=REGISTER + ) + + assert resp.status == 201 + register_json = await resp.json() + assert CONF_WEBHOOK_ID in register_json + assert CONF_SECRET in register_json + + keylen = SecretBox.KEY_SIZE + key = register_json[CONF_SECRET].encode("utf-8") + key = key[:keylen] + key = key.ljust(keylen, b'\0') + + payload = json.dumps({'template': 'Hello world'}).encode("utf-8") + + data = SecretBox(key).encrypt(payload, + encoder=Base64Encoder).decode("utf-8") + + container = { + 'type': 'render_template', + 'encrypted': True, + 'encrypted_data': data, + } + + mobile_app_client = await hass_client() + + resp = await mobile_app_client.post( + '/api/webhook/{}'.format(register_json[CONF_WEBHOOK_ID]), + json=container + ) + + assert resp.status == 200 + + webhook_json = await resp.json() + assert webhook_json == {'rendered': 'Hello world'} diff --git a/tests/components/mqtt/test_climate.py b/tests/components/mqtt/test_climate.py index ecbdc39e22bca..7bdfe8f452fa6 100644 --- a/tests/components/mqtt/test_climate.py +++ b/tests/components/mqtt/test_climate.py @@ -7,11 +7,15 @@ import pytest import voluptuous as vol -from homeassistant.components import climate, mqtt +from homeassistant.components import mqtt from homeassistant.components.climate import ( - DEFAULT_MAX_TEMP, DEFAULT_MIN_TEMP, SUPPORT_AUX_HEAT, SUPPORT_AWAY_MODE, + DEFAULT_MAX_TEMP, DEFAULT_MIN_TEMP) +from homeassistant.components.climate.const import ( + DOMAIN as CLIMATE_DOMAIN, + SUPPORT_AUX_HEAT, SUPPORT_AWAY_MODE, SUPPORT_FAN_MODE, SUPPORT_HOLD_MODE, SUPPORT_OPERATION_MODE, - SUPPORT_SWING_MODE, SUPPORT_TARGET_TEMPERATURE) + SUPPORT_SWING_MODE, SUPPORT_TARGET_TEMPERATURE, STATE_AUTO, + STATE_COOL, STATE_HEAT, STATE_DRY, STATE_FAN_ONLY) from homeassistant.components.mqtt.discovery import async_start from homeassistant.const import STATE_OFF, STATE_UNAVAILABLE from homeassistant.setup import setup_component @@ -54,7 +58,7 @@ def tearDown(self): # pylint: disable=invalid-name def test_setup_params(self): """Test the initial parameters.""" - assert setup_component(self.hass, climate.DOMAIN, DEFAULT_CONFIG) + assert setup_component(self.hass, CLIMATE_DOMAIN, DEFAULT_CONFIG) state = self.hass.states.get(ENTITY_CLIMATE) assert 21 == state.attributes.get('temperature') @@ -66,7 +70,7 @@ def test_setup_params(self): def test_supported_features(self): """Test the supported_features.""" - assert setup_component(self.hass, climate.DOMAIN, DEFAULT_CONFIG) + assert setup_component(self.hass, CLIMATE_DOMAIN, DEFAULT_CONFIG) state = self.hass.states.get(ENTITY_CLIMATE) support = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_OPERATION_MODE | @@ -77,13 +81,13 @@ def test_supported_features(self): def test_get_operation_modes(self): """Test that the operation list returns the correct modes.""" - assert setup_component(self.hass, climate.DOMAIN, DEFAULT_CONFIG) + assert setup_component(self.hass, CLIMATE_DOMAIN, DEFAULT_CONFIG) state = self.hass.states.get(ENTITY_CLIMATE) modes = state.attributes.get('operation_list') assert [ - climate.STATE_AUTO, STATE_OFF, climate.STATE_COOL, - climate.STATE_HEAT, climate.STATE_DRY, climate.STATE_FAN_ONLY + STATE_AUTO, STATE_OFF, STATE_COOL, + STATE_HEAT, STATE_DRY, STATE_FAN_ONLY ] == modes def test_set_operation_bad_attr_and_state(self): @@ -91,7 +95,7 @@ def test_set_operation_bad_attr_and_state(self): Also check the state. """ - assert setup_component(self.hass, climate.DOMAIN, DEFAULT_CONFIG) + assert setup_component(self.hass, CLIMATE_DOMAIN, DEFAULT_CONFIG) state = self.hass.states.get(ENTITY_CLIMATE) assert "off" == state.attributes.get('operation_mode') @@ -105,7 +109,7 @@ def test_set_operation_bad_attr_and_state(self): def test_set_operation(self): """Test setting of new operation mode.""" - assert setup_component(self.hass, climate.DOMAIN, DEFAULT_CONFIG) + assert setup_component(self.hass, CLIMATE_DOMAIN, DEFAULT_CONFIG) state = self.hass.states.get(ENTITY_CLIMATE) assert "off" == state.attributes.get('operation_mode') @@ -122,7 +126,7 @@ def test_set_operation_pessimistic(self): """Test setting operation mode in pessimistic mode.""" config = copy.deepcopy(DEFAULT_CONFIG) config['climate']['mode_state_topic'] = 'mode-state' - assert setup_component(self.hass, climate.DOMAIN, config) + assert setup_component(self.hass, CLIMATE_DOMAIN, config) state = self.hass.states.get(ENTITY_CLIMATE) assert state.attributes.get('operation_mode') is None @@ -150,7 +154,7 @@ def test_set_operation_with_power_command(self): """Test setting of new operation mode with power command enabled.""" config = copy.deepcopy(DEFAULT_CONFIG) config['climate']['power_command_topic'] = 'power-command' - assert setup_component(self.hass, climate.DOMAIN, config) + assert setup_component(self.hass, CLIMATE_DOMAIN, config) state = self.hass.states.get(ENTITY_CLIMATE) assert "off" == state.attributes.get('operation_mode') @@ -179,7 +183,7 @@ def test_set_operation_with_power_command(self): def test_set_fan_mode_bad_attr(self): """Test setting fan mode without required attribute.""" - assert setup_component(self.hass, climate.DOMAIN, DEFAULT_CONFIG) + assert setup_component(self.hass, CLIMATE_DOMAIN, DEFAULT_CONFIG) state = self.hass.states.get(ENTITY_CLIMATE) assert "low" == state.attributes.get('fan_mode') @@ -193,7 +197,7 @@ def test_set_fan_mode_pessimistic(self): """Test setting of new fan mode in pessimistic mode.""" config = copy.deepcopy(DEFAULT_CONFIG) config['climate']['fan_mode_state_topic'] = 'fan-state' - assert setup_component(self.hass, climate.DOMAIN, config) + assert setup_component(self.hass, CLIMATE_DOMAIN, config) state = self.hass.states.get(ENTITY_CLIMATE) assert state.attributes.get('fan_mode') is None @@ -215,7 +219,7 @@ def test_set_fan_mode_pessimistic(self): def test_set_fan_mode(self): """Test setting of new fan mode.""" - assert setup_component(self.hass, climate.DOMAIN, DEFAULT_CONFIG) + assert setup_component(self.hass, CLIMATE_DOMAIN, DEFAULT_CONFIG) state = self.hass.states.get(ENTITY_CLIMATE) assert "low" == state.attributes.get('fan_mode') @@ -228,7 +232,7 @@ def test_set_fan_mode(self): def test_set_swing_mode_bad_attr(self): """Test setting swing mode without required attribute.""" - assert setup_component(self.hass, climate.DOMAIN, DEFAULT_CONFIG) + assert setup_component(self.hass, CLIMATE_DOMAIN, DEFAULT_CONFIG) state = self.hass.states.get(ENTITY_CLIMATE) assert "off" == state.attributes.get('swing_mode') @@ -242,7 +246,7 @@ def test_set_swing_pessimistic(self): """Test setting swing mode in pessimistic mode.""" config = copy.deepcopy(DEFAULT_CONFIG) config['climate']['swing_mode_state_topic'] = 'swing-state' - assert setup_component(self.hass, climate.DOMAIN, config) + assert setup_component(self.hass, CLIMATE_DOMAIN, config) state = self.hass.states.get(ENTITY_CLIMATE) assert state.attributes.get('swing_mode') is None @@ -264,7 +268,7 @@ def test_set_swing_pessimistic(self): def test_set_swing(self): """Test setting of new swing mode.""" - assert setup_component(self.hass, climate.DOMAIN, DEFAULT_CONFIG) + assert setup_component(self.hass, CLIMATE_DOMAIN, DEFAULT_CONFIG) state = self.hass.states.get(ENTITY_CLIMATE) assert "off" == state.attributes.get('swing_mode') @@ -277,7 +281,7 @@ def test_set_swing(self): def test_set_target_temperature(self): """Test setting the target temperature.""" - assert setup_component(self.hass, climate.DOMAIN, DEFAULT_CONFIG) + assert setup_component(self.hass, CLIMATE_DOMAIN, DEFAULT_CONFIG) state = self.hass.states.get(ENTITY_CLIMATE) assert 21 == state.attributes.get('temperature') @@ -315,7 +319,7 @@ def test_set_target_temperature_pessimistic(self): """Test setting the target temperature.""" config = copy.deepcopy(DEFAULT_CONFIG) config['climate']['temperature_state_topic'] = 'temperature-state' - assert setup_component(self.hass, climate.DOMAIN, config) + assert setup_component(self.hass, CLIMATE_DOMAIN, config) state = self.hass.states.get(ENTITY_CLIMATE) assert state.attributes.get('temperature') is None @@ -342,7 +346,7 @@ def test_receive_mqtt_temperature(self): config = copy.deepcopy(DEFAULT_CONFIG) config['climate']['current_temperature_topic'] = 'current_temperature' mock_component(self.hass, 'mqtt') - assert setup_component(self.hass, climate.DOMAIN, config) + assert setup_component(self.hass, CLIMATE_DOMAIN, config) fire_mqtt_message(self.hass, 'current_temperature', '47') self.hass.block_till_done() @@ -353,7 +357,7 @@ def test_set_away_mode_pessimistic(self): """Test setting of the away mode.""" config = copy.deepcopy(DEFAULT_CONFIG) config['climate']['away_mode_state_topic'] = 'away-state' - assert setup_component(self.hass, climate.DOMAIN, config) + assert setup_component(self.hass, CLIMATE_DOMAIN, config) state = self.hass.states.get(ENTITY_CLIMATE) assert 'off' == state.attributes.get('away_mode') @@ -384,7 +388,7 @@ def test_set_away_mode(self): config['climate']['payload_on'] = 'AN' config['climate']['payload_off'] = 'AUS' - assert setup_component(self.hass, climate.DOMAIN, config) + assert setup_component(self.hass, CLIMATE_DOMAIN, config) state = self.hass.states.get(ENTITY_CLIMATE) assert 'off' == state.attributes.get('away_mode') @@ -407,7 +411,7 @@ def test_set_hold_pessimistic(self): """Test setting the hold mode in pessimistic mode.""" config = copy.deepcopy(DEFAULT_CONFIG) config['climate']['hold_state_topic'] = 'hold-state' - assert setup_component(self.hass, climate.DOMAIN, config) + assert setup_component(self.hass, CLIMATE_DOMAIN, config) state = self.hass.states.get(ENTITY_CLIMATE) assert state.attributes.get('hold_mode') is None @@ -429,7 +433,7 @@ def test_set_hold_pessimistic(self): def test_set_hold(self): """Test setting the hold mode.""" - assert setup_component(self.hass, climate.DOMAIN, DEFAULT_CONFIG) + assert setup_component(self.hass, CLIMATE_DOMAIN, DEFAULT_CONFIG) state = self.hass.states.get(ENTITY_CLIMATE) assert state.attributes.get('hold_mode') is None @@ -452,7 +456,7 @@ def test_set_aux_pessimistic(self): """Test setting of the aux heating in pessimistic mode.""" config = copy.deepcopy(DEFAULT_CONFIG) config['climate']['aux_state_topic'] = 'aux-state' - assert setup_component(self.hass, climate.DOMAIN, config) + assert setup_component(self.hass, CLIMATE_DOMAIN, config) state = self.hass.states.get(ENTITY_CLIMATE) assert 'off' == state.attributes.get('aux_heat') @@ -479,7 +483,7 @@ def test_set_aux_pessimistic(self): def test_set_aux(self): """Test setting of the aux heating.""" - assert setup_component(self.hass, climate.DOMAIN, DEFAULT_CONFIG) + assert setup_component(self.hass, CLIMATE_DOMAIN, DEFAULT_CONFIG) state = self.hass.states.get(ENTITY_CLIMATE) assert 'off' == state.attributes.get('aux_heat') @@ -505,7 +509,7 @@ def test_custom_availability_payload(self): config['climate']['payload_available'] = 'good' config['climate']['payload_not_available'] = 'nogood' - assert setup_component(self.hass, climate.DOMAIN, config) + assert setup_component(self.hass, CLIMATE_DOMAIN, config) state = self.hass.states.get('climate.test') assert STATE_UNAVAILABLE == state.state @@ -543,7 +547,7 @@ def test_set_with_templates(self): config['climate']['aux_state_topic'] = 'aux-state' config['climate']['current_temperature_topic'] = 'current-temperature' - assert setup_component(self.hass, climate.DOMAIN, config) + assert setup_component(self.hass, CLIMATE_DOMAIN, config) # Operation Mode state = self.hass.states.get(ENTITY_CLIMATE) @@ -638,7 +642,7 @@ def test_min_temp_custom(self): config = copy.deepcopy(DEFAULT_CONFIG) config['climate']['min_temp'] = 26 - assert setup_component(self.hass, climate.DOMAIN, config) + assert setup_component(self.hass, CLIMATE_DOMAIN, config) state = self.hass.states.get(ENTITY_CLIMATE) min_temp = state.attributes.get('min_temp') @@ -651,7 +655,7 @@ def test_max_temp_custom(self): config = copy.deepcopy(DEFAULT_CONFIG) config['climate']['max_temp'] = 60 - assert setup_component(self.hass, climate.DOMAIN, config) + assert setup_component(self.hass, CLIMATE_DOMAIN, config) state = self.hass.states.get(ENTITY_CLIMATE) max_temp = state.attributes.get('max_temp') @@ -664,7 +668,7 @@ def test_temp_step_custom(self): config = copy.deepcopy(DEFAULT_CONFIG) config['climate']['temp_step'] = 0.01 - assert setup_component(self.hass, climate.DOMAIN, config) + assert setup_component(self.hass, CLIMATE_DOMAIN, config) state = self.hass.states.get(ENTITY_CLIMATE) temp_step = state.attributes.get('target_temp_step') @@ -675,8 +679,8 @@ def test_temp_step_custom(self): async def test_setting_attribute_via_mqtt_json_message(hass, mqtt_mock): """Test the setting of attribute via MQTT with JSON payload.""" - assert await async_setup_component(hass, climate.DOMAIN, { - climate.DOMAIN: { + assert await async_setup_component(hass, CLIMATE_DOMAIN, { + CLIMATE_DOMAIN: { 'platform': 'mqtt', 'name': 'test', 'power_state_topic': 'test-topic', @@ -694,8 +698,8 @@ async def test_setting_attribute_via_mqtt_json_message(hass, mqtt_mock): async def test_update_with_json_attrs_not_dict(hass, mqtt_mock, caplog): """Test attributes get extracted from a JSON result.""" - assert await async_setup_component(hass, climate.DOMAIN, { - climate.DOMAIN: { + assert await async_setup_component(hass, CLIMATE_DOMAIN, { + CLIMATE_DOMAIN: { 'platform': 'mqtt', 'name': 'test', 'power_state_topic': 'test-topic', @@ -714,8 +718,8 @@ async def test_update_with_json_attrs_not_dict(hass, mqtt_mock, caplog): async def test_update_with_json_attrs_bad_JSON(hass, mqtt_mock, caplog): """Test attributes get extracted from a JSON result.""" - assert await async_setup_component(hass, climate.DOMAIN, { - climate.DOMAIN: { + assert await async_setup_component(hass, CLIMATE_DOMAIN, { + CLIMATE_DOMAIN: { 'platform': 'mqtt', 'name': 'test', 'power_state_topic': 'test-topic', @@ -781,8 +785,8 @@ async def test_discovery_update_attr(hass, mqtt_mock, caplog): async def test_unique_id(hass): """Test unique id option only creates one climate per unique_id.""" await async_mock_mqtt_component(hass) - assert await async_setup_component(hass, climate.DOMAIN, { - climate.DOMAIN: [{ + assert await async_setup_component(hass, CLIMATE_DOMAIN, { + CLIMATE_DOMAIN: [{ 'platform': 'mqtt', 'name': 'Test 1', 'power_state_topic': 'test-topic', @@ -798,7 +802,7 @@ async def test_unique_id(hass): }) async_fire_mqtt_message(hass, 'test-topic', 'payload') await hass.async_block_till_done() - assert len(hass.states.async_entity_ids(climate.DOMAIN)) == 1 + assert len(hass.states.async_entity_ids(CLIMATE_DOMAIN)) == 1 async def test_discovery_removal_climate(hass, mqtt_mock, caplog): @@ -974,8 +978,8 @@ async def test_entity_id_update(hass, mqtt_mock): """Test MQTT subscriptions are managed when entity_id is updated.""" registry = mock_registry(hass, {}) mock_mqtt = await async_mock_mqtt_component(hass) - assert await async_setup_component(hass, climate.DOMAIN, { - climate.DOMAIN: [{ + assert await async_setup_component(hass, CLIMATE_DOMAIN, { + CLIMATE_DOMAIN: [{ 'platform': 'mqtt', 'name': 'beer', 'mode_state_topic': 'test-topic', diff --git a/tests/components/person/test_init.py b/tests/components/person/test_init.py index 6c8c6ebd0ddd4..ef129a555beee 100644 --- a/tests/components/person/test_init.py +++ b/tests/components/person/test_init.py @@ -4,8 +4,10 @@ from homeassistant.components.person import ( ATTR_SOURCE, ATTR_USER_ID, DOMAIN, PersonManager) from homeassistant.const import ( - ATTR_ID, ATTR_LATITUDE, ATTR_LONGITUDE, STATE_UNKNOWN, - EVENT_HOMEASSISTANT_START) + ATTR_ID, ATTR_LATITUDE, ATTR_LONGITUDE, ATTR_GPS_ACCURACY, + STATE_UNKNOWN, EVENT_HOMEASSISTANT_START) +from homeassistant.components.device_tracker import ( + ATTR_SOURCE_TYPE, SOURCE_TYPE_GPS, SOURCE_TYPE_ROUTER) from homeassistant.core import CoreState, State from homeassistant.setup import async_setup_component @@ -134,15 +136,18 @@ async def test_setup_tracker(hass, hass_admin_user): assert state.attributes.get(ATTR_USER_ID) == user_id hass.states.async_set( - DEVICE_TRACKER, 'not_home', - {ATTR_LATITUDE: 10.123456, ATTR_LONGITUDE: 11.123456}) + DEVICE_TRACKER, 'not_home', { + ATTR_LATITUDE: 10.123456, + ATTR_LONGITUDE: 11.123456, + ATTR_GPS_ACCURACY: 10}) await hass.async_block_till_done() state = hass.states.get('person.tracked_person') assert state.state == 'not_home' assert state.attributes.get(ATTR_ID) == '1234' - assert state.attributes.get(ATTR_LATITUDE) == 10.12346 - assert state.attributes.get(ATTR_LONGITUDE) == 11.12346 + assert state.attributes.get(ATTR_LATITUDE) == 10.123456 + assert state.attributes.get(ATTR_LONGITUDE) == 11.123456 + assert state.attributes.get(ATTR_GPS_ACCURACY) == 10 assert state.attributes.get(ATTR_SOURCE) == DEVICE_TRACKER assert state.attributes.get(ATTR_USER_ID) == user_id @@ -166,7 +171,8 @@ async def test_setup_two_trackers(hass, hass_admin_user): hass.bus.async_fire(EVENT_HOMEASSISTANT_START) await hass.async_block_till_done() - hass.states.async_set(DEVICE_TRACKER, 'home') + hass.states.async_set( + DEVICE_TRACKER, 'home', {ATTR_SOURCE_TYPE: SOURCE_TYPE_ROUTER}) await hass.async_block_till_done() state = hass.states.get('person.tracked_person') @@ -174,22 +180,49 @@ async def test_setup_two_trackers(hass, hass_admin_user): assert state.attributes.get(ATTR_ID) == '1234' assert state.attributes.get(ATTR_LATITUDE) is None assert state.attributes.get(ATTR_LONGITUDE) is None + assert state.attributes.get(ATTR_GPS_ACCURACY) is None assert state.attributes.get(ATTR_SOURCE) == DEVICE_TRACKER assert state.attributes.get(ATTR_USER_ID) == user_id hass.states.async_set( - DEVICE_TRACKER_2, 'not_home', - {ATTR_LATITUDE: 12.123456, ATTR_LONGITUDE: 13.123456}) + DEVICE_TRACKER_2, 'not_home', { + ATTR_LATITUDE: 12.123456, + ATTR_LONGITUDE: 13.123456, + ATTR_GPS_ACCURACY: 12, + ATTR_SOURCE_TYPE: SOURCE_TYPE_GPS}) + await hass.async_block_till_done() + hass.states.async_set( + DEVICE_TRACKER, 'not_home', {ATTR_SOURCE_TYPE: SOURCE_TYPE_ROUTER}) await hass.async_block_till_done() state = hass.states.get('person.tracked_person') assert state.state == 'not_home' assert state.attributes.get(ATTR_ID) == '1234' - assert state.attributes.get(ATTR_LATITUDE) == 12.12346 - assert state.attributes.get(ATTR_LONGITUDE) == 13.12346 + assert state.attributes.get(ATTR_LATITUDE) == 12.123456 + assert state.attributes.get(ATTR_LONGITUDE) == 13.123456 + assert state.attributes.get(ATTR_GPS_ACCURACY) == 12 assert state.attributes.get(ATTR_SOURCE) == DEVICE_TRACKER_2 assert state.attributes.get(ATTR_USER_ID) == user_id + hass.states.async_set( + DEVICE_TRACKER_2, 'zone1', {ATTR_SOURCE_TYPE: SOURCE_TYPE_GPS}) + await hass.async_block_till_done() + + state = hass.states.get('person.tracked_person') + assert state.state == 'zone1' + assert state.attributes.get(ATTR_SOURCE) == DEVICE_TRACKER_2 + + hass.states.async_set( + DEVICE_TRACKER, 'home', {ATTR_SOURCE_TYPE: SOURCE_TYPE_ROUTER}) + await hass.async_block_till_done() + hass.states.async_set( + DEVICE_TRACKER_2, 'zone2', {ATTR_SOURCE_TYPE: SOURCE_TYPE_GPS}) + await hass.async_block_till_done() + + state = hass.states.get('person.tracked_person') + assert state.state == 'home' + assert state.attributes.get(ATTR_SOURCE) == DEVICE_TRACKER + async def test_ignore_unavailable_states(hass, hass_admin_user): """Test set up person with two device trackers, one unavailable.""" diff --git a/tests/components/ps4/__init__.py b/tests/components/ps4/__init__.py new file mode 100644 index 0000000000000..c80bcf9173db9 --- /dev/null +++ b/tests/components/ps4/__init__.py @@ -0,0 +1 @@ +"""Tests for the PlayStation 4 component.""" diff --git a/tests/components/ps4/test_config_flow.py b/tests/components/ps4/test_config_flow.py new file mode 100644 index 0000000000000..b0170beeb48ef --- /dev/null +++ b/tests/components/ps4/test_config_flow.py @@ -0,0 +1,149 @@ +"""Define tests for the PlayStation 4 config flow.""" +from unittest.mock import patch + +from homeassistant import data_entry_flow +from homeassistant.components import ps4 +from homeassistant.components.ps4.const import ( + DEFAULT_NAME, DEFAULT_REGION) +from homeassistant.const import ( + CONF_CODE, CONF_HOST, CONF_IP_ADDRESS, CONF_NAME, CONF_REGION, CONF_TOKEN) + +from tests.common import MockConfigEntry + +MOCK_TITLE = 'PlayStation 4' +MOCK_CODE = '12345678' +MOCK_CREDS = '000aa000' +MOCK_HOST = '192.0.0.0' +MOCK_DEVICE = { + CONF_HOST: MOCK_HOST, + CONF_NAME: DEFAULT_NAME, + CONF_REGION: DEFAULT_REGION +} +MOCK_CONFIG = { + CONF_IP_ADDRESS: MOCK_HOST, + CONF_NAME: DEFAULT_NAME, + CONF_REGION: DEFAULT_REGION, + CONF_CODE: MOCK_CODE +} +MOCK_DATA = { + CONF_TOKEN: MOCK_CREDS, + 'devices': MOCK_DEVICE +} +MOCK_UDP_PORT = int(987) +MOCK_TCP_PORT = int(997) + + +async def test_full_flow_implementation(hass): + """Test registering an implementation and flow works.""" + flow = ps4.PlayStation4FlowHandler() + flow.hass = hass + + # User Step Started, results in Step Creds + with patch('pyps4_homeassistant.Helper.port_bind', + return_value=None): + result = await flow.async_step_user() + assert result['type'] == data_entry_flow.RESULT_TYPE_FORM + assert result['step_id'] == 'creds' + + # Step Creds results with form in Step Link. + with patch('pyps4_homeassistant.Helper.get_creds', + return_value=MOCK_CREDS), \ + patch('pyps4_homeassistant.Helper.has_devices', + return_value=[{'host-ip': MOCK_HOST}]): + result = await flow.async_step_creds({}) + assert result['type'] == data_entry_flow.RESULT_TYPE_FORM + assert result['step_id'] == 'link' + + # User Input results in created entry. + with patch('pyps4_homeassistant.Helper.link', + return_value=(True, True)), \ + patch('pyps4_homeassistant.Helper.has_devices', + return_value=[{'host-ip': MOCK_HOST}]): + result = await flow.async_step_link(MOCK_CONFIG) + assert result['type'] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result['data'][CONF_TOKEN] == MOCK_CREDS + assert result['data']['devices'] == [MOCK_DEVICE] + assert result['title'] == MOCK_TITLE + + +async def test_port_bind_abort(hass): + """Test that flow aborted when cannot bind to ports 987, 997.""" + flow = ps4.PlayStation4FlowHandler() + flow.hass = hass + + with patch('pyps4_homeassistant.Helper.port_bind', + return_value=MOCK_UDP_PORT): + reason = 'port_987_bind_error' + result = await flow.async_step_user(user_input=None) + assert result['type'] == data_entry_flow.RESULT_TYPE_ABORT + assert result['reason'] == reason + + with patch('pyps4_homeassistant.Helper.port_bind', + return_value=MOCK_TCP_PORT): + reason = 'port_997_bind_error' + result = await flow.async_step_user(user_input=None) + assert result['type'] == data_entry_flow.RESULT_TYPE_ABORT + assert result['reason'] == reason + + +async def test_duplicate_abort(hass): + """Test that Flow aborts when already configured.""" + MockConfigEntry(domain=ps4.DOMAIN, data=MOCK_DATA).add_to_hass(hass) + flow = ps4.PlayStation4FlowHandler() + flow.hass = hass + + result = await flow.async_step_user(user_input=None) + assert result['type'] == data_entry_flow.RESULT_TYPE_ABORT + assert result['reason'] == 'devices_configured' + + +async def test_no_devices_found_abort(hass): + """Test that failure to find devices aborts flow.""" + flow = ps4.PlayStation4FlowHandler() + flow.hass = hass + + with patch('pyps4_homeassistant.Helper.has_devices', return_value=None): + result = await flow.async_step_link(MOCK_CONFIG) + assert result['type'] == data_entry_flow.RESULT_TYPE_ABORT + assert result['reason'] == 'no_devices_found' + + +async def test_credential_abort(hass): + """Test that failure to get credentials aborts flow.""" + flow = ps4.PlayStation4FlowHandler() + flow.hass = hass + + with patch('pyps4_homeassistant.Helper.get_creds', return_value=None): + result = await flow.async_step_creds({}) + assert result['type'] == data_entry_flow.RESULT_TYPE_ABORT + assert result['reason'] == 'credential_error' + + +async def test_invalid_pin_error(hass): + """Test that invalid pin throws an error.""" + flow = ps4.PlayStation4FlowHandler() + flow.hass = hass + + with patch('pyps4_homeassistant.Helper.link', + return_value=(True, False)), \ + patch('pyps4_homeassistant.Helper.has_devices', + return_value=[{'host-ip': MOCK_HOST}]): + result = await flow.async_step_link(MOCK_CONFIG) + assert result['type'] == data_entry_flow.RESULT_TYPE_FORM + assert result['step_id'] == 'link' + assert result['errors'] == {'base': 'login_failed'} + + +async def test_device_connection_error(hass): + """Test that device not connected or on throws an error.""" + flow = ps4.PlayStation4FlowHandler() + flow.hass = hass + + with patch('pyps4_homeassistant.Helper.link', + return_value=(False, True)), \ + patch('pyps4_homeassistant.Helper.has_devices', + return_value=[{'host-ip': MOCK_HOST}]): + result = await flow.async_step_link(MOCK_CONFIG) + assert result['type'] == data_entry_flow.RESULT_TYPE_FORM + assert result['step_id'] == 'link' + assert result['errors'] == {'base': 'not_ready'} diff --git a/tests/components/sensor/test_filter.py b/tests/components/sensor/test_filter.py index b43d38da5e83c..29308f2a83d96 100644 --- a/tests/components/sensor/test_filter.py +++ b/tests/components/sensor/test_filter.py @@ -106,6 +106,23 @@ def test_outlier(self): precision=2, entity=None, radius=4.0) + for state in self.values: + filtered = filt.filter_state(state) + assert 21 == filtered.state + + def test_outlier_step(self): + """ + Test step-change handling in outlier. + + Test if outlier filter handles long-running step-changes correctly. + It should converge to no longer filter once just over half the + window_size is occupied by the new post step-change values. + """ + filt = OutlierFilter(window_size=3, + precision=2, + entity=None, + radius=1.1) + self.values[-1].state = 22 for state in self.values: filtered = filt.filter_state(state) assert 22 == filtered.state @@ -119,7 +136,7 @@ def test_initial_outlier(self): out = ha.State('sensor.test_monitored', 4000) for state in [out]+self.values: filtered = filt.filter_state(state) - assert 22 == filtered.state + assert 21 == filtered.state def test_lowpass(self): """Test if lowpass filter works.""" diff --git a/tests/components/sensor/test_rest.py b/tests/components/sensor/test_rest.py index 3e71be8a6f6a0..343cc696763f4 100644 --- a/tests/components/sensor/test_rest.py +++ b/tests/components/sensor/test_rest.py @@ -89,6 +89,7 @@ def test_setup_get(self, mock_req): 'name': 'foo', 'unit_of_measurement': 'MB', 'verify_ssl': 'true', + 'timeout': 30, 'authentication': 'basic', 'username': 'my username', 'password': 'my password', @@ -112,6 +113,7 @@ def test_setup_post(self, mock_req): 'name': 'foo', 'unit_of_measurement': 'MB', 'verify_ssl': 'true', + 'timeout': 30, 'authentication': 'basic', 'username': 'my username', 'password': 'my password', @@ -280,8 +282,10 @@ def setUp(self): self.method = "GET" self.resource = "http://localhost" self.verify_ssl = True + self.timeout = 10 self.rest = rest.RestData( - self.method, self.resource, None, None, None, self.verify_ssl) + self.method, self.resource, None, None, None, self.verify_ssl, + self.timeout) @requests_mock.Mocker() def test_update(self, mock_req): diff --git a/tests/components/smartthings/conftest.py b/tests/components/smartthings/conftest.py index ee892fb03b9f9..27e833bff2594 100644 --- a/tests/components/smartthings/conftest.py +++ b/tests/components/smartthings/conftest.py @@ -4,8 +4,8 @@ from uuid import uuid4 from pysmartthings import ( - CLASSIFICATION_AUTOMATION, AppEntity, AppSettings, DeviceEntity, - InstalledApp, Location) + CLASSIFICATION_AUTOMATION, AppEntity, AppOAuthClient, AppSettings, + DeviceEntity, InstalledApp, Location, SceneEntity, Subscription) from pysmartthings.api import Api import pytest @@ -13,8 +13,9 @@ from homeassistant.components.smartthings import DeviceBroker from homeassistant.components.smartthings.const import ( APP_NAME_PREFIX, CONF_APP_ID, CONF_INSTALLED_APP_ID, CONF_INSTANCE_ID, - CONF_LOCATION_ID, DATA_BROKERS, DOMAIN, SETTINGS_INSTANCE_ID, STORAGE_KEY, - STORAGE_VERSION) + CONF_LOCATION_ID, CONF_OAUTH_CLIENT_ID, CONF_OAUTH_CLIENT_SECRET, + CONF_REFRESH_TOKEN, DATA_BROKERS, DOMAIN, SETTINGS_INSTANCE_ID, + STORAGE_KEY, STORAGE_VERSION) from homeassistant.config_entries import ( CONN_CLASS_CLOUD_PUSH, SOURCE_USER, ConfigEntry) from homeassistant.const import CONF_ACCESS_TOKEN, CONF_WEBHOOK_ID @@ -23,12 +24,16 @@ from tests.common import mock_coro -async def setup_platform(hass, platform: str, *devices): +async def setup_platform(hass, platform: str, *, + devices=None, scenes=None): """Set up the SmartThings platform and prerequisites.""" hass.config.components.add(DOMAIN) - broker = DeviceBroker(hass, devices, '') - config_entry = ConfigEntry("1", DOMAIN, "Test", {}, + config_entry = ConfigEntry(2, DOMAIN, "Test", + {CONF_INSTALLED_APP_ID: str(uuid4())}, SOURCE_USER, CONN_CLASS_CLOUD_PUSH) + broker = DeviceBroker(hass, config_entry, Mock(), Mock(), + devices or [], scenes or []) + hass.data[DOMAIN] = { DATA_BROKERS: { config_entry.entry_id: broker @@ -98,6 +103,15 @@ def app_fixture(hass, config_file): return app +@pytest.fixture(name="app_oauth_client") +def app_oauth_client_fixture(): + """Fixture for a single app's oauth.""" + return AppOAuthClient({ + 'oauthClientId': str(uuid4()), + 'oauthClientSecret': str(uuid4()) + }) + + @pytest.fixture(name='app_settings') def app_settings_fixture(app, config_file): """Fixture for an app settings.""" @@ -225,12 +239,25 @@ def config_entry_fixture(hass, installed_app, location): CONF_ACCESS_TOKEN: str(uuid4()), CONF_INSTALLED_APP_ID: installed_app.installed_app_id, CONF_APP_ID: installed_app.app_id, - CONF_LOCATION_ID: location.location_id + CONF_LOCATION_ID: location.location_id, + CONF_REFRESH_TOKEN: str(uuid4()), + CONF_OAUTH_CLIENT_ID: str(uuid4()), + CONF_OAUTH_CLIENT_SECRET: str(uuid4()) } - return ConfigEntry("1", DOMAIN, location.name, data, SOURCE_USER, + return ConfigEntry(2, DOMAIN, location.name, data, SOURCE_USER, CONN_CLASS_CLOUD_PUSH) +@pytest.fixture(name="subscription_factory") +def subscription_factory_fixture(): + """Fixture for creating mock subscriptions.""" + def _factory(capability): + sub = Subscription() + sub.capability = capability + return sub + return _factory + + @pytest.fixture(name="device_factory") def device_factory_fixture(): """Fixture for creating mock devices.""" @@ -270,6 +297,31 @@ def _factory(label, capabilities, status: dict = None): return _factory +@pytest.fixture(name="scene_factory") +def scene_factory_fixture(location): + """Fixture for creating mock devices.""" + api = Mock(spec=Api) + api.execute_scene.side_effect = \ + lambda *args, **kwargs: mock_coro(return_value={}) + + def _factory(name): + scene_data = { + 'sceneId': str(uuid4()), + 'sceneName': name, + 'sceneIcon': '', + 'sceneColor': '', + 'locationId': location.location_id + } + return SceneEntity(api, scene_data) + return _factory + + +@pytest.fixture(name="scene") +def scene_fixture(scene_factory): + """Fixture for an individual scene.""" + return scene_factory('Test Scene') + + @pytest.fixture(name="event_factory") def event_factory_fixture(): """Fixture for creating mock devices.""" diff --git a/tests/components/smartthings/test_binary_sensor.py b/tests/components/smartthings/test_binary_sensor.py index 4b47537fa19e0..d1de9f8f02077 100644 --- a/tests/components/smartthings/test_binary_sensor.py +++ b/tests/components/smartthings/test_binary_sensor.py @@ -6,31 +6,15 @@ """ from pysmartthings import ATTRIBUTES, CAPABILITIES, Attribute, Capability -from homeassistant.components.binary_sensor import DEVICE_CLASSES -from homeassistant.components.smartthings import DeviceBroker, binary_sensor +from homeassistant.components.binary_sensor import ( + DEVICE_CLASSES, DOMAIN as BINARY_SENSOR_DOMAIN) +from homeassistant.components.smartthings import binary_sensor from homeassistant.components.smartthings.const import ( - DATA_BROKERS, DOMAIN, SIGNAL_SMARTTHINGS_UPDATE) -from homeassistant.config_entries import ( - CONN_CLASS_CLOUD_PUSH, SOURCE_USER, ConfigEntry) + DOMAIN, SIGNAL_SMARTTHINGS_UPDATE) from homeassistant.const import ATTR_FRIENDLY_NAME from homeassistant.helpers.dispatcher import async_dispatcher_send - -async def _setup_platform(hass, *devices): - """Set up the SmartThings binary_sensor platform and prerequisites.""" - hass.config.components.add(DOMAIN) - broker = DeviceBroker(hass, devices, '') - config_entry = ConfigEntry("1", DOMAIN, "Test", {}, - SOURCE_USER, CONN_CLASS_CLOUD_PUSH) - hass.data[DOMAIN] = { - DATA_BROKERS: { - config_entry.entry_id: broker - } - } - await hass.config_entries.async_forward_entry_setup( - config_entry, 'binary_sensor') - await hass.async_block_till_done() - return config_entry +from .conftest import setup_platform async def test_mapping_integrity(): @@ -56,7 +40,7 @@ async def test_entity_state(hass, device_factory): """Tests the state attributes properly match the light types.""" device = device_factory('Motion Sensor 1', [Capability.motion_sensor], {Attribute.motion: 'inactive'}) - await _setup_platform(hass, device) + await setup_platform(hass, BINARY_SENSOR_DOMAIN, devices=[device]) state = hass.states.get('binary_sensor.motion_sensor_1_motion') assert state.state == 'off' assert state.attributes[ATTR_FRIENDLY_NAME] ==\ @@ -71,7 +55,7 @@ async def test_entity_and_device_attributes(hass, device_factory): entity_registry = await hass.helpers.entity_registry.async_get_registry() device_registry = await hass.helpers.device_registry.async_get_registry() # Act - await _setup_platform(hass, device) + await setup_platform(hass, BINARY_SENSOR_DOMAIN, devices=[device]) # Assert entry = entity_registry.async_get('binary_sensor.motion_sensor_1_motion') assert entry @@ -89,7 +73,7 @@ async def test_update_from_signal(hass, device_factory): # Arrange device = device_factory('Motion Sensor 1', [Capability.motion_sensor], {Attribute.motion: 'inactive'}) - await _setup_platform(hass, device) + await setup_platform(hass, BINARY_SENSOR_DOMAIN, devices=[device]) device.status.apply_attribute_update( 'main', Capability.motion_sensor, Attribute.motion, 'active') # Act @@ -107,7 +91,8 @@ async def test_unload_config_entry(hass, device_factory): # Arrange device = device_factory('Motion Sensor 1', [Capability.motion_sensor], {Attribute.motion: 'inactive'}) - config_entry = await _setup_platform(hass, device) + config_entry = await setup_platform(hass, BINARY_SENSOR_DOMAIN, + devices=[device]) # Act await hass.config_entries.async_forward_entry_unload( config_entry, 'binary_sensor') diff --git a/tests/components/smartthings/test_climate.py b/tests/components/smartthings/test_climate.py index 306bcacdb18bb..29134d6ba6a12 100644 --- a/tests/components/smartthings/test_climate.py +++ b/tests/components/smartthings/test_climate.py @@ -8,18 +8,19 @@ from pysmartthings.device import Status import pytest -from homeassistant.components.climate import ( +from homeassistant.components.climate.const import ( ATTR_CURRENT_HUMIDITY, ATTR_CURRENT_TEMPERATURE, ATTR_FAN_LIST, ATTR_FAN_MODE, ATTR_OPERATION_LIST, ATTR_OPERATION_MODE, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, DOMAIN as CLIMATE_DOMAIN, SERVICE_SET_FAN_MODE, SERVICE_SET_OPERATION_MODE, SERVICE_SET_TEMPERATURE, - STATE_AUTO, STATE_COOL, STATE_ECO, STATE_HEAT, STATE_OFF, SUPPORT_FAN_MODE, + STATE_AUTO, STATE_COOL, STATE_ECO, STATE_HEAT, SUPPORT_FAN_MODE, SUPPORT_OPERATION_MODE, SUPPORT_TARGET_TEMPERATURE, SUPPORT_TARGET_TEMPERATURE_HIGH, SUPPORT_TARGET_TEMPERATURE_LOW) from homeassistant.components.smartthings import climate from homeassistant.components.smartthings.const import DOMAIN from homeassistant.const import ( - ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, ATTR_TEMPERATURE, STATE_UNKNOWN) + ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, ATTR_TEMPERATURE, STATE_OFF, + STATE_UNKNOWN) from .conftest import setup_platform @@ -121,7 +122,7 @@ async def test_async_setup_platform(): async def test_legacy_thermostat_entity_state(hass, legacy_thermostat): """Tests the state attributes properly match the thermostat type.""" - await setup_platform(hass, CLIMATE_DOMAIN, legacy_thermostat) + await setup_platform(hass, CLIMATE_DOMAIN, devices=[legacy_thermostat]) state = hass.states.get('climate.legacy_thermostat') assert state.state == STATE_AUTO assert state.attributes[ATTR_SUPPORTED_FEATURES] == \ @@ -140,7 +141,7 @@ async def test_legacy_thermostat_entity_state(hass, legacy_thermostat): async def test_basic_thermostat_entity_state(hass, basic_thermostat): """Tests the state attributes properly match the thermostat type.""" - await setup_platform(hass, CLIMATE_DOMAIN, basic_thermostat) + await setup_platform(hass, CLIMATE_DOMAIN, devices=[basic_thermostat]) state = hass.states.get('climate.basic_thermostat') assert state.state == STATE_OFF assert state.attributes[ATTR_SUPPORTED_FEATURES] == \ @@ -154,7 +155,7 @@ async def test_basic_thermostat_entity_state(hass, basic_thermostat): async def test_thermostat_entity_state(hass, thermostat): """Tests the state attributes properly match the thermostat type.""" - await setup_platform(hass, CLIMATE_DOMAIN, thermostat) + await setup_platform(hass, CLIMATE_DOMAIN, devices=[thermostat]) state = hass.states.get('climate.thermostat') assert state.state == STATE_HEAT assert state.attributes[ATTR_SUPPORTED_FEATURES] == \ @@ -173,7 +174,7 @@ async def test_thermostat_entity_state(hass, thermostat): async def test_buggy_thermostat_entity_state(hass, buggy_thermostat): """Tests the state attributes properly match the thermostat type.""" - await setup_platform(hass, CLIMATE_DOMAIN, buggy_thermostat) + await setup_platform(hass, CLIMATE_DOMAIN, devices=[buggy_thermostat]) state = hass.states.get('climate.buggy_thermostat') assert state.state == STATE_UNKNOWN assert state.attributes[ATTR_SUPPORTED_FEATURES] == \ @@ -189,14 +190,14 @@ async def test_buggy_thermostat_invalid_mode(hass, buggy_thermostat): buggy_thermostat.status.update_attribute_value( Attribute.supported_thermostat_modes, ['heat', 'emergency heat', 'other']) - await setup_platform(hass, CLIMATE_DOMAIN, buggy_thermostat) + await setup_platform(hass, CLIMATE_DOMAIN, devices=[buggy_thermostat]) state = hass.states.get('climate.buggy_thermostat') assert state.attributes[ATTR_OPERATION_LIST] == {'heat'} async def test_set_fan_mode(hass, thermostat): """Test the fan mode is set successfully.""" - await setup_platform(hass, CLIMATE_DOMAIN, thermostat) + await setup_platform(hass, CLIMATE_DOMAIN, devices=[thermostat]) await hass.services.async_call( CLIMATE_DOMAIN, SERVICE_SET_FAN_MODE, { ATTR_ENTITY_ID: 'climate.thermostat', @@ -208,7 +209,7 @@ async def test_set_fan_mode(hass, thermostat): async def test_set_operation_mode(hass, thermostat): """Test the operation mode is set successfully.""" - await setup_platform(hass, CLIMATE_DOMAIN, thermostat) + await setup_platform(hass, CLIMATE_DOMAIN, devices=[thermostat]) await hass.services.async_call( CLIMATE_DOMAIN, SERVICE_SET_OPERATION_MODE, { ATTR_ENTITY_ID: 'climate.thermostat', @@ -221,7 +222,7 @@ async def test_set_operation_mode(hass, thermostat): async def test_set_temperature_heat_mode(hass, thermostat): """Test the temperature is set successfully when in heat mode.""" thermostat.status.thermostat_mode = 'heat' - await setup_platform(hass, CLIMATE_DOMAIN, thermostat) + await setup_platform(hass, CLIMATE_DOMAIN, devices=[thermostat]) await hass.services.async_call( CLIMATE_DOMAIN, SERVICE_SET_TEMPERATURE, { ATTR_ENTITY_ID: 'climate.thermostat', @@ -236,7 +237,7 @@ async def test_set_temperature_heat_mode(hass, thermostat): async def test_set_temperature_cool_mode(hass, thermostat): """Test the temperature is set successfully when in cool mode.""" thermostat.status.thermostat_mode = 'cool' - await setup_platform(hass, CLIMATE_DOMAIN, thermostat) + await setup_platform(hass, CLIMATE_DOMAIN, devices=[thermostat]) await hass.services.async_call( CLIMATE_DOMAIN, SERVICE_SET_TEMPERATURE, { ATTR_ENTITY_ID: 'climate.thermostat', @@ -249,7 +250,7 @@ async def test_set_temperature_cool_mode(hass, thermostat): async def test_set_temperature(hass, thermostat): """Test the temperature is set successfully.""" thermostat.status.thermostat_mode = 'auto' - await setup_platform(hass, CLIMATE_DOMAIN, thermostat) + await setup_platform(hass, CLIMATE_DOMAIN, devices=[thermostat]) await hass.services.async_call( CLIMATE_DOMAIN, SERVICE_SET_TEMPERATURE, { ATTR_ENTITY_ID: 'climate.thermostat', @@ -263,7 +264,7 @@ async def test_set_temperature(hass, thermostat): async def test_set_temperature_with_mode(hass, thermostat): """Test the temperature and mode is set successfully.""" - await setup_platform(hass, CLIMATE_DOMAIN, thermostat) + await setup_platform(hass, CLIMATE_DOMAIN, devices=[thermostat]) await hass.services.async_call( CLIMATE_DOMAIN, SERVICE_SET_TEMPERATURE, { ATTR_ENTITY_ID: 'climate.thermostat', @@ -279,7 +280,7 @@ async def test_set_temperature_with_mode(hass, thermostat): async def test_entity_and_device_attributes(hass, thermostat): """Test the attributes of the entries are correct.""" - await setup_platform(hass, CLIMATE_DOMAIN, thermostat) + await setup_platform(hass, CLIMATE_DOMAIN, devices=[thermostat]) entity_registry = await hass.helpers.entity_registry.async_get_registry() device_registry = await hass.helpers.device_registry.async_get_registry() diff --git a/tests/components/smartthings/test_config_flow.py b/tests/components/smartthings/test_config_flow.py index 7d3357031319c..28aa759a35964 100644 --- a/tests/components/smartthings/test_config_flow.py +++ b/tests/components/smartthings/test_config_flow.py @@ -8,6 +8,9 @@ from homeassistant import data_entry_flow from homeassistant.components.smartthings.config_flow import ( SmartThingsFlowHandler) +from homeassistant.components.smartthings.const import ( + CONF_INSTALLED_APP_ID, CONF_INSTALLED_APPS, CONF_LOCATION_ID, + CONF_REFRESH_TOKEN, DOMAIN) from homeassistant.config_entries import ConfigEntry from tests.common import mock_coro @@ -171,14 +174,16 @@ async def test_unknown_error(hass, smartthings_mock): assert result['errors'] == {'base': 'app_setup_error'} -async def test_app_created_then_show_wait_form(hass, app, smartthings_mock): +async def test_app_created_then_show_wait_form( + hass, app, app_oauth_client, smartthings_mock): """Test SmartApp is created when one does not exist and shows wait form.""" flow = SmartThingsFlowHandler() flow.hass = hass smartthings = smartthings_mock.return_value smartthings.apps.return_value = mock_coro(return_value=[]) - smartthings.create_app.return_value = mock_coro(return_value=(app, None)) + smartthings.create_app.return_value = \ + mock_coro(return_value=(app, app_oauth_client)) smartthings.update_app_settings.return_value = mock_coro() smartthings.update_app_oauth.return_value = mock_coro() @@ -189,13 +194,15 @@ async def test_app_created_then_show_wait_form(hass, app, smartthings_mock): async def test_app_updated_then_show_wait_form( - hass, app, smartthings_mock): + hass, app, app_oauth_client, smartthings_mock): """Test SmartApp is updated when an existing is already created.""" flow = SmartThingsFlowHandler() flow.hass = hass api = smartthings_mock.return_value api.apps.return_value = mock_coro(return_value=[app]) + api.generate_app_oauth.return_value = \ + mock_coro(return_value=app_oauth_client) result = await flow.async_step_user({'access_token': str(uuid4())}) @@ -219,8 +226,6 @@ async def test_wait_form_displayed_after_checking(hass, smartthings_mock): flow = SmartThingsFlowHandler() flow.hass = hass flow.access_token = str(uuid4()) - flow.api = smartthings_mock.return_value - flow.api.installed_apps.return_value = mock_coro(return_value=[]) result = await flow.async_step_wait_install({}) @@ -235,19 +240,29 @@ async def test_config_entry_created_when_installed( flow = SmartThingsFlowHandler() flow.hass = hass flow.access_token = str(uuid4()) - flow.api = smartthings_mock.return_value flow.app_id = installed_app.app_id - flow.api.installed_apps.return_value = \ - mock_coro(return_value=[installed_app]) + flow.api = smartthings_mock.return_value + flow.oauth_client_id = str(uuid4()) + flow.oauth_client_secret = str(uuid4()) + data = { + CONF_REFRESH_TOKEN: str(uuid4()), + CONF_LOCATION_ID: installed_app.location_id, + CONF_INSTALLED_APP_ID: installed_app.installed_app_id + } + hass.data[DOMAIN][CONF_INSTALLED_APPS].append(data) result = await flow.async_step_wait_install({}) + assert not hass.data[DOMAIN][CONF_INSTALLED_APPS] assert result['type'] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert result['data']['app_id'] == installed_app.app_id assert result['data']['installed_app_id'] == \ installed_app.installed_app_id assert result['data']['location_id'] == installed_app.location_id assert result['data']['access_token'] == flow.access_token + assert result['data']['refresh_token'] == data[CONF_REFRESH_TOKEN] + assert result['data']['client_secret'] == flow.oauth_client_secret + assert result['data']['client_id'] == flow.oauth_client_id assert result['title'] == location.name @@ -259,17 +274,31 @@ async def test_multiple_config_entry_created_when_installed( flow.access_token = str(uuid4()) flow.app_id = app.app_id flow.api = smartthings_mock.return_value - flow.api.installed_apps.return_value = \ - mock_coro(return_value=installed_apps) + flow.oauth_client_id = str(uuid4()) + flow.oauth_client_secret = str(uuid4()) + for installed_app in installed_apps: + data = { + CONF_REFRESH_TOKEN: str(uuid4()), + CONF_LOCATION_ID: installed_app.location_id, + CONF_INSTALLED_APP_ID: installed_app.installed_app_id + } + hass.data[DOMAIN][CONF_INSTALLED_APPS].append(data) + install_data = hass.data[DOMAIN][CONF_INSTALLED_APPS].copy() result = await flow.async_step_wait_install({}) + assert not hass.data[DOMAIN][CONF_INSTALLED_APPS] + assert result['type'] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert result['data']['app_id'] == installed_apps[0].app_id assert result['data']['installed_app_id'] == \ installed_apps[0].installed_app_id assert result['data']['location_id'] == installed_apps[0].location_id assert result['data']['access_token'] == flow.access_token + assert result['data']['refresh_token'] == \ + install_data[0][CONF_REFRESH_TOKEN] + assert result['data']['client_secret'] == flow.oauth_client_secret + assert result['data']['client_id'] == flow.oauth_client_id assert result['title'] == locations[0].name await hass.async_block_till_done() @@ -280,4 +309,6 @@ async def test_multiple_config_entry_created_when_installed( installed_apps[1].installed_app_id assert entries[0].data['location_id'] == installed_apps[1].location_id assert entries[0].data['access_token'] == flow.access_token + assert entries[0].data['client_secret'] == flow.oauth_client_secret + assert entries[0].data['client_id'] == flow.oauth_client_id assert entries[0].title == locations[1].name diff --git a/tests/components/smartthings/test_cover.py b/tests/components/smartthings/test_cover.py new file mode 100644 index 0000000000000..7e41237e3e70f --- /dev/null +++ b/tests/components/smartthings/test_cover.py @@ -0,0 +1,196 @@ +""" +Test for the SmartThings cover platform. + +The only mocking required is of the underlying SmartThings API object so +real HTTP calls are not initiated during testing. +""" +from pysmartthings import Attribute, Capability + +from homeassistant.components.cover import ( + ATTR_CURRENT_POSITION, ATTR_POSITION, DOMAIN as COVER_DOMAIN, + SERVICE_CLOSE_COVER, SERVICE_OPEN_COVER, SERVICE_SET_COVER_POSITION, + STATE_CLOSED, STATE_CLOSING, STATE_OPEN, STATE_OPENING) +from homeassistant.components.smartthings import cover +from homeassistant.components.smartthings.const import ( + DOMAIN, SIGNAL_SMARTTHINGS_UPDATE) +from homeassistant.const import ATTR_BATTERY_LEVEL, ATTR_ENTITY_ID +from homeassistant.helpers.dispatcher import async_dispatcher_send + +from .conftest import setup_platform + + +async def test_async_setup_platform(): + """Test setup platform does nothing (it uses config entries).""" + await cover.async_setup_platform(None, None, None) + + +async def test_entity_and_device_attributes(hass, device_factory): + """Test the attributes of the entity are correct.""" + # Arrange + device = device_factory('Garage', [Capability.garage_door_control], + {Attribute.door: 'open'}) + entity_registry = await hass.helpers.entity_registry.async_get_registry() + device_registry = await hass.helpers.device_registry.async_get_registry() + # Act + await setup_platform(hass, COVER_DOMAIN, devices=[device]) + # Assert + entry = entity_registry.async_get('cover.garage') + assert entry + assert entry.unique_id == device.device_id + + entry = device_registry.async_get_device( + {(DOMAIN, device.device_id)}, []) + assert entry + assert entry.name == device.label + assert entry.model == device.device_type_name + assert entry.manufacturer == 'Unavailable' + + +async def test_open(hass, device_factory): + """Test the cover opens doors, garages, and shades successfully.""" + # Arrange + devices = { + device_factory('Door', [Capability.door_control], + {Attribute.door: 'closed'}), + device_factory('Garage', [Capability.garage_door_control], + {Attribute.door: 'closed'}), + device_factory('Shade', [Capability.window_shade], + {Attribute.window_shade: 'closed'}) + } + await setup_platform(hass, COVER_DOMAIN, devices=devices) + entity_ids = [ + 'cover.door', + 'cover.garage', + 'cover.shade' + ] + # Act + await hass.services.async_call( + COVER_DOMAIN, SERVICE_OPEN_COVER, + {ATTR_ENTITY_ID: entity_ids}, + blocking=True) + # Assert + for entity_id in entity_ids: + state = hass.states.get(entity_id) + assert state is not None + assert state.state == STATE_OPENING + + +async def test_close(hass, device_factory): + """Test the cover closes doors, garages, and shades successfully.""" + # Arrange + devices = { + device_factory('Door', [Capability.door_control], + {Attribute.door: 'open'}), + device_factory('Garage', [Capability.garage_door_control], + {Attribute.door: 'open'}), + device_factory('Shade', [Capability.window_shade], + {Attribute.window_shade: 'open'}) + } + await setup_platform(hass, COVER_DOMAIN, devices=devices) + entity_ids = [ + 'cover.door', + 'cover.garage', + 'cover.shade' + ] + # Act + await hass.services.async_call( + COVER_DOMAIN, SERVICE_CLOSE_COVER, + {ATTR_ENTITY_ID: entity_ids}, + blocking=True) + # Assert + for entity_id in entity_ids: + state = hass.states.get(entity_id) + assert state is not None + assert state.state == STATE_CLOSING + + +async def test_set_cover_position(hass, device_factory): + """Test the cover sets to the specific position.""" + # Arrange + device = device_factory( + 'Shade', + [Capability.window_shade, Capability.battery, + Capability.switch_level], + {Attribute.window_shade: 'opening', Attribute.battery: 95, + Attribute.level: 10}) + 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) + + state = hass.states.get('cover.shade') + # Result of call does not update state + assert state.state == STATE_OPENING + assert state.attributes[ATTR_BATTERY_LEVEL] == 95 + assert state.attributes[ATTR_CURRENT_POSITION] == 10 + # Ensure API called + # pylint: disable=protected-access + assert device._api.post_device_command.call_count == 1 # type: ignore + + +async def test_set_cover_position_unsupported(hass, device_factory): + """Test set position does nothing when not supported by device.""" + # Arrange + device = device_factory( + 'Shade', + [Capability.window_shade], + {Attribute.window_shade: 'opening'}) + 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) + + # Ensure API was notcalled + # pylint: disable=protected-access + assert device._api.post_device_command.call_count == 0 # type: ignore + + +async def test_update_to_open_from_signal(hass, device_factory): + """Test the cover updates to open when receiving a signal.""" + # Arrange + device = device_factory('Garage', [Capability.garage_door_control], + {Attribute.door: 'opening'}) + await setup_platform(hass, COVER_DOMAIN, devices=[device]) + device.status.update_attribute_value(Attribute.door, 'open') + assert hass.states.get('cover.garage').state == STATE_OPENING + # Act + async_dispatcher_send(hass, SIGNAL_SMARTTHINGS_UPDATE, + [device.device_id]) + # Assert + await hass.async_block_till_done() + state = hass.states.get('cover.garage') + assert state is not None + assert state.state == STATE_OPEN + + +async def test_update_to_closed_from_signal(hass, device_factory): + """Test the cover updates to closed when receiving a signal.""" + # Arrange + device = device_factory('Garage', [Capability.garage_door_control], + {Attribute.door: 'closing'}) + await setup_platform(hass, COVER_DOMAIN, devices=[device]) + device.status.update_attribute_value(Attribute.door, 'closed') + assert hass.states.get('cover.garage').state == STATE_CLOSING + # Act + async_dispatcher_send(hass, SIGNAL_SMARTTHINGS_UPDATE, + [device.device_id]) + # Assert + await hass.async_block_till_done() + state = hass.states.get('cover.garage') + assert state is not None + assert state.state == STATE_CLOSED + + +async def test_unload_config_entry(hass, device_factory): + """Test the lock is removed when the config entry is unloaded.""" + # Arrange + device = device_factory('Garage', [Capability.garage_door_control], + {Attribute.door: 'open'}) + config_entry = await setup_platform(hass, COVER_DOMAIN, devices=[device]) + # Act + await hass.config_entries.async_forward_entry_unload( + config_entry, COVER_DOMAIN) + # Assert + assert not hass.states.get('cover.garage') diff --git a/tests/components/smartthings/test_fan.py b/tests/components/smartthings/test_fan.py index db8d9b512de39..dffffa7b340e0 100644 --- a/tests/components/smartthings/test_fan.py +++ b/tests/components/smartthings/test_fan.py @@ -7,31 +7,15 @@ from pysmartthings import Attribute, Capability from homeassistant.components.fan import ( - ATTR_SPEED, ATTR_SPEED_LIST, SPEED_HIGH, SPEED_LOW, SPEED_MEDIUM, - SPEED_OFF, SUPPORT_SET_SPEED) -from homeassistant.components.smartthings import DeviceBroker, fan + ATTR_SPEED, ATTR_SPEED_LIST, DOMAIN as FAN_DOMAIN, SPEED_HIGH, SPEED_LOW, + SPEED_MEDIUM, SPEED_OFF, SUPPORT_SET_SPEED) +from homeassistant.components.smartthings import fan from homeassistant.components.smartthings.const import ( - DATA_BROKERS, DOMAIN, SIGNAL_SMARTTHINGS_UPDATE) -from homeassistant.config_entries import ( - CONN_CLASS_CLOUD_PUSH, SOURCE_USER, ConfigEntry) + DOMAIN, SIGNAL_SMARTTHINGS_UPDATE) from homeassistant.const import ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES from homeassistant.helpers.dispatcher import async_dispatcher_send - -async def _setup_platform(hass, *devices): - """Set up the SmartThings fan platform and prerequisites.""" - hass.config.components.add(DOMAIN) - broker = DeviceBroker(hass, devices, '') - config_entry = ConfigEntry("1", DOMAIN, "Test", {}, - SOURCE_USER, CONN_CLASS_CLOUD_PUSH) - hass.data[DOMAIN] = { - DATA_BROKERS: { - config_entry.entry_id: broker - } - } - await hass.config_entries.async_forward_entry_setup(config_entry, 'fan') - await hass.async_block_till_done() - return config_entry +from .conftest import setup_platform async def test_async_setup_platform(): @@ -45,7 +29,7 @@ async def test_entity_state(hass, device_factory): "Fan 1", capabilities=[Capability.switch, Capability.fan_speed], status={Attribute.switch: 'on', Attribute.fan_speed: 2}) - await _setup_platform(hass, device) + await setup_platform(hass, FAN_DOMAIN, devices=[device]) # Dimmer 1 state = hass.states.get('fan.fan_1') @@ -63,11 +47,10 @@ async def test_entity_and_device_attributes(hass, device_factory): "Fan 1", capabilities=[Capability.switch, Capability.fan_speed], status={Attribute.switch: 'on', Attribute.fan_speed: 2}) - await _setup_platform(hass, device) + # Act + await setup_platform(hass, FAN_DOMAIN, devices=[device]) entity_registry = await hass.helpers.entity_registry.async_get_registry() device_registry = await hass.helpers.device_registry.async_get_registry() - # Act - await _setup_platform(hass, device) # Assert entry = entity_registry.async_get("fan.fan_1") assert entry @@ -88,7 +71,7 @@ async def test_turn_off(hass, device_factory): "Fan 1", capabilities=[Capability.switch, Capability.fan_speed], status={Attribute.switch: 'on', Attribute.fan_speed: 2}) - await _setup_platform(hass, device) + await setup_platform(hass, FAN_DOMAIN, devices=[device]) # Act await hass.services.async_call( 'fan', 'turn_off', {'entity_id': 'fan.fan_1'}, @@ -106,7 +89,7 @@ async def test_turn_on(hass, device_factory): "Fan 1", capabilities=[Capability.switch, Capability.fan_speed], status={Attribute.switch: 'off', Attribute.fan_speed: 0}) - await _setup_platform(hass, device) + await setup_platform(hass, FAN_DOMAIN, devices=[device]) # Act await hass.services.async_call( 'fan', 'turn_on', {ATTR_ENTITY_ID: "fan.fan_1"}, @@ -124,7 +107,7 @@ async def test_turn_on_with_speed(hass, device_factory): "Fan 1", capabilities=[Capability.switch, Capability.fan_speed], status={Attribute.switch: 'off', Attribute.fan_speed: 0}) - await _setup_platform(hass, device) + await setup_platform(hass, FAN_DOMAIN, devices=[device]) # Act await hass.services.async_call( 'fan', 'turn_on', @@ -145,7 +128,7 @@ async def test_set_speed(hass, device_factory): "Fan 1", capabilities=[Capability.switch, Capability.fan_speed], status={Attribute.switch: 'off', Attribute.fan_speed: 0}) - await _setup_platform(hass, device) + await setup_platform(hass, FAN_DOMAIN, devices=[device]) # Act await hass.services.async_call( 'fan', 'set_speed', @@ -166,7 +149,7 @@ async def test_update_from_signal(hass, device_factory): "Fan 1", capabilities=[Capability.switch, Capability.fan_speed], status={Attribute.switch: 'off', Attribute.fan_speed: 0}) - await _setup_platform(hass, device) + await setup_platform(hass, FAN_DOMAIN, devices=[device]) await device.switch_on(True) # Act async_dispatcher_send(hass, SIGNAL_SMARTTHINGS_UPDATE, @@ -185,7 +168,7 @@ async def test_unload_config_entry(hass, device_factory): "Fan 1", capabilities=[Capability.switch, Capability.fan_speed], status={Attribute.switch: 'off', Attribute.fan_speed: 0}) - config_entry = await _setup_platform(hass, device) + config_entry = await setup_platform(hass, FAN_DOMAIN, devices=[device]) # Act await hass.config_entries.async_forward_entry_unload( config_entry, 'fan') diff --git a/tests/components/smartthings/test_init.py b/tests/components/smartthings/test_init.py index 014cfe7da9820..ec0b39825171a 100644 --- a/tests/components/smartthings/test_init.py +++ b/tests/components/smartthings/test_init.py @@ -8,14 +8,33 @@ from homeassistant.components import smartthings from homeassistant.components.smartthings.const import ( - DATA_BROKERS, DOMAIN, EVENT_BUTTON, SIGNAL_SMARTTHINGS_UPDATE, - SUPPORTED_PLATFORMS) + CONF_INSTALLED_APP_ID, CONF_REFRESH_TOKEN, DATA_BROKERS, DOMAIN, + EVENT_BUTTON, SIGNAL_SMARTTHINGS_UPDATE, SUPPORTED_PLATFORMS) from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.dispatcher import async_dispatcher_connect from tests.common import mock_coro +async def test_migration_creates_new_flow( + hass, smartthings_mock, config_entry): + """Test migration deletes app and creates new flow.""" + config_entry.version = 1 + setattr(hass.config_entries, '_entries', [config_entry]) + api = smartthings_mock.return_value + api.delete_installed_app.return_value = mock_coro() + + await smartthings.async_migrate_entry(hass, config_entry) + + assert api.delete_installed_app.call_count == 1 + await hass.async_block_till_done() + assert not hass.config_entries.async_entries(DOMAIN) + flows = hass.config_entries.flow.async_progress() + assert len(flows) == 1 + assert flows[0]['handler'] == 'smartthings' + assert flows[0]['context'] == {'source': 'import'} + + async def test_unrecoverable_api_errors_create_new_flow( hass, config_entry, smartthings_mock): """ @@ -58,6 +77,19 @@ async def test_recoverable_api_errors_raise_not_ready( await smartthings.async_setup_entry(hass, config_entry) +async def test_scenes_api_errors_raise_not_ready( + hass, config_entry, app, installed_app, smartthings_mock): + """Test if scenes are unauthorized we continue to load platforms.""" + setattr(hass.config_entries, '_entries', [config_entry]) + api = smartthings_mock.return_value + api.app.return_value = mock_coro(return_value=app) + api.installed_app.return_value = mock_coro(return_value=installed_app) + api.scenes.return_value = mock_coro( + exception=ClientResponseError(None, None, status=500)) + with pytest.raises(ConfigEntryNotReady): + await smartthings.async_setup_entry(hass, config_entry) + + async def test_connection_errors_raise_not_ready( hass, config_entry, smartthings_mock): """Test config entry not ready raised for connection errors.""" @@ -99,16 +131,52 @@ async def test_unauthorized_installed_app_raises_not_ready( await smartthings.async_setup_entry(hass, config_entry) +async def test_scenes_unauthorized_loads_platforms( + hass, config_entry, app, installed_app, + device, smartthings_mock, subscription_factory): + """Test if scenes are unauthorized we continue to load platforms.""" + setattr(hass.config_entries, '_entries', [config_entry]) + api = smartthings_mock.return_value + api.app.return_value = mock_coro(return_value=app) + api.installed_app.return_value = mock_coro(return_value=installed_app) + api.devices.side_effect = \ + lambda *args, **kwargs: mock_coro(return_value=[device]) + api.scenes.return_value = mock_coro( + exception=ClientResponseError(None, None, status=403)) + mock_token = Mock() + mock_token.access_token.return_value = str(uuid4()) + mock_token.refresh_token.return_value = str(uuid4()) + api.generate_tokens.return_value = mock_coro(return_value=mock_token) + subscriptions = [subscription_factory(capability) + for capability in device.capabilities] + api.subscriptions.return_value = mock_coro(return_value=subscriptions) + + with patch.object(hass.config_entries, 'async_forward_entry_setup', + return_value=mock_coro()) as forward_mock: + assert await smartthings.async_setup_entry(hass, config_entry) + # Assert platforms loaded + await hass.async_block_till_done() + assert forward_mock.call_count == len(SUPPORTED_PLATFORMS) + + async def test_config_entry_loads_platforms( hass, config_entry, app, installed_app, - device, smartthings_mock): + device, smartthings_mock, subscription_factory, scene): """Test config entry loads properly and proxies to platforms.""" setattr(hass.config_entries, '_entries', [config_entry]) - api = smartthings_mock.return_value api.app.return_value = mock_coro(return_value=app) api.installed_app.return_value = mock_coro(return_value=installed_app) - api.devices.return_value = mock_coro(return_value=[device]) + api.devices.side_effect = \ + lambda *args, **kwargs: mock_coro(return_value=[device]) + api.scenes.return_value = mock_coro(return_value=[scene]) + mock_token = Mock() + mock_token.access_token.return_value = str(uuid4()) + mock_token.refresh_token.return_value = str(uuid4()) + api.generate_tokens.return_value = mock_coro(return_value=mock_token) + subscriptions = [subscription_factory(capability) + for capability in device.capabilities] + api.subscriptions.return_value = mock_coro(return_value=subscriptions) with patch.object(hass.config_entries, 'async_forward_entry_setup', return_value=mock_coro()) as forward_mock: @@ -120,8 +188,12 @@ async def test_config_entry_loads_platforms( async def test_unload_entry(hass, config_entry): """Test entries are unloaded correctly.""" - broker = Mock() - broker.event_handler_disconnect = Mock() + connect_disconnect = Mock() + smart_app = Mock() + smart_app.connect_event.return_value = connect_disconnect + broker = smartthings.DeviceBroker( + hass, config_entry, Mock(), smart_app, [], []) + broker.connect() hass.data[DOMAIN][DATA_BROKERS][config_entry.entry_id] = broker with patch.object(hass.config_entries, 'async_forward_entry_unload', @@ -129,15 +201,41 @@ async def test_unload_entry(hass, config_entry): return_value=True )) as forward_mock: assert await smartthings.async_unload_entry(hass, config_entry) - assert broker.event_handler_disconnect.call_count == 1 + + assert connect_disconnect.call_count == 1 assert config_entry.entry_id not in hass.data[DOMAIN][DATA_BROKERS] # Assert platforms unloaded await hass.async_block_till_done() assert forward_mock.call_count == len(SUPPORTED_PLATFORMS) +async def test_broker_regenerates_token( + hass, config_entry): + """Test the device broker regenerates the refresh token.""" + token = Mock() + token.refresh_token = str(uuid4()) + token.refresh.return_value = mock_coro() + stored_action = None + + def async_track_time_interval(hass, action, interval): + nonlocal stored_action + stored_action = action + + with patch('homeassistant.components.smartthings' + '.async_track_time_interval', + new=async_track_time_interval): + broker = smartthings.DeviceBroker( + hass, config_entry, token, Mock(), [], []) + broker.connect() + + assert stored_action + await stored_action(None) # pylint:disable=not-callable + assert token.refresh.call_count == 1 + assert config_entry.data[CONF_REFRESH_TOKEN] == token.refresh_token + + async def test_event_handler_dispatches_updated_devices( - hass, device_factory, event_request_factory): + hass, config_entry, device_factory, event_request_factory): """Test the event handler dispatches updated devices.""" devices = [ device_factory('Bedroom 1 Switch', ['switch']), @@ -147,6 +245,7 @@ async def test_event_handler_dispatches_updated_devices( device_ids = [devices[0].device_id, devices[1].device_id, devices[2].device_id] request = event_request_factory(device_ids) + config_entry.data[CONF_INSTALLED_APP_ID] = request.installed_app_id called = False def signal(ids): @@ -154,10 +253,13 @@ def signal(ids): called = True assert device_ids == ids async_dispatcher_connect(hass, SIGNAL_SMARTTHINGS_UPDATE, signal) + broker = smartthings.DeviceBroker( - hass, devices, request.installed_app_id) + hass, config_entry, Mock(), Mock(), devices, []) + broker.connect() - await broker.event_handler(request, None, None) + # pylint:disable=protected-access + await broker._event_handler(request, None, None) await hass.async_block_till_done() assert called @@ -166,7 +268,7 @@ def signal(ids): async def test_event_handler_ignores_other_installed_app( - hass, device_factory, event_request_factory): + hass, config_entry, device_factory, event_request_factory): """Test the event handler dispatches updated devices.""" device = device_factory('Bedroom 1 Switch', ['switch']) request = event_request_factory([device.device_id]) @@ -176,21 +278,26 @@ def signal(ids): nonlocal called called = True async_dispatcher_connect(hass, SIGNAL_SMARTTHINGS_UPDATE, signal) - broker = smartthings.DeviceBroker(hass, [device], str(uuid4())) + broker = smartthings.DeviceBroker( + hass, config_entry, Mock(), Mock(), [device], []) + broker.connect() - await broker.event_handler(request, None, None) + # pylint:disable=protected-access + await broker._event_handler(request, None, None) await hass.async_block_till_done() assert not called async def test_event_handler_fires_button_events( - hass, device_factory, event_factory, event_request_factory): + hass, config_entry, device_factory, event_factory, + event_request_factory): """Test the event handler fires button events.""" device = device_factory('Button 1', ['button']) event = event_factory(device.device_id, capability='button', attribute='button', value='pushed') request = event_request_factory(events=[event]) + config_entry.data[CONF_INSTALLED_APP_ID] = request.installed_app_id called = False def handler(evt): @@ -205,8 +312,11 @@ def handler(evt): } hass.bus.async_listen(EVENT_BUTTON, handler) broker = smartthings.DeviceBroker( - hass, [device], request.installed_app_id) - await broker.event_handler(request, None, None) + hass, config_entry, Mock(), Mock(), [device], []) + broker.connect() + + # pylint:disable=protected-access + await broker._event_handler(request, None, None) await hass.async_block_till_done() assert called diff --git a/tests/components/smartthings/test_light.py b/tests/components/smartthings/test_light.py index 72bc5da906319..6efd88d72377f 100644 --- a/tests/components/smartthings/test_light.py +++ b/tests/components/smartthings/test_light.py @@ -9,15 +9,16 @@ from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_HS_COLOR, ATTR_TRANSITION, - SUPPORT_BRIGHTNESS, SUPPORT_COLOR, SUPPORT_COLOR_TEMP, SUPPORT_TRANSITION) -from homeassistant.components.smartthings import DeviceBroker, light + DOMAIN as LIGHT_DOMAIN, SUPPORT_BRIGHTNESS, SUPPORT_COLOR, + SUPPORT_COLOR_TEMP, SUPPORT_TRANSITION) +from homeassistant.components.smartthings import light from homeassistant.components.smartthings.const import ( - DATA_BROKERS, DOMAIN, SIGNAL_SMARTTHINGS_UPDATE) -from homeassistant.config_entries import ( - CONN_CLASS_CLOUD_PUSH, SOURCE_USER, ConfigEntry) + DOMAIN, SIGNAL_SMARTTHINGS_UPDATE) from homeassistant.const import ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES from homeassistant.helpers.dispatcher import async_dispatcher_send +from .conftest import setup_platform + @pytest.fixture(name="light_devices") def light_devices_fixture(device_factory): @@ -44,22 +45,6 @@ def light_devices_fixture(device_factory): ] -async def _setup_platform(hass, *devices): - """Set up the SmartThings light platform and prerequisites.""" - hass.config.components.add(DOMAIN) - broker = DeviceBroker(hass, devices, '') - config_entry = ConfigEntry("1", DOMAIN, "Test", {}, - SOURCE_USER, CONN_CLASS_CLOUD_PUSH) - hass.data[DOMAIN] = { - DATA_BROKERS: { - config_entry.entry_id: broker - } - } - await hass.config_entries.async_forward_entry_setup(config_entry, 'light') - await hass.async_block_till_done() - return config_entry - - async def test_async_setup_platform(): """Test setup platform does nothing (it uses config entries).""" await light.async_setup_platform(None, None, None) @@ -67,7 +52,7 @@ async def test_async_setup_platform(): async def test_entity_state(hass, light_devices): """Tests the state attributes properly match the light types.""" - await _setup_platform(hass, *light_devices) + await setup_platform(hass, LIGHT_DOMAIN, devices=light_devices) # Dimmer 1 state = hass.states.get('light.dimmer_1') @@ -101,7 +86,7 @@ async def test_entity_and_device_attributes(hass, device_factory): entity_registry = await hass.helpers.entity_registry.async_get_registry() device_registry = await hass.helpers.device_registry.async_get_registry() # Act - await _setup_platform(hass, device) + await setup_platform(hass, LIGHT_DOMAIN, devices=[device]) # Assert entry = entity_registry.async_get("light.light_1") assert entry @@ -118,7 +103,7 @@ async def test_entity_and_device_attributes(hass, device_factory): async def test_turn_off(hass, light_devices): """Test the light turns of successfully.""" # Arrange - await _setup_platform(hass, *light_devices) + await setup_platform(hass, LIGHT_DOMAIN, devices=light_devices) # Act await hass.services.async_call( 'light', 'turn_off', {'entity_id': 'light.color_dimmer_2'}, @@ -132,7 +117,7 @@ async def test_turn_off(hass, light_devices): async def test_turn_off_with_transition(hass, light_devices): """Test the light turns of successfully with transition.""" # Arrange - await _setup_platform(hass, *light_devices) + await setup_platform(hass, LIGHT_DOMAIN, devices=light_devices) # Act await hass.services.async_call( 'light', 'turn_off', @@ -147,7 +132,7 @@ async def test_turn_off_with_transition(hass, light_devices): async def test_turn_on(hass, light_devices): """Test the light turns of successfully.""" # Arrange - await _setup_platform(hass, *light_devices) + await setup_platform(hass, LIGHT_DOMAIN, devices=light_devices) # Act await hass.services.async_call( 'light', 'turn_on', {ATTR_ENTITY_ID: "light.color_dimmer_1"}, @@ -161,7 +146,7 @@ async def test_turn_on(hass, light_devices): async def test_turn_on_with_brightness(hass, light_devices): """Test the light turns on to the specified brightness.""" # Arrange - await _setup_platform(hass, *light_devices) + await setup_platform(hass, LIGHT_DOMAIN, devices=light_devices) # Act await hass.services.async_call( 'light', 'turn_on', @@ -185,7 +170,7 @@ async def test_turn_on_with_minimal_brightness(hass, light_devices): set the level to zero, which turns off the lights in SmartThings. """ # Arrange - await _setup_platform(hass, *light_devices) + await setup_platform(hass, LIGHT_DOMAIN, devices=light_devices) # Act await hass.services.async_call( 'light', 'turn_on', @@ -203,7 +188,7 @@ async def test_turn_on_with_minimal_brightness(hass, light_devices): async def test_turn_on_with_color(hass, light_devices): """Test the light turns on with color.""" # Arrange - await _setup_platform(hass, *light_devices) + await setup_platform(hass, LIGHT_DOMAIN, devices=light_devices) # Act await hass.services.async_call( 'light', 'turn_on', @@ -220,7 +205,7 @@ async def test_turn_on_with_color(hass, light_devices): async def test_turn_on_with_color_temp(hass, light_devices): """Test the light turns on with color temp.""" # Arrange - await _setup_platform(hass, *light_devices) + await setup_platform(hass, LIGHT_DOMAIN, devices=light_devices) # Act await hass.services.async_call( 'light', 'turn_on', @@ -244,7 +229,7 @@ async def test_update_from_signal(hass, device_factory): status={Attribute.switch: 'off', Attribute.level: 100, Attribute.hue: 76.0, Attribute.saturation: 55.0, Attribute.color_temperature: 4500}) - await _setup_platform(hass, device) + await setup_platform(hass, LIGHT_DOMAIN, devices=[device]) await device.switch_on(True) # Act async_dispatcher_send(hass, SIGNAL_SMARTTHINGS_UPDATE, @@ -266,7 +251,7 @@ async def test_unload_config_entry(hass, device_factory): status={Attribute.switch: 'off', Attribute.level: 100, Attribute.hue: 76.0, Attribute.saturation: 55.0, Attribute.color_temperature: 4500}) - config_entry = await _setup_platform(hass, device) + config_entry = await setup_platform(hass, LIGHT_DOMAIN, devices=[device]) # Act await hass.config_entries.async_forward_entry_unload( config_entry, 'light') diff --git a/tests/components/smartthings/test_lock.py b/tests/components/smartthings/test_lock.py index 3739a2dc9b517..1d98e5f9bdba6 100644 --- a/tests/components/smartthings/test_lock.py +++ b/tests/components/smartthings/test_lock.py @@ -5,6 +5,7 @@ real HTTP calls are not initiated during testing. """ from pysmartthings import Attribute, Capability +from pysmartthings.device import Status from homeassistant.components.lock import DOMAIN as LOCK_DOMAIN from homeassistant.components.smartthings import lock @@ -28,7 +29,7 @@ async def test_entity_and_device_attributes(hass, device_factory): entity_registry = await hass.helpers.entity_registry.async_get_registry() device_registry = await hass.helpers.device_registry.async_get_registry() # Act - await setup_platform(hass, LOCK_DOMAIN, device) + await setup_platform(hass, LOCK_DOMAIN, devices=[device]) # Assert entry = entity_registry.async_get('lock.lock_1') assert entry @@ -45,9 +46,16 @@ async def test_entity_and_device_attributes(hass, device_factory): async def test_lock(hass, device_factory): """Test the lock locks successfully.""" # Arrange - device = device_factory('Lock_1', [Capability.lock], - {Attribute.lock: 'unlocked'}) - await setup_platform(hass, LOCK_DOMAIN, device) + device = device_factory('Lock_1', [Capability.lock]) + device.status.attributes[Attribute.lock] = Status( + 'unlocked', None, { + 'method': 'Manual', + 'codeId': None, + 'codeName': 'Code 1', + 'lockName': 'Front Door', + 'usedCode': 'Code 2' + }) + await setup_platform(hass, LOCK_DOMAIN, devices=[device]) # Act await hass.services.async_call( LOCK_DOMAIN, 'lock', {'entity_id': 'lock.lock_1'}, @@ -56,6 +64,12 @@ async def test_lock(hass, device_factory): state = hass.states.get('lock.lock_1') assert state is not None assert state.state == 'locked' + assert state.attributes['method'] == 'Manual' + assert state.attributes['lock_state'] == 'locked' + assert state.attributes['code_name'] == 'Code 1' + assert state.attributes['used_code'] == 'Code 2' + assert state.attributes['lock_name'] == 'Front Door' + assert 'code_id' not in state.attributes async def test_unlock(hass, device_factory): @@ -63,7 +77,7 @@ async def test_unlock(hass, device_factory): # Arrange device = device_factory('Lock_1', [Capability.lock], {Attribute.lock: 'locked'}) - await setup_platform(hass, LOCK_DOMAIN, device) + await setup_platform(hass, LOCK_DOMAIN, devices=[device]) # Act await hass.services.async_call( LOCK_DOMAIN, 'unlock', {'entity_id': 'lock.lock_1'}, @@ -79,7 +93,7 @@ async def test_update_from_signal(hass, device_factory): # Arrange device = device_factory('Lock_1', [Capability.lock], {Attribute.lock: 'unlocked'}) - await setup_platform(hass, LOCK_DOMAIN, device) + await setup_platform(hass, LOCK_DOMAIN, devices=[device]) await device.lock(True) # Act async_dispatcher_send(hass, SIGNAL_SMARTTHINGS_UPDATE, @@ -96,7 +110,7 @@ async def test_unload_config_entry(hass, device_factory): # Arrange device = device_factory('Lock_1', [Capability.lock], {Attribute.lock: 'locked'}) - config_entry = await setup_platform(hass, LOCK_DOMAIN, device) + config_entry = await setup_platform(hass, LOCK_DOMAIN, devices=[device]) # Act await hass.config_entries.async_forward_entry_unload( config_entry, 'lock') diff --git a/tests/components/smartthings/test_scene.py b/tests/components/smartthings/test_scene.py new file mode 100644 index 0000000000000..2d4990675f879 --- /dev/null +++ b/tests/components/smartthings/test_scene.py @@ -0,0 +1,54 @@ +""" +Test for the SmartThings scene platform. + +The only mocking required is of the underlying SmartThings API object so +real HTTP calls are not initiated during testing. +""" +from homeassistant.components.scene import DOMAIN as SCENE_DOMAIN +from homeassistant.components.smartthings import scene as scene_platform +from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_ON + +from .conftest import setup_platform + + +async def test_async_setup_platform(): + """Test setup platform does nothing (it uses config entries).""" + await scene_platform.async_setup_platform(None, None, None) + + +async def test_entity_and_device_attributes(hass, scene): + """Test the attributes of the entity are correct.""" + # Arrange + entity_registry = await hass.helpers.entity_registry.async_get_registry() + # Act + await setup_platform(hass, SCENE_DOMAIN, scenes=[scene]) + # Assert + entry = entity_registry.async_get('scene.test_scene') + assert entry + assert entry.unique_id == scene.scene_id + + +async def test_scene_activate(hass, scene): + """Test the scene is activated.""" + await setup_platform(hass, SCENE_DOMAIN, scenes=[scene]) + await hass.services.async_call( + SCENE_DOMAIN, SERVICE_TURN_ON, { + ATTR_ENTITY_ID: 'scene.test_scene'}, + blocking=True) + state = hass.states.get('scene.test_scene') + assert state.attributes['icon'] == scene.icon + assert state.attributes['color'] == scene.color + assert state.attributes['location_id'] == scene.location_id + # pylint: disable=protected-access + assert scene._api.execute_scene.call_count == 1 # type: ignore + + +async def test_unload_config_entry(hass, scene): + """Test the scene is removed when the config entry is unloaded.""" + # Arrange + config_entry = await setup_platform(hass, SCENE_DOMAIN, scenes=[scene]) + # Act + await hass.config_entries.async_forward_entry_unload( + config_entry, SCENE_DOMAIN) + # Assert + assert not hass.states.get('scene.test_scene') diff --git a/tests/components/smartthings/test_sensor.py b/tests/components/smartthings/test_sensor.py index 773f157dd877e..879aae1994dc8 100644 --- a/tests/components/smartthings/test_sensor.py +++ b/tests/components/smartthings/test_sensor.py @@ -37,7 +37,7 @@ async def test_entity_state(hass, device_factory): """Tests the state attributes properly match the light types.""" device = device_factory('Sensor 1', [Capability.battery], {Attribute.battery: 100}) - await setup_platform(hass, SENSOR_DOMAIN, device) + await setup_platform(hass, SENSOR_DOMAIN, devices=[device]) state = hass.states.get('sensor.sensor_1_battery') assert state.state == '100' assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == '%' @@ -53,7 +53,7 @@ async def test_entity_and_device_attributes(hass, device_factory): entity_registry = await hass.helpers.entity_registry.async_get_registry() device_registry = await hass.helpers.device_registry.async_get_registry() # Act - await setup_platform(hass, SENSOR_DOMAIN, device) + await setup_platform(hass, SENSOR_DOMAIN, devices=[device]) # Assert entry = entity_registry.async_get('sensor.sensor_1_battery') assert entry @@ -71,7 +71,7 @@ async def test_update_from_signal(hass, device_factory): # Arrange device = device_factory('Sensor 1', [Capability.battery], {Attribute.battery: 100}) - await setup_platform(hass, SENSOR_DOMAIN, device) + await setup_platform(hass, SENSOR_DOMAIN, devices=[device]) device.status.apply_attribute_update( 'main', Capability.battery, Attribute.battery, 75) # Act @@ -89,7 +89,7 @@ async def test_unload_config_entry(hass, device_factory): # Arrange device = device_factory('Sensor 1', [Capability.battery], {Attribute.battery: 100}) - config_entry = await setup_platform(hass, SENSOR_DOMAIN, device) + config_entry = await setup_platform(hass, SENSOR_DOMAIN, devices=[device]) # Act await hass.config_entries.async_forward_entry_unload( config_entry, 'sensor') diff --git a/tests/components/smartthings/test_smartapp.py b/tests/components/smartthings/test_smartapp.py index 162a8f9a4e5c7..46bd1f42f7faa 100644 --- a/tests/components/smartthings/test_smartapp.py +++ b/tests/components/smartthings/test_smartapp.py @@ -5,7 +5,9 @@ from pysmartthings import AppEntity, Capability from homeassistant.components.smartthings import smartapp -from homeassistant.components.smartthings.const import DATA_MANAGER, DOMAIN +from homeassistant.components.smartthings.const import ( + CONF_INSTALLED_APP_ID, CONF_INSTALLED_APPS, CONF_LOCATION_ID, + CONF_REFRESH_TOKEN, DATA_MANAGER, DOMAIN) from tests.common import mock_coro @@ -35,31 +37,26 @@ async def test_update_app_updated_needed(hass, app): assert mock_app.classifications == app.classifications -async def test_smartapp_install_abort_if_no_other( +async def test_smartapp_install_store_if_no_other( hass, smartthings_mock, device_factory): """Test aborts if no other app was configured already.""" # Arrange - api = smartthings_mock.return_value - api.create_subscription.return_value = mock_coro() app = Mock() app.app_id = uuid4() request = Mock() - request.installed_app_id = uuid4() - request.auth_token = uuid4() - request.location_id = uuid4() - devices = [ - device_factory('', [Capability.battery, 'ping']), - device_factory('', [Capability.switch, Capability.switch_level]), - device_factory('', [Capability.switch]) - ] - api.devices = Mock() - api.devices.return_value = mock_coro(return_value=devices) + request.installed_app_id = str(uuid4()) + request.auth_token = str(uuid4()) + request.location_id = str(uuid4()) + request.refresh_token = str(uuid4()) # Act await smartapp.smartapp_install(hass, request, None, app) # Assert entries = hass.config_entries.async_entries('smartthings') assert not entries - assert api.create_subscription.call_count == 3 + data = hass.data[DOMAIN][CONF_INSTALLED_APPS][0] + assert data[CONF_REFRESH_TOKEN] == request.refresh_token + assert data[CONF_LOCATION_ID] == request.location_id + assert data[CONF_INSTALLED_APP_ID] == request.installed_app_id async def test_smartapp_install_creates_flow( @@ -68,12 +65,12 @@ async def test_smartapp_install_creates_flow( # Arrange setattr(hass.config_entries, '_entries', [config_entry]) api = smartthings_mock.return_value - api.create_subscription.return_value = mock_coro() app = Mock() app.app_id = config_entry.data['app_id'] request = Mock() request.installed_app_id = str(uuid4()) request.auth_token = str(uuid4()) + request.refresh_token = str(uuid4()) request.location_id = location.location_id devices = [ device_factory('', [Capability.battery, 'ping']), @@ -88,42 +85,42 @@ async def test_smartapp_install_creates_flow( await hass.async_block_till_done() entries = hass.config_entries.async_entries('smartthings') assert len(entries) == 2 - assert api.create_subscription.call_count == 3 assert entries[1].data['app_id'] == app.app_id assert entries[1].data['installed_app_id'] == request.installed_app_id assert entries[1].data['location_id'] == request.location_id assert entries[1].data['access_token'] == \ config_entry.data['access_token'] + assert entries[1].data['refresh_token'] == request.refresh_token + assert entries[1].data['client_secret'] == \ + config_entry.data['client_secret'] + assert entries[1].data['client_id'] == config_entry.data['client_id'] assert entries[1].title == location.name -async def test_smartapp_update_syncs_subs( - hass, smartthings_mock, config_entry, location, device_factory): - """Test update synchronizes subscriptions.""" +async def test_smartapp_update_saves_token( + hass, smartthings_mock, location, device_factory): + """Test update saves token.""" # Arrange - setattr(hass.config_entries, '_entries', [config_entry]) + entry = Mock() + entry.data = { + 'installed_app_id': str(uuid4()), + 'app_id': str(uuid4()) + } + entry.domain = DOMAIN + + setattr(hass.config_entries, '_entries', [entry]) app = Mock() - app.app_id = config_entry.data['app_id'] - api = smartthings_mock.return_value - api.delete_subscriptions = Mock() - api.delete_subscriptions.return_value = mock_coro() - api.create_subscription.return_value = mock_coro() + app.app_id = entry.data['app_id'] request = Mock() - request.installed_app_id = str(uuid4()) + request.installed_app_id = entry.data['installed_app_id'] request.auth_token = str(uuid4()) + request.refresh_token = str(uuid4()) request.location_id = location.location_id - devices = [ - device_factory('', [Capability.battery, 'ping']), - device_factory('', [Capability.switch, Capability.switch_level]), - device_factory('', [Capability.switch]) - ] - api.devices = Mock() - api.devices.return_value = mock_coro(return_value=devices) + # Act await smartapp.smartapp_update(hass, request, None, app) # Assert - assert api.create_subscription.call_count == 3 - assert api.delete_subscriptions.call_count == 1 + assert entry.data[CONF_REFRESH_TOKEN] == request.refresh_token async def test_smartapp_uninstall(hass, config_entry): @@ -152,3 +149,83 @@ async def test_smartapp_webhook(hass): result = await smartapp.smartapp_webhook(hass, '', request) assert result.body == b'{}' + + +async def test_smartapp_sync_subscriptions( + hass, smartthings_mock, device_factory, subscription_factory): + """Test synchronization adds and removes.""" + api = smartthings_mock.return_value + api.delete_subscription.side_effect = lambda loc_id, sub_id: mock_coro() + api.create_subscription.side_effect = lambda sub: mock_coro() + subscriptions = [ + subscription_factory(Capability.thermostat), + subscription_factory(Capability.switch), + subscription_factory(Capability.switch_level) + ] + api.subscriptions.return_value = mock_coro(return_value=subscriptions) + devices = [ + device_factory('', [Capability.battery, 'ping']), + device_factory('', [Capability.switch, Capability.switch_level]), + device_factory('', [Capability.switch]) + ] + + await smartapp.smartapp_sync_subscriptions( + hass, str(uuid4()), str(uuid4()), str(uuid4()), devices) + + assert api.subscriptions.call_count == 1 + assert api.delete_subscription.call_count == 1 + assert api.create_subscription.call_count == 1 + + +async def test_smartapp_sync_subscriptions_up_to_date( + hass, smartthings_mock, device_factory, subscription_factory): + """Test synchronization does nothing when current.""" + api = smartthings_mock.return_value + api.delete_subscription.side_effect = lambda loc_id, sub_id: mock_coro() + api.create_subscription.side_effect = lambda sub: mock_coro() + subscriptions = [ + subscription_factory(Capability.battery), + subscription_factory(Capability.switch), + subscription_factory(Capability.switch_level) + ] + api.subscriptions.return_value = mock_coro(return_value=subscriptions) + devices = [ + device_factory('', [Capability.battery, 'ping']), + device_factory('', [Capability.switch, Capability.switch_level]), + device_factory('', [Capability.switch]) + ] + + await smartapp.smartapp_sync_subscriptions( + hass, str(uuid4()), str(uuid4()), str(uuid4()), devices) + + assert api.subscriptions.call_count == 1 + assert api.delete_subscription.call_count == 0 + assert api.create_subscription.call_count == 0 + + +async def test_smartapp_sync_subscriptions_handles_exceptions( + hass, smartthings_mock, device_factory, subscription_factory): + """Test synchronization does nothing when current.""" + api = smartthings_mock.return_value + api.delete_subscription.side_effect = \ + lambda loc_id, sub_id: mock_coro(exception=Exception) + api.create_subscription.side_effect = \ + lambda sub: mock_coro(exception=Exception) + subscriptions = [ + subscription_factory(Capability.battery), + subscription_factory(Capability.switch), + subscription_factory(Capability.switch_level) + ] + api.subscriptions.return_value = mock_coro(return_value=subscriptions) + devices = [ + device_factory('', [Capability.thermostat, 'ping']), + device_factory('', [Capability.switch, Capability.switch_level]), + device_factory('', [Capability.switch]) + ] + + await smartapp.smartapp_sync_subscriptions( + hass, str(uuid4()), str(uuid4()), str(uuid4()), devices) + + assert api.subscriptions.call_count == 1 + assert api.delete_subscription.call_count == 1 + assert api.create_subscription.call_count == 1 diff --git a/tests/components/smartthings/test_switch.py b/tests/components/smartthings/test_switch.py index 3f2bedd4f1315..e3b1f46bf39ca 100644 --- a/tests/components/smartthings/test_switch.py +++ b/tests/components/smartthings/test_switch.py @@ -6,28 +6,14 @@ """ from pysmartthings import Attribute, Capability -from homeassistant.components.smartthings import DeviceBroker, switch +from homeassistant.components.smartthings import switch from homeassistant.components.smartthings.const import ( - DATA_BROKERS, DOMAIN, SIGNAL_SMARTTHINGS_UPDATE) -from homeassistant.config_entries import ( - CONN_CLASS_CLOUD_PUSH, SOURCE_USER, ConfigEntry) + DOMAIN, SIGNAL_SMARTTHINGS_UPDATE) +from homeassistant.components.switch import ( + ATTR_CURRENT_POWER_W, ATTR_TODAY_ENERGY_KWH, DOMAIN as SWITCH_DOMAIN) from homeassistant.helpers.dispatcher import async_dispatcher_send - -async def _setup_platform(hass, *devices): - """Set up the SmartThings switch platform and prerequisites.""" - hass.config.components.add(DOMAIN) - broker = DeviceBroker(hass, devices, '') - config_entry = ConfigEntry("1", DOMAIN, "Test", {}, - SOURCE_USER, CONN_CLASS_CLOUD_PUSH) - hass.data[DOMAIN] = { - DATA_BROKERS: { - config_entry.entry_id: broker - } - } - await hass.config_entries.async_forward_entry_setup(config_entry, 'switch') - await hass.async_block_till_done() - return config_entry +from .conftest import setup_platform async def test_async_setup_platform(): @@ -43,7 +29,7 @@ async def test_entity_and_device_attributes(hass, device_factory): entity_registry = await hass.helpers.entity_registry.async_get_registry() device_registry = await hass.helpers.device_registry.async_get_registry() # Act - await _setup_platform(hass, device) + await setup_platform(hass, SWITCH_DOMAIN, devices=[device]) # Assert entry = entity_registry.async_get('switch.switch_1') assert entry @@ -62,7 +48,7 @@ async def test_turn_off(hass, device_factory): # Arrange device = device_factory('Switch_1', [Capability.switch], {Attribute.switch: 'on'}) - await _setup_platform(hass, device) + await setup_platform(hass, SWITCH_DOMAIN, devices=[device]) # Act await hass.services.async_call( 'switch', 'turn_off', {'entity_id': 'switch.switch_1'}, @@ -76,9 +62,14 @@ async def test_turn_off(hass, device_factory): async def test_turn_on(hass, device_factory): """Test the switch turns of successfully.""" # Arrange - device = device_factory('Switch_1', [Capability.switch], - {Attribute.switch: 'off'}) - await _setup_platform(hass, device) + device = device_factory('Switch_1', + [Capability.switch, + Capability.power_meter, + Capability.energy_meter], + {Attribute.switch: 'off', + Attribute.power: 355, + Attribute.energy: 11.422}) + await setup_platform(hass, SWITCH_DOMAIN, devices=[device]) # Act await hass.services.async_call( 'switch', 'turn_on', {'entity_id': 'switch.switch_1'}, @@ -87,6 +78,8 @@ async def test_turn_on(hass, device_factory): state = hass.states.get('switch.switch_1') assert state is not None assert state.state == 'on' + assert state.attributes[ATTR_CURRENT_POWER_W] == 355 + assert state.attributes[ATTR_TODAY_ENERGY_KWH] == 11.422 async def test_update_from_signal(hass, device_factory): @@ -94,7 +87,7 @@ async def test_update_from_signal(hass, device_factory): # Arrange device = device_factory('Switch_1', [Capability.switch], {Attribute.switch: 'off'}) - await _setup_platform(hass, device) + await setup_platform(hass, SWITCH_DOMAIN, devices=[device]) await device.switch_on(True) # Act async_dispatcher_send(hass, SIGNAL_SMARTTHINGS_UPDATE, @@ -111,7 +104,7 @@ async def test_unload_config_entry(hass, device_factory): # Arrange device = device_factory('Switch 1', [Capability.switch], {Attribute.switch: 'on'}) - config_entry = await _setup_platform(hass, device) + config_entry = await setup_platform(hass, SWITCH_DOMAIN, devices=[device]) # Act await hass.config_entries.async_forward_entry_unload( config_entry, 'switch') diff --git a/tests/components/snips/test_init.py b/tests/components/snips/test_init.py index 7bcfb11ff5c60..e9719c0239525 100644 --- a/tests/components/snips/test_init.py +++ b/tests/components/snips/test_init.py @@ -113,7 +113,7 @@ async def test_snips_intent(hass): "input": "turn the lights green", "intent": { "intentName": "Lights", - "probability": 1 + "confidenceScore": 1 }, "slots": [ { @@ -140,7 +140,7 @@ async def test_snips_intent(hass): assert intent assert intent.slots == {'light_color': {'value': 'green'}, 'light_color_raw': {'value': 'green'}, - 'probability': {'value': 1}, + 'confidenceScore': {'value': 1}, 'site_id': {'value': 'default'}, 'session_id': {'value': '1234567890ABCDEF'}} assert intent.text_input == 'turn the lights green' @@ -161,7 +161,7 @@ async def test_snips_service_intent(hass): "input": "turn the light on", "intent": { "intentName": "Lights", - "probability": 0.85 + "confidenceScore": 0.85 }, "siteId": "default", "slots": [ @@ -188,7 +188,7 @@ async def test_snips_service_intent(hass): assert calls[0].domain == 'light' assert calls[0].service == 'turn_on' assert calls[0].data['entity_id'] == 'light.kitchen' - assert 'probability' not in calls[0].data + assert 'confidenceScore' not in calls[0].data assert 'site_id' not in calls[0].data @@ -205,7 +205,7 @@ async def test_snips_intent_with_duration(hass): "input": "set a timer of five minutes", "intent": { "intentName": "SetTimer", - "probability": 1 + "confidenceScore": 1 }, "slots": [ { @@ -241,7 +241,7 @@ async def test_snips_intent_with_duration(hass): intent = intents[0] assert intent.platform == 'snips' assert intent.intent_type == 'SetTimer' - assert intent.slots == {'probability': {'value': 1}, + assert intent.slots == {'confidenceScore': {'value': 1}, 'site_id': {'value': None}, 'session_id': {'value': None}, 'timer_duration': {'value': 300}, @@ -274,7 +274,7 @@ async def test_intent_speech_response(hass): "sessionId": "abcdef0123456789", "intent": { "intentName": "spokenIntent", - "probability": 1 + "confidenceScore": 1 }, "slots": [] } @@ -306,7 +306,7 @@ async def test_unknown_intent(hass, caplog): "sessionId": "abcdef1234567890", "intent": { "intentName": "unknownIntent", - "probability": 1 + "confidenceScore": 1 }, "slots": [] } @@ -330,7 +330,7 @@ async def test_snips_intent_user(hass): "input": "what to do", "intent": { "intentName": "user_ABCDEF123__Lights", - "probability": 1 + "confidenceScore": 1 }, "slots": [] } @@ -359,7 +359,7 @@ async def test_snips_intent_username(hass): "input": "what to do", "intent": { "intentName": "username:Lights", - "probability": 1 + "confidenceScore": 1 }, "slots": [] } @@ -391,7 +391,7 @@ async def test_snips_low_probability(hass, caplog): "input": "I am not sure what to say", "intent": { "intentName": "LightsMaybe", - "probability": 0.49 + "confidenceScore": 0.49 }, "slots": [] } @@ -419,7 +419,7 @@ async def test_intent_special_slots(hass): "action": { "service": "light.turn_on", "data_template": { - "probability": "{{ probability }}", + "confidenceScore": "{{ confidenceScore }}", "site_id": "{{ site_id }}" } } @@ -432,7 +432,7 @@ async def test_intent_special_slots(hass): "input": "turn the light on", "intent": { "intentName": "Lights", - "probability": 0.85 + "confidenceScore": 0.85 }, "siteId": "default", "slots": [] @@ -444,7 +444,7 @@ async def test_intent_special_slots(hass): assert len(calls) == 1 assert calls[0].domain == 'light' assert calls[0].service == 'turn_on' - assert calls[0].data['probability'] == '0.85' + assert calls[0].data['confidenceScore'] == '0.85' assert calls[0].data['site_id'] == 'default' diff --git a/tests/components/sonos/test_media_player.py b/tests/components/sonos/test_media_player.py index 55ff96f202a20..798c92eddadaa 100644 --- a/tests/components/sonos/test_media_player.py +++ b/tests/components/sonos/test_media_player.py @@ -21,7 +21,7 @@ class pysonosDiscoverMock(): """Mock class for the pysonos.discover method.""" - def discover(interface_addr): + def discover(interface_addr, all_households=False): """Return tuple of pysonos.SoCo objects representing found speakers.""" return {SoCoMock('192.0.2.1')} @@ -49,6 +49,14 @@ def get_sonos_favorites(self): return [] +class CacheMock(): + """Mock class for the _zgs_cache property on pysonos.SoCo object.""" + + def clear(self): + """Clear cache.""" + pass + + class SoCoMock(): """Mock class for the pysonos.SoCo object.""" @@ -63,6 +71,7 @@ def __init__(self, ip): self.dialog_mode = False self.music_library = MusicLibraryMock() self.avTransport = AvTransportMock() + self._zgs_cache = CacheMock() def get_sonos_favorites(self): """Get favorites list from sonos.""" @@ -123,10 +132,10 @@ def group(self): def add_entities_factory(hass): - """Add devices factory.""" - def add_entities(devices, update_befor_add=False): - """Fake add device.""" - hass.data[sonos.DATA_SONOS].devices = devices + """Add entities factory.""" + def add_entities(entities, update_befor_add=False): + """Fake add entity.""" + hass.data[sonos.DATA_SONOS].entities = list(entities) return add_entities @@ -144,14 +153,14 @@ def monkey_available(self): return True # Monkey patches - self.real_available = sonos.SonosDevice.available - sonos.SonosDevice.available = monkey_available + self.real_available = sonos.SonosEntity.available + sonos.SonosEntity.available = monkey_available # pylint: disable=invalid-name def tearDown(self): """Stop everything that was started.""" # Monkey patches - sonos.SonosDevice.available = self.real_available + sonos.SonosEntity.available = self.real_available self.hass.stop() @mock.patch('pysonos.SoCo', new=SoCoMock) @@ -162,9 +171,9 @@ def test_ensure_setup_discovery(self, *args): 'host': '192.0.2.1' }) - devices = list(self.hass.data[sonos.DATA_SONOS].devices) - assert len(devices) == 1 - assert devices[0].name == 'Kitchen' + entities = self.hass.data[sonos.DATA_SONOS].entities + assert len(entities) == 1 + assert entities[0].name == 'Kitchen' @mock.patch('pysonos.SoCo', new=SoCoMock) @mock.patch('socket.create_connection', side_effect=socket.error()) @@ -182,7 +191,7 @@ def test_ensure_setup_config_interface_addr(self, discover_mock, *args): assert setup_component(self.hass, DOMAIN, config) - assert len(self.hass.data[sonos.DATA_SONOS].devices) == 1 + assert len(self.hass.data[sonos.DATA_SONOS].entities) == 1 assert discover_mock.call_count == 1 @mock.patch('pysonos.SoCo', new=SoCoMock) @@ -198,9 +207,9 @@ def test_ensure_setup_config_hosts_string_single(self, *args): assert setup_component(self.hass, DOMAIN, config) - devices = self.hass.data[sonos.DATA_SONOS].devices - assert len(devices) == 1 - assert devices[0].name == 'Kitchen' + entities = self.hass.data[sonos.DATA_SONOS].entities + assert len(entities) == 1 + assert entities[0].name == 'Kitchen' @mock.patch('pysonos.SoCo', new=SoCoMock) @mock.patch('socket.create_connection', side_effect=socket.error()) @@ -215,9 +224,9 @@ def test_ensure_setup_config_hosts_string_multiple(self, *args): assert setup_component(self.hass, DOMAIN, config) - devices = self.hass.data[sonos.DATA_SONOS].devices - assert len(devices) == 2 - assert devices[0].name == 'Kitchen' + entities = self.hass.data[sonos.DATA_SONOS].entities + assert len(entities) == 2 + assert entities[0].name == 'Kitchen' @mock.patch('pysonos.SoCo', new=SoCoMock) @mock.patch('socket.create_connection', side_effect=socket.error()) @@ -232,9 +241,9 @@ def test_ensure_setup_config_hosts_list(self, *args): assert setup_component(self.hass, DOMAIN, config) - devices = self.hass.data[sonos.DATA_SONOS].devices - assert len(devices) == 2 - assert devices[0].name == 'Kitchen' + entities = self.hass.data[sonos.DATA_SONOS].entities + assert len(entities) == 2 + assert entities[0].name == 'Kitchen' @mock.patch('pysonos.SoCo', new=SoCoMock) @mock.patch.object(pysonos, 'discover', new=pysonosDiscoverMock.discover) @@ -242,9 +251,9 @@ def test_ensure_setup_config_hosts_list(self, *args): def test_ensure_setup_sonos_discovery(self, *args): """Test a single device using the autodiscovery provided by Sonos.""" sonos.setup_platform(self.hass, {}, add_entities_factory(self.hass)) - devices = list(self.hass.data[sonos.DATA_SONOS].devices) - assert len(devices) == 1 - assert devices[0].name == 'Kitchen' + entities = self.hass.data[sonos.DATA_SONOS].entities + assert len(entities) == 1 + assert entities[0].name == 'Kitchen' @mock.patch('pysonos.SoCo', new=SoCoMock) @mock.patch('socket.create_connection', side_effect=socket.error()) @@ -254,10 +263,10 @@ def test_sonos_set_sleep_timer(self, set_sleep_timerMock, *args): sonos.setup_platform(self.hass, {}, add_entities_factory(self.hass), { 'host': '192.0.2.1' }) - device = list(self.hass.data[sonos.DATA_SONOS].devices)[-1] - device.hass = self.hass + entity = self.hass.data[sonos.DATA_SONOS].entities[-1] + entity.hass = self.hass - device.set_sleep_timer(30) + entity.set_sleep_timer(30) set_sleep_timerMock.assert_called_once_with(30) @mock.patch('pysonos.SoCo', new=SoCoMock) @@ -268,10 +277,10 @@ def test_sonos_clear_sleep_timer(self, set_sleep_timerMock, *args): sonos.setup_platform(self.hass, {}, add_entities_factory(self.hass), { 'host': '192.0.2.1' }) - device = list(self.hass.data[sonos.DATA_SONOS].devices)[-1] - device.hass = self.hass + entity = self.hass.data[sonos.DATA_SONOS].entities[-1] + entity.hass = self.hass - device.set_sleep_timer(None) + entity.set_sleep_timer(None) set_sleep_timerMock.assert_called_once_with(None) @mock.patch('pysonos.SoCo', new=SoCoMock) @@ -282,8 +291,8 @@ def test_set_alarm(self, pysonos_mock, alarm_mock, *args): sonos.setup_platform(self.hass, {}, add_entities_factory(self.hass), { 'host': '192.0.2.1' }) - device = list(self.hass.data[sonos.DATA_SONOS].devices)[-1] - device.hass = self.hass + entity = self.hass.data[sonos.DATA_SONOS].entities[-1] + entity.hass = self.hass alarm1 = alarms.Alarm(pysonos_mock) alarm1.configure_mock(_alarm_id="1", start_time=None, enabled=False, include_linked_zones=False, volume=100) @@ -294,9 +303,9 @@ def test_set_alarm(self, pysonos_mock, alarm_mock, *args): 'include_linked_zones': True, 'volume': 0.30, } - device.set_alarm(alarm_id=2) + entity.set_alarm(alarm_id=2) alarm1.save.assert_not_called() - device.set_alarm(alarm_id=1, **attrs) + entity.set_alarm(alarm_id=1, **attrs) assert alarm1.enabled == attrs['enabled'] assert alarm1.start_time == attrs['time'] assert alarm1.include_linked_zones == \ @@ -312,11 +321,14 @@ def test_sonos_snapshot(self, snapshotMock, *args): sonos.setup_platform(self.hass, {}, add_entities_factory(self.hass), { 'host': '192.0.2.1' }) - device = list(self.hass.data[sonos.DATA_SONOS].devices)[-1] - device.hass = self.hass + entities = self.hass.data[sonos.DATA_SONOS].entities + entity = entities[-1] + entity.hass = self.hass snapshotMock.return_value = True - device.snapshot() + entity.soco.group = mock.MagicMock() + entity.soco.group.members = [e.soco for e in entities] + sonos.SonosEntity.snapshot_multi(entities, True) assert snapshotMock.call_count == 1 assert snapshotMock.call_args == mock.call() @@ -330,13 +342,14 @@ def test_sonos_restore(self, restoreMock, *args): sonos.setup_platform(self.hass, {}, add_entities_factory(self.hass), { 'host': '192.0.2.1' }) - device = list(self.hass.data[sonos.DATA_SONOS].devices)[-1] - device.hass = self.hass + entities = self.hass.data[sonos.DATA_SONOS].entities + entity = entities[-1] + entity.hass = self.hass restoreMock.return_value = True - device._snapshot_coordinator = mock.MagicMock() - device._snapshot_coordinator.soco_device = SoCoMock('192.0.2.17') - device._soco_snapshot = Snapshot(device._player) - device.restore() + entity._snapshot_group = mock.MagicMock() + entity._snapshot_group.members = [e.soco for e in entities] + entity._soco_snapshot = Snapshot(entity.soco) + sonos.SonosEntity.restore_multi(entities, True) assert restoreMock.call_count == 1 - assert restoreMock.call_args == mock.call(False) + assert restoreMock.call_args == mock.call() diff --git a/tests/components/switch/test_litejet.py b/tests/components/switch/test_litejet.py index f1d23f48b8666..a35b6f760f318 100644 --- a/tests/components/switch/test_litejet.py +++ b/tests/components/switch/test_litejet.py @@ -53,9 +53,9 @@ def on_switch_released(number, callback): 'port': '/tmp/this_will_be_mocked', } } - if method == self.test_include_switches_False: + if method == self.__class__.test_include_switches_False: config['litejet']['include_switches'] = False - elif method != self.test_include_switches_unspecified: + elif method != self.__class__.test_include_switches_unspecified: config['litejet']['include_switches'] = True assert setup.setup_component(self.hass, litejet.DOMAIN, config) diff --git a/tests/components/toon/__init__.py b/tests/components/toon/__init__.py new file mode 100644 index 0000000000000..96de853baff1a --- /dev/null +++ b/tests/components/toon/__init__.py @@ -0,0 +1 @@ +"""Tests for the Toon component.""" diff --git a/tests/components/toon/test_config_flow.py b/tests/components/toon/test_config_flow.py new file mode 100644 index 0000000000000..44cb54fc98ecc --- /dev/null +++ b/tests/components/toon/test_config_flow.py @@ -0,0 +1,177 @@ +"""Tests for the Toon config flow.""" + +from unittest.mock import patch + +import pytest +from toonapilib.toonapilibexceptions import ( + AgreementsRetrievalError, InvalidConsumerKey, InvalidConsumerSecret, + InvalidCredentials) + +from homeassistant import data_entry_flow +from homeassistant.components.toon import config_flow +from homeassistant.components.toon.const import ( + CONF_CLIENT_ID, CONF_CLIENT_SECRET, CONF_DISPLAY, CONF_TENANT, DOMAIN) +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.setup import async_setup_component + +from tests.common import MockConfigEntry, MockDependency + +FIXTURE_APP = { + DOMAIN: { + CONF_CLIENT_ID: '1234567890abcdef', + CONF_CLIENT_SECRET: '1234567890abcdef', + } +} + +FIXTURE_CREDENTIALS = { + CONF_USERNAME: 'john.doe', + CONF_PASSWORD: 'secret', + CONF_TENANT: 'eneco' +} + +FIXTURE_DISPLAY = { + CONF_DISPLAY: 'display1' +} + + +@pytest.fixture +def mock_toonapilib(): + """Mock toonapilib.""" + with MockDependency('toonapilib') as mock_toonapilib_: + mock_toonapilib_.Toon().display_names = [FIXTURE_DISPLAY[CONF_DISPLAY]] + yield mock_toonapilib_ + + +async def setup_component(hass): + """Set up Toon component.""" + with patch('os.path.isfile', return_value=False): + assert await async_setup_component(hass, DOMAIN, FIXTURE_APP) + await hass.async_block_till_done() + + +async def test_abort_if_no_app_configured(hass): + """Test abort if no app is configured.""" + flow = config_flow.ToonFlowHandler() + flow.hass = hass + result = await flow.async_step_user() + + assert result['type'] == data_entry_flow.RESULT_TYPE_ABORT + assert result['reason'] == 'no_app' + + +async def test_show_authenticate_form(hass): + """Test that the authentication form is served.""" + await setup_component(hass) + + flow = config_flow.ToonFlowHandler() + 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'] == 'authenticate' + + +@pytest.mark.parametrize('side_effect,reason', + [(InvalidConsumerKey, 'client_id'), + (InvalidConsumerSecret, 'client_secret'), + (AgreementsRetrievalError, 'no_agreements'), + (Exception, 'unknown_auth_fail')]) +async def test_toon_abort(hass, mock_toonapilib, side_effect, reason): + """Test we abort on Toon error.""" + await setup_component(hass) + + flow = config_flow.ToonFlowHandler() + flow.hass = hass + + mock_toonapilib.Toon.side_effect = side_effect + + result = await flow.async_step_authenticate(user_input=FIXTURE_CREDENTIALS) + + assert result['type'] == data_entry_flow.RESULT_TYPE_ABORT + assert result['reason'] == reason + + +async def test_invalid_credentials(hass, mock_toonapilib): + """Test we show authentication form on Toon auth error.""" + mock_toonapilib.Toon.side_effect = InvalidCredentials + + await setup_component(hass) + + flow = config_flow.ToonFlowHandler() + flow.hass = hass + result = await flow.async_step_user(user_input=FIXTURE_CREDENTIALS) + + assert result['type'] == data_entry_flow.RESULT_TYPE_FORM + assert result['step_id'] == 'authenticate' + assert result['errors'] == {'base': 'credentials'} + + +async def test_full_flow_implementation(hass, mock_toonapilib): + """Test registering an integration and finishing flow works.""" + await setup_component(hass) + + flow = config_flow.ToonFlowHandler() + 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'] == 'authenticate' + + result = await flow.async_step_user(user_input=FIXTURE_CREDENTIALS) + assert result['type'] == data_entry_flow.RESULT_TYPE_FORM + assert result['step_id'] == 'display' + + result = await flow.async_step_display(user_input=FIXTURE_DISPLAY) + assert result['type'] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result['title'] == FIXTURE_DISPLAY[CONF_DISPLAY] + assert result['data'][CONF_USERNAME] == FIXTURE_CREDENTIALS[CONF_USERNAME] + assert result['data'][CONF_PASSWORD] == FIXTURE_CREDENTIALS[CONF_PASSWORD] + assert result['data'][CONF_TENANT] == FIXTURE_CREDENTIALS[CONF_TENANT] + assert result['data'][CONF_DISPLAY] == FIXTURE_DISPLAY[CONF_DISPLAY] + + +async def test_no_displays(hass, mock_toonapilib): + """Test abort when there are no displays.""" + await setup_component(hass) + + mock_toonapilib.Toon().display_names = [] + + flow = config_flow.ToonFlowHandler() + flow.hass = hass + await flow.async_step_user(user_input=FIXTURE_CREDENTIALS) + + result = await flow.async_step_display(user_input=None) + + assert result['type'] == data_entry_flow.RESULT_TYPE_ABORT + assert result['reason'] == 'no_displays' + + +async def test_display_already_exists(hass, mock_toonapilib): + """Test showing display form again if display already exists.""" + await setup_component(hass) + + flow = config_flow.ToonFlowHandler() + flow.hass = hass + await flow.async_step_user(user_input=FIXTURE_CREDENTIALS) + + MockConfigEntry(domain=DOMAIN, data=FIXTURE_DISPLAY).add_to_hass(hass) + + result = await flow.async_step_display(user_input=FIXTURE_DISPLAY) + + assert result['type'] == data_entry_flow.RESULT_TYPE_FORM + assert result['step_id'] == 'display' + assert result['errors'] == {'base': 'display_exists'} + + +async def test_abort_last_minute_fail(hass, mock_toonapilib): + """Test we abort when API communication fails in the last step.""" + await setup_component(hass) + + flow = config_flow.ToonFlowHandler() + flow.hass = hass + await flow.async_step_user(user_input=FIXTURE_CREDENTIALS) + + mock_toonapilib.Toon.side_effect = Exception + + result = await flow.async_step_display(user_input=FIXTURE_DISPLAY) + assert result['type'] == data_entry_flow.RESULT_TYPE_ABORT + assert result['reason'] == 'unknown_auth_fail' diff --git a/tests/components/tplink/__init__.py b/tests/components/tplink/__init__.py new file mode 100644 index 0000000000000..865c6c1d97a6a --- /dev/null +++ b/tests/components/tplink/__init__.py @@ -0,0 +1 @@ +"""Tests for the TP-Link component.""" diff --git a/tests/components/tplink/test_init.py b/tests/components/tplink/test_init.py new file mode 100644 index 0000000000000..1b234428c9409 --- /dev/null +++ b/tests/components/tplink/test_init.py @@ -0,0 +1,181 @@ +"""Tests for the TP-Link component.""" +from unittest.mock import patch + +import pytest + +from homeassistant import config_entries, data_entry_flow +from homeassistant.components import tplink +from homeassistant.setup import async_setup_component +from pyHS100 import SmartPlug, SmartBulb +from tests.common import MockDependency, MockConfigEntry, mock_coro + +MOCK_PYHS100 = MockDependency("pyHS100") + + +async def test_creating_entry_tries_discover(hass): + """Test setting up does discovery.""" + with MOCK_PYHS100, patch( + "homeassistant.components.tplink.async_setup_entry", + return_value=mock_coro(True), + ) as mock_setup, patch( + "pyHS100.Discover.discover", return_value={"host": 1234} + ): + result = await hass.config_entries.flow.async_init( + tplink.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_tplink_causes_discovery(hass): + """Test that specifying empty config does discovery.""" + with MOCK_PYHS100, patch("pyHS100.Discover.discover") as discover: + discover.return_value = {"host": 1234} + await async_setup_component(hass, tplink.DOMAIN, {"tplink": {}}) + await hass.async_block_till_done() + + assert len(discover.mock_calls) == 1 + + +@pytest.mark.parametrize( + "name,cls,platform", + [ + ("pyHS100.SmartPlug", SmartPlug, "switch"), + ("pyHS100.SmartBulb", SmartBulb, "light"), + ], +) +@pytest.mark.parametrize("count", [1, 2, 3]) +async def test_configuring_device_types(hass, name, cls, platform, count): + """Test that light or switch platform list is filled correctly.""" + with patch("pyHS100.Discover.discover") as discover, patch( + "pyHS100.SmartDevice._query_helper" + ): + discovery_data = { + "123.123.123.{}".format(c): cls("123.123.123.123") + for c in range(count) + } + discover.return_value = discovery_data + await async_setup_component(hass, tplink.DOMAIN, {"tplink": {}}) + await hass.async_block_till_done() + + assert len(discover.mock_calls) == 1 + assert len(hass.data[tplink.DOMAIN][platform]) == count + + +async def test_is_dimmable(hass): + """Test that is_dimmable switches are correctly added as lights.""" + with patch("pyHS100.Discover.discover") as discover, patch( + "homeassistant.components.tplink.light.async_setup_entry", + return_value=mock_coro(True), + ) as setup, patch("pyHS100.SmartDevice._query_helper"), patch( + "pyHS100.SmartPlug.is_dimmable", True + ): + dimmable_switch = SmartPlug("123.123.123.123") + discover.return_value = {"host": dimmable_switch} + + await async_setup_component(hass, tplink.DOMAIN, {"tplink": {}}) + await hass.async_block_till_done() + + assert len(discover.mock_calls) == 1 + assert len(setup.mock_calls) == 1 + assert len(hass.data[tplink.DOMAIN]["light"]) == 1 + assert len(hass.data[tplink.DOMAIN]["switch"]) == 0 + + +async def test_configuring_discovery_disabled(hass): + """Test that discover does not get called when disabled.""" + with MOCK_PYHS100, patch( + "homeassistant.components.tplink.async_setup_entry", + return_value=mock_coro(True), + ) as mock_setup, patch( + "pyHS100.Discover.discover", return_value=[] + ) as discover: + await async_setup_component( + hass, + tplink.DOMAIN, + {tplink.DOMAIN: {tplink.CONF_DISCOVERY: False}}, + ) + await hass.async_block_till_done() + + assert len(discover.mock_calls) == 0 + assert len(mock_setup.mock_calls) == 1 + + +async def test_platforms_are_initialized(hass): + """Test that platforms are initialized per configuration array.""" + config = { + "tplink": { + "discovery": False, + "light": [{"host": "123.123.123.123"}], + "switch": [{"host": "321.321.321.321"}], + } + } + + with patch("pyHS100.Discover.discover") as discover, patch( + "pyHS100.SmartDevice._query_helper" + ), patch( + "homeassistant.components.tplink.light.async_setup_entry", + return_value=mock_coro(True), + ) as light_setup, patch( + "homeassistant.components.tplink.switch.async_setup_entry", + return_value=mock_coro(True), + ) as switch_setup, patch( + "pyHS100.SmartPlug.is_dimmable", False + ): + # patching is_dimmable is necessray to avoid misdetection as light. + await async_setup_component(hass, tplink.DOMAIN, config) + await hass.async_block_till_done() + + assert len(discover.mock_calls) == 0 + assert len(light_setup.mock_calls) == 1 + assert len(switch_setup.mock_calls) == 1 + + +async def test_no_config_creates_no_entry(hass): + """Test for when there is no tplink in config.""" + with MOCK_PYHS100, patch( + "homeassistant.components.tplink.async_setup_entry", + return_value=mock_coro(True), + ) as mock_setup: + await async_setup_component(hass, tplink.DOMAIN, {}) + await hass.async_block_till_done() + + assert len(mock_setup.mock_calls) == 0 + + +@pytest.mark.parametrize("platform", ["switch", "light"]) +async def test_unload(hass, platform): + """Test that the async_unload_entry works.""" + # As we have currently no configuration, we just to pass the domain here. + entry = MockConfigEntry(domain=tplink.DOMAIN) + entry.add_to_hass(hass) + + with patch("pyHS100.SmartDevice._query_helper"), patch( + "homeassistant.components.tplink.{}" + ".async_setup_entry".format(platform), + return_value=mock_coro(True), + ) as light_setup: + config = { + "tplink": { + platform: [{"host": "123.123.123.123"}], + "discovery": False, + } + } + assert await async_setup_component(hass, tplink.DOMAIN, config) + await hass.async_block_till_done() + + assert len(light_setup.mock_calls) == 1 + assert tplink.DOMAIN in hass.data + + assert await tplink.async_unload_entry(hass, entry) + assert not hass.data[tplink.DOMAIN] diff --git a/tests/components/utility_meter/test_sensor.py b/tests/components/utility_meter/test_sensor.py index 23fc8872570cd..03c95fdf8971a 100644 --- a/tests/components/utility_meter/test_sensor.py +++ b/tests/components/utility_meter/test_sensor.py @@ -57,6 +57,72 @@ async def test_state(hass): assert state.state == '1' +async def test_net_consumption(hass): + """Test utility sensor state.""" + config = { + 'utility_meter': { + 'energy_bill': { + 'source': 'sensor.energy', + 'net_consumption': True + } + } + } + + assert await async_setup_component(hass, DOMAIN, config) + assert await async_setup_component(hass, SENSOR_DOMAIN, config) + await hass.async_block_till_done() + + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + entity_id = config[DOMAIN]['energy_bill']['source'] + hass.states.async_set(entity_id, 2, {"unit_of_measurement": "kWh"}) + await hass.async_block_till_done() + + now = dt_util.utcnow() + timedelta(seconds=10) + with patch('homeassistant.util.dt.utcnow', + return_value=now): + hass.states.async_set(entity_id, 1, {"unit_of_measurement": "kWh"}, + force_update=True) + await hass.async_block_till_done() + + state = hass.states.get('sensor.energy_bill') + assert state is not None + + assert state.state == '-1' + + +async def test_non_net_consumption(hass): + """Test utility sensor state.""" + config = { + 'utility_meter': { + 'energy_bill': { + 'source': 'sensor.energy', + 'net_consumption': False + } + } + } + + assert await async_setup_component(hass, DOMAIN, config) + assert await async_setup_component(hass, SENSOR_DOMAIN, config) + await hass.async_block_till_done() + + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + entity_id = config[DOMAIN]['energy_bill']['source'] + hass.states.async_set(entity_id, 2, {"unit_of_measurement": "kWh"}) + await hass.async_block_till_done() + + now = dt_util.utcnow() + timedelta(seconds=10) + with patch('homeassistant.util.dt.utcnow', + return_value=now): + hass.states.async_set(entity_id, 1, {"unit_of_measurement": "kWh"}, + force_update=True) + await hass.async_block_till_done() + + state = hass.states.get('sensor.energy_bill') + assert state is not None + + assert state.state == '0' + + async def _test_self_reset(hass, cycle, start_time, expect_reset=True): """Test energy sensor self reset.""" config = { diff --git a/tests/components/websocket_api/test_commands.py b/tests/components/websocket_api/test_commands.py index 78a5bf6d57ea9..c9ec04c5d7ed8 100644 --- a/tests/components/websocket_api/test_commands.py +++ b/tests/components/websocket_api/test_commands.py @@ -7,6 +7,7 @@ TYPE_AUTH, TYPE_AUTH_OK, TYPE_AUTH_REQUIRED ) from homeassistant.components.websocket_api import const, commands +from homeassistant.exceptions import HomeAssistantError from homeassistant.setup import async_setup_component from tests.common import async_mock_service @@ -66,6 +67,51 @@ async def test_call_service_not_found(hass, websocket_client): assert msg['error']['code'] == const.ERR_NOT_FOUND +async def test_call_service_error(hass, websocket_client): + """Test call service command with error.""" + @callback + def ha_error_call(_): + raise HomeAssistantError('error_message') + + hass.services.async_register('domain_test', 'ha_error', ha_error_call) + + async def unknown_error_call(_): + raise ValueError('value_error') + + hass.services.async_register( + 'domain_test', 'unknown_error', unknown_error_call) + + await websocket_client.send_json({ + 'id': 5, + 'type': commands.TYPE_CALL_SERVICE, + 'domain': 'domain_test', + 'service': 'ha_error', + }) + + msg = await websocket_client.receive_json() + print(msg) + assert msg['id'] == 5 + assert msg['type'] == const.TYPE_RESULT + assert msg['success'] is False + assert msg['error']['code'] == 'home_assistant_error' + assert msg['error']['message'] == 'error_message' + + await websocket_client.send_json({ + 'id': 6, + 'type': commands.TYPE_CALL_SERVICE, + 'domain': 'domain_test', + 'service': 'unknown_error', + }) + + msg = await websocket_client.receive_json() + print(msg) + assert msg['id'] == 6 + assert msg['type'] == const.TYPE_RESULT + assert msg['success'] is False + assert msg['error']['code'] == 'unknown_error' + assert msg['error']['message'] == 'value_error' + + async def test_subscribe_unsubscribe_events(hass, websocket_client): """Test subscribe/unsubscribe events command.""" init_count = sum(hass.bus.async_listeners().values()) diff --git a/tests/components/xiaomi_miio/test_vacuum.py b/tests/components/xiaomi_miio/test_vacuum.py index a1e937cb244e0..0bb557b148823 100644 --- a/tests/components/xiaomi_miio/test_vacuum.py +++ b/tests/components/xiaomi_miio/test_vacuum.py @@ -18,7 +18,8 @@ ATTR_CLEANING_COUNT, ATTR_CLEANED_TOTAL_AREA, ATTR_CLEANING_TOTAL_TIME, CONF_HOST, CONF_NAME, CONF_TOKEN, SERVICE_MOVE_REMOTE_CONTROL, SERVICE_MOVE_REMOTE_CONTROL_STEP, - SERVICE_START_REMOTE_CONTROL, SERVICE_STOP_REMOTE_CONTROL) + SERVICE_START_REMOTE_CONTROL, SERVICE_STOP_REMOTE_CONTROL, + SERVICE_CLEAN_ZONE) from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, CONF_PLATFORM, STATE_OFF, STATE_ON) @@ -330,3 +331,13 @@ def test_xiaomi_specific_services(hass, caplog, mock_mirobo_is_on): [mock.call.Vacuum().manual_control_once(control_once)], any_order=True) mock_mirobo_is_on.assert_has_calls(status_calls, any_order=True) mock_mirobo_is_on.reset_mock() + + control = {"zone": [[123, 123, 123, 123]], "repeats": 2} + yield from hass.services.async_call( + DOMAIN, SERVICE_CLEAN_ZONE, + control, blocking=True) + mock_mirobo_is_off.assert_has_calls( + [mock.call.Vacuum().zoned_clean( + [[123, 123, 123, 123, 2]])], any_order=True) + mock_mirobo_is_off.assert_has_calls(status_calls, any_order=True) + mock_mirobo_is_off.reset_mock() diff --git a/tests/components/zha/test_light.py b/tests/components/zha/test_light.py index 38d7caedaad58..0ccad52d6aa3e 100644 --- a/tests/components/zha/test_light.py +++ b/tests/components/zha/test_light.py @@ -1,20 +1,24 @@ """Test zha light.""" -from unittest.mock import call, patch +import asyncio +from unittest.mock import MagicMock, call, patch, sentinel + from homeassistant.components.light import DOMAIN -from homeassistant.const import STATE_ON, STATE_OFF, STATE_UNAVAILABLE -from tests.common import mock_coro +from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE + from .common import ( - async_init_zigpy_device, make_attribute, make_entity_id, - async_test_device_join, async_enable_traffic -) + async_enable_traffic, async_init_zigpy_device, async_test_device_join, + make_attribute, make_entity_id) + +from tests.common import mock_coro ON = 1 OFF = 0 -async def test_light(hass, config_entry, zha_gateway): +async def test_light(hass, config_entry, zha_gateway, monkeypatch): """Test zha light platform.""" from zigpy.zcl.clusters.general import OnOff, LevelControl, Basic + from zigpy.zcl.foundation import Status from zigpy.profiles.zha import DeviceType # create zigpy devices @@ -52,6 +56,12 @@ async def test_light(hass, config_entry, zha_gateway): # dimmable light level_device_on_off_cluster = zigpy_device_level.endpoints.get(1).on_off level_device_level_cluster = zigpy_device_level.endpoints.get(1).level + on_off_mock = MagicMock(side_effect=asyncio.coroutine(MagicMock( + return_value=(sentinel.data, Status.SUCCESS)))) + level_mock = MagicMock(side_effect=asyncio.coroutine(MagicMock( + return_value=(sentinel.data, Status.SUCCESS)))) + monkeypatch.setattr(level_device_on_off_cluster, 'request', on_off_mock) + monkeypatch.setattr(level_device_level_cluster, 'request', level_mock) level_entity_id = make_entity_id(DOMAIN, zigpy_device_level, level_device_on_off_cluster, use_suffix=False) @@ -81,7 +91,8 @@ async def test_light(hass, config_entry, zha_gateway): hass, on_off_device_on_off_cluster, on_off_entity_id) await async_test_level_on_off_from_hass( - hass, level_device_on_off_cluster, level_entity_id) + hass, level_device_on_off_cluster, level_device_level_cluster, + level_entity_id) # test turning the lights on and off from the light await async_test_on_from_light( @@ -131,7 +142,7 @@ async def async_test_on_off_from_hass(hass, cluster, entity_id): await hass.services.async_call(DOMAIN, 'turn_on', { 'entity_id': entity_id }, blocking=True) - assert len(cluster.request.mock_calls) == 1 + assert cluster.request.call_count == 1 assert cluster.request.call_args == call( False, ON, (), expect_reply=True, manufacturer=None) @@ -148,28 +159,52 @@ async def async_test_off_from_hass(hass, cluster, entity_id): await hass.services.async_call(DOMAIN, 'turn_off', { 'entity_id': entity_id }, blocking=True) - assert len(cluster.request.mock_calls) == 1 + assert cluster.request.call_count == 1 assert cluster.request.call_args == call( False, OFF, (), expect_reply=True, manufacturer=None) -async def async_test_level_on_off_from_hass(hass, cluster, entity_id): +async def async_test_level_on_off_from_hass(hass, on_off_cluster, + level_cluster, entity_id): """Test on off functionality from hass.""" from zigpy import types - from zigpy.zcl.foundation import Status - with patch( - 'zigpy.zcl.Cluster.request', - return_value=mock_coro([Status.SUCCESS, Status.SUCCESS])): - # turn on via UI - await hass.services.async_call(DOMAIN, 'turn_on', { - 'entity_id': entity_id - }, blocking=True) - assert len(cluster.request.mock_calls) == 1 - assert cluster.request.call_args == call( - False, 4, (types.uint8_t, types.uint16_t), 255, 5.0, - expect_reply=True, manufacturer=None) - - await async_test_off_from_hass(hass, cluster, entity_id) + # turn on via UI + await hass.services.async_call(DOMAIN, 'turn_on', {'entity_id': entity_id}, + blocking=True) + assert on_off_cluster.request.call_count == 1 + assert level_cluster.request.call_count == 0 + assert on_off_cluster.request.call_args == call( + False, 1, (), expect_reply=True, manufacturer=None) + on_off_cluster.request.reset_mock() + level_cluster.request.reset_mock() + + await hass.services.async_call(DOMAIN, 'turn_on', + {'entity_id': entity_id, 'transition': 10}, + blocking=True) + assert on_off_cluster.request.call_count == 1 + assert level_cluster.request.call_count == 1 + assert on_off_cluster.request.call_args == call( + False, 1, (), expect_reply=True, manufacturer=None) + assert level_cluster.request.call_args == call( + False, 4, (types.uint8_t, types.uint16_t), 254, 100.0, + expect_reply=True, manufacturer=None) + on_off_cluster.request.reset_mock() + level_cluster.request.reset_mock() + + await hass.services.async_call(DOMAIN, 'turn_on', + {'entity_id': entity_id, 'brightness': 10}, + blocking=True) + assert on_off_cluster.request.call_count == 1 + assert level_cluster.request.call_count == 1 + assert on_off_cluster.request.call_args == call( + False, 1, (), expect_reply=True, manufacturer=None) + assert level_cluster.request.call_args == call( + False, 4, (types.uint8_t, types.uint16_t), 10, 5.0, + expect_reply=True, manufacturer=None) + on_off_cluster.request.reset_mock() + level_cluster.request.reset_mock() + + await async_test_off_from_hass(hass, on_off_cluster, entity_id) async def async_test_dimmer_from_light(hass, cluster, entity_id, diff --git a/tests/components/zwave/test_climate.py b/tests/components/zwave/test_climate.py index 9a9ed41381f7b..b5e5639bdc6d8 100644 --- a/tests/components/zwave/test_climate.py +++ b/tests/components/zwave/test_climate.py @@ -1,7 +1,7 @@ """Test Z-Wave climate devices.""" import pytest -from homeassistant.components.climate import STATE_COOL, STATE_HEAT +from homeassistant.components.climate.const import STATE_COOL, STATE_HEAT from homeassistant.components.zwave import climate from homeassistant.const import ( STATE_OFF, TEMP_CELSIUS, TEMP_FAHRENHEIT, ATTR_TEMPERATURE) diff --git a/tests/helpers/test_device_registry.py b/tests/helpers/test_device_registry.py index 93fffaa4ecc3f..caf1dafdf8fb0 100644 --- a/tests/helpers/test_device_registry.py +++ b/tests/helpers/test_device_registry.py @@ -133,7 +133,8 @@ async def test_loading_from_storage(hass, hass_storage): 'model': 'model', 'name': 'name', 'sw_version': 'version', - 'area_id': '12345A' + 'area_id': '12345A', + 'name_by_user': 'Test Friendly Name' } ] } @@ -148,6 +149,7 @@ async def test_loading_from_storage(hass, hass_storage): manufacturer='manufacturer', model='model') assert entry.id == 'abcdefghijklm' assert entry.area_id == '12345A' + assert entry.name_by_user == 'Test Friendly Name' assert isinstance(entry.config_entries, set) @@ -360,8 +362,11 @@ async def test_update(registry): }) assert not entry.area_id + assert not entry.name_by_user - updated_entry = registry.async_update_device(entry.id, area_id='12345A') + updated_entry = registry.async_update_device( + entry.id, area_id='12345A', name_by_user='Test Friendly Name') assert updated_entry != entry assert updated_entry.area_id == '12345A' + assert updated_entry.name_by_user == 'Test Friendly Name' diff --git a/tests/test_config.py b/tests/test_config.py index 212fc247eb9a8..e860ff53b3d6a 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -5,6 +5,7 @@ import unittest import unittest.mock as mock from collections import OrderedDict +from ipaddress import ip_network import asynctest import pytest @@ -891,12 +892,14 @@ async def test_auth_provider_config_default_trusted_networks(hass): } if hasattr(hass, 'auth'): del hass.auth - await config_util.async_process_ha_core_config(hass, core_config, - has_trusted_networks=True) + await config_util.async_process_ha_core_config( + hass, core_config, trusted_networks=['192.168.0.1']) assert len(hass.auth.auth_providers) == 2 assert hass.auth.auth_providers[0].type == 'homeassistant' assert hass.auth.auth_providers[1].type == 'trusted_networks' + assert hass.auth.auth_providers[1].trusted_networks[0] == ip_network( + '192.168.0.1') async def test_disallowed_auth_provider_config(hass): diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index 496ad7852754e..8991035cc225f 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -6,6 +6,7 @@ import pytest from homeassistant import config_entries, loader, data_entry_flow +from homeassistant.core import callback from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.setup import async_setup_component from homeassistant.util import dt @@ -15,6 +16,14 @@ MockPlatform, MockEntity) +@config_entries.HANDLERS.register('test') +@config_entries.HANDLERS.register('comp') +class MockFlowHandler(config_entries.ConfigFlow): + """Define a mock flow handler.""" + + VERSION = 1 + + @pytest.fixture def manager(hass): """Fixture of a loaded config manager.""" @@ -25,20 +34,128 @@ def manager(hass): return manager -@asyncio.coroutine -def test_call_setup_entry(hass): +async def test_call_setup_entry(hass): """Test we call .setup_entry.""" - MockConfigEntry(domain='comp').add_to_hass(hass) + entry = MockConfigEntry(domain='comp') + entry.add_to_hass(hass) mock_setup_entry = MagicMock(return_value=mock_coro(True)) + mock_migrate_entry = MagicMock(return_value=mock_coro(True)) loader.set_component( hass, 'comp', - MockModule('comp', async_setup_entry=mock_setup_entry)) + MockModule('comp', async_setup_entry=mock_setup_entry, + async_migrate_entry=mock_migrate_entry)) - result = yield from async_setup_component(hass, 'comp', {}) + result = await async_setup_component(hass, 'comp', {}) assert result + assert len(mock_migrate_entry.mock_calls) == 0 assert len(mock_setup_entry.mock_calls) == 1 + assert entry.state == config_entries.ENTRY_STATE_LOADED + + +async def test_call_async_migrate_entry(hass): + """Test we call .async_migrate_entry when version mismatch.""" + entry = MockConfigEntry(domain='comp') + entry.version = 2 + entry.add_to_hass(hass) + + mock_migrate_entry = MagicMock(return_value=mock_coro(True)) + mock_setup_entry = MagicMock(return_value=mock_coro(True)) + + loader.set_component( + hass, 'comp', + MockModule('comp', async_setup_entry=mock_setup_entry, + async_migrate_entry=mock_migrate_entry)) + + result = await async_setup_component(hass, 'comp', {}) + assert result + assert len(mock_migrate_entry.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + assert entry.state == config_entries.ENTRY_STATE_LOADED + + +async def test_call_async_migrate_entry_failure_false(hass): + """Test migration fails if returns false.""" + entry = MockConfigEntry(domain='comp') + entry.version = 2 + entry.add_to_hass(hass) + + mock_migrate_entry = MagicMock(return_value=mock_coro(False)) + mock_setup_entry = MagicMock(return_value=mock_coro(True)) + + loader.set_component( + hass, 'comp', + MockModule('comp', async_setup_entry=mock_setup_entry, + async_migrate_entry=mock_migrate_entry)) + + result = await async_setup_component(hass, 'comp', {}) + assert result + assert len(mock_migrate_entry.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 0 + assert entry.state == config_entries.ENTRY_STATE_MIGRATION_ERROR + + +async def test_call_async_migrate_entry_failure_exception(hass): + """Test migration fails if exception raised.""" + entry = MockConfigEntry(domain='comp') + entry.version = 2 + entry.add_to_hass(hass) + + mock_migrate_entry = MagicMock( + return_value=mock_coro(exception=Exception)) + mock_setup_entry = MagicMock(return_value=mock_coro(True)) + + loader.set_component( + hass, 'comp', + MockModule('comp', async_setup_entry=mock_setup_entry, + async_migrate_entry=mock_migrate_entry)) + + result = await async_setup_component(hass, 'comp', {}) + assert result + assert len(mock_migrate_entry.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 0 + assert entry.state == config_entries.ENTRY_STATE_MIGRATION_ERROR + + +async def test_call_async_migrate_entry_failure_not_bool(hass): + """Test migration fails if boolean not returned.""" + entry = MockConfigEntry(domain='comp') + entry.version = 2 + entry.add_to_hass(hass) + + mock_migrate_entry = MagicMock( + return_value=mock_coro()) + mock_setup_entry = MagicMock(return_value=mock_coro(True)) + + loader.set_component( + hass, 'comp', + MockModule('comp', async_setup_entry=mock_setup_entry, + async_migrate_entry=mock_migrate_entry)) + + result = await async_setup_component(hass, 'comp', {}) + assert result + assert len(mock_migrate_entry.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 0 + assert entry.state == config_entries.ENTRY_STATE_MIGRATION_ERROR + + +async def test_call_async_migrate_entry_failure_not_supported(hass): + """Test migration fails if async_migrate_entry not implemented.""" + entry = MockConfigEntry(domain='comp') + entry.version = 2 + entry.add_to_hass(hass) + + mock_setup_entry = MagicMock(return_value=mock_coro(True)) + + loader.set_component( + hass, 'comp', + MockModule('comp', async_setup_entry=mock_setup_entry)) + + result = await async_setup_component(hass, 'comp', {}) + assert result + assert len(mock_setup_entry.mock_calls) == 0 + assert entry.state == config_entries.ENTRY_STATE_MIGRATION_ERROR async def test_remove_entry(hass, manager): @@ -428,6 +545,31 @@ async def test_updating_entry_data(manager): } +async def test_update_entry_options_and_trigger_listener(hass, manager): + """Test that we can update entry options and trigger listener.""" + entry = MockConfigEntry( + domain='test', + options={'first': True}, + ) + entry.add_to_manager(manager) + + async def update_listener(hass, entry): + """Test function.""" + assert entry.options == { + 'second': True + } + + entry.add_update_listener(update_listener) + + manager.async_update_entry(entry, options={ + 'second': True + }) + + assert entry.options == { + 'second': True + } + + async def test_setup_raise_not_ready(hass, caplog): """Test a setup raising not ready.""" entry = MockConfigEntry(domain='test') @@ -472,3 +614,39 @@ async def test_setup_retrying_during_unload(hass): assert entry.state == config_entries.ENTRY_STATE_NOT_LOADED assert len(mock_call.return_value.mock_calls) == 1 + + +async def test_entry_options(hass, manager): + """Test that we can set options on an entry.""" + entry = MockConfigEntry( + domain='test', + data={'first': True}, + options=None + ) + entry.add_to_manager(manager) + + class TestFlow: + @staticmethod + @callback + def async_get_options_flow(config, options): + class OptionsFlowHandler(data_entry_flow.FlowHandler): + def __init__(self, config, options): + pass + return OptionsFlowHandler(config, options) + + config_entries.HANDLERS['test'] = TestFlow() + flow = await manager.options._async_create_flow( + entry.entry_id, context={'source': 'test'}, data=None) + + flow.handler = entry.entry_id # Used to keep reference to config entry + + await manager.options._async_finish_flow( + flow, {'data': {'second': True}}) + + assert entry.data == { + 'first': True + } + + assert entry.options == { + 'second': True + } diff --git a/tests/test_core.py b/tests/test_core.py index 4acb1de667702..ef9621bdac755 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -726,8 +726,7 @@ def test_async_service(self): """Test registering and calling an async service.""" calls = [] - @asyncio.coroutine - def service_handler(call): + async def service_handler(call): """Service handler coroutine.""" calls.append(call) @@ -803,6 +802,45 @@ def mock_event_remove(event): self.hass.block_till_done() assert len(calls_remove) == 0 + def test_async_service_raise_exception(self): + """Test registering and calling an async service raise exception.""" + async def service_handler(_): + """Service handler coroutine.""" + raise ValueError + + self.services.register( + 'test_domain', 'register_calls', service_handler) + self.hass.block_till_done() + + with pytest.raises(ValueError): + assert self.services.call('test_domain', 'REGISTER_CALLS', + blocking=True) + self.hass.block_till_done() + + # Non-blocking service call never throw exception + self.services.call('test_domain', 'REGISTER_CALLS', blocking=False) + self.hass.block_till_done() + + def test_callback_service_raise_exception(self): + """Test registering and calling an callback service raise exception.""" + @ha.callback + def service_handler(_): + """Service handler coroutine.""" + raise ValueError + + self.services.register( + 'test_domain', 'register_calls', service_handler) + self.hass.block_till_done() + + with pytest.raises(ValueError): + assert self.services.call('test_domain', 'REGISTER_CALLS', + blocking=True) + self.hass.block_till_done() + + # Non-blocking service call never throw exception + self.services.call('test_domain', 'REGISTER_CALLS', blocking=False) + self.hass.block_till_done() + class TestConfig(unittest.TestCase): """Test configuration methods.""" @@ -1028,6 +1066,7 @@ def test_track_task_functions(loop): async def test_service_executed_with_subservices(hass): """Test we block correctly till all services done.""" calls = async_mock_service(hass, 'test', 'inner') + context = ha.Context() async def handle_outer(call): """Handle outer service call.""" @@ -1041,11 +1080,13 @@ async def handle_outer(call): hass.services.async_register('test', 'outer', handle_outer) - await hass.services.async_call('test', 'outer', blocking=True) + await hass.services.async_call('test', 'outer', blocking=True, + context=context) assert len(calls) == 4 assert [call.service for call in calls] == [ 'outer', 'inner', 'inner', 'outer'] + assert all(call.context is context for call in calls) async def test_service_call_event_contains_original_data(hass): @@ -1062,11 +1103,14 @@ def callback(event): 'number': vol.Coerce(int) })) + context = ha.Context() await hass.services.async_call('test', 'service', { 'number': '23' - }, blocking=True) + }, blocking=True, context=context) await hass.async_block_till_done() assert len(events) == 1 assert events[0].data['service_data']['number'] == '23' + assert events[0].context is context assert len(calls) == 1 assert calls[0].data['number'] == 23 + assert calls[0].context is context diff --git a/tests/test_loader.py b/tests/test_loader.py index cceb9839d99c5..09f830a8eab8a 100644 --- a/tests/test_loader.py +++ b/tests/test_loader.py @@ -135,3 +135,10 @@ async def test_get_platform(hass, caplog): legacy_platform = loader.get_platform(hass, 'switch', 'test') assert legacy_platform.__name__ == 'custom_components.switch.test' assert 'Integrations need to be in their own folder.' in caplog.text + + +async def test_get_platform_enforces_component_path(hass, caplog): + """Test that existence of a component limits lookup path of platforms.""" + assert loader.get_platform(hass, 'comp_path_test', 'hue') is None + assert ('Search path was limited to path of component: ' + 'homeassistant.components') in caplog.text diff --git a/tests/testing_config/custom_components/hue/comp_path_test.py b/tests/testing_config/custom_components/hue/comp_path_test.py new file mode 100644 index 0000000000000..3214c58a44d00 --- /dev/null +++ b/tests/testing_config/custom_components/hue/comp_path_test.py @@ -0,0 +1 @@ +"""Custom platform for a built-in component, should not be allowed."""