diff --git a/.coveragerc b/.coveragerc index 8e5b61136c0..bbed9b7e742 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 e58659c876a..57244b44d9a 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 d9ab141d61c..2abfa6f9b6f 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 920e8b57047..be00f989290 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 64263598121..ac8f98a11b0 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 3377bb2a6aa..bb90f296468 100644 --- a/homeassistant/auth/__init__.py +++ b/homeassistant/auth/__init__.py @@ -170,8 +170,7 @@ class AuthManager: 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 3c26f8b4bde..310abff9484 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 @@ class NotifyAuthModule(MultiFactorAuthModule): 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 @@ class NotifyAuthModule(MultiFactorAuthModule): 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 68f4e1d0596..dc51152f565 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 @@ class TotpAuthModule(MultiFactorAuthModule): 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 @@ class TotpAuthModule(MultiFactorAuthModule): 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 b22f93f11f1..2187d272800 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 8a7e1d67c6d..d0bc45c326a 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 @@ class TrustedNetworksAuthProvider(AuthProvider): 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 @@ class TrustedNetworksAuthProvider(AuthProvider): 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 a018d540033..eef36b026e1 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 71a1dcdd590..591bae1a9cf 100644 --- a/homeassistant/components/abode/__init__.py +++ b/homeassistant/components/abode/__init__.py @@ -17,7 +17,8 @@ REQUIREMENTS = ['abodepy==0.15.0'] _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 @@ class AbodeDevice(Entity): 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 @@ class AbodeAutomation(Entity): 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 ec5038a7a84..838d09b73af 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 @@ class AbodeAlarm(AbodeDevice, alarm.AlarmControlPanel): 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 cfd0f37caa0..1b90e645af4 100644 --- a/homeassistant/components/ads/__init__.py +++ b/homeassistant/components/ads/__init__.py @@ -4,9 +4,11 @@ import struct 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 @@ _LOGGER = logging.getLogger(__name__) DATA_ADS = 'data_ads' # Supported Types -ADSTYPE_INT = 'int' -ADSTYPE_UINT = 'uint' -ADSTYPE_BYTE = 'byte' ADSTYPE_BOOL = 'bool' +ADSTYPE_BYTE = 'byte' +ADSTYPE_DINT = 'dint' +ADSTYPE_INT = 'int' +ADSTYPE_UDINT = 'udint' +ADSTYPE_UINT = 'uint' -DOMAIN = 'ads' - +CONF_ADS_FACTOR = 'factor' +CONF_ADS_TYPE = 'adstype' +CONF_ADS_VALUE = 'value' CONF_ADS_VAR = 'adsvar' CONF_ADS_VAR_BRIGHTNESS = 'adsvar_brightness' -CONF_ADS_TYPE = 'adstype' -CONF_ADS_FACTOR = 'factor' -CONF_ADS_VALUE = 'value' + +DOMAIN = 'ads' SERVICE_WRITE_DATA_BY_NAME = 'write_data_by_name' @@ -41,7 +45,8 @@ CONFIG_SCHEMA = vol.Schema({ 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 @@ class AdsHub: 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 @@ class AdsHub: 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 @@ class AdsHub: value = struct.unpack(' 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 d77c0c9bfe2..8462e40be5b 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 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession 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 4e2383bb43d..a856a3d8e82 100644 --- a/homeassistant/components/alexa/smart_home.py +++ b/homeassistant/components/alexa/smart_home.py @@ -11,8 +11,9 @@ import aiohttp 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 @@ from homeassistant.const import ( 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 @@ API_THERMOSTAT_MODES = OrderedDict([ (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 @@ class _AlexaThermostatController(_AlexaInterface): 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 00000000000..1431efbf167 --- /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 00000000000..268a6ba001e --- /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 00000000000..d6732423a7e --- /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 00000000000..f5afbca71c0 --- /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 00000000000..222b512c39f --- /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 00000000000..f87c987a79f --- /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 01078bbddfe..92746b29f3d 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 00000000000..906a6b404c4 --- /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 00000000000..c429d439503 --- /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 16b86a0e298..70f6ce9fbba 100644 --- a/homeassistant/components/ambient_station/__init__.py +++ b/homeassistant/components/ambient_station/__init__.py @@ -20,13 +20,14 @@ from .const import ( 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 @@ class AmbientStation: """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 @@ class AmbientStation: """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 @@ class AmbientStation: 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 7e81836e522..cbb720778e5 100644 --- a/homeassistant/components/arlo/__init__.py +++ b/homeassistant/components/arlo/__init__.py @@ -15,7 +15,7 @@ REQUIREMENTS = ['pyarlo==0.2.3'] _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 8c21a448a23..931dfa1b15d 100644 --- a/homeassistant/components/arlo/alarm_control_panel.py +++ b/homeassistant/components/arlo/alarm_control_panel.py @@ -9,7 +9,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect 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 @@ class ArloBaseStation(AlarmControlPanel): 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 3ad7b70a947..1c3cc933438 100644 --- a/homeassistant/components/arlo/sensor.py +++ b/homeassistant/components/arlo/sensor.py @@ -6,7 +6,7 @@ import voluptuous as vol 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 @@ class ArloSensor(Entity): """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 6caa9d49993..852965596e0 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 0a3a3c58820..a2d132d9073 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 25dad4c1aeb..be06f0209c4 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 edf136bd7f3..5092e079250 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 04b402722b2..cfd683346ff 100644 --- a/homeassistant/components/binary_sensor/aurora.py +++ b/homeassistant/components/binary_sensor/aurora.py @@ -19,8 +19,8 @@ from homeassistant.util import Throttle _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 @@ class AuroraSensor(BinarySensorDevice): 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 57618ca2652..fdefc40d8fd 100644 --- a/homeassistant/components/binary_sensor/hikvision.py +++ b/homeassistant/components/binary_sensor/hikvision.py @@ -18,7 +18,7 @@ from homeassistant.const import ( 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 @@ DEVICE_CLASS_MAP = { '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 ac82ab126fd..304ed701148 100644 --- a/homeassistant/components/binary_sensor/rest.py +++ b/homeassistant/components/binary_sensor/rest.py @@ -14,7 +14,7 @@ from homeassistant.components.binary_sensor import ( 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 @@ _LOGGER = logging.getLogger(__name__) 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 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ 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 5a65917f40b..79fc61a62d4 100644 --- a/homeassistant/components/binary_sensor/ring.py +++ b/homeassistant/components/binary_sensor/ring.py @@ -11,7 +11,7 @@ import voluptuous as vol 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 @@ class RingBinarySensor(BinarySensorDevice): 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 @@ class RingBinarySensor(BinarySensorDevice): 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 00000000000..7dc6e5ebe81 --- /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 494c3154b84..0d4e9631650 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.entity import generate_entity_id 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 dbb83e53e9f..e48ac3039ae 100644 --- a/homeassistant/components/binary_sensor/uptimerobot.py +++ b/homeassistant/components/binary_sensor/uptimerobot.py @@ -19,7 +19,7 @@ _LOGGER = logging.getLogger(__name__) 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 @@ class UptimeRobotBinarySensor(BinarySensorDevice): 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 a42eb34004b..7f924929662 100644 --- a/homeassistant/components/bloomsky/__init__.py +++ b/homeassistant/components/bloomsky/__init__.py @@ -66,7 +66,7 @@ class BloomSky: 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 da1119281b3..ce9ceb7b76f 100644 --- a/homeassistant/components/camera/ring.py +++ b/homeassistant/components/camera/ring.py @@ -13,7 +13,7 @@ import voluptuous as vol 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 @@ SCAN_INTERVAL = timedelta(seconds=90) 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 @@ class RingCam(Camera): 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 e1d3093995c..0283359b1f2 100644 --- a/homeassistant/components/climate/__init__.py +++ b/homeassistant/components/climate/__init__.py @@ -51,6 +51,17 @@ from .const import ( 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 @@ DEFAULT_MAX_HUMIDITY = 99 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 2f84ee27bbd..e213ae09de6 100644 --- a/homeassistant/components/climate/const.py +++ b/homeassistant/components/climate/const.py @@ -20,6 +20,11 @@ ATTR_TARGET_TEMP_HIGH = 'target_temp_high' 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_HUMIDITY = 'set_humidity' 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 32c77b93eea..fd00c9f22c4 100644 --- a/homeassistant/components/climate/coolmaster.py +++ b/homeassistant/components/climate/coolmaster.py @@ -9,10 +9,11 @@ import logging 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 14c22cefbe9..5b4775982a6 100644 --- a/homeassistant/components/climate/demo.py +++ b/homeassistant/components/climate/demo.py @@ -4,8 +4,9 @@ Demo platform that offers a fake climate device. 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 0b09ec7f0b4..09196a82bed 100644 --- a/homeassistant/components/climate/dyson.py +++ b/homeassistant/components/climate/dyson.py @@ -7,8 +7,9 @@ https://home-assistant.io/components/climate.dyson/ 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 cd410cf3be4..9884d81a199 100644 --- a/homeassistant/components/climate/ephember.py +++ b/homeassistant/components/climate/ephember.py @@ -8,12 +8,12 @@ import logging 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 1eaaaa9d34e..c7c5973fb86 100644 --- a/homeassistant/components/climate/eq3btsmart.py +++ b/homeassistant/components/climate/eq3btsmart.py @@ -8,13 +8,14 @@ import logging 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 e0453b8bf90..fe7b5ff8e7c 100644 --- a/homeassistant/components/climate/flexit.py +++ b/homeassistant/components/climate/flexit.py @@ -17,8 +17,9 @@ import voluptuous as vol from homeassistant.const import ( CONF_NAME, CONF_SLAVE, TEMP_CELSIUS, ATTR_TEMPERATURE, DEVICE_DEFAULT_NAME) -from homeassistant.components.climate import ( - ClimateDevice, PLATFORM_SCHEMA, 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 ffab50c989d..da4f79ec1e6 100644 --- a/homeassistant/components/climate/generic_thermostat.py +++ b/homeassistant/components/climate/generic_thermostat.py @@ -11,10 +11,11 @@ import voluptuous as vol 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 a03d1567e01..ff495706be7 100644 --- a/homeassistant/components/climate/heatmiser.py +++ b/homeassistant/components/climate/heatmiser.py @@ -8,8 +8,9 @@ import logging 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 e0f104a84b1..dbcbebff566 100644 --- a/homeassistant/components/climate/honeywell.py +++ b/homeassistant/components/climate/honeywell.py @@ -12,8 +12,9 @@ import requests 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 25beedfe0dd..b9eb28a61d7 100644 --- a/homeassistant/components/climate/melissa.py +++ b/homeassistant/components/climate/melissa.py @@ -6,8 +6,9 @@ https://home-assistant.io/components/climate.melissa/ """ 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 b735927cb80..6867f57ee48 100644 --- a/homeassistant/components/climate/mill.py +++ b/homeassistant/components/climate/mill.py @@ -9,8 +9,9 @@ import logging 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 d0bfe5add58..f52d2c7b501 100644 --- a/homeassistant/components/climate/nuheat.py +++ b/homeassistant/components/climate/nuheat.py @@ -9,8 +9,8 @@ from datetime import timedelta 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 e006242331c..f1e03396b05 100644 --- a/homeassistant/components/climate/oem.py +++ b/homeassistant/components/climate/oem.py @@ -13,11 +13,12 @@ import requests 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 76160a28c6e..c88ece033df 100644 --- a/homeassistant/components/climate/proliphix.py +++ b/homeassistant/components/climate/proliphix.py @@ -6,11 +6,12 @@ https://home-assistant.io/components/climate.proliphix/ """ 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 a72bf711242..bad20884536 100644 --- a/homeassistant/components/climate/radiotherm.py +++ b/homeassistant/components/climate/radiotherm.py @@ -9,12 +9,14 @@ import logging 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 bf1cf5bf345..7850b08fd6b 100644 --- a/homeassistant/components/climate/sensibo.py +++ b/homeassistant/components/climate/sensibo.py @@ -15,8 +15,9 @@ import voluptuous as vol 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 641f6e9a1d8..fa38bd37c8f 100644 --- a/homeassistant/components/climate/touchline.py +++ b/homeassistant/components/climate/touchline.py @@ -8,8 +8,9 @@ import logging 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 16c0b206154..820443ee186 100644 --- a/homeassistant/components/climate/venstar.py +++ b/homeassistant/components/climate/venstar.py @@ -8,14 +8,14 @@ import logging 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 b564e9d1fa4..78cd7d16c48 100644 --- a/homeassistant/components/climate/zhong_hong.py +++ b/homeassistant/components/climate/zhong_hong.py @@ -8,10 +8,11 @@ import logging 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 68890a79ca6..65f65cbcec5 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 @@ class ConfigManagerEntryIndexView(HomeAssistantView): '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 0677531242a..9554f6aeee6 100644 --- a/homeassistant/components/config/device_registry.py +++ b/homeassistant/components/config/device_registry.py @@ -19,6 +19,7 @@ SCHEMA_WS_UPDATE = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({ 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 bd003f1ad67..8b4031f09ed 100644 --- a/homeassistant/components/cover/__init__.py +++ b/homeassistant/components/cover/__init__.py @@ -35,12 +35,27 @@ ENTITY_ID_ALL_COVERS = group.ENTITY_ID_FORMAT.format('all_covers') 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 00000000000..dae3afdba6f --- /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 623fab6828a..cbca935f551 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 00000000000..0b8151d23f6 --- /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 83c42f4280c..0549aa3b160 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 00000000000..0f1247197aa --- /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 d97b506e273..775e4a216e5 100644 --- a/homeassistant/components/daikin/climate.py +++ b/homeassistant/components/daikin/climate.py @@ -4,17 +4,17 @@ import re 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 d6123a25f23..f4a7b92c17c 100644 --- a/homeassistant/components/danfoss_air/__init__.py +++ b/homeassistant/components/danfoss_air/__init__.py @@ -9,11 +9,11 @@ from homeassistant.helpers import discovery 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 @@ class DanfossAir: """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 @@ class DanfossAir: = 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 bf8fe952993..4052a100540 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 @@ class DanfossAirBinarySensor(BinarySensorDevice): @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 2f3807c4999..9902184e624 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 @@ class DanfossAir(Entity): 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 00000000000..ec85757be59 --- /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 ab47a5b43c8..c2298a5fcc2 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 fbb5c26ba04..06211f61bf2 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 87dcd0610f2..c0a23d47be3 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 88cf8742acd..a1157cbfb9c 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 8015324be13..d107cba8f7b 100644 --- a/homeassistant/components/deconz/__init__.py +++ b/homeassistant/components/deconz/__init__.py @@ -12,10 +12,7 @@ from .config_flow import configured_hosts 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 77d01c5c40b..cb68b842f4a 100644 --- a/homeassistant/components/deconz/binary_sensor.py +++ b/homeassistant/components/deconz/binary_sensor.py @@ -5,7 +5,8 @@ from homeassistant.core import callback 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 @@ async def async_setup_entry(hass, config_entry, async_add_entities): 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 00000000000..1f39b8705c7 --- /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 b08f3d71824..bf0799d1fa2 100644 --- a/homeassistant/components/deconz/const.py +++ b/homeassistant/components/deconz/const.py @@ -10,13 +10,27 @@ DEFAULT_PORT = 80 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 48f06a894bb..fda4fe4309c 100644 --- a/homeassistant/components/deconz/cover.py +++ b/homeassistant/components/deconz/cover.py @@ -5,7 +5,8 @@ from homeassistant.components.cover import ( 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 @@ async def async_setup_entry(hass, config_entry, async_add_entities): 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 fe9fc4b7752..829485e1e92 100644 --- a/homeassistant/components/deconz/gateway.py +++ b/homeassistant/components/deconz/gateway.py @@ -8,7 +8,8 @@ from homeassistant.helpers.dispatcher import ( 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 @@ class DeconzGateway: 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 @@ class DeconzGateway: """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 @@ class DeconzEvent: 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 50e22c84d6f..3b63da8d9f8 100644 --- a/homeassistant/components/deconz/light.py +++ b/homeassistant/components/deconz/light.py @@ -9,8 +9,8 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect 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 @@ async def async_setup_entry(hass, config_entry, async_add_entities): 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 @@ async def async_setup_entry(hass, config_entry, async_add_entities): 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 d3a6df810ba..22b4c47f2ab 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 @@ async def async_setup_entry(hass, config_entry, async_add_entities): 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 3083f0c6732..e6b033906e7 100644 --- a/homeassistant/components/deconz/sensor.py +++ b/homeassistant/components/deconz/sensor.py @@ -6,7 +6,8 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect 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 @@ async def async_setup_entry(hass, config_entry, async_add_entities): 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 c48c7205e01..56d37d504cb 100644 --- a/homeassistant/components/deconz/switch.py +++ b/homeassistant/components/deconz/switch.py @@ -3,7 +3,7 @@ from homeassistant.components.switch import SwitchDevice 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 @@ async def async_setup_entry(hass, config_entry, async_add_entities): 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 d56cf9a4ee8..badc403c7c8 100644 --- a/homeassistant/components/default_config/__init__.py +++ b/homeassistant/components/default_config/__init__.py @@ -11,6 +11,7 @@ DEPENDENCIES = ( '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 2b047e92c1e..00adefc6b5c 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 af33453c9d5..1263811aae7 100644 --- a/homeassistant/components/device_tracker/__init__.py +++ b/homeassistant/components/device_tracker/__init__.py @@ -291,7 +291,7 @@ class DeviceTracker: """ 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 @@ class Device(RestoreEntity): 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 c324f3c2757..3de60d6cb38 100644 --- a/homeassistant/components/device_tracker/google_maps.py +++ b/homeassistant/components/device_tracker/google_maps.py @@ -61,8 +61,9 @@ class GoogleMapsScanner: 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 30b09834b68..f60e8edd8c4 100644 --- a/homeassistant/components/device_tracker/luci.py +++ b/homeassistant/components/device_tracker/luci.py @@ -4,21 +4,17 @@ Support for OpenWRT (luci) routers. 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 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -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.debug("Luci get_all_connected_devices returned:" + " %s", result) - _LOGGER.info("Checking ARP") + last_results = [] + for device in result: + last_results.append(device) - 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) + self.last_results = last_results diff --git a/homeassistant/components/device_tracker/synology_srm.py b/homeassistant/components/device_tracker/synology_srm.py index 5c7ac9a5d00..bf5653d681b 100644 --- a/homeassistant/components/device_tracker/synology_srm.py +++ b/homeassistant/components/device_tracker/synology_srm.py @@ -13,7 +13,7 @@ from homeassistant.const import ( 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 b288b8633d1..1447f7c896c 100644 --- a/homeassistant/components/device_tracker/traccar.py +++ b/homeassistant/components/device_tracker/traccar.py @@ -12,14 +12,15 @@ import voluptuous as vol 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_SPEED = 'speed' 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 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ 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 @@ class TraccarScanner: 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 @@ class TraccarScanner: 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 00000000000..f4ecc7d4855 --- /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 94e3b407d13..96f2f60c1e5 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 00000000000..41a66b038f5 --- /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 00000000000..cc1a7ac8510 --- /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 8625780e65c..899f776c095 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 00000000000..07fe5e11217 --- /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 d061dad6726..7975a6eea0d 100644 --- a/homeassistant/components/digital_ocean/__init__.py +++ b/homeassistant/components/digital_ocean/__init__.py @@ -22,7 +22,8 @@ ATTR_MEMORY = 'memory' 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 255f43b67ba..88df56cc629 100644 --- a/homeassistant/components/digital_ocean/binary_sensor.py +++ b/homeassistant/components/digital_ocean/binary_sensor.py @@ -9,7 +9,7 @@ from homeassistant.components.binary_sensor import ( 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 @@ class DigitalOceanBinarySensor(BinarySensorDevice): 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 a10c961b8e4..9b5ddda3408 100644 --- a/homeassistant/components/digital_ocean/switch.py +++ b/homeassistant/components/digital_ocean/switch.py @@ -8,7 +8,7 @@ from homeassistant.components.switch import (SwitchDevice, PLATFORM_SCHEMA) 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 @@ class DigitalOceanSwitch(SwitchDevice): 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 00000000000..7a6291e3f17 --- /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 00000000000..7a6291e3f17 --- /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 00000000000..0232fc3044d --- /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 00000000000..a5ab8f0d194 --- /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 00000000000..92f4355066d --- /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 00000000000..9925fdfab9c --- /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 00000000000..de2ca81f8a8 --- /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 00000000000..92f4355066d --- /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 00000000000..2e7a22e49a3 --- /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 aa6440894e1..bfc67e7cfaf 100644 --- a/homeassistant/components/ecobee/climate.py +++ b/homeassistant/components/ecobee/climate.py @@ -4,15 +4,16 @@ import logging 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 467d542ee6d..72f93b5419c 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 00000000000..f9c8a21240a --- /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 00000000000..51c18c764db --- /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 00000000000..c38e6890d8a --- /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 00000000000..cba89add799 --- /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 286cd58dd89..138e077d4a4 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 00000000000..4ae7a356c4c --- /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 9fe0bb5e9d0..74d2c1d4b9a 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 53331ebc0a9..3a73e54c345 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 00000000000..84000783435 --- /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 8010b330b88..c4b18899eaf 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 7fe5da59de6..1e72bd8030c 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 00000000000..d3c51f0497f --- /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 24f84851254..f58d43f9df9 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 13dddd0beca..a240debfaf5 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 fc74825e188..2b631ea219c 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 00000000000..6eadcb4e18e --- /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 055100cf077..65817470860 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 004162341b1..51f565a0980 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.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, \ 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 @@ from .config_flow import EsphomeFlowHandler # noqa 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 @@ class RuntimeEntryData: 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 @@ class RuntimeEntryData: 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 async_setup_entry(hass: HomeAssistantType, 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 async_setup_entry(hass: HomeAssistantType, 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 e509455c12e..f6b8bb9abd7 100644 --- a/homeassistant/components/esphome/config_flow.py +++ b/homeassistant/components/esphome/config_flow.py @@ -26,18 +26,7 @@ class EsphomeFlowHandler(config_entries.ConfigFlow): 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 @@ class EsphomeFlowHandler(config_entries.ConfigFlow): 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 @@ class EsphomeFlowHandler(config_entries.ConfigFlow): 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 @@ class EsphomeFlowHandler(config_entries.ConfigFlow): 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 56eeed8ea41..8f691d9cb00 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 d5a0938bf66..b0bd9109363 100644 --- a/homeassistant/components/eufy/__init__.py +++ b/homeassistant/components/eufy/__init__.py @@ -9,7 +9,7 @@ from homeassistant.const import ( 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 ef82a3dc81c..955b82e37e3 100644 --- a/homeassistant/components/evohome/climate.py +++ b/homeassistant/components/evohome/climate.py @@ -4,13 +4,13 @@ import logging 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 @@ from homeassistant.const import ( 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 64d99ebf133..e8c20061b4e 100644 --- a/homeassistant/components/fritzbox/climate.py +++ b/homeassistant/components/fritzbox/climate.py @@ -8,13 +8,14 @@ from homeassistant.components.fritzbox import ( 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 caf6bbccb5c..d7c1aabdb49 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -21,7 +21,7 @@ from homeassistant.loader import bind_hass 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 f01abc79e8e..17aae14c820 100644 --- a/homeassistant/components/frontend/storage.py +++ b/homeassistant/components/frontend/storage.py @@ -11,7 +11,7 @@ STORAGE_KEY_USER_DATA = 'frontend.user_data_{}' 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 9095ce617aa..75c99ecc74c 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 0e7274e7a0a..523e125a737 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 cbfe605e722..e89616126d5 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 @@ import voluptuous as vol 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 e0974ed415d..38491feb32f 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 @@ import voluptuous as vol 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 6a7bbba4464..1d11b1971cc 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 @@ import voluptuous as vol 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 00000000000..ad4722fa9fc --- /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 00000000000..637a430a1f8 --- /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 cd14e21db10..a81fc927b6b 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 00000000000..85f71d74434 --- /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 00000000000..1adad3825a3 --- /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 00000000000..bc68c3ec822 --- /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 2460e28393a..6c699d21ce6 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 00000000000..88c9709147f --- /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 f265bd3492a..f27798e9e0d 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 @@ CONF_MOBILE_BEACONS = 'mobile_beacons' 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 @@ WEBHOOK_SCHEMA = vol.Schema({ 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 eea0960ec11..51201240c1c 100644 --- a/homeassistant/components/geofency/device_tracker.py +++ b/homeassistant/components/geofency/device_tracker.py @@ -6,10 +6,10 @@ https://home-assistant.io/components/device_tracker.geofency/ """ 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 c16390302d6..6f0149f657a 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 setup(hass, config): '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 49cb195d6c9..8fba016df57 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 @@ CONFIG_SCHEMA = vol.Schema({ _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 do_authentication(hass, hass_config, config): '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 abb4fd28dd4..cc65c6d655d 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 0d449083f72..49a945cbbfd 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 @@ class GoogleProvider(Provider): ) 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 c0dff15d888..0fd167c2729 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 @@ from .const import ( 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 @@ ENTITY_SCHEMA = vol.Schema({ 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 @@ GOOGLE_ASSISTANT_SCHEMA = vol.Schema({ 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 bfeb0fcadf5..220ed6dd58c 100644 --- a/homeassistant/components/google_assistant/const.py +++ b/homeassistant/components/google_assistant/const.py @@ -42,3 +42,8 @@ ERR_NOT_SUPPORTED = "notSupported" 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 bab63bdb7ae..21316c62085 100644 --- a/homeassistant/components/google_assistant/smart_home.py +++ b/homeassistant/components/google_assistant/smart_home.py @@ -8,7 +8,7 @@ from homeassistant.util.decorator import Registry 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 @@ from .const import ( 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 7153115e3ef..d0368ee0775 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 @@ from homeassistant.components import ( switch, vacuum, ) +from homeassistant.components.climate import const as climate from homeassistant.const import ( ATTR_ENTITY_ID, SERVICE_TURN_OFF, @@ -24,6 +24,7 @@ from homeassistant.const import ( 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 @@ class TemperatureSettingTrait(_Trait): 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 @@ class TemperatureSettingTrait(_Trait): 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 32bdb79557a..f884e46cc4c 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 async_setup(hass, config): 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 af8bb60f8b1..18c068ea454 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 @@ CONFIG_SCHEMA = vol.Schema({ 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 f2d5ad09350..6ebc2f512b1 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 @@ NAME = 'GoogleHome' 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 c4b490ab316..462f5db3b9b 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.components.googlehome import ( 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 90b9cda80bb..7577ee0b017 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.const import DEVICE_CLASS_TIMESTAMP from homeassistant.helpers.entity import Entity import homeassistant.util.dt as dt_util - DEPENDENCIES = ['googlehome'] SCAN_INTERVAL = timedelta(seconds=10) @@ -23,13 +17,13 @@ _LOGGER = logging.getLogger(__name__) 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 00000000000..82c1dfa3e53 --- /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 00000000000..960198eb04e --- /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 00000000000..2d1dcad2174 --- /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 00000000000..aab8edbe44a --- /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 00000000000..4dcfda52753 --- /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 ac9c1c2d43e..366cb1735d5 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 00000000000..3a927a70e61 --- /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 39d795dcd25..2e956bd7fc5 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_BATTERY, default=DEFAULT_BATTERY): vol.Coerce(float), - vol.Optional(ATTR_SPEED): vol.Coerce(float), - vol.Optional(ATTR_DIRECTION): 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_DIRECTION): 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 8a312afa024..90d2dc04f89 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 26cd80d8da2..e3f9e359f5a 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 @@ class GraphiteFeeder(threading.Thread): 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 @@ class GraphiteFeeder(threading.Thread): 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 c1e2f285772..aedc98aac31 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 d1cd88a8438..e0315209ba1 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 8e77c6bf50b..23113a1388b 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 d2f13eb30e6..fb3a5670c2b 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 a3699db08ae..ab78213b53a 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 01d81cc466c..4796744c170 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 @@ import voluptuous as vol 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 @@ CONFIG_SCHEMA = vol.Schema({ 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 9d66338dff0..5eecc24d45e 100644 --- a/homeassistant/components/hangouts/config_flow.py +++ b/homeassistant/components/hangouts/config_flow.py @@ -5,8 +5,7 @@ from homeassistant import config_entries 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 cf5374c317e..ca0fdf986ee 100644 --- a/homeassistant/components/hangouts/const.py +++ b/homeassistant/components/hangouts/const.py @@ -3,8 +3,8 @@ import logging 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 748079452d8..fe72c50de77 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 be52f059139..3887a644700 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 7261663b99f..c3b5450be05 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 25a33929c1a..12ccc78077e 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 489fe9144f2..4ea199bdcd1 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_SYNC_SCHEMA = vol.Schema({ 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 3c058281b0a..e070c889f31 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.auth.const import GROUP_ID_ADMIN 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 4be3ba9956c..b104d53aff9 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 a5f62b9e1a1..804247d2407 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 c33125d840e..640ed29e578 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 6b8004f7664..01ded9ca11d 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 5fb2e19edcf..8eb13c5ab21 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 @@ ICONS_BY_TYPE = { 1: ICON_RECORDER, 3: ICON_TUNER, 4: ICON_PLAYER, - 5: ICON_AUDIO + 5: ICON_AUDIO, } CEC_DEVICES = defaultdict(list) @@ -87,7 +81,7 @@ SERVICE_SEND_COMMAND_SCHEMA = vol.Schema({ 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 6e691cad94f..553983a1f03 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 @@ ENTITY_ID_FORMAT = DOMAIN + '.{}' 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 1016e91d8d2..ff423890ba5 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 1773a55b3f1..7b07fac19a6 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 @@ CONFIG_SCHEMA = vol.Schema({ }) }, 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 7d9db379705..893f3514d77 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 @@ GRAPH_SCHEMA = vol.Schema({ 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 09319849933..934c44028ac 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 @@ DEVICETYPES = { '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 e114e67f90f..dee27c5c710 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 @@ class HiveBinarySensorEntity(BinarySensorDevice): @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 87d426d6f05..45829cda087 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 @@ class HiveClimateEntity(ClimateDevice): 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 c2bb95f40da..2bec60f0ee4 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 @@ class HiveDeviceLight(Light): 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 e989074fb4b..142c8c7ee94 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 @@ class HiveSensorEntity(Entity): 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 a50323f0a4e..c897e37f34b 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 @@ class HiveDevicePlug(SwitchDevice): 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 cfbb8ac010c..aab3f79b8b2 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 d76528c56f0..b1bfc5ce23d 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 c34b527252f..01979f03b9a 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 @@ from homeassistant.const import ( 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 ca1b560e336..2738fafbfdb 100644 --- a/homeassistant/components/homekit/accessories.py +++ b/homeassistant/components/homekit/accessories.py @@ -9,10 +9,9 @@ from pyhap.accessory_driver import AccessoryDriver 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 @@ from .const import ( 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 b3beb11c8b6..5273480b6ce 100644 --- a/homeassistant/components/homekit/type_covers.py +++ b/homeassistant/components/homekit/type_covers.py @@ -11,11 +11,11 @@ from homeassistant.const import ( 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 dcc93b7cf9e..d2777a296dc 100644 --- a/homeassistant/components/homekit/type_fans.py +++ b/homeassistant/components/homekit/type_fans.py @@ -13,7 +13,7 @@ from homeassistant.const import ( 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 a9007ace35b..f549958f755 100644 --- a/homeassistant/components/homekit/type_lights.py +++ b/homeassistant/components/homekit/type_lights.py @@ -5,17 +5,17 @@ from pyhap.const import CATEGORY_LIGHTBULB 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 22c47d59c62..4ed1cebd207 100644 --- a/homeassistant/components/homekit/type_locks.py +++ b/homeassistant/components/homekit/type_locks.py @@ -13,13 +13,19 @@ from .const import CHAR_LOCK_CURRENT_STATE, CHAR_LOCK_TARGET_STATE, SERV_LOCK _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 @@ class Lock(HomeAccessory): 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 @@ class Lock(HomeAccessory): 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 09088871fd2..f8f4ef96992 100644 --- a/homeassistant/components/homekit/type_media_players.py +++ b/homeassistant/components/homekit/type_media_players.py @@ -3,12 +3,12 @@ import logging 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 @@ from .const import ( _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 e210217df2f..10befb4af61 100644 --- a/homeassistant/components/homekit/type_security_systems.py +++ b/homeassistant/components/homekit/type_security_systems.py @@ -5,10 +5,10 @@ from pyhap.const import CATEGORY_ALARM_SYSTEM 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 @@ from .const import ( _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 09da361ddb8..0d7dd94d014 100644 --- a/homeassistant/components/homekit/type_sensors.py +++ b/homeassistant/components/homekit/type_sensors.py @@ -4,7 +4,7 @@ import logging 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 @@ from .const import ( 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 @@ BINARY_SENSOR_SERVICE_MAP = { 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 b41e1a01543..7629e33a4d7 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 f78a05b1a45..85cf7938fbd 100644 --- a/homeassistant/components/homekit/type_thermostats.py +++ b/homeassistant/components/homekit/type_thermostats.py @@ -3,32 +3,32 @@ import logging 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 f1327f8b527..2ba5819a202 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 77d0825ef0b..eb748a3d883 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_ACCESSORY_DISPATCH = { HOMEKIT_IGNORE = [ 'BSB002', 'Home Assistant Bridge', - 'TRADFRI gateway' + 'TRADFRI gateway', ] KNOWN_ACCESSORIES = "{}-accessories".format(DOMAIN) @@ -348,9 +343,17 @@ def setup(hass, config): # 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 3a2e5170453..5d366b6e27b 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 @@ CURRENT_STATE_MAP = { 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 531297dc911..5d83ce6d984 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 15378e2b046..ceadcd46b9d 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 c8f087254bb..3951cf577d4 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 74ef8948f45..f39e793c184 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 e27ed444528..635d457198a 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 @@ CURRENT_STATE_MAP = { 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 ba4a04022f0..daa4ede6898 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 3439a23adb3..dba4add216d 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 @@ import voluptuous as vol 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_IGNORE_DISCOVERY_NODE_EXCEPTIONS = { 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 9cfe4bbd6a7..1704411c9cc 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 5233501ec30..e5eb292b4ff 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 93574321203..79a1afe9a0e 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 de11c96f8b7..21b875742c4 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 9d9f2a28b4f..5d857617fde 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 2897123c690..e6ef1a60e28 100644 --- a/homeassistant/components/homematic/notify.py +++ b/homeassistant/components/homematic/notify.py @@ -8,12 +8,12 @@ import logging import voluptuous as vol -from homeassistant.components.notify import ( - BaseNotificationService, PLATFORM_SCHEMA, ATTR_DATA) -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) + ATTR_ADDRESS, ATTR_CHANNEL, ATTR_INTERFACE, ATTR_PARAM, ATTR_VALUE, DOMAIN, + SERVICE_SET_DEVICE_VALUE) +from homeassistant.components.notify import ( + ATTR_DATA, PLATFORM_SCHEMA, BaseNotificationService) +import homeassistant.helpers.config_validation as cv 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 84cf19652a1..c4d97dca3fe 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 @@ _LOGGER = logging.getLogger(__name__) 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 b5921819ea4..cfcd26891e0 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 8675d6e12b1..5102b25aaee 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 cfb4f5e87fd..61ff5ac5fe2 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 9ef1abd500c..6e6d7c8a59f 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 da6bde77ae3..f155e8fd1c1 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 f048a50d1d0..fd07356d7fb 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 3fdfc768c52..efa1ea1f46e 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 @@ class HomematicipSecurityZone(HomematicipGenericDevice, AlarmControlPanel): 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 910666f93cb..4b82a500bde 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 @@ class HomematicipWaterDetector(HomematicipGenericDevice, BinarySensorDevice): 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 966cd95ade1..08c88bbb796 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 ea251a3bf87..458186bcce1 100644 --- a/homeassistant/components/homematicip_cloud/config_flow.py +++ b/homeassistant/components/homematicip_cloud/config_flow.py @@ -4,9 +4,9 @@ import voluptuous as vol 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 80fc8f7b430..86c11dab70d 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 c43f0e24e2b..85cc3c0c77a 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 @@ class HomematicipGenericDevice(Entity): 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 9af6669652d..64721c0a96c 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 @@ class HomematicipAuth: 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 5d604d2c665..73c607683ba 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 911c00e45bc..d755735e0e0 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 @@ class HomematicipAccesspointStatus(HomematicipGenericDevice): """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 a1b3e1789bf..f129febb5e7 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 @@ class HomematicipSwitch(HomematicipGenericDevice, SwitchDevice): 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 @@ class HomematicipSwitchMeasuring(HomematicipSwitch): 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 b0510cfe9b5..d0769ed25e6 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 @@ CV_FADE_RATE = vol.All(vol.Coerce(float), vol.Range(min=0, max=20)) 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 @@ CONFIG_SCHEMA = vol.Schema({ 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 3ba5f805c52..7f5d7f6aab7 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 02b9affefd4..4928ae2ab17 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 @@ from homeassistant.const import ( 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_SERVER_HOST = '0.0.0.0' 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 @@ HTTP_SCHEMA = vol.Schema({ 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 @@ class HomeAssistantHTTP: 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 a515fcd198e..312fc2164c3 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 import hdrs 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 0d748c91c66..92c41157a33 100644 --- a/homeassistant/components/http/ban.py +++ b/homeassistant/components/http/ban.py @@ -9,11 +9,12 @@ from aiohttp.web import middleware 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_BAN = 'ip-ban' 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 @@ def setup_bans(hass, app, login_threshold): 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 5698c6048e3..6da3b0e51d7 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 8fc7cd8e658..98686e5cabd 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 27a8550ab8c..9bbf30bd9d1 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 54e72c88ff3..4fac9bf1ae9 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 @@ class CachingStaticResource(StaticResource): 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 beb5c647266..9662f3e6c23 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.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.components.http.ban import process_success_login +from homeassistant.const import CONTENT_TYPE_JSON +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 9d223df3344..2ff21c4d5a7 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.const import ( 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 d30a413898f..69bf42fb3fe 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.components.device_tracker import ( 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 a406a7ec2d8..5e20a774c25 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 @@ import homeassistant.helpers.config_validation as cv 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 ae376045544..42ad4b52f8d 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 @@ import homeassistant.helpers.config_validation as cv 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 efbcfa544f5..a7ffc7bacb2 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 0871d961a93..8f5c27f6516 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 @@ from .bridge import HueBridge # 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 6e3d818db68..9df5b0a6730 100644 --- a/homeassistant/components/hue/bridge.py +++ b/homeassistant/components/hue/bridge.py @@ -46,7 +46,7 @@ class HueBridge: 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 51e50f629b5..0725c86bd95 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 @@ SUPPORT_HUE = { '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 @@ GAMUT_TYPE_UNAVAILABLE = 'None' 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 @@ class HueLight(Light): _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 800d19d7efe..9c7baf6db2e 100644 --- a/homeassistant/components/hydrawise/__init__.py +++ b/homeassistant/components/hydrawise/__init__.py @@ -20,7 +20,8 @@ _LOGGER = logging.getLogger(__name__) 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 @@ class HydrawiseEntity(Entity): 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 8ec6f49b95d..3de7aa7cc8c 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 00000000000..46096bbe631 --- /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 ac81f073347..e5dc76b7923 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 dc846993e2e..4184d2dfadc 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 7dee93b2260..0a06947b00f 100644 --- a/homeassistant/components/ifttt/__init__.py +++ b/homeassistant/components/ifttt/__init__.py @@ -5,9 +5,9 @@ import logging 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 bbb9a02c8a1..98a176b1e82 100644 --- a/homeassistant/components/ifttt/alarm_control_panel.py +++ b/homeassistant/components/ifttt/alarm_control_panel.py @@ -10,9 +10,9 @@ from homeassistant.components.alarm_control_panel import ( 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 823f9d2657d..bd45a52944c 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 @@ from homeassistant.helpers import discovery 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 fb5b4c0bfc2..7e3371a834c 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 f80c9b2fd6f..2590ea83222 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 f5a45599bb7..930ac221629 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 e217d109cbc..bbab9d3e68c 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 8ca6e4d8a53..7cb5184b116 100644 --- a/homeassistant/components/image_processing/opencv.py +++ b/homeassistant/components/image_processing/opencv.py @@ -16,7 +16,7 @@ from homeassistant.components.image_processing import ( 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 cc25756f2d0..f0e8f5182fc 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.core import split_entity_id 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 e82e47dc5f4..a462ac0f63e 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_PASSWORD = 'password' 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 5b0a291e92b..06eddb9a004 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 @@ class InsteonBinarySensor(InsteonEntity, BinarySensorDevice): """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 f0cf93c13e9..7de2e872489 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 @@ DEPENDENCIES = ['insteon'] 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 604063a9aa3..2b6097a4ba2 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 - -DEPENDENCIES = ['insteon'] - -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] +from homeassistant.const import STATE_OFF _LOGGER = logging.getLogger(__name__) +DEPENDENCIES = ['insteon'] -async def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +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] + + +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 4829ce631a6..e8ffc226716 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 @@ DEPENDENCIES = ['insteon'] 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 7854967395b..d895d972027 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 @@ DEPENDENCIES = ['insteon'] _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 454b3ef39cb..2a6b97a39d1 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 @@ DEPENDENCIES = ['insteon'] _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 00000000000..38a12e7411a --- /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 3f587b7ee64..c2c5042e295 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 0b1282b605a..737216af5c9 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 e6a37d707ad..1f8aade4ec1 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 @@ class iOSNotificationService(BaseNotificationService): 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 d206cd1df87..404b313368c 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 00000000000..01ac2194f35 --- /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 00000000000..59813ae0455 --- /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 00000000000..c333d7c74c8 --- /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 00000000000..acb8b51a44c --- /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 00000000000..c364ca286e3 --- /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 00000000000..4931fcaf94c --- /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 00000000000..62ddd85e6ef --- /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 00000000000..d751d8a317f --- /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 00000000000..1d5aa9c40cf --- /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 00000000000..da6a1dac859 --- /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 00000000000..4bdba6f0d08 --- /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 00000000000..bb294cc5d21 --- /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 87f62371b55..9bb54a1a019 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 @@ DEFAULT_NAME = 'ipma' 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 dbb19294541..bdd97c74e6a 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 ec9b6fec2e8..7122957ad12 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 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -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 2b5f8fcb13f..4eaa71deece 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 @@ CONFIG_SCHEMA = vol.Schema({ 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 b5d676f233f..013b99fbb15 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 4ead61e6b7a..22ea1629794 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 314200ba1c4..142eaedd66b 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 d54aa3cd4ce..cc39a6d1a3b 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 9481e619a61..a2e8b1a1e56 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 eca7e88a17e..60212d081de 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 6bb9c07de5b..96f17c80bef 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 ca7037fe81d..c84e5820f04 100644 --- a/homeassistant/components/knx/binary_sensor.py +++ b/homeassistant/components/knx/binary_sensor.py @@ -5,12 +5,10 @@ from homeassistant.components.binary_sensor import ( 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 @@ AUTOMATION_SCHEMA = vol.Schema({ 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 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -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 82eaa52ae5a..96b9f2ea91f 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 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ 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 f2a6f15e08b..baba7edd21a 100644 --- a/homeassistant/components/knx/light.py +++ b/homeassistant/components/knx/light.py @@ -3,19 +3,15 @@ from enum import Enum 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 2488114aa41..1e1d7f185f0 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 008e81508b9..b1bb2bf3109 100644 --- a/homeassistant/components/knx/scene.py +++ b/homeassistant/components/knx/scene.py @@ -3,11 +3,10 @@ import voluptuous as vol 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 6a2d8144b1e..abbb61e150d 100644 --- a/homeassistant/components/knx/sensor.py +++ b/homeassistant/components/knx/sensor.py @@ -3,14 +3,11 @@ import voluptuous as vol 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 305234e1eec..cef14fb74dc 100644 --- a/homeassistant/components/knx/switch.py +++ b/homeassistant/components/knx/switch.py @@ -3,11 +3,10 @@ import voluptuous as vol 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 941160b6397..c7c180737f0 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 @@ CONFIG_SCHEMA = vol.Schema({ 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 00000000000..02b35b06797 --- /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 00000000000..4b4542fd623 --- /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 2b7f4ed4074..5f1008cbd57 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 60eda2ea779..09f35d26718 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 00000000000..905ec3ce2bf --- /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 00000000000..b4f940bc66b --- /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 816f93b5881..93d7a67c6f0 100644 --- a/homeassistant/components/light/__init__.py +++ b/homeassistant/components/light/__init__.py @@ -440,6 +440,9 @@ class Light(ToggleEntity): 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 @@ class Light(ToggleEntity): 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 a3fe0f6b71e..b0b9ef1b763 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_LEDS = 'leds' CONF_DRIVER = 'driver' CONF_PINS = 'pins' CONF_FREQUENCY = 'frequency' -CONF_ADDRESS = 'address' CONF_DRIVER_GPIO = 'gpio' CONF_DRIVER_PCA9685 = 'pca9685' @@ -46,11 +40,11 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ { 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 9836bf97f90..10cbeb42aa4 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 231821ffc13..b4b540f729b 100644 --- a/homeassistant/components/light/yeelight.py +++ b/homeassistant/components/light/yeelight.py @@ -46,8 +46,13 @@ DATA_KEY = 'light.yeelight' 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_SERVICE_SCHEMA = vol.Schema({ 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 @@ class YeelightLight(Light): 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 00000000000..14e0523fcf6 --- /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 00000000000..8fb63ff18c7 --- /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 00000000000..e90910c29a2 --- /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 00000000000..de62d2ac2f7 --- /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 00000000000..2104ad90607 --- /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 ff07393da04..70f08595f3a 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 00000000000..0296d079938 --- /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 74a90f0f5f0..dbedc8c6d70 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 50500f47e42..ef006ef8b4d 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 51bd7c124a3..4f349dd986e 100644 --- a/homeassistant/components/logi_circle/camera.py +++ b/homeassistant/components/logi_circle/camera.py @@ -7,7 +7,7 @@ import voluptuous as vol 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 @@ class LogiCam(Camera): 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 74c2039c120..4830219091c 100644 --- a/homeassistant/components/logi_circle/sensor.py +++ b/homeassistant/components/logi_circle/sensor.py @@ -5,7 +5,7 @@ import voluptuous as vol 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 @@ class LogiSensor(Entity): 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 00000000000..8e81e9e52a1 --- /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 48914a94465..b8b2b1fc0d8 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 00000000000..27951378295 --- /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 00000000000..01fd9ec721b --- /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 e4ebec4cc5a..fae44d3584d 100644 --- a/homeassistant/components/lutron/__init__.py +++ b/homeassistant/components/lutron/__init__.py @@ -134,19 +134,18 @@ class LutronButton: """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 00000000000..fd0c543241b --- /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 00000000000..4dea652aa3f --- /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 b1828ee28ef..39503154b6c 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 00000000000..f26234e84cf --- /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 f5c4533123f..170a3ba349c 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 efc3e8bddc8..d0b3c9e3c00 100644 --- a/homeassistant/components/media_extractor/__init__.py +++ b/homeassistant/components/media_extractor/__init__.py @@ -12,7 +12,7 @@ from homeassistant.const import ( 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 d48f90d2bd7..f867a10ccd0 100644 --- a/homeassistant/components/media_player/anthemav.py +++ b/homeassistant/components/media_player/anthemav.py @@ -18,7 +18,7 @@ from homeassistant.const import ( 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 c6a8c51ca58..b25916c7f66 100644 --- a/homeassistant/components/media_player/bluesound.py +++ b/homeassistant/components/media_player/bluesound.py @@ -345,9 +345,8 @@ class BluesoundPlayer(MediaPlayerDevice): 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 58f1913b9f9..fb7df736e51 100644 --- a/homeassistant/components/media_player/firetv.py +++ b/homeassistant/components/media_player/firetv.py @@ -6,7 +6,6 @@ https://home-assistant.io/components/media_player.firetv/ """ import functools import logging -import threading import voluptuous as vol from homeassistant.components.media_player import ( @@ -20,22 +19,22 @@ from homeassistant.const import ( 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 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ 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 None - return returns + return _adb_exception_catcher - return _adb_wrapper - - 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 @@ class FireTVDevice(MediaPlayerDevice): @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 e5ce22e9524..e6546f7c1e2 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 @@ class PanasonicVieraTVDevice(MediaPlayerDevice): self._playing = True self._state = None self._remote = remote + self._host = host self._volume = 0 @property @@ -140,7 +141,7 @@ class PanasonicVieraTVDevice(MediaPlayerDevice): 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 4f8a1339781..97ec758e6cf 100644 --- a/homeassistant/components/media_player/philips_js.py +++ b/homeassistant/components/media_player/philips_js.py @@ -19,13 +19,12 @@ from homeassistant.const import ( 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 @@ class PhilipsTV(MediaPlayerDevice): 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 @@ class PhilipsTV(MediaPlayerDevice): """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 @@ class PhilipsTV(MediaPlayerDevice): 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 @@ class PhilipsTV(MediaPlayerDevice): 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 395f5bb369e..af3fdd1e15a 100644 --- a/homeassistant/components/media_player/vizio.py +++ b/homeassistant/components/media_player/vizio.py @@ -92,25 +92,31 @@ class VizioDevice(MediaPlayerDevice): 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 00000000000..e084cff3c79 --- /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 00000000000..f0ef926793e --- /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 00000000000..849c9d9da10 --- /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 00000000000..19a81b4aa45 --- /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 f42423bf9a8..182e3dc28fa 100644 --- a/homeassistant/components/modbus/__init__.py +++ b/homeassistant/components/modbus/__init__.py @@ -4,27 +4,34 @@ import threading 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 @@ CONFIG_SCHEMA = vol.Schema({ 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 setup(hass, config): 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 @@ class ModbusHub: """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 38511ffed7e..4e0ab74445d 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.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 +from homeassistant.const import CONF_NAME, CONF_SLAVE +from homeassistant.helpers import config_validation as cv _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 ed8cbda863f..44daedac9c1 100644 --- a/homeassistant/components/modbus/climate.py +++ b/homeassistant/components/modbus/climate.py @@ -4,19 +4,15 @@ import struct 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 @@ CONF_PRECISION = 'precision' 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 @@ class ModbusThermostat(ClimateDevice): 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 @@ class ModbusThermostat(ClimateDevice): @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 @@ class ModbusThermostat(ClimateDevice): 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 @@ class ModbusThermostat(ClimateDevice): 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 6ba8d92d155..3f8c68b25ff 100644 --- a/homeassistant/components/modbus/sensor.py +++ b/homeassistant/components/modbus/sensor.py @@ -6,50 +6,50 @@ import voluptuous as vol from homeassistant.components.modbus import ( CONF_HUB, DEFAULT_HUB, DOMAIN as MODBUS_DOMAIN) -from homeassistant.const import ( - CONF_NAME, CONF_OFFSET, CONF_UNIT_OF_MEASUREMENT, CONF_SLAVE, - CONF_STRUCTURE) -from homeassistant.helpers.restore_state import RestoreEntity -from homeassistant.helpers import config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import ( + CONF_NAME, CONF_OFFSET, CONF_SLAVE, CONF_STRUCTURE, + CONF_UNIT_OF_MEASUREMENT) +from homeassistant.helpers import config_validation as cv +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_REGISTERS = 'registers' -CONF_SCALE = 'scale' -CONF_DATA_TYPE = 'data_type' CONF_REGISTER_TYPE = 'register_type' +CONF_REGISTERS = 'registers' +CONF_REVERSE_ORDER = 'reverse_order' +CONF_SCALE = 'scale' + +DATA_TYPE_CUSTOM = 'custom' +DATA_TYPE_FLOAT = 'float' +DATA_TYPE_INT = 'int' +DATA_TYPE_UINT = 'uint' + +DEPENDENCIES = ['modbus'] REGISTER_TYPE_HOLDING = 'holding' REGISTER_TYPE_INPUT = 'input' -DATA_TYPE_INT = 'int' -DATA_TYPE_UINT = 'uint' -DATA_TYPE_FLOAT = 'float' -DATA_TYPE_CUSTOM = 'custom' - 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_OFFSET, default=0): vol.Coerce(float), - vol.Optional(CONF_PRECISION, default=0): cv.positive_int, - 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_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_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 @@ class ModbusRegisterSensor(RestoreEntity): """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 47ad8e98958..b7039a01da3 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_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.Required(CONF_COMMAND_ON): cv.positive_int, + vol.Required(CONF_NAME): cv.string, + vol.Required(CONF_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 e9e869ae966..4f54e11a112 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 f08c601633e..26361b0e363 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 e56860cd675..ed33b182a96 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 663d79f3c14..ad3a90383b1 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 c028ca5a6f6..7be47185322 100644 --- a/homeassistant/components/mqtt/climate.py +++ b/homeassistant/components/mqtt/climate.py @@ -10,11 +10,13 @@ import voluptuous as vol 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 @@ TEMPLATE_KEYS = ( 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 20d608e1ca5..f8c52f65cda 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 2b4af3e1e91..bb717b8d230 100644 --- a/homeassistant/components/neato/__init__.py +++ b/homeassistant/components/neato/__init__.py @@ -18,6 +18,7 @@ DOMAIN = 'neato' 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 @@ class NeatoHub: 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 @@ class NeatoHub: _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 45cfd273aca..ff78a087de8 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 @@ DEPENDENCIES = ['neato'] 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_BATTERY_END = 'battery_level_at_clean_end' ATTR_CLEAN_SUSP_COUNT = 'clean_suspension_count' ATTR_CLEAN_SUSP_TIME = 'clean_suspension_time' +ATTR_MODE = 'mode' +ATTR_NAVIGATION = 'navigation' +ATTR_CATEGORY = 'category' +ATTR_ZONE = 'zone' + +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 @@ class NeatoConnectedVacuum(StateVacuumDevice): 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 @@ class NeatoConnectedVacuum(StateVacuumDevice): ['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 @@ class NeatoConnectedVacuum(StateVacuumDevice): 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 dae244ece3f..c7175a0c3c7 100644 --- a/homeassistant/components/ness_alarm/__init__.py +++ b/homeassistant/components/ness_alarm/__init__.py @@ -10,7 +10,7 @@ from homeassistant.helpers import config_validation as cv 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 0dfb5283d8f..117a4500d58 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 e24c38f8608..dc26862f5ea 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 e4a19ebd521..b55c6d00683 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 54ff1dff999..ff86c34ac71 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 fe6a34cf404..21aaa2109a1 100644 --- a/homeassistant/components/nest/__init__.py +++ b/homeassistant/components/nest/__init__.py @@ -7,7 +7,7 @@ import threading 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 8746a1959ae..88b6cbbbeb0 100644 --- a/homeassistant/components/nest/climate.py +++ b/homeassistant/components/nest/climate.py @@ -5,14 +5,15 @@ import voluptuous as vol 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 393a36e4a9c..5cb63956aea 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 10fa83d23e0..bde3f681c2b 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 495e22aae24..2e580627543 100644 --- a/homeassistant/components/netatmo/__init__.py +++ b/homeassistant/components/netatmo/__init__.py @@ -6,21 +6,60 @@ from urllib.error import HTTPError 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 @@ CONFIG_SCHEMA = vol.Schema({ 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 @@ class CameraData: 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 727ed0a68c7..7986010ef64 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 a3a5461631d..57d30d6cbc9 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 2b9bcbebaf2..1e16f2d3e05 100644 --- a/homeassistant/components/netatmo/climate.py +++ b/homeassistant/components/netatmo/climate.py @@ -4,8 +4,9 @@ from datetime import timedelta 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 78a118528b9..307b76ca434 100644 --- a/homeassistant/components/netatmo/sensor.py +++ b/homeassistant/components/netatmo/sensor.py @@ -47,6 +47,7 @@ SENSOR_TYPES = { '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 @@ MODULE_TYPE_INDOOR = 'NAModule4' 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 - add_entities(dev, True) + +def all_product_classes(): + """Provide all handled Netatmo product classes.""" + import pyatmo + + return [pyatmo.WeatherStationData, pyatmo.HomeCoachData] class NetAtmoSensor(Entity): @@ -151,6 +189,13 @@ class NetAtmoSensor(Entity): 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 @@ class NetAtmoSensor(Entity): 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 @@ class NetAtmoSensor(Entity): 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 @@ class NetAtmoData: 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 @@ class NetAtmoData: 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 @@ class NetAtmoData: 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 00000000000..7bb990caf97 --- /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 00000000000..cb101c0a530 --- /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 00000000000..2397405ec20 --- /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 00000000000..1ca7fceb911 --- /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 00000000000..f6206f1f4ef --- /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 00000000000..ef60dfb4a65 --- /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 00000000000..60b9a6630cd --- /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 74bfe61d3f2..50d2246cd29 100644 --- a/homeassistant/components/notify/kodi.py +++ b/homeassistant/components/notify/kodi.py @@ -90,7 +90,7 @@ class KodiNotificationService(BaseNotificationService): 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 faf5e90e016..f99d97574b4 100644 --- a/homeassistant/components/notify/nfandroidtv.py +++ b/homeassistant/components/notify/nfandroidtv.py @@ -248,7 +248,7 @@ class NFAndroidTVNotificationService(BaseNotificationService): 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 443d56521c1..94dc08a8113 100644 --- a/homeassistant/components/notify/pushsafer.py +++ b/homeassistant/components/notify/pushsafer.py @@ -149,8 +149,7 @@ class PushsaferNotificationService(BaseNotificationService): 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 8e23c9f4fa0..961f671203f 100644 --- a/homeassistant/components/notify/slack.py +++ b/homeassistant/components/notify/slack.py @@ -152,7 +152,7 @@ class SlackNotificationService(BaseNotificationService): 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 ff6acc1a884..584be4c0c64 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 6b391c20a0a..332a21f99f5 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 a18d36693d5..82dfd63184a 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 00000000000..b7ad7ab9152 --- /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 00000000000..cb66278150a --- /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 00000000000..f8d4db3ec1e --- /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 00000000000..b91cc387718 --- /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 00000000000..f56cff977d0 --- /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 00000000000..f866aa6e403 --- /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 00000000000..9b66b693c33 --- /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 bb9c7f39c5b..6ebaa31cacf 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 00000000000..2077cceeb4d --- /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 cc918dcf674..c0d3d152270 100644 --- a/homeassistant/components/owntracks/__init__.py +++ b/homeassistant/components/owntracks/__init__.py @@ -16,7 +16,7 @@ from homeassistant.setup import async_when_setup 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 6818efbbf75..59e8c4825df 100644 --- a/homeassistant/components/owntracks/config_flow.py +++ b/homeassistant/components/owntracks/config_flow.py @@ -9,7 +9,7 @@ CONF_SECRET = 'secret' 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 e85ebbe6fe1..be8698a47b1 100644 --- a/homeassistant/components/owntracks/device_tracker.py +++ b/homeassistant/components/owntracks/device_tracker.py @@ -4,7 +4,6 @@ Device tracker platform that adds support for OwnTracks over MQTT. 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 f6602169eb2..2fce5d9857c 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 f2bca91205c..e6f83b80ba4 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 @@ class Person(RestoreEntity): 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 @@ class Person(RestoreEntity): 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 @@ class Person(RestoreEntity): @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 @@ class Person(RestoreEntity): self._source = None self._latitude = None self._longitude = None + self._gps_accuracy = None self.async_schedule_update_ha_state() @@ -406,6 +423,7 @@ class Person(RestoreEntity): 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 6a66735e6d0..b50a1169a53 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 00000000000..c20e3350272 --- /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 00000000000..815f8fbf9af --- /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 2d52069d5ba..3192454550d 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 00e2cb02358..324801009ca 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 7bcf275f96e..60c1d62ab91 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 6464434eda4..c68fd29f7fc 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 f223ded998f..dc839756469 100644 --- a/homeassistant/components/point/__init__.py +++ b/homeassistant/components/point/__init__.py @@ -20,7 +20,7 @@ from .const import ( 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 @@ class MinutPointClient(): 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 @@ class MinutPointClient(): 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 @@ class MinutPointClient(): """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 @@ class MinutPointEntity(Entity): '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 00000000000..a50dffe42b9 --- /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 4508611e51b..9053a872134 100644 --- a/homeassistant/components/prometheus/__init__.py +++ b/homeassistant/components/prometheus/__init__.py @@ -5,7 +5,7 @@ from aiohttp import web 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 @@ class PrometheusMetrics: 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 00000000000..350b65ca815 --- /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 00000000000..5c4e67a324c --- /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 00000000000..7c5f9e7621c --- /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 00000000000..8f4e8838673 --- /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 00000000000..c0b476ff4e2 --- /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 00000000000..093ee552951 --- /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 00000000000..41cbd28492a --- /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 00000000000..d9fa42b9e47 --- /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 00000000000..5e83d7bd39c --- /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 00000000000..ca77537e4e1 --- /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 00000000000..15b90cb6b6b --- /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 00000000000..32687882da2 --- /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 00000000000..eea4eda0810 --- /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 00000000000..34a5ebfc4db --- /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 00000000000..41232ddc2d4 --- /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 00000000000..d35efbd4b00 --- /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 00000000000..b4f45986c1e --- /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 00000000000..51260f5d86e --- /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 00000000000..3557c3fd930 --- /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 00000000000..0618ca9675f --- /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 00000000000..bf7be1bbf91 --- /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 00000000000..b7d1e8df96f --- /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 00000000000..5f4e2a7c8b4 --- /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 3d0952b89fb..d639b638033 100644 --- a/homeassistant/components/python_script/__init__.py +++ b/homeassistant/components/python_script/__init__.py @@ -125,13 +125,13 @@ def execute(hass, filename, source, data=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 47f6176d5f8..7ccf9f33ada 100644 --- a/homeassistant/components/raincloud/__init__.py +++ b/homeassistant/components/raincloud/__init__.py @@ -24,7 +24,8 @@ _LOGGER = logging.getLogger(__name__) 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 @@ class RainCloudEntity(Entity): 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 969169edc64..1b76e8974b0 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 @@ class RainCloudSwitch(RainCloudEntity, SwitchDevice): 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 00000000000..2cb49dc0ac1 --- /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 00000000000..2cb49dc0ac1 --- /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 ff98eccbe5a..0f5b6b71126 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 00000000000..40b49a926c7 --- /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 00000000000..03f9c671c35 --- /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 69b03a36769..622b98223aa 100644 --- a/homeassistant/components/raspihats/__init__.py +++ b/homeassistant/components/raspihats/__init__.py @@ -14,7 +14,6 @@ DOMAIN = 'raspihats' 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 04885402e72..b0ebc2e3579 100644 --- a/homeassistant/components/raspihats/binary_sensor.py +++ b/homeassistant/components/raspihats/binary_sensor.py @@ -6,10 +6,10 @@ import voluptuous as vol 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 10bb2f748c4..26fcda3c8d7 100644 --- a/homeassistant/components/raspihats/switch.py +++ b/homeassistant/components/raspihats/switch.py @@ -4,11 +4,10 @@ import logging 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 9b852b4a00a..6c338457b34 100644 --- a/homeassistant/components/recorder/__init__.py +++ b/homeassistant/components/recorder/__init__.py @@ -25,7 +25,7 @@ from . import migration, purge 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 @@ class Recorder(threading.Thread): 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 c6390e5d8e2..449f910fda9 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 00000000000..3c810cdb1d8 --- /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 00000000000..1b6a960669c --- /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 526388a0918..94f3be305fa 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 f22a6d340ae..39c7f22f7af 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 ff99dce5e06..b9e7a3315e3 100644 --- a/homeassistant/components/sensor/airvisual.py +++ b/homeassistant/components/sensor/airvisual.py @@ -18,7 +18,7 @@ from homeassistant.helpers import aiohttp_client, config_validation as cv 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 @@ class AirVisualData: 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 79943a8b084..774a3fe95f6 100644 --- a/homeassistant/components/sensor/alpha_vantage.py +++ b/homeassistant/components/sensor/alpha_vantage.py @@ -23,7 +23,8 @@ ATTR_CLOSE = 'close' 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 @@ class AlphaVantageSensor(Entity): """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 @@ class AlphaVantageForeignExchange(Entity): """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 34855d19104..e654f29f42a 100644 --- a/homeassistant/components/sensor/bitcoin.py +++ b/homeassistant/components/sensor/bitcoin.py @@ -19,7 +19,7 @@ REQUIREMENTS = ['blockchain==1.4.4'] _LOGGER = logging.getLogger(__name__) -CONF_ATTRIBUTION = "Data provided by blockchain.info" +ATTRIBUTION = "Data provided by blockchain.info" DEFAULT_CURRENCY = 'USD' @@ -112,7 +112,7 @@ class BitcoinSensor(Entity): 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 e51db7edcad..241c98d2328 100644 --- a/homeassistant/components/sensor/blockchain.py +++ b/homeassistant/components/sensor/blockchain.py @@ -18,8 +18,9 @@ REQUIREMENTS = ['python-blockchain-api==0.0.2'] _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 @@ class BlockchainSensor(Entity): 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 df8b5391359..62a3706034a 100644 --- a/homeassistant/components/sensor/bom.py +++ b/homeassistant/components/sensor/bom.py @@ -34,7 +34,8 @@ ATTR_STATION_ID = 'station_id' 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 @@ class BOMCurrentSensor(Entity): 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 36585b8e103..4ceb1b221c0 100644 --- a/homeassistant/components/sensor/buienradar.py +++ b/homeassistant/components/sensor/buienradar.py @@ -137,6 +137,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ '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 d25b7b786f8..54af94944d6 100644 --- a/homeassistant/components/sensor/coinbase.py +++ b/homeassistant/components/sensor/coinbase.py @@ -16,9 +16,10 @@ CURRENCY_ICONS = { '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 @@ class AccountSensor(Entity): 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 @@ class ExchangeRateSensor(Entity): 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 18d3f0a3d00..9143405a553 100644 --- a/homeassistant/components/sensor/coinmarketcap.py +++ b/homeassistant/components/sensor/coinmarketcap.py @@ -32,7 +32,8 @@ ATTR_RANK = 'rank' 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 @@ class CoinMarketCapSensor(Entity): 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 12b8e917f9d..1771fd0f1a3 100644 --- a/homeassistant/components/sensor/comed_hourly_pricing.py +++ b/homeassistant/components/sensor/comed_hourly_pricing.py @@ -25,7 +25,8 @@ _RESOURCE = 'https://hourlypricing.comed.com/api' 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 @@ class ComedHourlyPricingSensor(Entity): @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 2f1db42a127..13934675517 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.distance import convert 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 67c9c7bbf19..9b7186e8e09 100644 --- a/homeassistant/components/sensor/currencylayer.py +++ b/homeassistant/components/sensor/currencylayer.py @@ -19,7 +19,7 @@ from homeassistant.helpers.entity import Entity _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 @@ class CurrencylayerSensor(Entity): 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 @@ class CurrencylayerData: 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 70d7155fec7..ecbd6d9cab1 100644 --- a/homeassistant/components/sensor/discogs.py +++ b/homeassistant/components/sensor/discogs.py @@ -6,11 +6,13 @@ https://home-assistant.io/components/sensor.discogs/ """ 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 @@ _LOGGER = logging.getLogger(__name__) ATTR_IDENTITY = 'identity' -CONF_ATTRIBUTION = "Data provided by Discogs" +ATTRIBUTION = "Data provided by Discogs" DEFAULT_NAME = 'Discogs' -ICON = 'mdi:album' +ICON_RECORD = 'mdi:album' +ICON_PLAYER = 'mdi:record-player' +UNIT_RECORDS = 'records' -SCAN_INTERVAL = timedelta(hours=2) +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 @@ class DiscogsSensor(Entity): @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 8b7d78aa038..1bb7b44cab6 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 02527f1e333..7a70d7af3a7 100644 --- a/homeassistant/components/sensor/dublin_bus_transport.py +++ b/homeassistant/components/sensor/dublin_bus_transport.py @@ -28,7 +28,8 @@ ATTR_DUE_IN = 'Due in' 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 @@ class DublinPublicTransportSensor(Entity): 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 64884764523..330f5f8cc56 100644 --- a/homeassistant/components/sensor/entur_public_transport.py +++ b/homeassistant/components/sensor/entur_public_transport.py @@ -26,7 +26,8 @@ ATTR_NEXT_UP_IN = 'next_due_in' 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 @@ class EnturPublicTransportSensor(Entity): 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 24cf046cca0..082295bfea5 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 @@ class EtherscanSensor(Entity): @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 3d05dd28e79..92e2cc751ac 100644 --- a/homeassistant/components/sensor/filter.py +++ b/homeassistant/components/sensor/filter.py @@ -313,6 +313,7 @@ class Filter: 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 @@ class Filter: """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 @@ class OutlierFilter(Filter): 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 @@ class OutlierFilter(Filter): _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 f5b44d577a7..d5d9150e4e8 100644 --- a/homeassistant/components/sensor/fitbit.py +++ b/homeassistant/components/sensor/fitbit.py @@ -35,7 +35,7 @@ ATTR_LAST_SAVED_AT = 'last_saved_at' 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 @@ class FitbitSensor(Entity): """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 @@ class FitbitSensor(Entity): """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 1bdd9e71272..c46fa751319 100644 --- a/homeassistant/components/sensor/fixer.py +++ b/homeassistant/components/sensor/fixer.py @@ -20,8 +20,8 @@ _LOGGER = logging.getLogger(__name__) 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 @@ class ExchangeRateSensor(Entity): """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 1e55a7d6997..7f3b444bb75 100644 --- a/homeassistant/components/sensor/gitlab_ci.py +++ b/homeassistant/components/sensor/gitlab_ci.py @@ -28,8 +28,8 @@ ATTR_BUILD_FINISHED = 'build_finished' 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 @@ class GitLabSensor(Entity): 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 5ac0816a0c1..53db254e4b3 100644 --- a/homeassistant/components/sensor/glances.py +++ b/homeassistant/components/sensor/glances.py @@ -47,6 +47,7 @@ SENSOR_TYPES = { '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 @@ class GlancesSensor(Entity): 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 6c197475653..1f4d8425d6e 100644 --- a/homeassistant/components/sensor/google_travel_time.py +++ b/homeassistant/components/sensor/google_travel_time.py @@ -4,21 +4,21 @@ Support for Google travel time sensors. 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 setup_platform(hass, config, add_entities_callback, discovery_info=None): 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 94f21287e39..eec08be093f 100644 --- a/homeassistant/components/sensor/gtfs.py +++ b/homeassistant/components/sensor/gtfs.py @@ -40,6 +40,7 @@ ICONS = { 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 4d651ea81c7..a4ae2349e24 100644 --- a/homeassistant/components/sensor/haveibeenpwned.py +++ b/homeassistant/components/sensor/haveibeenpwned.py @@ -12,7 +12,7 @@ import requests 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 @@ import homeassistant.util.dt as dt_util _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 @@ class HaveIBeenPwnedSensor(Entity): @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 0eb6dfaa00c..00000000000 --- 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 10f4004ae74..e17ecfde59d 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 voluptuous as vol 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_DUE_AT = "Due at" 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 @@ class IrishRailTransportSensor(Entity): 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 @@ class IrishRailTransportSensor(Entity): 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 @@ class IrishRailTransportData: 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 @@ class IrishRailTransportData: 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 fa69a916495..bb5a09771c2 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 @@ _LOGGER = logging.getLogger(__name__) 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 @@ class LastfmSensor(Entity): 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 316da010ae4..8130961bfc0 100644 --- a/homeassistant/components/sensor/linky.py +++ b/homeassistant/components/sensor/linky.py @@ -16,7 +16,7 @@ from homeassistant.helpers.entity import Entity 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 d44806cf481..1c93d6a1bcb 100644 --- a/homeassistant/components/sensor/london_underground.py +++ b/homeassistant/components/sensor/london_underground.py @@ -18,8 +18,11 @@ REQUIREMENTS = ['london-tube-status==0.2'] _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 @@ TUBE_LINES = [ '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 1e18b1518a7..00000000000 --- 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 8cebecb7124..3d9c9485da3 100644 --- a/homeassistant/components/sensor/metoffice.py +++ b/homeassistant/components/sensor/metoffice.py @@ -26,7 +26,7 @@ ATTR_SENSOR_ID = 'sensor_id' 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 @@ class MetOfficeCurrentSensor(Entity): 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 15e225fd2c0..f8bee17978d 100644 --- a/homeassistant/components/sensor/mitemp_bt.py +++ b/homeassistant/components/sensor/mitemp_bt.py @@ -12,7 +12,8 @@ from homeassistant.components.sensor import PLATFORM_SCHEMA 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 @@ DEFAULT_TIMEOUT = 10 # 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 @@ class MiTempBtSensor(Entity): """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 81cfada25f6..5d9376ad9eb 100644 --- a/homeassistant/components/sensor/nederlandse_spoorwegen.py +++ b/homeassistant/components/sensor/nederlandse_spoorwegen.py @@ -21,7 +21,8 @@ REQUIREMENTS = ['nsapi==2.7.4'] _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 @@ class NSDepartureSensor(Entity): '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 e13ca18af5f..e677a072ef3 100644 --- a/homeassistant/components/sensor/nmbs.py +++ b/homeassistant/components/sensor/nmbs.py @@ -9,7 +9,9 @@ import logging 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 @@ import homeassistant.util.dt as dt_util _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 @@ DEFAULT_ICON_ALERT = "mdi:alert-octagon" 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 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ 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 @@ class NMBSLiveBoard(Entity): @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 @@ class NMBSLiveBoard(Entity): '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 @@ class NMBSLiveBoard(Entity): 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 @@ class NMBSSensor(Entity): @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 @@ class NMBSSensor(Entity): @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 @@ class NMBSSensor(Entity): 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 @@ class NMBSSensor(Entity): 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 @@ class NMBSSensor(Entity): """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 @@ class NMBSSensor(Entity): 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 60ad83a82f2..f0da619a6e7 100644 --- a/homeassistant/components/sensor/nsw_fuel_station.py +++ b/homeassistant/components/sensor/nsw_fuel_station.py @@ -28,7 +28,8 @@ CONF_FUEL_TYPES = 'fuel_types' 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 @@ class StationPriceSensor(Entity): 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 01c84c63034..6361b823dea 100644 --- a/homeassistant/components/sensor/openexchangerates.py +++ b/homeassistant/components/sensor/openexchangerates.py @@ -20,7 +20,7 @@ from homeassistant.util import Throttle _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 @@ class OpenexchangeratesSensor(Entity): 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 b6a4ff0860e..a137836138b 100644 --- a/homeassistant/components/sensor/openweathermap.py +++ b/homeassistant/components/sensor/openweathermap.py @@ -21,7 +21,8 @@ REQUIREMENTS = ['pyowm==2.10.0'] _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 @@ class OpenWeatherMapSensor(Entity): 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 eab5a14b8ca..d553dd8730f 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 @@ import homeassistant.helpers.config_validation as cv 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 bade1bd6315..7a8cddb6179 100755 --- a/homeassistant/components/sensor/rejseplanen.py +++ b/homeassistant/components/sensor/rejseplanen.py @@ -31,7 +31,8 @@ ATTR_DUE_IN = 'Due in' 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 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ 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 @@ class RejseplanenTransportSensor(Entity): 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 @@ class RejseplanenTransportSensor(Entity): 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 4eb4b940095..a9446ee3503 100644 --- a/homeassistant/components/sensor/rest.py +++ b/homeassistant/components/sensor/rest.py @@ -16,7 +16,7 @@ from homeassistant.components.sensor import ( 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_METHOD = 'GET' 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 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ 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 @@ class RestSensor(Entity): 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 @@ class RestData: 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 9478768f889..d58e0cf8b3f 100644 --- a/homeassistant/components/sensor/ring.py +++ b/homeassistant/components/sensor/ring.py @@ -11,7 +11,7 @@ import voluptuous as vol 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 @@ class RingSensor(Entity): """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 beb7bf22269..54530571c3e 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 @@ class RippleSensor(Entity): 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 0b7f43f0973..07be331f23f 100644 --- a/homeassistant/components/sensor/rova.py +++ b/homeassistant/components/sensor/rova.py @@ -17,11 +17,12 @@ import homeassistant.helpers.config_validation as cv 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 @@ SENSOR_TYPES = { 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 6dd52789f71..70dfae392be 100644 --- a/homeassistant/components/sensor/scrape.py +++ b/homeassistant/components/sensor/scrape.py @@ -26,6 +26,7 @@ _LOGGER = logging.getLogger(__name__) CONF_ATTR = 'attribute' CONF_SELECT = 'select' +CONF_INDEX = 'index' DEFAULT_NAME = 'Web scrape' DEFAULT_VERIFY_SSL = True @@ -34,6 +35,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ 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 @@ class ScrapeSensor(Entity): 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 1cce17cf64a..ee64eecf3fe 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.components.sensor import PLATFORM_SCHEMA 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 b582ba04567..ef6a53b7091 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 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -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 @@ class SochainSensor(Entity): 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 f780158dd4e..bd246c0d01c 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 @@ from homeassistant.helpers.entity import Entity _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 a0c6f23e496..9cb57670740 100644 --- a/homeassistant/components/sensor/starlingbank.py +++ b/homeassistant/components/sensor/starlingbank.py @@ -14,7 +14,7 @@ from homeassistant.const import CONF_ACCESS_TOKEN, CONF_NAME 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 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ 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 @@ class StarlingBalanceSensor(Entity): 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 6b34930075a..d9f2410f8ca 100644 --- a/homeassistant/components/sensor/swiss_public_transport.py +++ b/homeassistant/components/sensor/swiss_public_transport.py @@ -30,7 +30,8 @@ ATTR_TARGET = 'destination' 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 @@ class SwissPublicTransportSensor(Entity): 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 39a9e75c47b..2b443738230 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 @@ REQUIREMENTS = ['python-synology==0.2.0'] _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 @@ class SynoNasSensor(Entity): 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 d0b3df5dd0e..70fb1f91051 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 @@ from homeassistant.helpers.entity import Entity 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 082342a0393..f8ef18fcffe 100644 --- a/homeassistant/components/sensor/sytadin.py +++ b/homeassistant/components/sensor/sytadin.py @@ -24,8 +24,7 @@ _LOGGER = logging.getLogger(__name__) 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 @@ class SytadinSensor(Entity): 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 433bb8e9ed1..38cc23dabbe 100644 --- a/homeassistant/components/sensor/trafikverket_weatherstation.py +++ b/homeassistant/components/sensor/trafikverket_weatherstation.py @@ -24,12 +24,13 @@ REQUIREMENTS = ['pytrafikverket==0.1.5.8'] _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 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -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 @@ class TrafikverketWeatherStation(Entity): 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 2e28d81a2c3..3c40bf4f709 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_REAL_TIME = 'real_time' 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 @@ ICONS = { '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 @@ class TransportNSWSensor(Entity): 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 e1bd74b993c..c96bb18e958 100644 --- a/homeassistant/components/sensor/travisci.py +++ b/homeassistant/components/sensor/travisci.py @@ -20,7 +20,8 @@ REQUIREMENTS = ['TravisPy==0.3.5'] _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 @@ class TravisCISensor(Entity): 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 124b0ff44ea..8148a5c2fc7 100644 --- a/homeassistant/components/sensor/vasttrafik.py +++ b/homeassistant/components/sensor/vasttrafik.py @@ -24,8 +24,8 @@ ATTR_ACCESSIBILITY = 'accessibility' 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 @@ class VasttrafikDepartureSensor(Entity): 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 82068c456b6..2b8de2042fa 100644 --- a/homeassistant/components/sensor/viaggiatreno.py +++ b/homeassistant/components/sensor/viaggiatreno.py @@ -18,7 +18,8 @@ from homeassistant.helpers.entity import Entity _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 @@ MONITORED_INFO = [ 'orarioPartenza', 'origine', 'subTitle', - ] +] DEFAULT_NAME = "Train {}" @@ -121,7 +122,7 @@ class ViaggiaTrenoSensor(Entity): @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 ae38c529fe2..83b4f3ad934 100644 --- a/homeassistant/components/sensor/waze_travel_time.py +++ b/homeassistant/components/sensor/waze_travel_time.py @@ -18,7 +18,7 @@ from homeassistant.helpers import location 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_DURATION = 'duration' 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 @@ REGIONS = ['US', 'NA', 'EU', 'IL', 'AU'] 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 @@ class WazeTravelTime(Entity): 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 @@ class WazeTravelTime(Entity): 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 fea3e92a140..0f7bfeaa900 100644 --- a/homeassistant/components/sensor/worldtidesinfo.py +++ b/homeassistant/components/sensor/worldtidesinfo.py @@ -19,7 +19,7 @@ from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) -CONF_ATTRIBUTION = "Data provided by WorldTides" +ATTRIBUTION = "Data provided by WorldTides" DEFAULT_NAME = 'WorldTidesInfo' @@ -72,7 +72,7 @@ class WorldTidesInfoSensor(Entity): @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 84f2e8622c6..4e53a2c17c4 100644 --- a/homeassistant/components/sensor/wsdot.py +++ b/homeassistant/components/sensor/wsdot.py @@ -26,7 +26,7 @@ ATTR_DESCRIPTION = 'Description' 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 @@ class WashingtonStateTravelTimeSensor(WashingtonStateTransportSensor): 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 be42e10e894..74a4c2089b2 100644 --- a/homeassistant/components/sensor/wunderground.py +++ b/homeassistant/components/sensor/wunderground.py @@ -29,7 +29,8 @@ import homeassistant.helpers.config_validation as cv _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 @@ class WUndergroundSensor(Entity): 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 0cb9c3765ec..665c482f050 100644 --- a/homeassistant/components/sensor/yr.py +++ b/homeassistant/components/sensor/yr.py @@ -29,8 +29,8 @@ REQUIREMENTS = ['xmltodict==0.11.0'] _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 @@ class YrSensor(Entity): 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 243329680e1..349ee2c7aae 100644 --- a/homeassistant/components/sensor/yweather.py +++ b/homeassistant/components/sensor/yweather.py @@ -21,7 +21,8 @@ REQUIREMENTS = ['yahooweather==0.10'] _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 @@ class YahooWeatherSensor(Entity): @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 a04df22cf07..ed3af84d396 100644 --- a/homeassistant/components/sensor/zestimate.py +++ b/homeassistant/components/sensor/zestimate.py @@ -22,8 +22,9 @@ REQUIREMENTS = ['xmltodict==0.11.0'] _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 @@ class ZestimateDataSensor(Entity): 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 00000000000..709d045c348 --- /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 00000000000..4666a9ea182 --- /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 fcd9d15839b..f494ccf390e 100644 --- a/homeassistant/components/simplisafe/__init__.py +++ b/homeassistant/components/simplisafe/__init__.py @@ -18,7 +18,7 @@ from homeassistant.helpers import config_validation as cv 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 8724f7d3d66..31d1339fbcf 100644 --- a/homeassistant/components/skybell/__init__.py +++ b/homeassistant/components/skybell/__init__.py @@ -13,7 +13,7 @@ REQUIREMENTS = ['skybellpy==0.3.0'] _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 @@ class SkybellDevice(Entity): 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 3c0ca05a8d5..1f27e781ee3 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 1c571b4e639..18412069394 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 f65c338bf03..dd672dee9f6 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 00000000000..4dc94324695 --- /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 00000000000..4edeb153921 --- /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 00000000000..c38afd989d2 --- /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 00000000000..e4970780bc0 --- /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 00000000000..486a61847a7 --- /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 e4131543d50..f7d86af8394 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 fd59d187314..fc80ba9f722 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 4d7df8bd65d..fe93407b429 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 570f1130383..33803994764 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 d805cfc563d..f49fe04ae8e 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 334e5d8cb23..6e34cf8a49a 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 00000000000..e274d8c9394 --- /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 00000000000..6da4624fa39 --- /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 952eafec60c..10d73f8be35 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 3cf38c358bc..64e717cbc92 100644 --- a/homeassistant/components/smartthings/__init__.py +++ b/homeassistant/components/smartthings/__init__.py @@ -14,16 +14,20 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession 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 async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry): 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 async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry): 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 @@ class DeviceBroker: 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 @@ class DeviceBroker: 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 f784ed101a7..f660e905274 100644 --- a/homeassistant/components/smartthings/climate.py +++ b/homeassistant/components/smartthings/climate.py @@ -4,11 +4,12 @@ import logging 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 4663222c3b4..c290f0f8e55 100644 --- a/homeassistant/components/smartthings/config_flow.py +++ b/homeassistant/components/smartthings/config_flow.py @@ -9,7 +9,8 @@ from homeassistant.const import CONF_ACCESS_TOKEN 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 @@ class SmartThingsFlowHandler(config_entries.ConfigFlow): 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 @@ class SmartThingsFlowHandler(config_entries.ConfigFlow): 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 @@ class SmartThingsFlowHandler(config_entries.ConfigFlow): 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 @@ class SmartThingsFlowHandler(config_entries.ConfigFlow): 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 @@ class SmartThingsFlowHandler(config_entries.ConfigFlow): 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 - })) + data=installed_app)) - # 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 - }) + # 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 27260b155d1..105c9760e12 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 @@ SETTINGS_INSTANCE_ID = "hassInstanceId" 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 00000000000..131da75f4fe --- /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 d3f633ed0e4..c7ab091454c 100644 --- a/homeassistant/components/smartthings/lock.py +++ b/homeassistant/components/smartthings/lock.py @@ -10,14 +10,17 @@ DEPENDENCIES = ['smartthings'] 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 00000000000..9bf3211d8e3 --- /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 32047c179b4..50beefdb5b2 100644 --- a/homeassistant/components/smartthings/sensor.py +++ b/homeassistant/components/smartthings/sensor.py @@ -43,8 +43,6 @@ CAPABILITY_TO_SENSORS = { 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 @@ CAPABILITY_TO_SENSORS = { '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 @@ CAPABILITY_TO_SENSORS = { 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 89043d4f76c..5527fda54f4 100644 --- a/homeassistant/components/smartthings/smartapp.py +++ b/homeassistant/components/smartthings/smartapp.py @@ -13,15 +13,16 @@ from uuid import uuid4 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 smartapp_sync_subscriptions( _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 5a1224f4fc2..d30aa3a2303 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 @@ class SmartThingsSwitch(SmartThingsEntity, SwitchDevice): # 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 00000000000..a3fb9ee5e27 --- /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 425cf927631..8c79ff3bfaf 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 00000000000..b8c228f7e9e --- /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 6af8c14843b..608ee9b6a6d 100644 --- a/homeassistant/components/smhi/__init__.py +++ b/homeassistant/components/smhi/__init__.py @@ -6,7 +6,7 @@ from homeassistant.core import Config, HomeAssistant 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 75a0c51d010..6136d093a33 100644 --- a/homeassistant/components/smhi/weather.py +++ b/homeassistant/components/smhi/weather.py @@ -218,7 +218,7 @@ class SmhiWeather(WeatherEntity): 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 9a5508c8f32..20cc7137ef8 100644 --- a/homeassistant/components/snips/__init__.py +++ b/homeassistant/components/snips/__init__.py @@ -105,10 +105,10 @@ async def async_setup(hass, config): _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 async_setup(hass, config): '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 e32557f1d95..06c873b436e 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 69d5a9bfc33..e9f297e4f07 100644 --- a/homeassistant/components/sonos/__init__.py +++ b/homeassistant/components/sonos/__init__.py @@ -4,7 +4,7 @@ from homeassistant.helpers import config_entry_flow 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 2a7eafaf835..e0f881f723d 100644 --- a/homeassistant/components/sonos/media_player.py +++ b/homeassistant/components/sonos/media_player.py @@ -48,7 +48,7 @@ SERVICE_CLEAR_TIMER = 'sonos_clear_sleep_timer' 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] + entities = [e for e in entities if e.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 - - 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) + 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) - device.schedule_update_ha_state(True) + entity.schedule_update_ha_state(True) hass.services.register( DOMAIN, SERVICE_JOIN, service_handle, @@ -270,9 +268,9 @@ class _ProcessSonosEventQueue: 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 soco_error(errorcodes=None): 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 @@ class SonosDevice(MediaPlayerDevice): 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 @@ class SonosDevice(MediaPlayerDevice): 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 @@ class SonosDevice(MediaPlayerDevice): """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 @@ class SonosDevice(MediaPlayerDevice): @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 @@ class SonosDevice(MediaPlayerDevice): @property def soco(self): - """Return soco device.""" + """Return soco object.""" return self._player @property @@ -434,7 +437,7 @@ class SonosDevice(MediaPlayerDevice): 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 @@ class SonosDevice(MediaPlayerDevice): 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 @@ class SonosDevice(MediaPlayerDevice): 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 @@ class SonosDevice(MediaPlayerDevice): 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 @@ class SonosDevice(MediaPlayerDevice): 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) + # 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) - # restore groups - if with_group and self._snapshot_group: - old = self._snapshot_group - actual = self.soco.group + self._soco_snapshot = None + self._snapshot_group = None + self._restore_pending = False - ## - # 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 + @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) - # remove new members - for soco_dev in list(remove): - soco_dev.unjoin() + for entity in entities: + entity.snapshot(with_group) - # add old members - for soco_dev in list(add): - soco_dev.join(old.coordinator) - return + @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) - ## - # old is already master, rejoin - if old.coordinator.group.coordinator == old.coordinator: - self.soco.join(old.coordinator) - return + # Pause all current coordinators + for entity in (e for e in entities if e.is_coordinator): + if entity.state == STATE_PLAYING: + entity.media_pause() - ## - # restore old master, update group - old.coordinator.unjoin() - coordinator = _get_entity_from_soco_uid( - self.hass, old.coordinator.uid) - coordinator.restore(False) + # 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) - for s_dev in list(old.members): - if s_dev != old.coordinator: - s_dev.join(old.coordinator) + # 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 @@ class SonosDevice(MediaPlayerDevice): @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 08af44ad1ad..b3380ec8fb4 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 00000000000..5b3ffeed75f --- /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 9cd2927d832..a85357b525a 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 be80ef19169..60497e0207b 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 16786bdeba4..d6877c32f0d 100644 --- a/homeassistant/components/system_log/__init__.py +++ b/homeassistant/components/system_log/__init__.py @@ -91,15 +91,15 @@ class LogEntry: 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 1812d36b7cd..d5f152bbd76 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 e76cadc7ce3..1807667da87 100644 --- a/homeassistant/components/tahoma/__init__.py +++ b/homeassistant/components/tahoma/__init__.py @@ -43,6 +43,7 @@ TAHOMA_TYPES = { '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 18f206541df..78d45535c48 100644 --- a/homeassistant/components/telegram_bot/__init__.py +++ b/homeassistant/components/telegram_bot/__init__.py @@ -84,6 +84,8 @@ PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA.extend({ 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 @@ class BaseTelegramBotEntity: 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 @@ class BaseTelegramBotEntity: 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 5bca4321a5f..9936b690985 100644 --- a/homeassistant/components/telegram_bot/polling.py +++ b/homeassistant/components/telegram_bot/polling.py @@ -61,7 +61,7 @@ def message_handler(handler): """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 4e9c32a1ee5..a9f91f16b11 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 00000000000..bf74d104835 --- /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 4e7de72edc4..bf1aedab17d 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 ffa983db093..6057d7b3212 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 00000000000..90f13184a67 --- /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 90da12451df..a13f71f7505 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 00000000000..5636e137948 --- /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 c632b543634..c95e96b21c9 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 3373e9cc2f7..62463bc0a9e 100644 --- a/homeassistant/components/tellduslive/config_flow.py +++ b/homeassistant/components/tellduslive/config_flow.py @@ -84,8 +84,7 @@ class FlowHandler(config_entries.ConfigFlow): 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 5a22311d7f0..1bd3158d100 100644 --- a/homeassistant/components/tellduslive/cover.py +++ b/homeassistant/components/tellduslive/cover.py @@ -44,11 +44,14 @@ class TelldusLiveCover(TelldusLiveEntity, CoverDevice): 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 10eaee1ad8b..12baf8384f6 100644 --- a/homeassistant/components/tellduslive/light.py +++ b/homeassistant/components/tellduslive/light.py @@ -45,6 +45,7 @@ class TelldusLiveLight(TelldusLiveEntity, Light): 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 63d1512698c..bb0164b10bb 100644 --- a/homeassistant/components/tellduslive/switch.py +++ b/homeassistant/components/tellduslive/switch.py @@ -44,7 +44,9 @@ class TelldusLiveSwitch(TelldusLiveEntity, ToggleEntity): 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 302c0006bcf..118e7204bca 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 ba9ae43f13b..f254774eea4 100644 --- a/homeassistant/components/tibber/__init__.py +++ b/homeassistant/components/tibber/__init__.py @@ -11,7 +11,7 @@ from homeassistant.const import (EVENT_HOMEASSISTANT_STOP, CONF_ACCESS_TOKEN, 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 00000000000..0a88b82f829 --- /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 00000000000..cea3146a3a5 --- /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 00000000000..db064def53b --- /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 00000000000..3a0698aed8e --- /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 00000000000..6ea86c00057 --- /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 00000000000..0cc162218e9 --- /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 00000000000..b09d921268c --- /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 96d8b4e6d15..0ca0a414fa5 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) + +REQUIREMENTS = ['toonapilib==3.2.1'] _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) - -TOON_HANDLE = 'toon_handle' - # 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 - def __init__( - self, username, password, gas=DEFAULT_GAS, solar=DEFAULT_SOLAR): - """Initialize Toon.""" - from toonlib import Toon + conf = hass.data.get(DATA_TOON_CONFIG) - toon = Toon(username, password) + 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])) + hass.data.setdefault(DATA_TOON_CLIENT, {})[entry.entry_id] = toon + + # 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() + @property + def name(self) -> str: + """Return the name of the entity.""" + return self._name - @Throttle(MIN_TIME_BETWEEN_UPDATES) - def update(self): - """Update Toon data.""" - self.last_update = datetime.now() + @property + def icon(self) -> str: + """Return the mdi icon of the entity.""" + return self._icon - 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' +class ToonDisplayDeviceEntity(ToonEntity): + """Defines a Toon display device entity.""" - 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) + @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, + } - 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 +class ToonElectricityMeterDeviceEntity(ToonEntity): + """Defines a Electricity Meter device entity.""" - 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, - } + @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'), + } - 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 +class ToonGasMeterDeviceEntity(ToonEntity): + """Defines a Gas Meter device entity.""" - 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 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 00000000000..a50a67085ec --- /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 3397e3dacc2..13f1c1269a1 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): + def unique_id(self) -> str: + """Return the unique ID for this thermostat.""" + return '_'.join([DOMAIN, self.toon.agreement.id, 'climate']) + + @property + def supported_features(self) -> int: """Return the list of supported features.""" return SUPPORT_FLAGS @property - def name(self): - """Return the name of this thermostat.""" - return self._name - - @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 - def set_temperature(self, **kwargs): + @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) -> 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 00000000000..a09b3dd49a7 --- /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 00000000000..29b58fbfff9 --- /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 ebd25e02cde..e263bda9fc7 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 +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) + +DEPENDENCIES = ['toon'] _LOGGER = logging.getLogger(__name__) -STATE_ATTR_DEVICE_TYPE = 'device_type' -STATE_ATTR_LAST_CONNECTED_CHANGE = 'last_connected_change' +MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=5) +SCAN_INTERVAL = timedelta(seconds=30) -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the Toon sensors.""" - _toon_main = hass.data[toon_main.TOON_HANDLE] +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] - sensor_items = [] - sensor_items.extend([ - ToonSensor(hass, 'Power_current', 'power-plug', 'Watt'), - ToonSensor(hass, 'Power_today', 'power-plug', 'kWh'), - ]) + 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_main.gas: - sensor_items.extend([ - ToonSensor(hass, 'Gas_current', 'gas-cylinder', 'CM3'), - ToonSensor(hass, 'Gas_today', 'gas-cylinder', 'M3'), + 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 + + super().__init__(toon, name, icon) @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 + 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) + + if value is None: + value = getattr(section, self.measurement) + + if self.section == 'power' and \ + self.measurement in ['meter_reading', 'meter_reading_low', + 'average_daily']: + value = round(float(value)/1000.0, 2) + + if self.section == 'solar' and \ + self.measurement in ['meter_reading_produced', + 'meter_reading_low_produced']: + value = float(value)/1000.0 + + if self.section == 'gas' and \ + self.measurement in ['average_daily', 'daily_usage', + 'meter_reading']: + value = round(float(value)/1000.0, 2) + + self._state = max(0, value) -class FibaroSensor(Entity): - """Representation of a Fibaro sensor.""" +class ToonElectricityMeterDeviceSensor(ToonSensor, + ToonElectricityMeterDeviceEntity): + """Defines a Eletricity Meter sensor.""" - 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] - - @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 - - @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) - - @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 -class SolarSensor(Entity): - """Representation of a Solar sensor.""" +class ToonGasMeterDeviceSensor(ToonSensor, ToonGasMeterDeviceEntity): + """Defines a Gas Meter 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] - - @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 - - @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() + pass -class FibaroSmokeDetector(Entity): - """Representation of a Fibaro smoke detector.""" +class ToonSolarDeviceSensor(ToonSensor, ToonSolarDeviceEntity): + """Defines a Solar sensor.""" - 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] + pass - @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 00000000000..80d71d4e421 --- /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 08ccec588b4..00000000000 --- 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 00000000000..cf286f853f2 --- /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 00000000000..cdd953ff5c3 --- /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 00000000000..268d8ed0717 --- /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 00000000000..ff349fe1b68 --- /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 00000000000..1d9fb41fc8c --- /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 00000000000..9b6e34f6c35 --- /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 00000000000..094174b09c1 --- /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 00000000000..c31e686a76d --- /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 00000000000..11ca7218e11 --- /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 00000000000..4946eb81f02 --- /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 00000000000..fa90495a5bf --- /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 00000000000..b7d76793245 --- /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 00000000000..14b6417d593 --- /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 00000000000..d44faf195e5 --- /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 00000000000..bc285150890 --- /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 bd1621a0b35..de1a943c33a 100644 --- a/homeassistant/components/light/tplink.py +++ b/homeassistant/components/tplink/light.py @@ -7,19 +7,20 @@ https://home-assistant.io/components/light.tplink/ 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) -REQUIREMENTS = ['pyHS100==0.3.4'] +DEPENDENCIES = ['tplink'] + +PARALLEL_UPDATES = 0 _LOGGER = logging.getLogger(__name__) @@ -27,20 +28,25 @@ ATTR_CURRENT_POWER_W = 'current_power_w' 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.') -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 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)) + + 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 @@ class TPLinkSmartBulb(Light): 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 @@ class TPLinkSmartBulb(Light): 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 @@ class TPLinkSmartBulb(Light): 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 @@ class TPLinkSmartBulb(Light): 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 @@ class TPLinkSmartBulb(Light): 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 00000000000..e353c1363ab --- /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 67c8094a1f2..65b884169c7 100644 --- a/homeassistant/components/switch/tplink.py +++ b/homeassistant/components/tplink/switch.py @@ -7,58 +7,77 @@ https://home-assistant.io/components/switch.tplink/ 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 -REQUIREMENTS = ['pyHS100==0.3.4'] +DEPENDENCIES = ['tplink'] + +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. -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, -}) + Deprecated. + """ + _LOGGER.warning('Loading as a platform is no longer supported, ' + 'convert to use the tplink component.') -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 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)) - add_entities([SmartPlugSwitch(SmartPlug(host), name, leds_on)], True) + async_add_entities(devs, 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 @@ class SmartPlugSwitch(SwitchDevice): """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 @@ class SmartPlugSwitch(SwitchDevice): 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 00000000000..55016606e2d --- /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 3d5101bbce8..4c114492336 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 97ff18ba911..06714760a02 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 00000000000..a5fd83abef4 --- /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 257dd24f082..ae96d08976d 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 00000000000..4f8926c23e5 --- /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 c195392be22..b8d6f11f7ef 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 00000000000..673997d5aa9 --- /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 00000000000..9b729e4c4ab --- /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 4a664a40c74..6f78beaffd6 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 00000000000..407371bf89f --- /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 00000000000..864c887d6fe --- /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 00000000000..bd95b48359e --- /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 6afdeca8047..fa299cc379f 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 f2fd380b1e3..7d3827e76da 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 00000000000..798f6578093 --- /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 d38d5be58ba..9fa37e1236d 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 63c63781845..e3864aee4da 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 3cf1b2fea61..2f062851ee6 100644 --- a/homeassistant/components/utility_meter/__init__.py +++ b/homeassistant/components/utility_meter/__init__.py @@ -12,9 +12,9 @@ from homeassistant.helpers.restore_state import RestoreEntity 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 @@ METER_CONFIG_SCHEMA = vol.Schema({ 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 4d2df0372b5..c5cb6b8aa33 100644 --- a/homeassistant/components/utility_meter/const.py +++ b/homeassistant/components/utility_meter/const.py @@ -15,6 +15,7 @@ CONF_METER = 'meter' 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 a01c53b20e3..21dc1099442 100644 --- a/homeassistant/components/utility_meter/sensor.py +++ b/homeassistant/components/utility_meter/sensor.py @@ -17,7 +17,7 @@ from .const import ( 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 @@ class UtilityMeterSensor(RestoreEntity): 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 @@ class UtilityMeterSensor(RestoreEntity): 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 6e40b3d67fc..fe5bb77cefe 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 ae7a2828492..1f45408a666 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 1018f72fdbc..6ea50ae6c0d 100644 --- a/homeassistant/components/velux/__init__.py +++ b/homeassistant/components/velux/__init__.py @@ -12,7 +12,7 @@ DATA_VELUX = "data_velux" 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 7cd3129bc14..9c812da9208 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 a0220927f16..b551993aca5 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 93ae98ed94b..69fde44bdd2 100644 --- a/homeassistant/components/water_heater/econet.py +++ b/homeassistant/components/water_heater/econet.py @@ -13,7 +13,7 @@ from homeassistant.const import ( 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 @@ DELETE_VACATION_SCHEMA = vol.Schema({ 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 @@ class EcoNetWaterHeater(WaterHeaterDevice): ["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 @@ class EcoNetWaterHeater(WaterHeaterDevice): 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 d479725657b..34cd86347f2 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 1ed54496c6f..b05d5fc594d 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 1ec3fc513e9..31f51824146 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 -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 -from homeassistant.helpers import config_validation as cv # Reuse data and API logic from the sensor implementation -from homeassistant.components.sensor.buienradar import ( - BrData) +from homeassistant.components.sensor.buienradar import BrData +from homeassistant.components.weather import ( + 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 REQUIREMENTS = ['buienradar==0.91'] diff --git a/homeassistant/components/weather/darksky.py b/homeassistant/components/weather/darksky.py index 4ac3d2a1d22..17e3cbbcf14 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 @@ from requests.exceptions import ( 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 6bcb8918504..d20e91b1f93 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 c905e6b6ce3..6c9613ac5d2 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 @@ class MetWeather(WeatherEntity): """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 @@ class MetWeather(WeatherEntity): @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 7382319e7a4..3b52eebcff6 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 @@ class MetOfficeWeather(WeatherEntity): @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 2b86359361a..58016dd3e2c 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 @@ import voluptuous as vol 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 @@ _LOGGER = logging.getLogger(__name__) ATTRIBUTION = 'Data provided by OpenWeatherMap' -FORECAST_MODE = ['hourly', 'daily'] +FORECAST_MODE = ['hourly', 'daily', 'freedaily'] DEFAULT_NAME = 'OpenWeatherMap' @@ -158,7 +152,12 @@ class OpenWeatherMapWeather(WeatherEntity): 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 567b8e235a8..e4eb34a039a 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 f76b733ef0b..60707fa5e30 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 -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) -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.zamg import ( - ATTRIBUTION, closest_station, CONF_STATION_ID, zamg_stations, ZamgData) + ATTRIBUTION, CONF_STATION_ID, ZamgData, closest_station, zamg_stations) +from homeassistant.components.weather import ( + 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_LATITUDE, CONF_LONGITUDE, CONF_NAME, TEMP_CELSIUS) +from homeassistant.helpers import config_validation as cv _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/websocket_api/commands.py b/homeassistant/components/websocket_api/commands.py index 34bb04cb394..3313971e79e 100644 --- a/homeassistant/components/websocket_api/commands.py +++ b/homeassistant/components/websocket_api/commands.py @@ -3,7 +3,8 @@ import voluptuous as vol 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 fd8f7eb7b08..01145275b31 100644 --- a/homeassistant/components/websocket_api/const.py +++ b/homeassistant/components/websocket_api/const.py @@ -9,6 +9,7 @@ MAX_PENDING_MSG = 512 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 42c2c0a5751..1ab2b09d7fa 100644 --- a/homeassistant/components/websocket_api/http.py +++ b/homeassistant/components/websocket_api/http.py @@ -130,7 +130,7 @@ class WebSocketHandler: 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 8d946bf03df..efd8eecf5af 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 5e47adc47f9..19d7aaaa30d 100644 --- a/homeassistant/components/xiaomi_aqara/__init__.py +++ b/homeassistant/components/xiaomi_aqara/__init__.py @@ -16,7 +16,7 @@ from homeassistant.helpers.entity import Entity 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 82a92dd317c..36196158600 100644 --- a/homeassistant/components/xiaomi_miio/vacuum.py +++ b/homeassistant/components/xiaomi_miio/vacuum.py @@ -33,6 +33,7 @@ SERVICE_MOVE_REMOTE_CONTROL = 'xiaomi_remote_control_move' SERVICE_MOVE_REMOTE_CONTROL_STEP = 'xiaomi_remote_control_move_step' SERVICE_START_REMOTE_CONTROL = 'xiaomi_remote_control_start' SERVICE_STOP_REMOTE_CONTROL = 'xiaomi_remote_control_stop' +SERVICE_CLEAN_ZONE = 'xiaomi_clean_zone' FAN_SPEEDS = { 'Quiet': 38, @@ -58,6 +59,8 @@ ATTR_RC_DURATION = 'duration' 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 @@ SERVICE_SCHEMA_REMOTE_CONTROL = VACUUM_SERVICE_SCHEMA.extend({ 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_TO_METHOD = { 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_setup_platform(hass, config, async_add_entities, 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 @@ class MiroboVacuum(StateVacuumDevice): _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 92a2c75895c..e579761474b 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 280c941b427..686c1f35a98 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 00000000000..0047c762a9d --- /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 00000000000..9984a316884 --- /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 11b2a9fc833..39c00a4dee3 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 00000000000..e4b87c9d7b6 --- /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 00000000000..029f0391657 --- /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 6c7e83689ad..cafbae13421 100644 --- a/homeassistant/components/zha/__init__.py +++ b/homeassistant/components/zha/__init__.py @@ -29,11 +29,11 @@ from .core.gateway import establish_device_mappings 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 @@ async def async_setup_entry(hass, config_entry): """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 0dd6dd78400..f0739f9a073 100644 --- a/homeassistant/components/zha/api.py +++ b/homeassistant/components/zha/api.py @@ -251,7 +251,7 @@ def async_load_api(hass, application_controller, zha_gateway): 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 @@ def async_load_api(hass, application_controller, zha_gateway): 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 @@ def async_load_api(hass, application_controller, zha_gateway): 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 @@ def async_load_api(hass, application_controller, zha_gateway): 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 0c0e1ed2173..a070343b775 100644 --- a/homeassistant/components/zha/core/channels/__init__.py +++ b/homeassistant/components/zha/core/channels/__init__.py @@ -5,6 +5,7 @@ For more details about this component, please refer to the documentation at 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 @@ def decorate_command(channel, command): 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 ee88a30e828..9c904a7a001 100644 --- a/homeassistant/components/zha/core/channels/lighting.py +++ b/homeassistant/components/zha/core/channels/lighting.py @@ -28,8 +28,16 @@ class ColorChannel(ZigbeeChannel): """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 1ee800d8559..102c9bed2d3 100644 --- a/homeassistant/components/zha/core/device.py +++ b/homeassistant/components/zha/core/device.py @@ -8,6 +8,7 @@ import asyncio 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 @@ class ZHADevice: """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 @@ class ZHADevice: 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 @@ class ZHADevice: 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 @@ class ZHADevice: 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 @@ class ZHADevice: 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 cb5e5bf7774..563543fa4bd 100644 --- a/homeassistant/components/zha/core/gateway.py +++ b/homeassistant/components/zha/core/gateway.py @@ -5,11 +5,11 @@ For more details about this component, please refer to the documentation at 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 @@ class ZHAGateway: ) ) - 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 @@ class ZHAGateway: 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 @@ class ZHAGateway: 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 @@ class ZHAGateway: ) ) await zha_device.async_initialize(from_cache=False) + else: + await zha_device.async_initialize(from_cache=True) - async def _async_process_endpoint( + for discovery_info in discovery_infos: + _async_dispatch_discovery_info( + self._hass, + is_new_join, + discovery_info + ) + + 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 @@ class ZHAGateway: 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 @@ class ZHAGateway: )) -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 efa6f679ae8..740d67db1bd 100644 --- a/homeassistant/components/zha/light.py +++ b/homeassistant/components/zha/light.py @@ -20,7 +20,7 @@ _LOGGER = logging.getLogger(__name__) DEPENDENCIES = ['zha'] -DEFAULT_DURATION = 0.5 +DEFAULT_DURATION = 5 CAPABILITIES_COLOR_XY = 0x08 CAPABILITIES_COLOR_TEMP = 0x10 @@ -110,8 +110,13 @@ class Light(ZhaEntity, light.Light): 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 @@ class Light(ZhaEntity, light.Light): 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 @@ class Light(ZhaEntity, light.Light): 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 00000000000..2e246fb9931 --- /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 e326c5152a6..2842c535984 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 86a61307814..c380d8e5625 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 bf7b64549ac..b0ab273e86a 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 3310cd3e160..492db240eee 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 c7dfc0c889b..7b22c2e197c 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -7,7 +7,11 @@ component. 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 @@ If the result of the step is to show a form, the user will be able to continue 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 @@ from homeassistant.exceptions import HomeAssistantError, ConfigEntryNotReady 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 @@ FLOWS = [ 'openuv', 'owntracks', 'point', + 'ps4', 'rainmachine', 'simplisafe', 'smartthings', 'smhi', 'sonos', 'tellduslive', + 'toon', + 'tplink', 'tradfri', 'twilio', 'unifi', 'upnp', 'zha', 'zone', - 'zwave' + 'zwave', ] @@ -188,6 +196,8 @@ SAVE_DELAY = 1 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 @@ CONN_CLASS_UNKNOWN = 'unknown' 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 @@ class ConfigEntry: # 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 @@ class ConfigEntry: # 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 @@ class ConfigEntry: 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 @@ class ConfigEntry: 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 @@ class ConfigEntry: 'domain': self.domain, 'title': self.title, 'data': self.data, + 'options': self.options, 'source': self.source, 'connection_class': self.connection_class, } @@ -364,6 +439,7 @@ class ConfigEntries: 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 @@ class ConfigEntries: 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 @@ class ConfigEntries: 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 @@ class ConfigEntries: domain=result['handler'], title=result['title'], data=result['data'], + options={}, source=flow.context['source'], connection_class=flow.CONNECTION_CLASS, ) @@ -544,7 +640,7 @@ class ConfigEntries: 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 @@ class ConfigFlow(data_entry_flow.FlowHandler): 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 e2329a2de26..5b943ddb3cf 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 e7f654f5184..48ef4f46272 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -1120,7 +1120,7 @@ class ServiceRegistry: 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 bc8c05ed0a6..3fa820f8350 100644 --- a/homeassistant/helpers/area_registry.py +++ b/homeassistant/helpers/area_registry.py @@ -75,8 +75,8 @@ class AreaRegistry: 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 b5716431217..4bba80aa154 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 83827cca235..21c3b0d0209 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 @@ class DeviceRegistry: ) @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 @@ class DeviceRegistry: 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 @@ class DeviceRegistry: 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 @@ class DeviceRegistry: # 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 @@ class DeviceRegistry: '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 c13ebe7cfab..dd9677f6515 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 9c76d244138..87cc4d4fd90 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -334,9 +334,9 @@ class EntityPlatform: 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 c1dae00bed5..5e262a47565 100644 --- a/homeassistant/helpers/event.py +++ b/homeassistant/helpers/event.py @@ -370,7 +370,7 @@ def async_track_utc_time_change(hass, action, 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 d2211d031f5..22138d7c2aa 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 7d69defed48..bbed1ffbbcd 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 962b168aa97..e36ad5451c1 100644 --- a/homeassistant/loader.py +++ b/homeassistant/loader.py @@ -15,7 +15,7 @@ import importlib 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 @@ _LOGGER = logging.getLogger(__name__) 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 838c3f31bc5..fa1fe5a959d 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 02cc0bff362..3050379a496 100644 --- a/homeassistant/scripts/__init__.py +++ b/homeassistant/scripts/__init__.py @@ -9,7 +9,8 @@ from typing import List 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() - if not is_virtual_env(): - asyncio.get_event_loop().run_until_complete( - async_mount_local_lib_path(config_dir)) + loop = asyncio.get_event_loop() - pip_kwargs = requirements.pip_kwargs(config_dir) + if not is_virtual_env(): + loop.run_until_complete(async_mount_local_lib_path(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 67bc97da992..1b8c6719395 100644 --- a/homeassistant/scripts/check_config.py +++ b/homeassistant/scripts/check_config.py @@ -5,7 +5,6 @@ import logging 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.util import yaml 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 629dca5cccf..72185c594cd 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 b9da9890c61..531fb0b78f6 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 1ee80607150..33a88ac4391 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 926aadfc3a5..7db76b1361b 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -8,31 +8,33 @@ import sys import fnmatch COMMENT_REQUIREMENTS = ( - 'RPi.GPIO', - 'raspihats', - 'rpi-rf', 'Adafruit-DHT', 'Adafruit_BBIO', - 'fritzconnection', - 'pybluez', + 'avion', 'beacontools', + 'blinkt', 'bluepy', + 'bme680', + 'credstash', + 'decora', + 'envirophat', + 'evdev', + 'face_recognition', + 'fritzconnection', + 'i2csense', 'opencv-python', + 'py_noaa', + 'pybluez', + 'pycups', + 'PySwitchbot', + 'pySwitchmate', + 'python-eq3bt', 'python-lirc', 'pyuserinput', - 'evdev', - 'pycups', - 'python-eq3bt', - 'avion', - 'decora', - 'face_recognition', - 'blinkt', + 'raspihats', + 'rpi-rf', + 'RPi.GPIO', 'smbus-cffi', - 'envirophat', - 'i2csense', - 'credstash', - 'bme680', - 'py_noaa', ) TEST_REQUIREMENTS = ( @@ -90,6 +92,7 @@ TEST_REQUIREMENTS = ( 'pynx584', 'pyopenuv', 'pyotp', + 'pyps4-homeassistant', 'pysmartapp', 'pysmartthings', 'pysonos', @@ -104,6 +107,8 @@ TEST_REQUIREMENTS = ( 'pyunifi', 'pyupnp-async', 'pywebpush', + 'pyHS100', + 'PyNaCl', 'regenmaschine', 'restrictedpython', 'rflink', @@ -116,6 +121,7 @@ TEST_REQUIREMENTS = ( 'sqlalchemy', 'srpenergy', 'statsd', + 'toonapilib', 'uvcclient', 'vsure', 'warrant', @@ -124,8 +130,8 @@ TEST_REQUIREMENTS = ( 'vultr', 'YesssSMS', 'ruamel.yaml', - 'zigpy', - 'bellows', + 'zigpy-homeassistant', + 'bellows-homeassistant', ) IGNORE_PACKAGES = ( diff --git a/setup.py b/setup.py index 52be310574a..c4c9d0e53ed 100755 --- a/setup.py +++ b/setup.py @@ -33,10 +33,10 @@ PACKAGES = find_packages(exclude=['tests', 'tests.*']) 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 @@ REQUIRES = [ '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 748b5507824..c0680024dae 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 d400fe80672..35ab21ae6de 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 ffc4d67f21d..c466a1fa42b 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 0ca302f8273..57e74e750d5 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 409b020f728..a55546da73b 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 @@ class MockModule: 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 @@ class MockToggleDevice(entity.ToggleEntity): 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 00000000000..3c083141962 --- /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 d1626e1f235..b5b6137a0a8 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 3a023916741..3166b2d3158 100644 --- a/tests/components/climate/test_demo.py +++ b/tests/components/climate/test_demo.py @@ -8,7 +8,9 @@ from homeassistant.util.unit_system import ( 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 @@ class TestDemoClimate(unittest.TestCase): """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 @@ class TestDemoClimate(unittest.TestCase): 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 8d2346260d9..1d532f4757c 100644 --- a/tests/components/climate/test_generic_thermostat.py +++ b/tests/components/climate/test_generic_thermostat.py @@ -20,8 +20,9 @@ from homeassistant.const import ( ) 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 7daed2ff4a9..b01b5b35c35 100644 --- a/tests/components/climate/test_honeywell.py +++ b/tests/components/climate/test_honeywell.py @@ -8,7 +8,7 @@ import somecomfort 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 2c135bfc09d..6b77981a914 100644 --- a/tests/components/climate/test_melissa.py +++ b/tests/components/climate/test_melissa.py @@ -4,8 +4,9 @@ import json 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 40b0732f661..19919d25954 100644 --- a/tests/components/climate/test_nuheat.py +++ b/tests/components/climate/test_nuheat.py @@ -3,7 +3,7 @@ import unittest 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 c16151320b4..8ec8e7b1429 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 be73906c1bf..87ed83d9a7e 100644 --- a/tests/components/config/test_config_entries.py +++ b/tests/components/config/test_config_entries.py @@ -7,8 +7,9 @@ from unittest.mock import patch 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 test_get_progress_flow_unauth(hass, client, hass_admin_user): '/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 aa1b9e4e2d4..de603707ae2 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 00000000000..fa274f1d676 --- /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 dbc45c955b5..d73f225b2ac 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 081fd61ec4e..49c3f280d8a 100644 --- a/tests/components/deconz/test_light.py +++ b/tests/components/deconz/test_light.py @@ -41,14 +41,15 @@ GROUP = { "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 788c6dc1c3e..963f1064b35 100644 --- a/tests/components/deconz/test_scene.py +++ b/tests/components/deconz/test_scene.py @@ -20,6 +20,7 @@ GROUP = { "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 5e65e0a75c7..8e868296703 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 @@ FIVE_M = TEST_ZONE_DEG_PER_M * 5.0 # 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 @@ BAD_MESSAGE = { 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 context(hass, setup_comp): 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 6b38edc3ce9..677a6d1f310 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 +def setup_comp_deps(hass, mock_device_tracker_conf): + """Set up component dependencies.""" + mock_component(hass, 'zone') + mock_component(hass, 'group') + yield -class TestUPCConnect: - """Tests for the Ddwrt device tracker platform.""" +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', + ) - 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') + assert await async_setup_component( + hass, DOMAIN, { + DOMAIN: {CONF_PLATFORM: 'upc_connect', CONF_HOST: HOST}}) - self.host = "127.0.0.1" + assert len(aioclient_mock.mock_calls) == 1 - def teardown_method(self): - """Stop everything that was started.""" - self.hass.stop() + 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) - 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 - }}) +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 len(aioclient_mock.mock_calls) == 1 + assert await async_setup_component( + hass, DOMAIN, { + DOMAIN: {CONF_PLATFORM: 'upc_connect', CONF_HOST: HOST}}) - @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() - ) + assert len(aioclient_mock.mock_calls) == 1 - with assert_setup_component(1, DOMAIN): - assert setup_component( - self.hass, DOMAIN, {DOMAIN: { - CONF_PLATFORM: 'upc_connect', - CONF_HOST: self.host - }}) + assert 'Error setting up platform' in caplog.text - 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.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' + ) - @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 await async_setup_component( + hass, DOMAIN, {DOMAIN: { + CONF_PLATFORM: 'upc_connect', + CONF_HOST: HOST + }}) - 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 len(aioclient_mock.mock_calls) == 1 - assert 'Error setting up platform' in \ - str(mock_error.call_args_list[-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'} + ) - 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 = await platform.async_get_scanner( + hass, { + DOMAIN: {CONF_PLATFORM: 'upc_connect', CONF_HOST: HOST}}) - 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 - 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'} + ) - 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 = await scanner.async_scan_devices() - 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'] - 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'} - ) +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 = run_coroutine_threadsafe(platform.async_get_scanner( - self.hass, {DOMAIN: { - CONF_PLATFORM: 'upc_connect', - CONF_HOST: self.host - }} - ), self.hass.loop).result() + scanner = await platform.async_get_scanner( + hass, { + DOMAIN: {CONF_PLATFORM: 'upc_connect', CONF_HOST: HOST}}) - assert len(aioclient_mock.mock_calls) == 1 + 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'} - ) + 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 = run_coroutine_threadsafe( - scanner.async_scan_devices(), self.hass.loop).result() + 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'] + 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() +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'} + ) - assert len(aioclient_mock.mock_calls) == 1 + scanner = await platform.async_get_scanner( + hass, { + DOMAIN: {CONF_PLATFORM: 'upc_connect', CONF_HOST: HOST}}) - 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'} - ) + assert len(aioclient_mock.mock_calls) == 1 - scanner.token = None - mac_list = run_coroutine_threadsafe( - scanner.async_scan_devices(), self.hass.loop).result() + 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'} + ) - assert len(aioclient_mock.mock_calls) == 2 - assert aioclient_mock.mock_calls[1][2] == 'token=654321&fun=123' - assert mac_list == [] + scanner.token = None + mac_list = await scanner.async_scan_devices() - 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'} - ) + assert len(aioclient_mock.mock_calls) == 2 + assert aioclient_mock.mock_calls[1][2] == 'token=654321&fun=123' + assert mac_list == [] - 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 +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'} + ) - aioclient_mock.clear_requests() - aioclient_mock.post( - "http://{}/xml/getter.xml".format(self.host), - text="Blablebla blabalble", - cookies={'sessionToken': '1235678'} - ) + scanner = await platform.async_get_scanner( + hass, { + DOMAIN: {CONF_PLATFORM: 'upc_connect', CONF_HOST: HOST}}) - mac_list = run_coroutine_threadsafe( - scanner.async_scan_devices(), self.hass.loop).result() + assert len(aioclient_mock.mock_calls) == 1 - 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 == [] + 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 965fb37dcb8..f4412b5d564 100644 --- a/tests/components/ecobee/test_climate.py +++ b/tests/components/ecobee/test_climate.py @@ -3,7 +3,7 @@ import unittest 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 8c870c6ad73..076ec0066a6 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 41c232a51c3..dd87a6d9503 100644 --- a/tests/components/geofency/test_init.py +++ b/tests/components/geofency/test_init.py @@ -10,11 +10,10 @@ from homeassistant.components.geofency import ( 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 - await geofency.async_unload_entry(hass, entry) + entry = hass.config_entries.async_entries(DOMAIN)[0] + + 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 89e9090da98..18f99c82685 100644 --- a/tests/components/google_assistant/test_google_assistant.py +++ b/tests/components/google_assistant/test_google_assistant.py @@ -8,7 +8,8 @@ import pytest 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 36971224f92..d1ec80844b6 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.core import State 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 e9169c9bbbe..e051a5de4da 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 @@ from homeassistant.components import ( 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 f645cddf730..ce6774796d3 100644 --- a/tests/components/homekit/test_type_thermostats.py +++ b/tests/components/homekit/test_type_thermostats.py @@ -4,7 +4,7 @@ from unittest.mock import patch 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 d3c1f9ab07b..d543cf51749 100644 --- a/tests/components/homekit_controller/common.py +++ b/tests/components/homekit_controller/common.py @@ -134,8 +134,11 @@ class FakeService(AbstractService): 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 9f5cc9d8764..b04a57fa967 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 062ecc54041..62fce4325c7 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 c39e7d4e26b..61ca3300d60 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 18537227247..8b02b36de20 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 6c89995a1a1..cdad8e02d25 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 877d25d04bd..f757080eadc 100644 --- a/tests/components/locative/test_init.py +++ b/tests/components/locative/test_init.py @@ -8,11 +8,11 @@ from homeassistant.components import locative from homeassistant.components.device_tracker import \ DOMAIN as DEVICE_TRACKER_DOMAIN from homeassistant.components.locative import DOMAIN, TRACKER_UPDATE -from homeassistant.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 b213cf0b5c1..8cbe0a594d2 100644 --- a/tests/components/media_player/test_demo.py +++ b/tests/components/media_player/test_demo.py @@ -6,22 +6,13 @@ import asyncio 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 @@ class TestDemoMediaPlayer(unittest.TestCase): assert mock_seek.called -class TestMediaPlayerWeb(unittest.TestCase): - """Test the media player web views sensor.""" +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 setUp(self): - """Set up things to be run when tests are started.""" - self.hass = get_test_home_assistant() + fake_picture_data = 'test.test' - assert setup_component(self.hass, http.DOMAIN, { - http.DOMAIN: { - http.CONF_SERVER_PORT: SERVER_PORT, - http.CONF_API_PASSWORD: API_PASSWORD, - }, - }) + class MockResponse(): + def __init__(self): + self.status = 200 + self.headers = {'Content-Type': 'sometype'} - assert setup_component( - self.hass, mp.DOMAIN, - {'media_player': {'platform': 'demo'}}) + @asyncio.coroutine + def read(self): + return fake_picture_data.encode('ascii') - self.hass.start() + @asyncio.coroutine + def release(self): + pass - def tearDown(self): - """Stop everything that was started.""" - self.hass.stop() + class MockWebsession(): - def test_media_image_proxy(self): - """Test the media server image proxy server .""" - fake_picture_data = 'test.test' + @asyncio.coroutine + def get(self, url): + return MockResponse() - class MockResponse(): - def __init__(self): - self.status = 200 - self.headers = {'Content-Type': 'sometype'} + def detach(self): + pass - @asyncio.coroutine - def read(self): - return fake_picture_data.encode('ascii') + hass.data[DATA_CLIENTSESSION] = MockWebsession() - @asyncio.coroutine - def release(self): - pass - - class MockWebsession(): - - @asyncio.coroutine - def get(self, url): - return MockResponse() - - def detach(self): - pass - - self.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 00000000000..becdc2841f3 --- /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 00000000000..d0c1ae02c6c --- /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 ecbdc39e22b..7bdfe8f452f 100644 --- a/tests/components/mqtt/test_climate.py +++ b/tests/components/mqtt/test_climate.py @@ -7,11 +7,15 @@ from unittest.mock import ANY 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 @@ class TestMQTTClimate(unittest.TestCase): 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 @@ class TestMQTTClimate(unittest.TestCase): 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 @@ class TestMQTTClimate(unittest.TestCase): 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 @@ class TestMQTTClimate(unittest.TestCase): 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 @@ class TestMQTTClimate(unittest.TestCase): 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 @@ class TestMQTTClimate(unittest.TestCase): """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 @@ class TestMQTTClimate(unittest.TestCase): """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 @@ class TestMQTTClimate(unittest.TestCase): 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 @@ class TestMQTTClimate(unittest.TestCase): """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 @@ class TestMQTTClimate(unittest.TestCase): 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 @@ class TestMQTTClimate(unittest.TestCase): 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 @@ class TestMQTTClimate(unittest.TestCase): """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 @@ class TestMQTTClimate(unittest.TestCase): 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 @@ class TestMQTTClimate(unittest.TestCase): 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 @@ class TestMQTTClimate(unittest.TestCase): """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 @@ class TestMQTTClimate(unittest.TestCase): 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 @@ class TestMQTTClimate(unittest.TestCase): """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 @@ class TestMQTTClimate(unittest.TestCase): 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 @@ class TestMQTTClimate(unittest.TestCase): """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 @@ class TestMQTTClimate(unittest.TestCase): 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 @@ class TestMQTTClimate(unittest.TestCase): """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 @@ class TestMQTTClimate(unittest.TestCase): 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 @@ class TestMQTTClimate(unittest.TestCase): 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 @@ class TestMQTTClimate(unittest.TestCase): 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 @@ class TestMQTTClimate(unittest.TestCase): 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 @@ class TestMQTTClimate(unittest.TestCase): 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 @@ class TestMQTTClimate(unittest.TestCase): 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 @@ class TestMQTTClimate(unittest.TestCase): 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 6c8c6ebd0dd..ef129a555be 100644 --- a/tests/components/person/test_init.py +++ b/tests/components/person/test_init.py @@ -4,8 +4,10 @@ from unittest.mock import Mock 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 00000000000..c80bcf9173d --- /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 00000000000..b0170beeb48 --- /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 b43d38da5e8..29308f2a83d 100644 --- a/tests/components/sensor/test_filter.py +++ b/tests/components/sensor/test_filter.py @@ -106,6 +106,23 @@ class TestFilterSensor(unittest.TestCase): 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 @@ class TestFilterSensor(unittest.TestCase): 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 3e71be8a6f6..343cc696763 100644 --- a/tests/components/sensor/test_rest.py +++ b/tests/components/sensor/test_rest.py @@ -89,6 +89,7 @@ class TestRestSensorSetup(unittest.TestCase): 'name': 'foo', 'unit_of_measurement': 'MB', 'verify_ssl': 'true', + 'timeout': 30, 'authentication': 'basic', 'username': 'my username', 'password': 'my password', @@ -112,6 +113,7 @@ class TestRestSensorSetup(unittest.TestCase): 'name': 'foo', 'unit_of_measurement': 'MB', 'verify_ssl': 'true', + 'timeout': 30, 'authentication': 'basic', 'username': 'my username', 'password': 'my password', @@ -280,8 +282,10 @@ class TestRestData(unittest.TestCase): 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 ee892fb03b9..27e833bff25 100644 --- a/tests/components/smartthings/conftest.py +++ b/tests/components/smartthings/conftest.py @@ -4,8 +4,8 @@ from unittest.mock import Mock, patch 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 import webhook 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 homeassistant.setup import async_setup_component 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 device_factory_fixture(): 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 4b47537fa19..d1de9f8f020 100644 --- a/tests/components/smartthings/test_binary_sensor.py +++ b/tests/components/smartthings/test_binary_sensor.py @@ -6,31 +6,15 @@ real HTTP calls are not initiated during testing. """ 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 306bcacdb18..29134d6ba6a 100644 --- a/tests/components/smartthings/test_climate.py +++ b/tests/components/smartthings/test_climate.py @@ -8,18 +8,19 @@ from pysmartthings import Attribute, Capability 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 7d335703131..28aa759a359 100644 --- a/tests/components/smartthings/test_config_flow.py +++ b/tests/components/smartthings/test_config_flow.py @@ -8,6 +8,9 @@ from pysmartthings import APIResponseError 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 00000000000..7e41237e3e7 --- /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 db8d9b512de..dffffa7b340 100644 --- a/tests/components/smartthings/test_fan.py +++ b/tests/components/smartthings/test_fan.py @@ -7,31 +7,15 @@ real HTTP calls are not initiated during testing. 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 014cfe7da98..ec0b3982517 100644 --- a/tests/components/smartthings/test_init.py +++ b/tests/components/smartthings/test_init.py @@ -8,14 +8,33 @@ import pytest 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_config_entry_loads_platforms( +async def test_scenes_unauthorized_loads_platforms( hass, config_entry, app, installed_app, - device, smartthings_mock): - """Test config entry loads properly and proxies to platforms.""" + 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.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( + 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, 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.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 @@ async def test_event_handler_dispatches_updated_devices( called = True assert device_ids == ids async_dispatcher_connect(hass, SIGNAL_SMARTTHINGS_UPDATE, signal) - broker = smartthings.DeviceBroker( - hass, devices, request.installed_app_id) - await broker.event_handler(request, None, None) + broker = smartthings.DeviceBroker( + hass, config_entry, Mock(), Mock(), devices, []) + broker.connect() + + # pylint:disable=protected-access + await broker._event_handler(request, None, None) await hass.async_block_till_done() assert called @@ -166,7 +268,7 @@ async def test_event_handler_dispatches_updated_devices( 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 @@ async def test_event_handler_ignores_other_installed_app( 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 @@ async def test_event_handler_fires_button_events( } 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 72bc5da9063..6efd88d7237 100644 --- a/tests/components/smartthings/test_light.py +++ b/tests/components/smartthings/test_light.py @@ -9,15 +9,16 @@ import pytest 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 3739a2dc9b5..1d98e5f9bdb 100644 --- a/tests/components/smartthings/test_lock.py +++ b/tests/components/smartthings/test_lock.py @@ -5,6 +5,7 @@ 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 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 00000000000..2d4990675f8 --- /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 773f157dd87..879aae1994d 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 162a8f9a4e5..46bd1f42f7f 100644 --- a/tests/components/smartthings/test_smartapp.py +++ b/tests/components/smartthings/test_smartapp.py @@ -5,7 +5,9 @@ from uuid import uuid4 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 3f2bedd4f13..e3b1f46bf39 100644 --- a/tests/components/smartthings/test_switch.py +++ b/tests/components/smartthings/test_switch.py @@ -6,28 +6,14 @@ real HTTP calls are not initiated during testing. """ 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 7bcfb11ff5c..e9719c02395 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 55ff96f202a..798c92eddad 100644 --- a/tests/components/sonos/test_media_player.py +++ b/tests/components/sonos/test_media_player.py @@ -21,7 +21,7 @@ ENTITY_ID = 'media_player.kitchen' 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 @@ class MusicLibraryMock(): 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 @@ class SoCoMock(): 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 @@ class SoCoMock(): 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 @@ class TestSonosMediaPlayer(unittest.TestCase): 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 @@ class TestSonosMediaPlayer(unittest.TestCase): '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 @@ class TestSonosMediaPlayer(unittest.TestCase): 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 @@ class TestSonosMediaPlayer(unittest.TestCase): 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 @@ class TestSonosMediaPlayer(unittest.TestCase): 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 @@ class TestSonosMediaPlayer(unittest.TestCase): 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 @@ class TestSonosMediaPlayer(unittest.TestCase): 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 @@ class TestSonosMediaPlayer(unittest.TestCase): 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 @@ class TestSonosMediaPlayer(unittest.TestCase): 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 @@ class TestSonosMediaPlayer(unittest.TestCase): 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 @@ class TestSonosMediaPlayer(unittest.TestCase): '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 @@ class TestSonosMediaPlayer(unittest.TestCase): 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 @@ class TestSonosMediaPlayer(unittest.TestCase): 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 f1d23f48b86..a35b6f760f3 100644 --- a/tests/components/switch/test_litejet.py +++ b/tests/components/switch/test_litejet.py @@ -53,9 +53,9 @@ class TestLiteJetSwitch(unittest.TestCase): '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 00000000000..96de853baff --- /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 00000000000..44cb54fc98e --- /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 00000000000..865c6c1d97a --- /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 00000000000..1b234428c94 --- /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 23fc8872570..03c95fdf897 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 78a5bf6d57e..c9ec04c5d7e 100644 --- a/tests/components/websocket_api/test_commands.py +++ b/tests/components/websocket_api/test_commands.py @@ -7,6 +7,7 @@ from homeassistant.components.websocket_api.auth import ( 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 a1e937cb244..0bb557b1488 100644 --- a/tests/components/xiaomi_miio/test_vacuum.py +++ b/tests/components/xiaomi_miio/test_vacuum.py @@ -18,7 +18,8 @@ from homeassistant.components.xiaomi_miio.vacuum import ( 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 38d7caedaad..0ccad52d6aa 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) + # 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 async_test_off_from_hass(hass, cluster, entity_id) + 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 9a9ed41381f..b5e5639bdc6 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 93fffaa4ecc..caf1dafdf8f 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 212fc247eb9..e860ff53b3d 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -5,6 +5,7 @@ import os 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 496ad785275..8991035cc22 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -6,6 +6,7 @@ from unittest.mock import MagicMock, patch 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 @@ from tests.common import ( 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,10 +34,117 @@ 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, + async_migrate_entry=mock_migrate_entry)) + + 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)) @@ -36,9 +152,10 @@ def test_call_setup_entry(hass): hass, 'comp', MockModule('comp', async_setup_entry=mock_setup_entry)) - result = yield from async_setup_component(hass, 'comp', {}) + result = await async_setup_component(hass, 'comp', {}) assert result - assert len(mock_setup_entry.mock_calls) == 1 + 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 4acb1de6677..ef9621bdac7 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -726,8 +726,7 @@ class TestServiceRegistry(unittest.TestCase): """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 @@ class TestServiceRegistry(unittest.TestCase): 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 test_service_executed_with_subservices(hass): 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 @@ async def test_service_call_event_contains_original_data(hass): '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 cceb9839d99..09f830a8eab 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 00000000000..3214c58a44d --- /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."""