Merge pull request #14392 from home-assistant/rc

0.69
This commit is contained in:
Paulus Schoutsen 2018-05-11 12:38:08 -04:00 committed by GitHub
commit c5cac04e54
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
349 changed files with 9201 additions and 2857 deletions

View File

@ -29,7 +29,7 @@ omit =
homeassistant/components/arduino.py
homeassistant/components/*/arduino.py
homeassistant/components/bmw_connected_drive.py
homeassistant/components/bmw_connected_drive/*.py
homeassistant/components/*/bmw_connected_drive.py
homeassistant/components/android_ip_webcam.py
@ -166,6 +166,9 @@ omit =
homeassistant/components/mailgun.py
homeassistant/components/*/mailgun.py
homeassistant/components/matrix.py
homeassistant/components/*/matrix.py
homeassistant/components/maxcube.py
homeassistant/components/*/maxcube.py
@ -208,6 +211,9 @@ omit =
homeassistant/components/raincloud.py
homeassistant/components/*/raincloud.py
homeassistant/components/rainmachine.py
homeassistant/components/*/rainmachine.py
homeassistant/components/raspihats.py
homeassistant/components/*/raspihats.py
@ -516,7 +522,6 @@ omit =
homeassistant/components/notify/lannouncer.py
homeassistant/components/notify/llamalab_automate.py
homeassistant/components/notify/mastodon.py
homeassistant/components/notify/matrix.py
homeassistant/components/notify/message_bird.py
homeassistant/components/notify/mycroft.py
homeassistant/components/notify/nfandroidtv.py
@ -574,6 +579,7 @@ omit =
homeassistant/components/sensor/discogs.py
homeassistant/components/sensor/dnsip.py
homeassistant/components/sensor/dovado.py
homeassistant/components/sensor/domain_expiry.py
homeassistant/components/sensor/dte_energy_bridge.py
homeassistant/components/sensor/dublin_bus_transport.py
homeassistant/components/sensor/dwd_weather_warnings.py
@ -615,6 +621,7 @@ omit =
homeassistant/components/sensor/lyft.py
homeassistant/components/sensor/metoffice.py
homeassistant/components/sensor/miflora.py
homeassistant/components/sensor/mitemp_bt.py
homeassistant/components/sensor/modem_callerid.py
homeassistant/components/sensor/mopar.py
homeassistant/components/sensor/mqtt_room.py
@ -635,6 +642,7 @@ omit =
homeassistant/components/sensor/plex.py
homeassistant/components/sensor/pocketcasts.py
homeassistant/components/sensor/pollen.py
homeassistant/components/sensor/postnl.py
homeassistant/components/sensor/pushbullet.py
homeassistant/components/sensor/pvoutput.py
homeassistant/components/sensor/pyload.py
@ -656,6 +664,7 @@ omit =
homeassistant/components/sensor/sma.py
homeassistant/components/sensor/snmp.py
homeassistant/components/sensor/sochain.py
homeassistant/components/sensor/socialblade.py
homeassistant/components/sensor/sonarr.py
homeassistant/components/sensor/speedtest.py
homeassistant/components/sensor/spotcrime.py
@ -710,7 +719,6 @@ omit =
homeassistant/components/switch/orvibo.py
homeassistant/components/switch/pulseaudio_loopback.py
homeassistant/components/switch/rainbird.py
homeassistant/components/switch/rainmachine.py
homeassistant/components/switch/rest.py
homeassistant/components/switch/rpi_rf.py
homeassistant/components/switch/snmp.py

50
.github/ISSUE_TEMPLATE/Bug_report.md vendored Normal file
View File

@ -0,0 +1,50 @@
---
name: Bug report
about: Create a report to help us improve
---
<!-- READ THIS FIRST:
- 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
- Do not report issues for components if you are using custom components: files in <config-dir>/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!
-->
**Home Assistant release with the issue:**
<!--
- Frontend -> Developer tools -> Info
- Or use this command: hass --version
-->
**Last working Home Assistant release (if known):**
**Operating environment (Hass.io/Docker/Windows/etc.):**
<!--
Please provide details about your environment.
-->
**Component/platform:**
<!--
Please add the link to the documentation at https://www.home-assistant.io/components/ of the component/platform in question.
-->
**Description of problem:**
**Problem-relevant `configuration.yaml` entries and (fill out even if it seems unimportant):**
```yaml
```
**Traceback (if applicable):**
```
```
**Additional information:**

View File

@ -54,8 +54,11 @@ homeassistant/components/device_tracker/tile.py @bachya
homeassistant/components/history_graph.py @andrey-git
homeassistant/components/light/tplink.py @rytilahti
homeassistant/components/light/yeelight.py @rytilahti
homeassistant/components/lock/nello.py @pschmitt
homeassistant/components/lock/nuki.py @pschmitt
homeassistant/components/media_player/emby.py @mezz64
homeassistant/components/media_player/kodi.py @armills
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/sonos.py @amelchio
@ -77,6 +80,7 @@ homeassistant/components/sensor/upnp.py @dgomes
homeassistant/components/sensor/waqi.py @andrey-git
homeassistant/components/switch/rainmachine.py @bachya
homeassistant/components/switch/tplink.py @rytilahti
homeassistant/components/vacuum/roomba.py @pschmitt
homeassistant/components/xiaomi_aqara.py @danielhiversen @syssi
homeassistant/components/*/axis.py @kane610
@ -90,6 +94,8 @@ homeassistant/components/*/hive.py @Rendili @KJonline
homeassistant/components/homekit/* @cdce8p
homeassistant/components/knx.py @Julius2342
homeassistant/components/*/knx.py @Julius2342
homeassistant/components/matrix.py @tinloaf
homeassistant/components/*/matrix.py @tinloaf
homeassistant/components/qwikswitch.py @kellerza
homeassistant/components/*/qwikswitch.py @kellerza
homeassistant/components/*/rfxtrx.py @danielhiversen

505
homeassistant/auth.py Normal file
View File

@ -0,0 +1,505 @@
"""Provide an authentication layer for Home Assistant."""
import asyncio
import binascii
from collections import OrderedDict
from datetime import datetime, timedelta
import os
import importlib
import logging
import uuid
import attr
import voluptuous as vol
from voluptuous.humanize import humanize_error
from homeassistant import data_entry_flow, requirements
from homeassistant.core import callback
from homeassistant.const import CONF_TYPE, CONF_NAME, CONF_ID
from homeassistant.exceptions import HomeAssistantError
from homeassistant.util.decorator import Registry
from homeassistant.util import dt as dt_util
_LOGGER = logging.getLogger(__name__)
AUTH_PROVIDERS = Registry()
AUTH_PROVIDER_SCHEMA = vol.Schema({
vol.Required(CONF_TYPE): str,
vol.Optional(CONF_NAME): str,
# Specify ID if you have two auth providers for same type.
vol.Optional(CONF_ID): str,
}, extra=vol.ALLOW_EXTRA)
ACCESS_TOKEN_EXPIRATION = timedelta(minutes=30)
DATA_REQS = 'auth_reqs_processed'
class AuthError(HomeAssistantError):
"""Generic authentication error."""
class InvalidUser(AuthError):
"""Raised when an invalid user has been specified."""
class InvalidPassword(AuthError):
"""Raised when an invalid password has been supplied."""
class UnknownError(AuthError):
"""When an unknown error occurs."""
def generate_secret(entropy=32):
"""Generate a secret.
Backport of secrets.token_hex from Python 3.6
Event loop friendly.
"""
return binascii.hexlify(os.urandom(entropy)).decode('ascii')
class AuthProvider:
"""Provider of user authentication."""
DEFAULT_TITLE = 'Unnamed auth provider'
initialized = False
def __init__(self, store, config):
"""Initialize an auth provider."""
self.store = store
self.config = config
@property
def id(self): # pylint: disable=invalid-name
"""Return id of the auth provider.
Optional, can be None.
"""
return self.config.get(CONF_ID)
@property
def type(self):
"""Return type of the provider."""
return self.config[CONF_TYPE]
@property
def name(self):
"""Return the name of the auth provider."""
return self.config.get(CONF_NAME, self.DEFAULT_TITLE)
async def async_credentials(self):
"""Return all credentials of this provider."""
return await self.store.credentials_for_provider(self.type, self.id)
@callback
def async_create_credentials(self, data):
"""Create credentials."""
return Credentials(
auth_provider_type=self.type,
auth_provider_id=self.id,
data=data,
)
# Implement by extending class
async def async_initialize(self):
"""Initialize the auth provider.
Optional.
"""
async def async_credential_flow(self):
"""Return the data flow for logging in with auth provider."""
raise NotImplementedError
async def async_get_or_create_credentials(self, flow_result):
"""Get credentials based on the flow result."""
raise NotImplementedError
async def async_user_meta_for_credentials(self, credentials):
"""Return extra user metadata for credentials.
Will be used to populate info when creating a new user.
"""
return {}
@attr.s(slots=True)
class User:
"""A user."""
id = attr.ib(type=str, default=attr.Factory(lambda: uuid.uuid4().hex))
is_owner = attr.ib(type=bool, default=False)
is_active = attr.ib(type=bool, default=False)
name = attr.ib(type=str, default=None)
# For persisting and see if saved?
# store = attr.ib(type=AuthStore, default=None)
# List of credentials of a user.
credentials = attr.ib(type=list, default=attr.Factory(list))
# Tokens associated with a user.
refresh_tokens = attr.ib(type=dict, default=attr.Factory(dict))
def as_dict(self):
"""Convert user object to a dictionary."""
return {
'id': self.id,
'is_owner': self.is_owner,
'is_active': self.is_active,
'name': self.name,
}
@attr.s(slots=True)
class RefreshToken:
"""RefreshToken for a user to grant new access tokens."""
user = attr.ib(type=User)
client_id = attr.ib(type=str)
id = attr.ib(type=str, default=attr.Factory(lambda: uuid.uuid4().hex))
created_at = attr.ib(type=datetime, default=attr.Factory(dt_util.utcnow))
access_token_expiration = attr.ib(type=timedelta,
default=ACCESS_TOKEN_EXPIRATION)
token = attr.ib(type=str,
default=attr.Factory(lambda: generate_secret(64)))
access_tokens = attr.ib(type=list, default=attr.Factory(list))
@attr.s(slots=True)
class AccessToken:
"""Access token to access the API.
These will only ever be stored in memory and not be persisted.
"""
refresh_token = attr.ib(type=RefreshToken)
created_at = attr.ib(type=datetime, default=attr.Factory(dt_util.utcnow))
token = attr.ib(type=str,
default=attr.Factory(generate_secret))
@property
def expires(self):
"""Return datetime when this token expires."""
return self.created_at + self.refresh_token.access_token_expiration
@attr.s(slots=True)
class Credentials:
"""Credentials for a user on an auth provider."""
auth_provider_type = attr.ib(type=str)
auth_provider_id = attr.ib(type=str)
# Allow the auth provider to store data to represent their auth.
data = attr.ib(type=dict)
id = attr.ib(type=str, default=attr.Factory(lambda: uuid.uuid4().hex))
is_new = attr.ib(type=bool, default=True)
@attr.s(slots=True)
class Client:
"""Client that interacts with Home Assistant on behalf of a user."""
name = attr.ib(type=str)
id = attr.ib(type=str, default=attr.Factory(lambda: uuid.uuid4().hex))
secret = attr.ib(type=str, default=attr.Factory(generate_secret))
async def load_auth_provider_module(hass, provider):
"""Load an auth provider."""
try:
module = importlib.import_module(
'homeassistant.auth_providers.{}'.format(provider))
except ImportError:
_LOGGER.warning('Unable to find auth provider %s', provider)
return None
if hass.config.skip_pip or not hasattr(module, 'REQUIREMENTS'):
return module
processed = hass.data.get(DATA_REQS)
if processed is None:
processed = hass.data[DATA_REQS] = set()
elif provider in processed:
return module
req_success = await requirements.async_process_requirements(
hass, 'auth provider {}'.format(provider), module.REQUIREMENTS)
if not req_success:
return None
return module
async def auth_manager_from_config(hass, provider_configs):
"""Initialize an auth manager from config."""
store = AuthStore(hass)
if provider_configs:
providers = await asyncio.gather(
*[_auth_provider_from_config(hass, store, config)
for config in provider_configs])
else:
providers = []
# So returned auth providers are in same order as config
provider_hash = OrderedDict()
for provider in providers:
if provider is None:
continue
key = (provider.type, provider.id)
if key in provider_hash:
_LOGGER.error(
'Found duplicate provider: %s. Please add unique IDs if you '
'want to have the same provider twice.', key)
continue
provider_hash[key] = provider
manager = AuthManager(hass, store, provider_hash)
return manager
async def _auth_provider_from_config(hass, store, config):
"""Initialize an auth provider from a config."""
provider_name = config[CONF_TYPE]
module = await load_auth_provider_module(hass, provider_name)
if module is None:
return None
try:
config = module.CONFIG_SCHEMA(config)
except vol.Invalid as err:
_LOGGER.error('Invalid configuration for auth provider %s: %s',
provider_name, humanize_error(config, err))
return None
return AUTH_PROVIDERS[provider_name](store, config)
class AuthManager:
"""Manage the authentication for Home Assistant."""
def __init__(self, hass, store, providers):
"""Initialize the auth manager."""
self._store = store
self._providers = providers
self.login_flow = data_entry_flow.FlowManager(
hass, self._async_create_login_flow,
self._async_finish_login_flow)
self.access_tokens = {}
@property
def async_auth_providers(self):
"""Return a list of available auth providers."""
return self._providers.values()
async def async_get_user(self, user_id):
"""Retrieve a user."""
return await self._store.async_get_user(user_id)
async def async_get_or_create_user(self, credentials):
"""Get or create a user."""
return await self._store.async_get_or_create_user(
credentials, self._async_get_auth_provider(credentials))
async def async_link_user(self, user, credentials):
"""Link credentials to an existing user."""
await self._store.async_link_user(user, credentials)
async def async_remove_user(self, user):
"""Remove a user."""
await self._store.async_remove_user(user)
async def async_create_refresh_token(self, user, client_id):
"""Create a new refresh token for a user."""
return await self._store.async_create_refresh_token(user, client_id)
async def async_get_refresh_token(self, token):
"""Get refresh token by token."""
return await self._store.async_get_refresh_token(token)
@callback
def async_create_access_token(self, refresh_token):
"""Create a new access token."""
access_token = AccessToken(refresh_token)
self.access_tokens[access_token.token] = access_token
return access_token
@callback
def async_get_access_token(self, token):
"""Get an access token."""
return self.access_tokens.get(token)
async def async_create_client(self, name):
"""Create a new client."""
return await self._store.async_create_client(name)
async def async_get_client(self, client_id):
"""Get a client."""
return await self._store.async_get_client(client_id)
async def _async_create_login_flow(self, handler, *, source, data):
"""Create a login flow."""
auth_provider = self._providers[handler]
if not auth_provider.initialized:
auth_provider.initialized = True
await auth_provider.async_initialize()
return await auth_provider.async_credential_flow()
async def _async_finish_login_flow(self, result):
"""Result of a credential login flow."""
auth_provider = self._providers[result['handler']]
return await auth_provider.async_get_or_create_credentials(
result['data'])
@callback
def _async_get_auth_provider(self, credentials):
"""Helper to get auth provider from a set of credentials."""
auth_provider_key = (credentials.auth_provider_type,
credentials.auth_provider_id)
return self._providers[auth_provider_key]
class AuthStore:
"""Stores authentication info.
Any mutation to an object should happen inside the auth store.
The auth store is lazy. It won't load the data from disk until a method is
called that needs it.
"""
def __init__(self, hass):
"""Initialize the auth store."""
self.hass = hass
self.users = None
self.clients = None
self._load_lock = asyncio.Lock(loop=hass.loop)
async def credentials_for_provider(self, provider_type, provider_id):
"""Return credentials for specific auth provider type and id."""
if self.users is None:
await self.async_load()
return [
credentials
for user in self.users.values()
for credentials in user.credentials
if (credentials.auth_provider_type == provider_type and
credentials.auth_provider_id == provider_id)
]
async def async_get_user(self, user_id):
"""Retrieve a user."""
if self.users is None:
await self.async_load()
return self.users.get(user_id)
async def async_get_or_create_user(self, credentials, auth_provider):
"""Get or create a new user for given credentials.
If link_user is passed in, the credentials will be linked to the passed
in user if the credentials are new.
"""
if self.users is None:
await self.async_load()
# New credentials, store in user
if credentials.is_new:
info = await auth_provider.async_user_meta_for_credentials(
credentials)
# Make owner and activate user if it's the first user.
if self.users:
is_owner = False
is_active = False
else:
is_owner = True
is_active = True
new_user = User(
is_owner=is_owner,
is_active=is_active,
name=info.get('name'),
)
self.users[new_user.id] = new_user
await self.async_link_user(new_user, credentials)
return new_user
for user in self.users.values():
for creds in user.credentials:
if (creds.auth_provider_type == credentials.auth_provider_type
and creds.auth_provider_id ==
credentials.auth_provider_id):
return user
raise ValueError('We got credentials with ID but found no user')
async def async_link_user(self, user, credentials):
"""Add credentials to an existing user."""
user.credentials.append(credentials)
await self.async_save()
credentials.is_new = False
async def async_remove_user(self, user):
"""Remove a user."""
self.users.pop(user.id)
await self.async_save()
async def async_create_refresh_token(self, user, client_id):
"""Create a new token for a user."""
refresh_token = RefreshToken(user, client_id)
user.refresh_tokens[refresh_token.token] = refresh_token
await self.async_save()
return refresh_token
async def async_get_refresh_token(self, token):
"""Get refresh token by token."""
if self.users is None:
await self.async_load()
for user in self.users.values():
refresh_token = user.refresh_tokens.get(token)
if refresh_token is not None:
return refresh_token
return None
async def async_create_client(self, name):
"""Create a new client."""
if self.clients is None:
await self.async_load()
client = Client(name)
self.clients[client.id] = client
await self.async_save()
return client
async def async_get_client(self, client_id):
"""Get a client."""
if self.clients is None:
await self.async_load()
return self.clients.get(client_id)
async def async_load(self):
"""Load the users."""
async with self._load_lock:
self.users = {}
self.clients = {}
async def async_save(self):
"""Save users."""
pass

View File

@ -0,0 +1 @@
"""Auth providers for Home Assistant."""

View File

@ -0,0 +1,116 @@
"""Example auth provider."""
from collections import OrderedDict
import hmac
import voluptuous as vol
from homeassistant import auth, data_entry_flow
from homeassistant.core import callback
USER_SCHEMA = vol.Schema({
vol.Required('username'): str,
vol.Required('password'): str,
vol.Optional('name'): str,
})
CONFIG_SCHEMA = auth.AUTH_PROVIDER_SCHEMA.extend({
vol.Required('users'): [USER_SCHEMA]
}, extra=vol.PREVENT_EXTRA)
@auth.AUTH_PROVIDERS.register('insecure_example')
class ExampleAuthProvider(auth.AuthProvider):
"""Example auth provider based on hardcoded usernames and passwords."""
async def async_credential_flow(self):
"""Return a flow to login."""
return LoginFlow(self)
@callback
def async_validate_login(self, username, password):
"""Helper to validate a username and password."""
user = None
# Compare all users to avoid timing attacks.
for usr in self.config['users']:
if hmac.compare_digest(username.encode('utf-8'),
usr['username'].encode('utf-8')):
user = usr
if user is None:
# Do one more compare to make timing the same as if user was found.
hmac.compare_digest(password.encode('utf-8'),
password.encode('utf-8'))
raise auth.InvalidUser
if not hmac.compare_digest(user['password'].encode('utf-8'),
password.encode('utf-8')):
raise auth.InvalidPassword
async def async_get_or_create_credentials(self, flow_result):
"""Get credentials based on the flow result."""
username = flow_result['username']
password = flow_result['password']
self.async_validate_login(username, password)
for credential in await self.async_credentials():
if credential.data['username'] == username:
return credential
# Create new credentials.
return self.async_create_credentials({
'username': username
})
async def async_user_meta_for_credentials(self, credentials):
"""Return extra user metadata for credentials.
Will be used to populate info when creating a new user.
"""
username = credentials.data['username']
for user in self.config['users']:
if user['username'] == username:
return {
'name': user.get('name')
}
return {}
class LoginFlow(data_entry_flow.FlowHandler):
"""Handler for the login flow."""
def __init__(self, auth_provider):
"""Initialize the login flow."""
self._auth_provider = auth_provider
async def async_step_init(self, user_input=None):
"""Handle the step of the form."""
errors = {}
if user_input is not None:
try:
self._auth_provider.async_validate_login(
user_input['username'], user_input['password'])
except (auth.InvalidUser, auth.InvalidPassword):
errors['base'] = 'invalid_auth'
if not errors:
return self.async_create_entry(
title=self._auth_provider.name,
data=user_input
)
schema = OrderedDict()
schema['username'] = str
schema['password'] = str
return self.async_show_form(
step_id='init',
data_schema=vol.Schema(schema),
errors=errors,
)

View File

@ -12,8 +12,7 @@ from typing import Any, Optional, Dict
import voluptuous as vol
from homeassistant import (
core, config as conf_util, config_entries, loader,
components as core_components)
core, config as conf_util, config_entries, components as core_components)
from homeassistant.components import persistent_notification
from homeassistant.const import EVENT_HOMEASSISTANT_CLOSE
from homeassistant.setup import async_setup_component
@ -67,16 +66,15 @@ def from_config_dict(config: Dict[str, Any],
return hass
@asyncio.coroutine
def async_from_config_dict(config: Dict[str, Any],
hass: core.HomeAssistant,
config_dir: Optional[str] = None,
enable_log: bool = True,
verbose: bool = False,
skip_pip: bool = False,
log_rotate_days: Any = None,
log_file: Any = None,
log_no_color: bool = False) \
async def async_from_config_dict(config: Dict[str, Any],
hass: core.HomeAssistant,
config_dir: Optional[str] = None,
enable_log: bool = True,
verbose: bool = False,
skip_pip: bool = False,
log_rotate_days: Any = None,
log_file: Any = None,
log_no_color: bool = False) \
-> Optional[core.HomeAssistant]:
"""Try to configure Home Assistant from a configuration dictionary.
@ -92,27 +90,24 @@ def async_from_config_dict(config: Dict[str, Any],
core_config = config.get(core.DOMAIN, {})
try:
yield from conf_util.async_process_ha_core_config(hass, core_config)
await conf_util.async_process_ha_core_config(hass, core_config)
except vol.Invalid as ex:
conf_util.async_log_exception(ex, 'homeassistant', core_config, hass)
return None
yield from hass.async_add_job(conf_util.process_ha_config_upgrade, hass)
await hass.async_add_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")
if not loader.PREPARED:
yield from hass.async_add_job(loader.prepare, hass)
# Make a copy because we are mutating it.
config = OrderedDict(config)
# Merge packages
conf_util.merge_packages_config(
config, core_config.get(conf_util.CONF_PACKAGES, {}))
hass, config, core_config.get(conf_util.CONF_PACKAGES, {}))
# Ensure we have no None values after merge
for key, value in config.items():
@ -120,7 +115,7 @@ def async_from_config_dict(config: Dict[str, Any],
config[key] = {}
hass.config_entries = config_entries.ConfigEntries(hass, config)
yield from hass.config_entries.async_load()
await hass.config_entries.async_load()
# Filter out the repeating and common config section [homeassistant]
components = set(key.split(' ')[0] for key in config.keys()
@ -129,13 +124,13 @@ def async_from_config_dict(config: Dict[str, Any],
# setup components
# pylint: disable=not-an-iterable
res = yield from core_components.async_setup(hass, config)
res = await core_components.async_setup(hass, config)
if not res:
_LOGGER.error("Home Assistant core failed to initialize. "
"further initialization aborted")
return hass
yield from persistent_notification.async_setup(hass, config)
await persistent_notification.async_setup(hass, config)
_LOGGER.info("Home Assistant core initialized")
@ -145,7 +140,7 @@ def async_from_config_dict(config: Dict[str, Any],
continue
hass.async_add_job(async_setup_component(hass, component, config))
yield from hass.async_block_till_done()
await hass.async_block_till_done()
# stage 2
for component in components:
@ -153,7 +148,7 @@ def async_from_config_dict(config: Dict[str, Any],
continue
hass.async_add_job(async_setup_component(hass, component, config))
yield from hass.async_block_till_done()
await hass.async_block_till_done()
stop = time()
_LOGGER.info("Home Assistant initialized in %.2fs", stop-start)
@ -187,14 +182,13 @@ def from_config_file(config_path: str,
return hass
@asyncio.coroutine
def async_from_config_file(config_path: str,
hass: core.HomeAssistant,
verbose: bool = False,
skip_pip: bool = True,
log_rotate_days: Any = None,
log_file: Any = None,
log_no_color: bool = False):
async def async_from_config_file(config_path: str,
hass: core.HomeAssistant,
verbose: bool = False,
skip_pip: bool = True,
log_rotate_days: Any = None,
log_file: Any = None,
log_no_color: bool = False):
"""Read the configuration file and try to start all the functionality.
Will add functionality to 'hass' parameter.
@ -203,13 +197,13 @@ def async_from_config_file(config_path: str,
# Set config dir to directory holding config file
config_dir = os.path.abspath(os.path.dirname(config_path))
hass.config.config_dir = config_dir
yield from async_mount_local_lib_path(config_dir, hass.loop)
await async_mount_local_lib_path(config_dir, hass.loop)
async_enable_logging(hass, verbose, log_rotate_days, log_file,
log_no_color)
try:
config_dict = yield from hass.async_add_job(
config_dict = await hass.async_add_job(
conf_util.load_yaml_config_file, config_path)
except HomeAssistantError as err:
_LOGGER.error("Error loading %s: %s", config_path, err)
@ -217,7 +211,7 @@ def async_from_config_file(config_path: str,
finally:
clear_secret_cache()
hass = yield from async_from_config_dict(
hass = await async_from_config_dict(
config_dict, hass, enable_log=False, skip_pip=skip_pip)
return hass
@ -294,11 +288,10 @@ def async_enable_logging(hass: core.HomeAssistant,
async_handler = AsyncHandler(hass.loop, err_handler)
@asyncio.coroutine
def async_stop_async_handler(event):
async def async_stop_async_handler(event):
"""Cleanup async handler."""
logging.getLogger('').removeHandler(async_handler)
yield from async_handler.async_close(blocking=True)
await async_handler.async_close(blocking=True)
hass.bus.async_listen_once(
EVENT_HOMEASSISTANT_CLOSE, async_stop_async_handler)
@ -323,15 +316,14 @@ def mount_local_lib_path(config_dir: str) -> str:
return deps_dir
@asyncio.coroutine
def async_mount_local_lib_path(config_dir: str,
loop: asyncio.AbstractEventLoop) -> str:
async def async_mount_local_lib_path(config_dir: str,
loop: asyncio.AbstractEventLoop) -> str:
"""Add local library to Python Path.
This function is a coroutine.
"""
deps_dir = os.path.join(config_dir, 'deps')
lib_dir = yield from async_get_user_site(deps_dir, loop=loop)
lib_dir = await async_get_user_site(deps_dir, loop=loop)
if lib_dir not in sys.path:
sys.path.insert(0, lib_dir)
return deps_dir

View File

@ -81,7 +81,7 @@ TRIGGER_SCHEMA = vol.Schema({
ABODE_PLATFORMS = [
'alarm_control_panel', 'binary_sensor', 'lock', 'switch', 'cover',
'camera', 'light'
'camera', 'light', 'sensor'
]

View File

@ -17,7 +17,7 @@ from homeassistant.const import (
from homeassistant.helpers.aiohttp_client import async_get_clientsession
import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['pyalarmdotcom==0.3.1']
REQUIREMENTS = ['pyalarmdotcom==0.3.2']
_LOGGER = logging.getLogger(__name__)
@ -93,6 +93,13 @@ class AlarmDotCom(alarm.AlarmControlPanel):
return STATE_ALARM_ARMED_AWAY
return STATE_UNKNOWN
@property
def device_state_attributes(self):
"""Return the state attributes."""
return {
'sensor_status': self._alarm.sensor_status
}
@asyncio.coroutine
def async_alarm_disarm(self, code=None):
"""Send disarm command."""

View File

@ -76,8 +76,7 @@ class APIEventStream(HomeAssistantView):
url = URL_API_STREAM
name = "api:stream"
@asyncio.coroutine
def get(self, request):
async def get(self, request):
"""Provide a streaming interface for the event bus."""
# pylint: disable=no-self-use
hass = request.app['hass']
@ -88,8 +87,7 @@ class APIEventStream(HomeAssistantView):
if restrict:
restrict = restrict.split(',') + [EVENT_HOMEASSISTANT_STOP]
@asyncio.coroutine
def forward_events(event):
async def forward_events(event):
"""Forward events to the open request."""
if event.event_type == EVENT_TIME_CHANGED:
return
@ -104,11 +102,11 @@ class APIEventStream(HomeAssistantView):
else:
data = json.dumps(event, cls=rem.JSONEncoder)
yield from to_write.put(data)
await to_write.put(data)
response = web.StreamResponse()
response.content_type = 'text/event-stream'
yield from response.prepare(request)
await response.prepare(request)
unsub_stream = hass.bus.async_listen(MATCH_ALL, forward_events)
@ -116,13 +114,13 @@ class APIEventStream(HomeAssistantView):
_LOGGER.debug('STREAM %s ATTACHED', id(stop_obj))
# Fire off one message so browsers fire open event right away
yield from to_write.put(STREAM_PING_PAYLOAD)
await to_write.put(STREAM_PING_PAYLOAD)
while True:
try:
with async_timeout.timeout(STREAM_PING_INTERVAL,
loop=hass.loop):
payload = yield from to_write.get()
payload = await to_write.get()
if payload is stop_obj:
break
@ -130,9 +128,9 @@ class APIEventStream(HomeAssistantView):
msg = "data: {}\n\n".format(payload)
_LOGGER.debug('STREAM %s WRITING %s', id(stop_obj),
msg.strip())
yield from response.write(msg.encode("UTF-8"))
await response.write(msg.encode("UTF-8"))
except asyncio.TimeoutError:
yield from to_write.put(STREAM_PING_PAYLOAD)
await to_write.put(STREAM_PING_PAYLOAD)
except asyncio.CancelledError:
_LOGGER.debug('STREAM %s ABORT', id(stop_obj))
@ -200,12 +198,11 @@ class APIEntityStateView(HomeAssistantView):
return self.json(state)
return self.json_message('Entity not found', HTTP_NOT_FOUND)
@asyncio.coroutine
def post(self, request, entity_id):
async def post(self, request, entity_id):
"""Update state of entity."""
hass = request.app['hass']
try:
data = yield from request.json()
data = await request.json()
except ValueError:
return self.json_message('Invalid JSON specified',
HTTP_BAD_REQUEST)
@ -257,10 +254,9 @@ class APIEventView(HomeAssistantView):
url = '/api/events/{event_type}'
name = "api:event"
@asyncio.coroutine
def post(self, request, event_type):
async def post(self, request, event_type):
"""Fire events."""
body = yield from request.text()
body = await request.text()
try:
event_data = json.loads(body) if body else None
except ValueError:
@ -292,10 +288,9 @@ class APIServicesView(HomeAssistantView):
url = URL_API_SERVICES
name = "api:services"
@asyncio.coroutine
def get(self, request):
async def get(self, request):
"""Get registered services."""
services = yield from async_services_json(request.app['hass'])
services = await async_services_json(request.app['hass'])
return self.json(services)
@ -305,14 +300,13 @@ class APIDomainServicesView(HomeAssistantView):
url = "/api/services/{domain}/{service}"
name = "api:domain-services"
@asyncio.coroutine
def post(self, request, domain, service):
async def post(self, request, domain, service):
"""Call a service.
Returns a list of changed states.
"""
hass = request.app['hass']
body = yield from request.text()
body = await request.text()
try:
data = json.loads(body) if body else None
except ValueError:
@ -320,7 +314,7 @@ class APIDomainServicesView(HomeAssistantView):
HTTP_BAD_REQUEST)
with AsyncTrackStates(hass) as changed_states:
yield from hass.services.async_call(domain, service, data, True)
await hass.services.async_call(domain, service, data, True)
return self.json(changed_states)
@ -343,11 +337,10 @@ class APITemplateView(HomeAssistantView):
url = URL_API_TEMPLATE
name = "api:template"
@asyncio.coroutine
def post(self, request):
async def post(self, request):
"""Render a template."""
try:
data = yield from request.json()
data = await request.json()
tpl = template.Template(data['template'], request.app['hass'])
return tpl.async_render(data.get('variables'))
except (ValueError, TemplateError) as ex:
@ -366,10 +359,9 @@ class APIErrorLog(HomeAssistantView):
return await self.file(request, request.app['hass'].data[DATA_LOGGING])
@asyncio.coroutine
def async_services_json(hass):
async def async_services_json(hass):
"""Generate services data to JSONify."""
descriptions = yield from async_get_all_descriptions(hass)
descriptions = await async_get_all_descriptions(hass)
return [{"domain": key, "services": value}
for key, value in descriptions.items()]

View File

@ -0,0 +1,344 @@
"""Component to allow users to login and get tokens.
All requests will require passing in a valid client ID and secret via HTTP
Basic Auth.
# GET /auth/providers
Return a list of auth providers. Example:
[
{
"name": "Local",
"id": null,
"type": "local_provider",
}
]
# POST /auth/login_flow
Create a login flow. Will return the first step of the flow.
Pass in parameter 'handler' to specify the auth provider to use. Auth providers
are identified by type and id.
{
"handler": ["local_provider", null]
}
Return value will be a step in a data entry flow. See the docs for data entry
flow for details.
{
"data_schema": [
{"name": "username", "type": "string"},
{"name": "password", "type": "string"}
],
"errors": {},
"flow_id": "8f7e42faab604bcab7ac43c44ca34d58",
"handler": ["insecure_example", null],
"step_id": "init",
"type": "form"
}
# POST /auth/login_flow/{flow_id}
Progress the flow. Most flows will be 1 page, but could optionally add extra
login challenges, like TFA. Once the flow has finished, the returned step will
have type "create_entry" and "result" key will contain an authorization code.
{
"flow_id": "8f7e42faab604bcab7ac43c44ca34d58",
"handler": ["insecure_example", null],
"result": "411ee2f916e648d691e937ae9344681e",
"source": "user",
"title": "Example",
"type": "create_entry",
"version": 1
}
# POST /auth/token
This is an OAuth2 endpoint for granting tokens. We currently support the grant
types "authorization_code" and "refresh_token". Because we follow the OAuth2
spec, data should be send in formatted as x-www-form-urlencoded. Examples will
be in JSON as it's more readable.
## Grant type authorization_code
Exchange the authorization code retrieved from the login flow for tokens.
{
"grant_type": "authorization_code",
"code": "411ee2f916e648d691e937ae9344681e"
}
Return value will be the access and refresh tokens. The access token will have
a limited expiration. New access tokens can be requested using the refresh
token.
{
"access_token": "ABCDEFGH",
"expires_in": 1800,
"refresh_token": "IJKLMNOPQRST",
"token_type": "Bearer"
}
## Grant type refresh_token
Request a new access token using a refresh token.
{
"grant_type": "refresh_token",
"refresh_token": "IJKLMNOPQRST"
}
Return value will be a new access token. The access token will have
a limited expiration.
{
"access_token": "ABCDEFGH",
"expires_in": 1800,
"token_type": "Bearer"
}
"""
import logging
import uuid
import aiohttp.web
import voluptuous as vol
from homeassistant import data_entry_flow
from homeassistant.core import callback
from homeassistant.helpers.data_entry_flow import (
FlowManagerIndexView, FlowManagerResourceView)
from homeassistant.components.http.view import HomeAssistantView
from homeassistant.components.http.data_validator import RequestDataValidator
from .client import verify_client
DOMAIN = 'auth'
DEPENDENCIES = ['http']
_LOGGER = logging.getLogger(__name__)
async def async_setup(hass, config):
"""Component to allow users to login."""
store_credentials, retrieve_credentials = _create_cred_store()
hass.http.register_view(AuthProvidersView)
hass.http.register_view(LoginFlowIndexView(hass.auth.login_flow))
hass.http.register_view(
LoginFlowResourceView(hass.auth.login_flow, store_credentials))
hass.http.register_view(GrantTokenView(retrieve_credentials))
hass.http.register_view(LinkUserView(retrieve_credentials))
return True
class AuthProvidersView(HomeAssistantView):
"""View to get available auth providers."""
url = '/auth/providers'
name = 'api:auth:providers'
requires_auth = False
@verify_client
async def get(self, request, client_id):
"""Get available auth providers."""
return self.json([{
'name': provider.name,
'id': provider.id,
'type': provider.type,
} for provider in request.app['hass'].auth.async_auth_providers])
class LoginFlowIndexView(FlowManagerIndexView):
"""View to create a config flow."""
url = '/auth/login_flow'
name = 'api:auth:login_flow'
requires_auth = False
async def get(self, request):
"""Do not allow index of flows in progress."""
return aiohttp.web.Response(status=405)
# pylint: disable=arguments-differ
@verify_client
async def post(self, request, client_id):
"""Create a new login flow."""
# pylint: disable=no-value-for-parameter
return await super().post(request)
class LoginFlowResourceView(FlowManagerResourceView):
"""View to interact with the flow manager."""
url = '/auth/login_flow/{flow_id}'
name = 'api:auth:login_flow:resource'
requires_auth = False
def __init__(self, flow_mgr, store_credentials):
"""Initialize the login flow resource view."""
super().__init__(flow_mgr)
self._store_credentials = store_credentials
# pylint: disable=arguments-differ
async def get(self, request):
"""Do not allow getting status of a flow in progress."""
return self.json_message('Invalid flow specified', 404)
# pylint: disable=arguments-differ
@verify_client
@RequestDataValidator(vol.Schema(dict), allow_empty=True)
async def post(self, request, client_id, flow_id, data):
"""Handle progressing a login flow request."""
try:
result = await self._flow_mgr.async_configure(flow_id, data)
except data_entry_flow.UnknownFlow:
return self.json_message('Invalid flow specified', 404)
except vol.Invalid:
return self.json_message('User input malformed', 400)
if result['type'] != data_entry_flow.RESULT_TYPE_CREATE_ENTRY:
return self.json(self._prepare_result_json(result))
result.pop('data')
result['result'] = self._store_credentials(client_id, result['result'])
return self.json(result)
class GrantTokenView(HomeAssistantView):
"""View to grant tokens."""
url = '/auth/token'
name = 'api:auth:token'
requires_auth = False
def __init__(self, retrieve_credentials):
"""Initialize the grant token view."""
self._retrieve_credentials = retrieve_credentials
@verify_client
async def post(self, request, client_id):
"""Grant a token."""
hass = request.app['hass']
data = await request.post()
grant_type = data.get('grant_type')
if grant_type == 'authorization_code':
return await self._async_handle_auth_code(
hass, client_id, data)
elif grant_type == 'refresh_token':
return await self._async_handle_refresh_token(
hass, client_id, data)
return self.json({
'error': 'unsupported_grant_type',
}, status_code=400)
async def _async_handle_auth_code(self, hass, client_id, data):
"""Handle authorization code request."""
code = data.get('code')
if code is None:
return self.json({
'error': 'invalid_request',
}, status_code=400)
credentials = self._retrieve_credentials(client_id, code)
if credentials is None:
return self.json({
'error': 'invalid_request',
}, status_code=400)
user = await hass.auth.async_get_or_create_user(credentials)
refresh_token = await hass.auth.async_create_refresh_token(user,
client_id)
access_token = hass.auth.async_create_access_token(refresh_token)
return self.json({
'access_token': access_token.token,
'token_type': 'Bearer',
'refresh_token': refresh_token.token,
'expires_in':
int(refresh_token.access_token_expiration.total_seconds()),
})
async def _async_handle_refresh_token(self, hass, client_id, data):
"""Handle authorization code request."""
token = data.get('refresh_token')
if token is None:
return self.json({
'error': 'invalid_request',
}, status_code=400)
refresh_token = await hass.auth.async_get_refresh_token(token)
if refresh_token is None or refresh_token.client_id != client_id:
return self.json({
'error': 'invalid_grant',
}, status_code=400)
access_token = hass.auth.async_create_access_token(refresh_token)
return self.json({
'access_token': access_token.token,
'token_type': 'Bearer',
'expires_in':
int(refresh_token.access_token_expiration.total_seconds()),
})
class LinkUserView(HomeAssistantView):
"""View to link existing users to new credentials."""
url = '/auth/link_user'
name = 'api:auth:link_user'
def __init__(self, retrieve_credentials):
"""Initialize the link user view."""
self._retrieve_credentials = retrieve_credentials
@RequestDataValidator(vol.Schema({
'code': str,
'client_id': str,
}))
async def post(self, request, data):
"""Link a user."""
hass = request.app['hass']
user = request['hass_user']
credentials = self._retrieve_credentials(
data['client_id'], data['code'])
if credentials is None:
return self.json_message('Invalid code', status_code=400)
await hass.auth.async_link_user(user, credentials)
return self.json_message('User linked')
@callback
def _create_cred_store():
"""Create a credential store."""
temp_credentials = {}
@callback
def store_credentials(client_id, credentials):
"""Store credentials and return a code to retrieve it."""
code = uuid.uuid4().hex
temp_credentials[(client_id, code)] = credentials
return code
@callback
def retrieve_credentials(client_id, code):
"""Retrieve credentials."""
return temp_credentials.pop((client_id, code), None)
return store_credentials, retrieve_credentials

View File

@ -0,0 +1,63 @@
"""Helpers to resolve client ID/secret."""
import base64
from functools import wraps
import hmac
import aiohttp.hdrs
def verify_client(method):
"""Decorator to verify client id/secret on requests."""
@wraps(method)
async def wrapper(view, request, *args, **kwargs):
"""Verify client id/secret before doing request."""
client_id = await _verify_client(request)
if client_id is None:
return view.json({
'error': 'invalid_client',
}, status_code=401)
return await method(
view, request, *args, client_id=client_id, **kwargs)
return wrapper
async def _verify_client(request):
"""Method to verify the client id/secret in consistent time.
By using a consistent time for looking up client id and comparing the
secret, we prevent attacks by malicious actors trying different client ids
and are able to derive from the time it takes to process the request if
they guessed the client id correctly.
"""
if aiohttp.hdrs.AUTHORIZATION not in request.headers:
return None
auth_type, auth_value = \
request.headers.get(aiohttp.hdrs.AUTHORIZATION).split(' ', 1)
if auth_type != 'Basic':
return None
decoded = base64.b64decode(auth_value).decode('utf-8')
try:
client_id, client_secret = decoded.split(':', 1)
except ValueError:
# If no ':' in decoded
return None
client = await request.app['hass'].auth.async_get_client(client_id)
if client is None:
# Still do a compare so we run same time as if a client was found.
hmac.compare_digest(client_secret.encode('utf-8'),
client_secret.encode('utf-8'))
return None
if hmac.compare_digest(client_secret.encode('utf-8'),
client.secret.encode('utf-8')):
return client_id
return None

View File

@ -6,6 +6,7 @@ https://home-assistant.io/components/automation/
"""
import asyncio
from functools import partial
import importlib
import logging
import voluptuous as vol
@ -22,7 +23,6 @@ from homeassistant.helpers import extract_domain_configs, script, condition
from homeassistant.helpers.entity import ToggleEntity
from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.restore_state import async_get_last_state
from homeassistant.loader import get_platform
from homeassistant.util.dt import utcnow
import homeassistant.helpers.config_validation as cv
@ -58,12 +58,14 @@ _LOGGER = logging.getLogger(__name__)
def _platform_validator(config):
"""Validate it is a valid platform."""
platform = get_platform(DOMAIN, config[CONF_PLATFORM])
try:
platform = importlib.import_module(
'homeassistant.components.automation.{}'.format(
config[CONF_PLATFORM]))
except ImportError:
raise vol.Invalid('Invalid platform specified') from None
if not hasattr(platform, 'TRIGGER_SCHEMA'):
return config
return getattr(platform, 'TRIGGER_SCHEMA')(config)
return platform.TRIGGER_SCHEMA(config)
_TRIGGER_SCHEMA = vol.All(
@ -71,7 +73,7 @@ _TRIGGER_SCHEMA = vol.All(
[
vol.All(
vol.Schema({
vol.Required(CONF_PLATFORM): cv.platform_validator(DOMAIN)
vol.Required(CONF_PLATFORM): str
}, extra=vol.ALLOW_EXTRA),
_platform_validator
),

View File

@ -50,13 +50,23 @@ DEVICE_CLASSES_SCHEMA = vol.All(vol.Lower, vol.In(DEVICE_CLASSES))
async def async_setup(hass, config):
"""Track states and offer events for binary sensors."""
component = EntityComponent(
component = hass.data[DOMAIN] = EntityComponent(
logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL)
await component.async_setup(config)
return True
async def async_setup_entry(hass, entry):
"""Setup a config entry."""
return await hass.data[DOMAIN].async_setup_entry(entry)
async def async_unload_entry(hass, entry):
"""Unload a config entry."""
return await hass.data[DOMAIN].async_unload_entry(entry)
# pylint: disable=no-self-use
class BinarySensorDevice(Entity):
"""Represent a binary sensor."""

View File

@ -11,7 +11,6 @@ import voluptuous as vol
from homeassistant.components.binary_sensor import (
BinarySensorDevice, PLATFORM_SCHEMA)
from homeassistant.const import CONF_MONITORED_CONDITIONS
from homeassistant.loader import get_component
import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__)
@ -31,7 +30,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the available BloomSky weather binary sensors."""
bloomsky = get_component('bloomsky')
bloomsky = hass.components.bloomsky
# Default needed in case of discovery
sensors = config.get(CONF_MONITORED_CONDITIONS, SENSOR_TYPES)

View File

@ -6,27 +6,35 @@ https://home-assistant.io/components/binary_sensor.deconz/
"""
from homeassistant.components.binary_sensor import BinarySensorDevice
from homeassistant.components.deconz import (
DOMAIN as DATA_DECONZ, DATA_DECONZ_ID)
DOMAIN as DATA_DECONZ, DATA_DECONZ_ID, DATA_DECONZ_UNSUB)
from homeassistant.const import ATTR_BATTERY_LEVEL
from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
DEPENDENCIES = ['deconz']
async def async_setup_platform(hass, config, async_add_devices,
discovery_info=None):
"""Old way of setting up deCONZ binary sensors."""
pass
async def async_setup_entry(hass, config_entry, async_add_devices):
"""Set up the deCONZ binary sensor."""
if discovery_info is None:
return
@callback
def async_add_sensor(sensors):
"""Add binary sensor from deCONZ."""
from pydeconz.sensor import DECONZ_BINARY_SENSOR
entities = []
for sensor in sensors:
if sensor.type in DECONZ_BINARY_SENSOR:
entities.append(DeconzBinarySensor(sensor))
async_add_devices(entities, True)
hass.data[DATA_DECONZ_UNSUB].append(
async_dispatcher_connect(hass, 'deconz_new_sensor', async_add_sensor))
from pydeconz.sensor import DECONZ_BINARY_SENSOR
sensors = hass.data[DATA_DECONZ].sensors
entities = []
for sensor in sensors.values():
if sensor and sensor.type in DECONZ_BINARY_SENSOR:
entities.append(DeconzBinarySensor(sensor))
async_add_devices(entities, True)
async_add_sensor(hass.data[DATA_DECONZ].sensors.values())
class DeconzBinarySensor(BinarySensorDevice):

View File

@ -23,7 +23,7 @@ SENSOR_TYPES = {'openClosedSensor': 'opening',
@asyncio.coroutine
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
"""Set up the INSTEON PLM device class for the hass platform."""
plm = hass.data['insteon_plm']
plm = hass.data['insteon_plm'].get('plm')
address = discovery_info['address']
device = plm.devices[address]

View File

@ -13,7 +13,6 @@ import voluptuous as vol
from homeassistant.components.binary_sensor import (
BinarySensorDevice, PLATFORM_SCHEMA)
from homeassistant.components.netatmo import CameraData
from homeassistant.loader import get_component
from homeassistant.const import CONF_TIMEOUT
from homeassistant.helpers import config_validation as cv
@ -61,7 +60,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the access to Netatmo binary sensor."""
netatmo = get_component('netatmo')
netatmo = hass.components.netatmo
home = config.get(CONF_HOME)
timeout = config.get(CONF_TIMEOUT)
if timeout is None:

View File

@ -14,7 +14,7 @@ from homeassistant.components.binary_sensor import (
from homeassistant.const import CONF_NAME
import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['tapsaff==0.1.3']
REQUIREMENTS = ['tapsaff==0.2.0']
_LOGGER = logging.getLogger(__name__)

View File

@ -23,7 +23,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.14.2']
REQUIREMENTS = ['numpy==1.14.3']
_LOGGER = logging.getLogger(__name__)

View File

@ -7,7 +7,6 @@ https://home-assistant.io/components/binary_sensor.wemo/
import logging
from homeassistant.components.binary_sensor import BinarySensorDevice
from homeassistant.loader import get_component
DEPENDENCIES = ['wemo']
@ -25,18 +24,18 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
device = discovery.device_from_description(location, mac)
if device:
add_devices_callback([WemoBinarySensor(device)])
add_devices_callback([WemoBinarySensor(hass, device)])
class WemoBinarySensor(BinarySensorDevice):
"""Representation a WeMo binary sensor."""
def __init__(self, device):
def __init__(self, hass, device):
"""Initialize the WeMo sensor."""
self.wemo = device
self._state = None
wemo = get_component('wemo')
wemo = hass.components.wemo
wemo.SUBSCRIPTION_REGISTRY.register(self.wemo)
wemo.SUBSCRIPTION_REGISTRY.on(self.wemo, None, self._update_callback)

View File

@ -17,16 +17,17 @@ import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__)
REQUIREMENTS = ['holidays==0.9.4']
REQUIREMENTS = ['holidays==0.9.5']
# List of all countries currently supported by holidays
# There seems to be no way to get the list out at runtime
ALL_COUNTRIES = ['Australia', 'AU', 'Austria', 'AT', 'Belgium', 'BE', 'Canada',
'CA', 'Colombia', 'CO', 'Czech', 'CZ', 'Denmark', 'DK',
'England', 'EuropeanCentralBank', 'ECB', 'TAR', 'Finland',
'FI', 'France', 'FRA', 'Germany', 'DE', 'Ireland',
'Isle of Man', 'Italy', 'IT', 'Japan', 'JP', 'Mexico', 'MX',
'Netherlands', 'NL', 'NewZealand', 'NZ', 'Northern Ireland',
ALL_COUNTRIES = ['Argentina', 'AR', 'Australia', 'AU', 'Austria', 'AT',
'Belgium', 'BE', 'Canada', 'CA', 'Colombia', 'CO', 'Czech',
'CZ', 'Denmark', 'DK', 'England', 'EuropeanCentralBank',
'ECB', 'TAR', 'Finland', 'FI', 'France', 'FRA', 'Germany',
'DE', 'Hungary', 'HU', 'Ireland', 'Isle of Man', 'Italy',
'IT', 'Japan', 'JP', 'Mexico', 'MX', 'Netherlands', 'NL',
'NewZealand', 'NZ', 'Northern Ireland',
'Norway', 'NO', 'Polish', 'PL', 'Portugal', 'PT',
'PortugalExt', 'PTE', 'Scotland', 'Slovenia', 'SI',
'Slovakia', 'SK', 'South Africa', 'ZA', 'Spain', 'ES',

View File

@ -25,30 +25,35 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
for (_, gateway) in hass.data[PY_XIAOMI_GATEWAY].gateways.items():
for device in gateway.devices['binary_sensor']:
model = device['model']
if model in ['motion', 'sensor_motion.aq2']:
if model in ['motion', 'sensor_motion', 'sensor_motion.aq2']:
devices.append(XiaomiMotionSensor(device, hass, gateway))
elif model in ['magnet', 'sensor_magnet.aq2']:
elif model in ['magnet', 'sensor_magnet', 'sensor_magnet.aq2']:
devices.append(XiaomiDoorSensor(device, gateway))
elif model == 'sensor_wleak.aq1':
devices.append(XiaomiWaterLeakSensor(device, gateway))
elif model == 'smoke':
elif model in ['smoke', 'sensor_smoke']:
devices.append(XiaomiSmokeSensor(device, gateway))
elif model == 'natgas':
elif model in ['natgas', 'sensor_natgas']:
devices.append(XiaomiNatgasSensor(device, gateway))
elif model in ['switch', 'sensor_switch.aq2', 'sensor_switch.aq3']:
devices.append(XiaomiButton(device, 'Switch', 'status',
elif model in ['switch', 'sensor_switch',
'sensor_switch.aq2', 'sensor_switch.aq3']:
if 'proto' not in device or int(device['proto'][0:1]) == 1:
data_key = 'status'
else:
data_key = 'channel_0'
devices.append(XiaomiButton(device, 'Switch', data_key,
hass, gateway))
elif model == '86sw1':
elif model in ['86sw1', 'sensor_86sw1.aq1']:
devices.append(XiaomiButton(device, 'Wall Switch', 'channel_0',
hass, gateway))
elif model == '86sw2':
elif model in ['86sw2', 'sensor_86sw2.aq1']:
devices.append(XiaomiButton(device, 'Wall Switch (Left)',
'channel_0', hass, gateway))
devices.append(XiaomiButton(device, 'Wall Switch (Right)',
'channel_1', hass, gateway))
devices.append(XiaomiButton(device, 'Wall Switch (Both)',
'dual_channel', hass, gateway))
elif model == 'cube':
elif model in ['cube', 'sensor_cube']:
devices.append(XiaomiCube(device, hass, gateway))
add_devices(devices)
@ -129,8 +134,12 @@ class XiaomiMotionSensor(XiaomiBinarySensor):
"""Initialize the XiaomiMotionSensor."""
self._hass = hass
self._no_motion_since = 0
if 'proto' not in device or int(device['proto'][0:1]) == 1:
data_key = 'status'
else:
data_key = 'motion_status'
XiaomiBinarySensor.__init__(self, device, 'Motion Sensor', xiaomi_hub,
'status', 'motion')
data_key, 'motion')
@property
def device_state_attributes(self):

View File

@ -31,12 +31,21 @@ async def async_setup_platform(hass, config, async_add_devices,
if discovery_info is None:
return
from zigpy.zcl.clusters.general import OnOff
from zigpy.zcl.clusters.security import IasZone
if IasZone.cluster_id in discovery_info['in_clusters']:
await _async_setup_iaszone(hass, config, async_add_devices,
discovery_info)
elif OnOff.cluster_id in discovery_info['out_clusters']:
await _async_setup_remote(hass, config, async_add_devices,
discovery_info)
in_clusters = discovery_info['in_clusters']
async def _async_setup_iaszone(hass, config, async_add_devices,
discovery_info):
device_class = None
cluster = in_clusters[IasZone.cluster_id]
from zigpy.zcl.clusters.security import IasZone
cluster = discovery_info['in_clusters'][IasZone.cluster_id]
if discovery_info['new_join']:
await cluster.bind()
ieee = cluster.endpoint.device.application.ieee
@ -53,8 +62,34 @@ async def async_setup_platform(hass, config, async_add_devices,
async_add_devices([sensor], update_before_add=True)
async def _async_setup_remote(hass, config, async_add_devices, discovery_info):
async def safe(coro):
"""Run coro, catching ZigBee delivery errors, and ignoring them."""
import zigpy.exceptions
try:
await coro
except zigpy.exceptions.DeliveryError as exc:
_LOGGER.warning("Ignoring error during setup: %s", exc)
if discovery_info['new_join']:
from zigpy.zcl.clusters.general import OnOff, LevelControl
out_clusters = discovery_info['out_clusters']
if OnOff.cluster_id in out_clusters:
cluster = out_clusters[OnOff.cluster_id]
await safe(cluster.bind())
await safe(cluster.configure_reporting(0, 0, 600, 1))
if LevelControl.cluster_id in out_clusters:
cluster = out_clusters[LevelControl.cluster_id]
await safe(cluster.bind())
await safe(cluster.configure_reporting(0, 1, 600, 1))
sensor = Switch(**discovery_info)
async_add_devices([sensor], update_before_add=True)
class BinarySensor(zha.Entity, BinarySensorDevice):
"""THe ZHA Binary Sensor."""
"""The ZHA Binary Sensor."""
_domain = DOMAIN
@ -102,3 +137,113 @@ class BinarySensor(zha.Entity, BinarySensorDevice):
state = result.get('zone_status', self._state)
if isinstance(state, (int, uint16_t)):
self._state = result.get('zone_status', self._state) & 3
class Switch(zha.Entity, BinarySensorDevice):
"""ZHA switch/remote controller/button."""
_domain = DOMAIN
class OnOffListener:
"""Listener for the OnOff ZigBee cluster."""
def __init__(self, entity):
"""Initialize OnOffListener."""
self._entity = entity
def cluster_command(self, tsn, command_id, args):
"""Handle commands received to this cluster."""
if command_id in (0x0000, 0x0040):
self._entity.set_state(False)
elif command_id in (0x0001, 0x0041, 0x0042):
self._entity.set_state(True)
elif command_id == 0x0002:
self._entity.set_state(not self._entity.is_on)
def attribute_updated(self, attrid, value):
"""Handle attribute updates on this cluster."""
if attrid == 0:
self._entity.set_state(value)
def zdo_command(self, *args, **kwargs):
"""Handle ZDO commands on this cluster."""
pass
class LevelListener:
"""Listener for the LevelControl ZigBee cluster."""
def __init__(self, entity):
"""Initialize LevelListener."""
self._entity = entity
def cluster_command(self, tsn, command_id, args):
"""Handle commands received to this cluster."""
if command_id in (0x0000, 0x0004): # move_to_level, -with_on_off
self._entity.set_level(args[0])
elif command_id in (0x0001, 0x0005): # move, -with_on_off
# We should dim slowly -- for now, just step once
rate = args[1]
if args[0] == 0xff:
rate = 10 # Should read default move rate
self._entity.move_level(-rate if args[0] else rate)
elif command_id == 0x0002: # step
# Step (technically shouldn't change on/off)
self._entity.move_level(-args[1] if args[0] else args[1])
def attribute_update(self, attrid, value):
"""Handle attribute updates on this cluster."""
if attrid == 0:
self._entity.set_level(value)
def zdo_command(self, *args, **kwargs):
"""Handle ZDO commands on this cluster."""
pass
def __init__(self, **kwargs):
"""Initialize Switch."""
super().__init__(**kwargs)
self._state = True
self._level = 255
from zigpy.zcl.clusters import general
self._out_listeners = {
general.OnOff.cluster_id: self.OnOffListener(self),
general.LevelControl.cluster_id: self.LevelListener(self),
}
@property
def is_on(self) -> bool:
"""Return true if the binary sensor is on."""
return self._state
@property
def device_state_attributes(self):
"""Return the device state attributes."""
return {'level': self._state and self._level or 0}
def move_level(self, change):
"""Increment the level, setting state if appropriate."""
if not self._state and change > 0:
self._level = 0
self._level = min(255, max(0, self._level + change))
self._state = bool(self._level)
self.async_schedule_update_ha_state()
def set_level(self, level):
"""Set the level, setting state if appropriate."""
self._level = level
self._state = bool(self._level)
self.async_schedule_update_ha_state()
def set_state(self, state):
"""Set the state."""
self._state = state
if self._level == 0:
self._level = 255
self.async_schedule_update_ha_state()
async def async_update(self):
"""Retrieve latest state."""
from zigpy.zcl.clusters.general import OnOff
result = await zha.safe_read(
self._endpoint.out_clusters[OnOff.cluster_id], ['on_off'])
self._state = result.get('on_off', self._state)

View File

@ -6,6 +6,7 @@ For more details about this component, please refer to the documentation at
https://home-assistant.io/components/camera/
"""
import asyncio
import base64
import collections
from contextlib import suppress
from datetime import timedelta
@ -13,20 +14,20 @@ import logging
import hashlib
from random import SystemRandom
import aiohttp
import attr
from aiohttp import web
import async_timeout
import voluptuous as vol
from homeassistant.core import callback
from homeassistant.const import (ATTR_ENTITY_ID, ATTR_ENTITY_PICTURE)
from homeassistant.const import ATTR_ENTITY_ID
from homeassistant.exceptions import HomeAssistantError
from homeassistant.loader import bind_hass
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa
from homeassistant.components.http import HomeAssistantView, KEY_AUTHENTICATED
from homeassistant.components import websocket_api
import homeassistant.helpers.config_validation as cv
DOMAIN = 'camera'
@ -53,6 +54,9 @@ ENTITY_IMAGE_URL = '/api/camera_proxy/{0}?token={1}'
TOKEN_CHANGE_INTERVAL = timedelta(minutes=5)
_RND = SystemRandom()
FALLBACK_STREAM_INTERVAL = 1 # seconds
MIN_STREAM_INTERVAL = 0.5 # seconds
CAMERA_SERVICE_SCHEMA = vol.Schema({
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
})
@ -61,6 +65,20 @@ CAMERA_SERVICE_SNAPSHOT = CAMERA_SERVICE_SCHEMA.extend({
vol.Required(ATTR_FILENAME): cv.template
})
WS_TYPE_CAMERA_THUMBNAIL = 'camera_thumbnail'
SCHEMA_WS_CAMERA_THUMBNAIL = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
'type': WS_TYPE_CAMERA_THUMBNAIL,
'entity_id': cv.entity_id
})
@attr.s
class Image:
"""Represent an image."""
content_type = attr.ib(type=str)
content = attr.ib(type=bytes)
@bind_hass
def enable_motion_detection(hass, entity_id=None):
@ -89,43 +107,40 @@ def async_snapshot(hass, filename, entity_id=None):
@bind_hass
@asyncio.coroutine
def async_get_image(hass, entity_id, timeout=10):
async def async_get_image(hass, entity_id, timeout=10):
"""Fetch an image from a camera entity."""
websession = async_get_clientsession(hass)
state = hass.states.get(entity_id)
component = hass.data.get(DOMAIN)
if state is None:
raise HomeAssistantError(
"No entity '{0}' for grab an image".format(entity_id))
if component is None:
raise HomeAssistantError('Camera component not setup')
url = "{0}{1}".format(
hass.config.api.base_url,
state.attributes.get(ATTR_ENTITY_PICTURE)
)
camera = component.get_entity(entity_id)
try:
if camera is None:
raise HomeAssistantError('Camera not found')
with suppress(asyncio.CancelledError, asyncio.TimeoutError):
with async_timeout.timeout(timeout, loop=hass.loop):
response = yield from websession.get(url)
image = await camera.async_camera_image()
if response.status != 200:
raise HomeAssistantError("Error {0} on {1}".format(
response.status, url))
if image:
return Image(camera.content_type, image)
image = yield from response.read()
return image
except (asyncio.TimeoutError, aiohttp.ClientError):
raise HomeAssistantError("Can't connect to {0}".format(url))
raise HomeAssistantError('Unable to get image')
@asyncio.coroutine
def async_setup(hass, config):
"""Set up the camera component."""
component = EntityComponent(_LOGGER, DOMAIN, hass, SCAN_INTERVAL)
component = hass.data[DOMAIN] = \
EntityComponent(_LOGGER, DOMAIN, hass, SCAN_INTERVAL)
hass.http.register_view(CameraImageView(component))
hass.http.register_view(CameraMjpegStream(component))
hass.components.websocket_api.async_register_command(
WS_TYPE_CAMERA_THUMBNAIL, websocket_camera_thumbnail,
SCHEMA_WS_CAMERA_THUMBNAIL
)
yield from component.async_setup(config)
@ -252,19 +267,21 @@ class Camera(Entity):
"""
return self.hass.async_add_job(self.camera_image)
@asyncio.coroutine
def handle_async_mjpeg_stream(self, request):
async def handle_async_still_stream(self, request, interval):
"""Generate an HTTP MJPEG stream from camera images.
This method must be run in the event loop.
"""
response = web.StreamResponse()
if interval < MIN_STREAM_INTERVAL:
raise ValueError("Stream interval must be be > {}"
.format(MIN_STREAM_INTERVAL))
response = web.StreamResponse()
response.content_type = ('multipart/x-mixed-replace; '
'boundary=--frameboundary')
yield from response.prepare(request)
await response.prepare(request)
async def write(img_bytes):
async def write_to_mjpeg_stream(img_bytes):
"""Write image to stream."""
await response.write(bytes(
'--frameboundary\r\n'
@ -277,21 +294,21 @@ class Camera(Entity):
try:
while True:
img_bytes = yield from self.async_camera_image()
img_bytes = await self.async_camera_image()
if not img_bytes:
break
if img_bytes and img_bytes != last_image:
yield from write(img_bytes)
await write_to_mjpeg_stream(img_bytes)
# Chrome seems to always ignore first picture,
# print it twice.
if last_image is None:
yield from write(img_bytes)
await write_to_mjpeg_stream(img_bytes)
last_image = img_bytes
yield from asyncio.sleep(.5)
await asyncio.sleep(interval)
except asyncio.CancelledError:
_LOGGER.debug("Stream closed by frontend.")
@ -299,7 +316,17 @@ class Camera(Entity):
finally:
if response is not None:
yield from response.write_eof()
await response.write_eof()
async def handle_async_mjpeg_stream(self, request):
"""Serve an HTTP MJPEG stream from the camera.
This method can be overridden by camera plaforms to proxy
a direct stream from the camera.
This method must be run in the event loop.
"""
await self.handle_async_still_stream(request,
FALLBACK_STREAM_INTERVAL)
@property
def state(self):
@ -329,20 +356,20 @@ class Camera(Entity):
@property
def state_attributes(self):
"""Return the camera state attributes."""
attr = {
attrs = {
'access_token': self.access_tokens[-1],
}
if self.model:
attr['model_name'] = self.model
attrs['model_name'] = self.model
if self.brand:
attr['brand'] = self.brand
attrs['brand'] = self.brand
if self.motion_detection_enabled:
attr['motion_detection'] = self.motion_detection_enabled
attrs['motion_detection'] = self.motion_detection_enabled
return attr
return attrs
@callback
def async_update_token(self):
@ -411,7 +438,40 @@ class CameraMjpegStream(CameraView):
url = '/api/camera_proxy_stream/{entity_id}'
name = 'api:camera:stream'
@asyncio.coroutine
def handle(self, request, camera):
"""Serve camera image."""
yield from camera.handle_async_mjpeg_stream(request)
async def handle(self, request, camera):
"""Serve camera stream, possibly with interval."""
interval = request.query.get('interval')
if interval is None:
await camera.handle_async_mjpeg_stream(request)
return
try:
# Compose camera stream from stills
interval = float(request.query.get('interval'))
await camera.handle_async_still_stream(request, interval)
return
except ValueError:
return web.Response(status=400)
@callback
def websocket_camera_thumbnail(hass, connection, msg):
"""Handle get camera thumbnail websocket command.
Async friendly.
"""
async def send_camera_still():
"""Send a camera still."""
try:
image = await async_get_image(hass, msg['entity_id'])
connection.send_message_outside(websocket_api.result_message(
msg['id'], {
'content_type': image.content_type,
'content': base64.b64encode(image.content).decode('utf-8')
}
))
except HomeAssistantError:
connection.send_message_outside(websocket_api.error_message(
msg['id'], 'image_fetch_failed', 'Unable to fetch image'))
hass.async_add_job(send_camera_still())

View File

@ -9,7 +9,6 @@ import logging
import requests
from homeassistant.components.camera import Camera
from homeassistant.loader import get_component
DEPENDENCIES = ['bloomsky']
@ -17,7 +16,7 @@ DEPENDENCIES = ['bloomsky']
# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up access to BloomSky cameras."""
bloomsky = get_component('bloomsky')
bloomsky = hass.components.bloomsky
for device in bloomsky.BLOOMSKY.devices.values():
add_devices([BloomSkyCamera(bloomsky.BLOOMSKY, device)])

View File

@ -11,31 +11,44 @@ import os
import voluptuous as vol
from homeassistant.const import CONF_NAME
from homeassistant.components.camera import Camera, PLATFORM_SCHEMA
from homeassistant.components.camera import (
Camera, CAMERA_SERVICE_SCHEMA, DOMAIN, PLATFORM_SCHEMA)
from homeassistant.helpers import config_validation as cv
_LOGGER = logging.getLogger(__name__)
CONF_FILE_PATH = 'file_path'
DEFAULT_NAME = 'Local File'
SERVICE_UPDATE_FILE_PATH = 'local_file_update_file_path'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_FILE_PATH): cv.string,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string
})
CAMERA_SERVICE_UPDATE_FILE_PATH = CAMERA_SERVICE_SCHEMA.extend({
vol.Required(CONF_FILE_PATH): cv.string
})
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Camera that works with local files."""
file_path = config[CONF_FILE_PATH]
camera = LocalFile(config[CONF_NAME], file_path)
# check filepath given is readable
if not os.access(file_path, os.R_OK):
_LOGGER.warning("Could not read camera %s image from file: %s",
config[CONF_NAME], file_path)
def update_file_path_service(call):
"""Update the file path."""
file_path = call.data.get(CONF_FILE_PATH)
camera.update_file_path(file_path)
return True
add_devices([LocalFile(config[CONF_NAME], file_path)])
hass.services.register(
DOMAIN,
SERVICE_UPDATE_FILE_PATH,
update_file_path_service,
schema=CAMERA_SERVICE_UPDATE_FILE_PATH)
add_devices([camera])
class LocalFile(Camera):
@ -46,6 +59,7 @@ class LocalFile(Camera):
super().__init__()
self._name = name
self.check_file_path_access(file_path)
self._file_path = file_path
# Set content type of local file
content, _ = mimetypes.guess_type(file_path)
@ -61,7 +75,26 @@ class LocalFile(Camera):
_LOGGER.warning("Could not read camera %s image from file: %s",
self._name, self._file_path)
def check_file_path_access(self, file_path):
"""Check that filepath given is readable."""
if not os.access(file_path, os.R_OK):
_LOGGER.warning("Could not read camera %s image from file: %s",
self._name, file_path)
def update_file_path(self, file_path):
"""Update the file_path."""
self.check_file_path_access(file_path)
self._file_path = file_path
self.schedule_update_ha_state()
@property
def name(self):
"""Return the name of this camera."""
return self._name
@property
def device_state_attributes(self):
"""Return the camera state attributes."""
return {
'file_path': self._file_path,
}

View File

@ -12,7 +12,6 @@ import voluptuous as vol
from homeassistant.const import CONF_VERIFY_SSL
from homeassistant.components.netatmo import CameraData
from homeassistant.components.camera import (Camera, PLATFORM_SCHEMA)
from homeassistant.loader import get_component
from homeassistant.helpers import config_validation as cv
DEPENDENCIES = ['netatmo']
@ -33,7 +32,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up access to Netatmo cameras."""
netatmo = get_component('netatmo')
netatmo = hass.components.netatmo
home = config.get(CONF_HOME)
verify_ssl = config.get(CONF_VERIFY_SSL, True)
import lnetatmo

View File

@ -24,6 +24,16 @@ snapshot:
description: Template of a Filename. Variable is entity_id.
example: '/tmp/snapshot_{{ entity_id }}'
local_file_update_file_path:
description: Update the file_path for a local_file camera.
fields:
entity_id:
description: Name(s) of entities to update.
example: 'camera.local_file'
file_path:
description: Path to the new image file.
example: '/images/newimage.jpg'
onvif_ptz:
description: Pan/Tilt/Zoom service for ONVIF camera.
fields:
@ -39,4 +49,3 @@ onvif_ptz:
zoom:
description: "Zoom. Allowed values: ZOOM_IN, ZOOM_OUT"
example: "ZOOM_IN"

View File

@ -13,7 +13,6 @@ from homeassistant.components.climate import (
STATE_HEAT, STATE_IDLE, ClimateDevice, PLATFORM_SCHEMA,
SUPPORT_TARGET_TEMPERATURE, SUPPORT_OPERATION_MODE, SUPPORT_AWAY_MODE)
from homeassistant.util import Throttle
from homeassistant.loader import get_component
import homeassistant.helpers.config_validation as cv
DEPENDENCIES = ['netatmo']
@ -42,7 +41,7 @@ SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_OPERATION_MODE |
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the NetAtmo Thermostat."""
netatmo = get_component('netatmo')
netatmo = hass.components.netatmo
device = config.get(CONF_RELAY)
import lnetatmo

View File

@ -21,6 +21,7 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.util import dt as dt_util
from homeassistant.components.alexa import smart_home as alexa_sh
from homeassistant.components.google_assistant import helpers as ga_h
from homeassistant.components.google_assistant import const as ga_c
from . import http_api, iot
from .const import CONFIG_DIR, DOMAIN, SERVERS
@ -52,7 +53,8 @@ ALEXA_ENTITY_SCHEMA = vol.Schema({
GOOGLE_ENTITY_SCHEMA = vol.Schema({
vol.Optional(CONF_NAME): cv.string,
vol.Optional(CONF_ALIASES): vol.All(cv.ensure_list, [cv.string])
vol.Optional(CONF_ALIASES): vol.All(cv.ensure_list, [cv.string]),
vol.Optional(ga_c.CONF_ROOM_HINT): cv.string,
})
ASSISTANT_SCHEMA = vol.Schema({

View File

@ -15,7 +15,7 @@ from homeassistant.const import (
CONF_IP_ADDRESS, CONF_NAME)
import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['pygogogate2==0.0.3']
REQUIREMENTS = ['pygogogate2==0.0.7']
_LOGGER = logging.getLogger(__name__)

View File

@ -79,5 +79,7 @@ class TahomaCover(TahomaDevice, CoverDevice):
if self.tahoma_device.type == \
'io:RollerShutterWithLowSpeedManagementIOComponent':
self.apply_action('setPosition', 'secured')
elif self.tahoma_device.type == 'rts:BlindRTSComponent':
self.apply_action('my')
else:
self.apply_action('stopIdentify')

View File

@ -0,0 +1,25 @@
{
"config": {
"abort": {
"no_bridges": "\u041d\u0435 \u0441\u0430 \u043e\u0442\u043a\u0440\u0438\u0442\u0438 \u043c\u043e\u0441\u0442\u043e\u0432\u0435 deCONZ",
"one_instance_only": "\u041a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442 \u043f\u043e\u0434\u0434\u044a\u0440\u0436\u0430 \u0441\u0430\u043c\u043e \u0435\u0434\u043d\u043e \u043a\u043e\u043f\u0438\u0435 \u043d\u0430 deCONZ"
},
"error": {
"no_key": "\u041d\u0435 \u043c\u043e\u0436\u0430 \u0434\u0430 \u0441\u0435 \u043f\u043e\u043b\u0443\u0447\u0438 API \u043a\u043b\u044e\u0447"
},
"step": {
"init": {
"data": {
"host": "\u0425\u043e\u0441\u0442",
"port": "\u041f\u043e\u0440\u0442 (\u0441\u0442\u043e\u0439\u043d\u043e\u0441\u0442 \u043f\u043e \u043f\u043e\u0434\u0440\u0430\u0437\u0431\u0438\u0440\u0430\u043d\u0435: '80')"
},
"title": "\u0414\u0435\u0444\u0438\u043d\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 deCONZ \u0448\u043b\u044e\u0437"
},
"link": {
"description": "\u041e\u0442\u043a\u043b\u044e\u0447\u0438 deCONZ \u0448\u043b\u044e\u0437\u0430 \u0437\u0430 \u0434\u0430 \u0441\u0435 \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u0430 \u0441 Home Assistant.\n\n1. \u041e\u0442\u0432\u043e\u0440\u0435\u0442\u0435 \u0441\u0438\u0441\u0442\u0435\u043c\u043d\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u043d\u0430 deCONZ\n2. \u041d\u0430\u0442\u0438\u0441\u043d\u0435\u0442\u0435 \u0431\u0443\u0442\u043e\u043d\u0430 \"Unlock Gateway\"",
"title": "\u0412\u0440\u044a\u0437\u043a\u0430 \u0441 deCONZ"
}
},
"title": "deCONZ"
}
}

View File

@ -0,0 +1,26 @@
{
"config": {
"abort": {
"already_configured": "Pont eisoes wedi'i ffurfweddu",
"no_bridges": "Dim pontydd deCONZ wedi eu darganfod",
"one_instance_only": "Elfen dim ond yn cefnogi enghraifft deCONZ"
},
"error": {
"no_key": "Methu cael allwedd API"
},
"step": {
"init": {
"data": {
"host": "Gwesteiwr",
"port": "Port (gwerth diofyn: '80')"
},
"title": "Diffiniwch porth dad-adeiladu"
},
"link": {
"description": "Datgloi eich porth deCONZ i gofrestru gyda Cynorthwydd Cartref.\n\n1. Ewch i osodiadau system deCONZ \n2. Bwyso botwm \"Datgloi porth\"",
"title": "Cysylltu \u00e2 deCONZ"
}
},
"title": "deCONZ"
}
}

View File

@ -0,0 +1,11 @@
{
"config": {
"step": {
"init": {
"data": {
"host": "V\u00e6rt"
}
}
}
}
}

View File

@ -0,0 +1,26 @@
{
"config": {
"abort": {
"already_configured": "Bridge ist bereits konfiguriert",
"no_bridges": "Keine deCON-Bridges entdeckt",
"one_instance_only": "Komponente unterst\u00fctzt nur eine deCONZ-Instanz"
},
"error": {
"no_key": "Es konnte kein API-Schl\u00fcssel abgerufen werden"
},
"step": {
"init": {
"data": {
"host": "Host",
"port": "Port (Standartwert : '80')"
},
"title": "Definieren Sie den deCONZ-Gateway"
},
"link": {
"description": "Entsperren Sie Ihr deCONZ-Gateway, um sich bei Home Assistant zu registrieren. \n\n 1. Gehen Sie zu den deCONZ-Systemeinstellungen \n 2. Dr\u00fccken Sie die Taste \"Gateway entsperren\"",
"title": "Mit deCONZ verbinden"
}
},
"title": "deCONZ"
}
}

View File

@ -1,26 +1,26 @@
{
"config": {
"title": "deCONZ",
"step": {
"init": {
"title": "Define deCONZ gateway",
"data": {
"host": "Host",
"port": "Port (default value: '80')"
}
},
"link": {
"title": "Link with deCONZ",
"description": "Unlock your deCONZ gateway to register with Home Assistant.\n\n1. Go to deCONZ system settings\n2. Press \"Unlock Gateway\" button"
}
},
"error": {
"no_key": "Couldn't get an API key"
},
"abort": {
"already_configured": "Bridge is already configured",
"no_bridges": "No deCONZ bridges discovered",
"one_instance_only": "Component only supports one deCONZ instance"
}
},
"error": {
"no_key": "Couldn't get an API key"
},
"step": {
"init": {
"data": {
"host": "Host",
"port": "Port (default value: '80')"
},
"title": "Define deCONZ gateway"
},
"link": {
"description": "Unlock your deCONZ gateway to register with Home Assistant.\n\n1. Go to deCONZ system settings\n2. Press \"Unlock Gateway\" button",
"title": "Link with deCONZ"
}
},
"title": "deCONZ"
}
}

View File

@ -0,0 +1,22 @@
{
"config": {
"abort": {
"one_instance_only": "Ez a komponens csak egy deCONZ egys\u00e9get t\u00e1mogat"
},
"error": {
"no_key": "API kulcs lek\u00e9r\u00e9se nem siker\u00fclt"
},
"step": {
"init": {
"data": {
"host": "H\u00e1zigazda (Host)",
"port": "Port (alap\u00e9rtelmezett \u00e9rt\u00e9k: '80')"
}
},
"link": {
"title": "Kapcsol\u00f3d\u00e1s a deCONZ-hoz"
}
},
"title": "deCONZ"
}
}

View File

@ -0,0 +1,26 @@
{
"config": {
"abort": {
"already_configured": "\ube0c\ub9bf\uc9c0\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
"no_bridges": "\ubc1c\uacac\ub41c deCONZ \ube0c\ub9bf\uc9c0\uac00 \uc5c6\uc2b5\ub2c8\ub2e4",
"one_instance_only": "\uad6c\uc131\uc694\uc18c\ub294 \ud558\ub098\uc758 deCONZ \uc778\uc2a4\ud134\uc2a4 \ub9cc \uc9c0\uc6d0\ud569\ub2c8\ub2e4"
},
"error": {
"no_key": "API \ud0a4\ub97c \uac00\uc838\uc62c \uc218 \uc5c6\uc2b5\ub2c8\ub2e4"
},
"step": {
"init": {
"data": {
"host": "\ud638\uc2a4\ud2b8",
"port": "\ud3ec\ud2b8 (\uae30\ubcf8\uac12: '80')"
},
"title": "deCONZ \uac8c\uc774\ud2b8\uc6e8\uc774 \uc815\uc758"
},
"link": {
"description": "deCONZ \uac8c\uc774\ud2b8\uc6e8\uc774\ub97c \uc5b8\ub77d\ud558\uc5ec Home Assistant \uc5d0 \uc5f0\uacb0\ud558\uae30\n\n1. deCONZ \uc2dc\uc2a4\ud15c \uc124\uc815\uc73c\ub85c \uc774\ub3d9\ud558\uc138\uc694\n2. \"Unlock Gateway\" \ubc84\ud2bc\uc744 \ub204\ub974\uc138\uc694 ",
"title": "deCONZ \uc640 \uc5f0\uacb0"
}
},
"title": "deCONZ"
}
}

View File

@ -0,0 +1,26 @@
{
"config": {
"abort": {
"already_configured": "Bridge ass schon konfigur\u00e9iert",
"no_bridges": "Keng dECONZ bridges fonnt",
"one_instance_only": "Komponent \u00ebnnerst\u00ebtzt n\u00ebmmen eng deCONZ Instanz"
},
"error": {
"no_key": "Konnt keen API Schl\u00ebssel kr\u00e9ien"
},
"step": {
"init": {
"data": {
"host": "Host",
"port": "Port (Standard Wert: '80')"
},
"title": "deCONZ gateway d\u00e9fin\u00e9ieren"
},
"link": {
"description": "Entsperrt \u00e4r deCONZ gateway fir se mat Home Assistant ze registr\u00e9ieren.\n\n1. Gidd op\u00a0deCONZ System Astellungen\n2. Dr\u00e9ckt \"Unlock\" Gateway Kn\u00e4ppchen",
"title": "Link mat deCONZ"
}
},
"title": "deCONZ"
}
}

View File

@ -0,0 +1,26 @@
{
"config": {
"abort": {
"already_configured": "Bridge is al geconfigureerd",
"no_bridges": "Geen deCONZ bruggen ontdekt",
"one_instance_only": "Component ondersteunt slechts \u00e9\u00e9n deCONZ instance"
},
"error": {
"no_key": "Kon geen API-sleutel ophalen"
},
"step": {
"init": {
"data": {
"host": "Host",
"port": "Poort (standaard: '80')"
},
"title": "Definieer deCONZ gateway"
},
"link": {
"description": "Ontgrendel je deCONZ gateway om te registreren met Home Assistant.\n\n1. Ga naar deCONZ systeeminstellingen\n2. Druk op de knop \"Gateway ontgrendelen\"",
"title": "Koppel met deCONZ"
}
},
"title": "deCONZ"
}
}

View File

@ -0,0 +1,26 @@
{
"config": {
"abort": {
"already_configured": "Broen er allerede konfigurert",
"no_bridges": "Ingen deCONZ broer oppdaget",
"one_instance_only": "Komponenten st\u00f8tter bare \u00e9n deCONZ forekomst"
},
"error": {
"no_key": "Kunne ikke f\u00e5 en API-n\u00f8kkel"
},
"step": {
"init": {
"data": {
"host": "Vert",
"port": "Port (standardverdi: '80')"
},
"title": "Definer deCONZ-gatewayen"
},
"link": {
"description": "L\u00e5s opp deCONZ-gatewayen din for \u00e5 registrere deg med Home Assistant. \n\n 1. G\u00e5 til deCONZ-systeminnstillinger \n 2. Trykk p\u00e5 \"L\u00e5s opp gateway\" knappen",
"title": "Koble til deCONZ"
}
},
"title": "deCONZ"
}
}

View File

@ -0,0 +1,26 @@
{
"config": {
"abort": {
"already_configured": "Mostek jest ju\u017c skonfigurowany",
"no_bridges": "Nie odkryto mostk\u00f3w deCONZ",
"one_instance_only": "Komponent obs\u0142uguje tylko jedn\u0105 instancj\u0119 deCONZ"
},
"error": {
"no_key": "Nie mo\u017cna uzyska\u0107 klucza API"
},
"step": {
"init": {
"data": {
"host": "Host",
"port": "Port (warto\u015b\u0107 domy\u015blna: \"80\")"
},
"title": "Zdefiniuj bramk\u0119 deCONZ"
},
"link": {
"description": "Odblokuj bramk\u0119 deCONZ, aby zarejestrowa\u0107 j\u0105 w Home Assistant. \n\n 1. Przejd\u017a do ustawie\u0144 systemu deCONZ \n 2. Naci\u015bnij przycisk \"Odblokuj bramk\u0119\"",
"title": "Po\u0142\u0105cz z deCONZ"
}
},
"title": "deCONZ"
}
}

View File

@ -0,0 +1,7 @@
{
"config": {
"abort": {
"already_configured": "Bridge j\u00e1 est\u00e1 configurada"
}
}
}

View File

@ -0,0 +1,26 @@
{
"config": {
"abort": {
"already_configured": "\u0428\u043b\u044e\u0437 \u0443\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d",
"no_bridges": "\u0428\u043b\u044e\u0437\u044b deCONZ \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b",
"one_instance_only": "\u041a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442 \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u0438\u043d \u044d\u043a\u0437\u0435\u043c\u043f\u043b\u044f\u0440 deCONZ"
},
"error": {
"no_key": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u043a\u043b\u044e\u0447 API"
},
"step": {
"init": {
"data": {
"host": "\u0425\u043e\u0441\u0442",
"port": "\u041f\u043e\u0440\u0442 (\u043f\u043e \u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e: '80')"
},
"title": "\u041e\u043f\u0440\u0435\u0434\u0435\u043b\u0438\u0442\u044c \u0448\u043b\u044e\u0437 deCONZ"
},
"link": {
"description": "\u0420\u0430\u0437\u0431\u043b\u043e\u043a\u0438\u0440\u0443\u0439\u0442\u0435 \u0448\u043b\u044e\u0437 deCONZ \u0434\u043b\u044f \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0430\u0446\u0438\u0438 \u0432 Home Assistant:\n\n1. \u041f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043a \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430\u043c \u0441\u0438\u0441\u0442\u0435\u043c\u044b deCONZ\n2. \u041d\u0430\u0436\u043c\u0438\u0442\u0435 \u043a\u043d\u043e\u043f\u043a\u0443 \u00ab\u0420\u0430\u0437\u0431\u043b\u043e\u043a\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u0448\u043b\u044e\u0437\u00bb",
"title": "\u0421\u0432\u044f\u0437\u044c \u0441 deCONZ"
}
},
"title": "deCONZ"
}
}

View File

@ -0,0 +1,26 @@
{
"config": {
"abort": {
"already_configured": "Most je \u017ee nastavljen",
"no_bridges": "Ni odkritih mostov deCONZ",
"one_instance_only": "Komponenta podpira le en primerek deCONZ"
},
"error": {
"no_key": "Klju\u010da API ni mogo\u010de dobiti"
},
"step": {
"init": {
"data": {
"host": "Gostitelj",
"port": "Vrata (privzeta vrednost: '80')"
},
"title": "Dolo\u010dite deCONZ prehod"
},
"link": {
"description": "Odklenite va\u0161 deCONZ gateway za registracijo z Home Assistant-om. \n1. Pojdite v deCONT sistemske nastavitve\n2. Pritisnite tipko \"odkleni prehod\"",
"title": "Povezava z deCONZ"
}
},
"title": "deCONZ"
}
}

View File

@ -0,0 +1,26 @@
{
"config": {
"abort": {
"already_configured": "\u6865\u63a5\u5668\u5df2\u914d\u7f6e\u5b8c\u6210",
"no_bridges": "\u6ca1\u6709\u53d1\u73b0 deCONZ \u7684\u6865\u63a5\u8bbe\u5907",
"one_instance_only": "\u7ec4\u4ef6\u53ea\u652f\u6301\u4e00\u4e2a deCONZ \u5b9e\u4f8b"
},
"error": {
"no_key": "\u65e0\u6cd5\u83b7\u53d6 API \u5bc6\u94a5"
},
"step": {
"init": {
"data": {
"host": "\u4e3b\u673a",
"port": "\u7aef\u53e3\uff08\u9ed8\u8ba4\u503c\uff1a'80'\uff09"
},
"title": "\u5b9a\u4e49 deCONZ \u7f51\u5173"
},
"link": {
"description": "\u89e3\u9501\u60a8\u7684 deCONZ \u7f51\u5173\u4ee5\u6ce8\u518c\u5230 Home Assistant\u3002 \n\n 1. \u524d\u5f80 deCONZ \u7cfb\u7edf\u8bbe\u7f6e\n 2. \u70b9\u51fb\u201c\u89e3\u9501\u7f51\u5173\u201d\u6309\u94ae",
"title": "\u8fde\u63a5 deCONZ"
}
},
"title": "deCONZ"
}
}

View File

@ -0,0 +1,25 @@
{
"config": {
"abort": {
"no_bridges": "\u672a\u641c\u5c0b\u5230 deCONZ Bridfe",
"one_instance_only": "\u7d44\u4ef6\u50c5\u652f\u63f4\u4e00\u7d44 deCONZ \u5be6\u4f8b"
},
"error": {
"no_key": "\u7121\u6cd5\u53d6\u5f97 API key"
},
"step": {
"init": {
"data": {
"host": "\u4e3b\u6a5f\u7aef",
"port": "\u901a\u8a0a\u57e0\uff08\u9810\u8a2d\u503c\uff1a'80'\uff09"
},
"title": "\u5b9a\u7fa9 deCONZ \u7db2\u95dc"
},
"link": {
"description": "\u89e3\u9664 deCONZ \u7db2\u95dc\u9396\u5b9a\uff0c\u4ee5\u65bc Home Assistant \u9032\u884c\u8a3b\u518a\u3002\n\n1. \u9032\u5165 deCONZ \u7cfb\u7d71\u8a2d\u5b9a\n2. \u6309\u4e0b\u300c\u89e3\u9664\u7db2\u95dc\u9396\u5b9a\uff08Unlock Gateway\uff09\u300d\u6309\u9215",
"title": "\u9023\u7d50\u81f3 deCONZ"
}
},
"title": "deCONZ"
}
}

View File

@ -7,17 +7,22 @@ https://home-assistant.io/components/deconz/
import voluptuous as vol
from homeassistant.const import (
CONF_API_KEY, CONF_HOST, CONF_PORT, EVENT_HOMEASSISTANT_STOP)
from homeassistant.core import callback
from homeassistant.helpers import (
aiohttp_client, discovery, config_validation as cv)
CONF_API_KEY, CONF_EVENT, CONF_HOST,
CONF_ID, CONF_PORT, EVENT_HOMEASSISTANT_STOP)
from homeassistant.core import EventOrigin, callback
from homeassistant.helpers import aiohttp_client, config_validation as cv
from homeassistant.helpers.dispatcher import (
async_dispatcher_connect, async_dispatcher_send)
from homeassistant.util import slugify
from homeassistant.util.json import load_json
# Loading the config flow file will register the flow
from .config_flow import configured_hosts
from .const import CONFIG_FILE, DATA_DECONZ_ID, DOMAIN, _LOGGER
from .const import (
CONFIG_FILE, DATA_DECONZ_EVENT, DATA_DECONZ_ID,
DATA_DECONZ_UNSUB, DOMAIN, _LOGGER)
REQUIREMENTS = ['pydeconz==36']
REQUIREMENTS = ['pydeconz==37']
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
@ -27,6 +32,8 @@ CONFIG_SCHEMA = vol.Schema({
})
}, extra=vol.ALLOW_EXTRA)
SERVICE_DECONZ = 'configure'
SERVICE_FIELD = 'field'
SERVICE_ENTITY = 'entity'
SERVICE_DATA = 'data'
@ -58,28 +65,27 @@ async def async_setup(hass, config):
return True
async def async_setup_entry(hass, entry):
"""Set up a deCONZ bridge for a config entry."""
if DOMAIN in hass.data:
_LOGGER.error(
"Config entry failed since one deCONZ instance already exists")
return False
result = await async_setup_deconz(hass, None, entry.data)
if result:
return True
return False
async def async_setup_deconz(hass, config, deconz_config):
"""Set up a deCONZ session.
async def async_setup_entry(hass, config_entry):
"""Set up a deCONZ bridge for a config entry.
Load config, group, light and sensor data for server information.
Start websocket for push notification of state changes from deCONZ.
"""
_LOGGER.debug("deCONZ config %s", deconz_config)
from pydeconz import DeconzSession
if DOMAIN in hass.data:
_LOGGER.error(
"Config entry failed since one deCONZ instance already exists")
return False
@callback
def async_add_device_callback(device_type, device):
"""Called when a new device has been created in deCONZ."""
async_dispatcher_send(
hass, 'deconz_new_{}'.format(device_type), [device])
session = aiohttp_client.async_get_clientsession(hass)
deconz = DeconzSession(hass.loop, session, **deconz_config)
deconz = DeconzSession(hass.loop, session, **config_entry.data,
async_add_device=async_add_device_callback)
result = await deconz.async_load_parameters()
if result is False:
_LOGGER.error("Failed to communicate with deCONZ")
@ -87,10 +93,25 @@ async def async_setup_deconz(hass, config, deconz_config):
hass.data[DOMAIN] = deconz
hass.data[DATA_DECONZ_ID] = {}
hass.data[DATA_DECONZ_EVENT] = []
hass.data[DATA_DECONZ_UNSUB] = []
for component in ['binary_sensor', 'light', 'scene', 'sensor']:
hass.async_add_job(discovery.async_load_platform(
hass, component, DOMAIN, {}, config))
hass.async_add_job(hass.config_entries.async_forward_entry_setup(
config_entry, component))
@callback
def async_add_remote(sensors):
"""Setup remote from deCONZ."""
from pydeconz.sensor import SWITCH as DECONZ_REMOTE
for sensor in sensors:
if sensor.type in DECONZ_REMOTE:
hass.data[DATA_DECONZ_EVENT].append(DeconzEvent(hass, sensor))
hass.data[DATA_DECONZ_UNSUB].append(
async_dispatcher_connect(hass, 'deconz_new_sensor', async_add_remote))
async_add_remote(deconz.sensors.values())
deconz.start()
async def async_configure(call):
@ -121,7 +142,7 @@ async def async_setup_deconz(hass, config, deconz_config):
return
await deconz.async_put_state(field, data)
hass.services.async_register(
DOMAIN, 'configure', async_configure, schema=SERVICE_SCHEMA)
DOMAIN, SERVICE_DECONZ, async_configure, schema=SERVICE_SCHEMA)
@callback
def deconz_shutdown(event):
@ -136,3 +157,43 @@ async def async_setup_deconz(hass, config, deconz_config):
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, deconz_shutdown)
return True
async def async_unload_entry(hass, config_entry):
"""Unload deCONZ config entry."""
deconz = hass.data.pop(DOMAIN)
hass.services.async_remove(DOMAIN, SERVICE_DECONZ)
deconz.close()
for component in ['binary_sensor', 'light', 'scene', 'sensor']:
await hass.config_entries.async_forward_entry_unload(
config_entry, component)
dispatchers = hass.data[DATA_DECONZ_UNSUB]
for unsub_dispatcher in dispatchers:
unsub_dispatcher()
hass.data[DATA_DECONZ_UNSUB] = []
hass.data[DATA_DECONZ_EVENT] = []
hass.data[DATA_DECONZ_ID] = []
return True
class DeconzEvent(object):
"""When you want signals instead of entities.
Stateless sensors such as remotes are expected to generate an event
instead of a sensor entity in hass.
"""
def __init__(self, hass, device):
"""Register callback that will be used for signals."""
self._hass = hass
self._device = device
self._device.register_async_callback(self.async_update_callback)
self._event = 'deconz_{}'.format(CONF_EVENT)
self._id = slugify(self._device.name)
@callback
def async_update_callback(self, reason):
"""Fire the event if reason is that state is updated."""
if reason['state']:
data = {CONF_ID: self._id, CONF_EVENT: self._device.state}
self._hass.bus.async_fire(self._event, data, EventOrigin.remote)

View File

@ -5,4 +5,6 @@ _LOGGER = logging.getLogger('homeassistant.components.deconz')
DOMAIN = 'deconz'
CONFIG_FILE = 'deconz.conf'
DATA_DECONZ_EVENT = 'deconz_events'
DATA_DECONZ_ID = 'deconz_entities'
DATA_DECONZ_UNSUB = 'deconz_dispatchers'

View File

@ -16,7 +16,6 @@ from homeassistant.const import STATE_HOME, STATE_NOT_HOME
from homeassistant.helpers.event import (
async_track_point_in_utc_time, async_track_state_change)
from homeassistant.helpers.sun import is_up, get_astral_event_next
from homeassistant.loader import get_component
import homeassistant.helpers.config_validation as cv
DOMAIN = 'device_sun_light_trigger'
@ -48,9 +47,9 @@ CONFIG_SCHEMA = vol.Schema({
def async_setup(hass, config):
"""Set up the triggers to control lights based on device presence."""
logger = logging.getLogger(__name__)
device_tracker = get_component('device_tracker')
group = get_component('group')
light = get_component('light')
device_tracker = hass.components.device_tracker
group = hass.components.group
light = hass.components.light
conf = config[DOMAIN]
disable_turn_off = conf.get(CONF_DISABLE_TURN_OFF)
light_group = conf.get(CONF_LIGHT_GROUP, light.ENTITY_ID_ALL_LIGHTS)
@ -58,14 +57,14 @@ def async_setup(hass, config):
device_group = conf.get(
CONF_DEVICE_GROUP, device_tracker.ENTITY_ID_ALL_DEVICES)
device_entity_ids = group.get_entity_ids(
hass, device_group, device_tracker.DOMAIN)
device_group, device_tracker.DOMAIN)
if not device_entity_ids:
logger.error("No devices found to track")
return False
# Get the light IDs from the specified group
light_ids = group.get_entity_ids(hass, light_group, light.DOMAIN)
light_ids = group.get_entity_ids(light_group, light.DOMAIN)
if not light_ids:
logger.error("No lights found to turn on")
@ -85,9 +84,9 @@ def async_setup(hass, config):
def async_turn_on_before_sunset(light_id):
"""Turn on lights."""
if not device_tracker.is_on(hass) or light.is_on(hass, light_id):
if not device_tracker.is_on() or light.is_on(light_id):
return
light.async_turn_on(hass, light_id,
light.async_turn_on(light_id,
transition=LIGHT_TRANSITION_TIME.seconds,
profile=light_profile)
@ -129,7 +128,7 @@ def async_setup(hass, config):
@callback
def check_light_on_dev_state_change(entity, old_state, new_state):
"""Handle tracked device state changes."""
lights_are_on = group.is_on(hass, light_group)
lights_are_on = group.is_on(light_group)
light_needed = not (lights_are_on or is_up(hass))
# These variables are needed for the elif check
@ -139,7 +138,7 @@ def async_setup(hass, config):
# Do we need lights?
if light_needed:
logger.info("Home coming event for %s. Turning lights on", entity)
light.async_turn_on(hass, light_ids, profile=light_profile)
light.async_turn_on(light_ids, profile=light_profile)
# Are we in the time span were we would turn on the lights
# if someone would be home?
@ -152,7 +151,7 @@ def async_setup(hass, config):
# when the fading in started and turn it on if so
for index, light_id in enumerate(light_ids):
if now > start_point + index * LIGHT_TRANSITION_TIME:
light.async_turn_on(hass, light_id)
light.async_turn_on(light_id)
else:
# If this light didn't happen to be turned on yet so
@ -169,12 +168,12 @@ def async_setup(hass, config):
@callback
def turn_off_lights_when_all_leave(entity, old_state, new_state):
"""Handle device group state change."""
if not group.is_on(hass, light_group):
if not group.is_on(light_group):
return
logger.info(
"Everyone has left but there are lights on. Turning them off")
light.async_turn_off(hass, light_ids)
light.async_turn_off(light_ids)
async_track_state_change(
hass, device_group, turn_off_lights_when_all_leave,

View File

@ -15,6 +15,7 @@ from homeassistant.setup import async_prepare_setup_platform
from homeassistant.core import callback
from homeassistant.loader import bind_hass
from homeassistant.components import group, zone
from homeassistant.components.zone.zone import async_active_zone
from homeassistant.config import load_yaml_config_file, async_log_exception
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import config_per_platform, discovery
@ -23,7 +24,6 @@ from homeassistant.helpers.event import async_track_time_interval
from homeassistant.helpers.restore_state import async_get_last_state
from homeassistant.helpers.typing import GPSType, ConfigType, HomeAssistantType
import homeassistant.helpers.config_validation as cv
from homeassistant.loader import get_component
import homeassistant.util as util
from homeassistant.util.async_ import run_coroutine_threadsafe
import homeassistant.util.dt as dt_util
@ -321,7 +321,7 @@ class DeviceTracker(object):
# During init, we ignore the group
if self.group and self.track_new:
self.group.async_set_group(
self.hass, util.slugify(GROUP_NAME_ALL_DEVICES), visible=False,
util.slugify(GROUP_NAME_ALL_DEVICES), visible=False,
name=GROUP_NAME_ALL_DEVICES, add=[device.entity_id])
self.hass.bus.async_fire(EVENT_NEW_DEVICE, {
@ -356,9 +356,9 @@ class DeviceTracker(object):
entity_ids = [dev.entity_id for dev in self.devices.values()
if dev.track]
self.group = get_component('group')
self.group = self.hass.components.group
self.group.async_set_group(
self.hass, util.slugify(GROUP_NAME_ALL_DEVICES), visible=False,
util.slugify(GROUP_NAME_ALL_DEVICES), visible=False,
name=GROUP_NAME_ALL_DEVICES, entity_ids=entity_ids)
@callback
@ -541,7 +541,7 @@ class Device(Entity):
elif self.location_name:
self._state = self.location_name
elif self.gps is not None and self.source_type == SOURCE_TYPE_GPS:
zone_state = zone.async_active_zone(
zone_state = async_active_zone(
self.hass, self.gps[0], self.gps[1], self.gps_accuracy)
if zone_state is None:
self._state = STATE_NOT_HOME

View File

@ -19,7 +19,7 @@ from homeassistant.util import slugify
_LOGGER = logging.getLogger(__name__)
REQUIREMENTS = ['locationsharinglib==1.2.1']
REQUIREMENTS = ['locationsharinglib==1.2.2']
CREDENTIALS_FILE = '.google_maps_location_sharing.cookies'
@ -79,5 +79,6 @@ class GoogleMapsScanner(object):
gps=(person.latitude, person.longitude),
picture=person.picture_url,
source_type=SOURCE_TYPE_GPS,
gps_accuracy=person.accuracy,
attributes=attrs
)

View File

@ -4,7 +4,6 @@ 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/
"""
import asyncio
import logging
from hmac import compare_digest
@ -22,6 +21,7 @@ from homeassistant.components.http import (
from homeassistant.components.device_tracker import ( # NOQA
DOMAIN, PLATFORM_SCHEMA
)
from homeassistant.helpers.typing import HomeAssistantType, ConfigType
_LOGGER = logging.getLogger(__name__)
@ -32,8 +32,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
})
@asyncio.coroutine
def async_setup_scanner(hass, config, async_see, discovery_info=None):
async def async_setup_scanner(hass: HomeAssistantType, config: ConfigType,
async_see, discovery_info=None):
"""Set up an endpoint for the GPSLogger application."""
hass.http.register_view(GPSLoggerView(async_see, config))
@ -54,8 +54,7 @@ class GPSLoggerView(HomeAssistantView):
# password is set
self.requires_auth = self._password is None
@asyncio.coroutine
def get(self, request: Request):
async def get(self, request: Request):
"""Handle for GPSLogger message received as GET."""
hass = request.app['hass']
data = request.query

View File

@ -13,7 +13,7 @@ import voluptuous as vol
from homeassistant.const import CONF_USERNAME, CONF_PASSWORD
from homeassistant.components.device_tracker import (
PLATFORM_SCHEMA, DOMAIN, ATTR_ATTRIBUTES, ENTITY_ID_FORMAT, DeviceScanner)
from homeassistant.components.zone import active_zone
from homeassistant.components.zone.zone import active_zone
from homeassistant.helpers.event import track_utc_time_change
import homeassistant.helpers.config_validation as cv
from homeassistant.util import slugify

View File

@ -12,21 +12,27 @@ import homeassistant.helpers.config_validation as cv
from homeassistant.components.device_tracker import (
DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
from homeassistant.const import (
CONF_HOST, CONF_PASSWORD, CONF_USERNAME, CONF_PORT)
CONF_HOST, CONF_PASSWORD, CONF_USERNAME, CONF_PORT, CONF_SSL,
CONF_DEVICES, CONF_EXCLUDE)
REQUIREMENTS = ['pynetgear==0.3.3']
REQUIREMENTS = ['pynetgear==0.4.0']
_LOGGER = logging.getLogger(__name__)
DEFAULT_HOST = 'routerlogin.net'
DEFAULT_USER = 'admin'
DEFAULT_PORT = 5000
CONF_APS = 'accesspoints'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string,
vol.Optional(CONF_USERNAME, default=DEFAULT_USER): cv.string,
vol.Optional(CONF_HOST, default=''): cv.string,
vol.Optional(CONF_SSL, default=False): cv.boolean,
vol.Optional(CONF_USERNAME, default=''): cv.string,
vol.Required(CONF_PASSWORD): cv.string,
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port
vol.Optional(CONF_PORT, default=None): vol.Any(None, cv.port),
vol.Optional(CONF_DEVICES, default=[]):
vol.All(cv.ensure_list, [cv.string]),
vol.Optional(CONF_EXCLUDE, default=[]):
vol.All(cv.ensure_list, [cv.string]),
vol.Optional(CONF_APS, default=[]):
vol.All(cv.ensure_list, [cv.string]),
})
@ -34,11 +40,16 @@ def get_scanner(hass, config):
"""Validate the configuration and returns a Netgear scanner."""
info = config[DOMAIN]
host = info.get(CONF_HOST)
ssl = info.get(CONF_SSL)
username = info.get(CONF_USERNAME)
password = info.get(CONF_PASSWORD)
port = info.get(CONF_PORT)
devices = info.get(CONF_DEVICES)
excluded_devices = info.get(CONF_EXCLUDE)
accesspoints = info.get(CONF_APS)
scanner = NetgearDeviceScanner(host, username, password, port)
scanner = NetgearDeviceScanner(host, ssl, username, password, port,
devices, excluded_devices, accesspoints)
return scanner if scanner.success_init else None
@ -46,16 +57,21 @@ def get_scanner(hass, config):
class NetgearDeviceScanner(DeviceScanner):
"""Queries a Netgear wireless router using the SOAP-API."""
def __init__(self, host, username, password, port):
def __init__(self, host, ssl, username, password, port, devices,
excluded_devices, accesspoints):
"""Initialize the scanner."""
import pynetgear
self.tracked_devices = devices
self.excluded_devices = excluded_devices
self.tracked_accesspoints = accesspoints
self.last_results = []
self._api = pynetgear.Netgear(password, host, username, port)
self._api = pynetgear.Netgear(password, host, username, port, ssl)
_LOGGER.info("Logging in")
results = self._api.get_attached_devices()
results = self.get_attached_devices()
self.success_init = results is not None
@ -68,15 +84,50 @@ class NetgearDeviceScanner(DeviceScanner):
"""Scan for new devices and return a list with found device IDs."""
self._update_info()
return (device.mac for device in self.last_results)
devices = []
for dev in self.last_results:
tracked = (not self.tracked_devices or
dev.mac in self.tracked_devices or
dev.name in self.tracked_devices)
tracked = tracked and (not self.excluded_devices or not(
dev.mac in self.excluded_devices or
dev.name in self.excluded_devices))
if tracked:
devices.append(dev.mac)
if (self.tracked_accesspoints and
dev.conn_ap_mac in self.tracked_accesspoints):
devices.append(dev.mac + "_" + dev.conn_ap_mac)
return devices
def get_device_name(self, device):
"""Return the name of the given device or None if we don't know."""
try:
return next(result.name for result in self.last_results
if result.mac == device)
except StopIteration:
return None
"""Return the name of the given device or the MAC if we don't know."""
parts = device.split("_")
mac = parts[0]
ap_mac = None
if len(parts) > 1:
ap_mac = parts[1]
name = None
for dev in self.last_results:
if dev.mac == mac:
name = dev.name
break
if not name or name == "--":
name = mac
if ap_mac:
ap_name = "Router"
for dev in self.last_results:
if dev.mac == ap_mac:
ap_name = dev.name
break
return name + " on " + ap_name
return name
def _update_info(self):
"""Retrieve latest information from the Netgear router.
@ -88,9 +139,21 @@ class NetgearDeviceScanner(DeviceScanner):
_LOGGER.info("Scanning")
results = self._api.get_attached_devices()
results = self.get_attached_devices()
if results is None:
_LOGGER.warning("Error scanning devices")
self.last_results = results or []
def get_attached_devices(self):
"""
List attached devices with pynetgear.
The v2 method takes more time and is more heavy on the router
so we only use it if we need connected AP info.
"""
if self.tracked_accesspoints:
return self._api.get_attached_devices_2()
return self._api.get_attached_devices()

View File

@ -207,7 +207,7 @@ def _req_json_rpc(url, session_id, rpcmethod, subsystem, method, **params):
try:
res = requests.post(url, data=data, timeout=5)
except requests.exceptions.Timeout:
except (requests.exceptions.ConnectionError, requests.exceptions.Timeout):
return
if res.status_code == 200:

View File

@ -4,7 +4,6 @@ Support for Dialogflow webhook.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/dialogflow/
"""
import asyncio
import logging
import voluptuous as vol
@ -37,8 +36,7 @@ class DialogFlowError(HomeAssistantError):
"""Raised when a DialogFlow error happens."""
@asyncio.coroutine
def async_setup(hass, config):
async def async_setup(hass, config):
"""Set up Dialogflow component."""
hass.http.register_view(DialogflowIntentsView)
@ -51,16 +49,15 @@ class DialogflowIntentsView(HomeAssistantView):
url = INTENTS_API_ENDPOINT
name = 'api:dialogflow'
@asyncio.coroutine
def post(self, request):
async def post(self, request):
"""Handle Dialogflow."""
hass = request.app['hass']
message = yield from request.json()
message = await request.json()
_LOGGER.debug("Received Dialogflow request: %s", message)
try:
response = yield from async_handle_message(hass, message)
response = await async_handle_message(hass, message)
return b'' if response is None else self.json(response)
except DialogFlowError as err:
@ -93,8 +90,7 @@ def dialogflow_error_response(hass, message, error):
return dialogflow_response.as_dict()
@asyncio.coroutine
def async_handle_message(hass, message):
async def async_handle_message(hass, message):
"""Handle a DialogFlow message."""
req = message.get('result')
action_incomplete = req['actionIncomplete']
@ -110,7 +106,7 @@ def async_handle_message(hass, message):
raise DialogFlowError(
"You have not defined an action in your Dialogflow intent.")
intent_response = yield from intent.async_handle(
intent_response = await intent.async_handle(
hass, DOMAIN, action,
{key: {'value': value} for key, value
in parameters.items()})

View File

@ -21,7 +21,7 @@ from homeassistant.helpers.event import async_track_point_in_utc_time
from homeassistant.helpers.discovery import async_load_platform, async_discover
import homeassistant.util.dt as dt_util
REQUIREMENTS = ['netdisco==1.3.1']
REQUIREMENTS = ['netdisco==1.4.1']
DOMAIN = 'discovery'
@ -79,6 +79,7 @@ SERVICE_HANDLERS = {
'bluesound': ('media_player', 'bluesound'),
'songpal': ('media_player', 'songpal'),
'kodi': ('media_player', 'kodi'),
'volumio': ('media_player', 'volumio'),
}
OPTIONAL_SERVICE_HANDLERS = {

View File

@ -22,7 +22,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 = ['pyeight==0.0.7']
REQUIREMENTS = ['pyeight==0.0.8']
_LOGGER = logging.getLogger(__name__)

View File

@ -20,7 +20,7 @@ import homeassistant.helpers.config_validation as cv
from homeassistant.util.json import load_json, save_json
from .hue_api import (
HueUsernameView, HueAllLightsStateView, HueOneLightStateView,
HueOneLightChangeView)
HueOneLightChangeView, HueGroupView)
from .upnp import DescriptionXmlView, UPNPResponderThread
DOMAIN = 'emulated_hue'
@ -104,6 +104,7 @@ def setup(hass, yaml_config):
server.register_view(HueAllLightsStateView(config))
server.register_view(HueOneLightStateView(config))
server.register_view(HueOneLightChangeView(config))
server.register_view(HueGroupView(config))
upnp_listener = UPNPResponderThread(
config.host_ip_addr, config.listen_port,

View File

@ -51,6 +51,29 @@ class HueUsernameView(HomeAssistantView):
return self.json([{'success': {'username': '12345678901234567890'}}])
class HueGroupView(HomeAssistantView):
"""Group handler to get Logitech Pop working."""
url = '/api/{username}/groups/0/action'
name = 'emulated_hue:groups:state'
requires_auth = False
def __init__(self, config):
"""Initialize the instance of the view."""
self.config = config
@core.callback
def put(self, request, username):
"""Process a request to make the Logitech Pop working."""
return self.json([{
'error': {
'address': '/groups/0/action/scene',
'type': 7,
'description': 'invalid value, dummy for parameter, scene'
}
}])
class HueAllLightsStateView(HomeAssistantView):
"""Handle requests for getting and setting info about entities."""

View File

@ -31,7 +31,7 @@ _LOGGER = logging.getLogger(__name__)
@asyncio.coroutine
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
"""Set up the INSTEON PLM device class for the hass platform."""
plm = hass.data['insteon_plm']
plm = hass.data['insteon_plm'].get('plm')
address = discovery_info['address']
device = plm.devices[address]

View File

@ -4,7 +4,6 @@ Support for MQTT fans.
For more details about this platform, please refer to the documentation
https://home-assistant.io/components/fan.mqtt/
"""
import asyncio
import logging
import voluptuous as vol
@ -19,6 +18,7 @@ from homeassistant.components.mqtt import (
CONF_PAYLOAD_AVAILABLE, CONF_PAYLOAD_NOT_AVAILABLE, CONF_QOS, CONF_RETAIN,
MqttAvailability)
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.typing import HomeAssistantType, ConfigType
from homeassistant.components.fan import (SPEED_LOW, SPEED_MEDIUM,
SPEED_HIGH, FanEntity,
SUPPORT_SET_SPEED, SUPPORT_OSCILLATE,
@ -77,8 +77,8 @@ PLATFORM_SCHEMA = mqtt.MQTT_RW_PLATFORM_SCHEMA.extend({
}).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema)
@asyncio.coroutine
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
async def async_setup_platform(hass: HomeAssistantType, config: ConfigType,
async_add_devices, discovery_info=None):
"""Set up the MQTT fan platform."""
if discovery_info is not None:
config = PLATFORM_SCHEMA(discovery_info)
@ -149,10 +149,9 @@ class MqttFan(MqttAvailability, FanEntity):
self._supported_features |= (topic[CONF_SPEED_STATE_TOPIC]
is not None and SUPPORT_SET_SPEED)
@asyncio.coroutine
def async_added_to_hass(self):
async def async_added_to_hass(self):
"""Subscribe to MQTT events."""
yield from super().async_added_to_hass()
await super().async_added_to_hass()
templates = {}
for key, tpl in list(self._templates.items()):
@ -173,7 +172,7 @@ class MqttFan(MqttAvailability, FanEntity):
self.async_schedule_update_ha_state()
if self._topic[CONF_STATE_TOPIC] is not None:
yield from mqtt.async_subscribe(
await mqtt.async_subscribe(
self.hass, self._topic[CONF_STATE_TOPIC], state_received,
self._qos)
@ -190,7 +189,7 @@ class MqttFan(MqttAvailability, FanEntity):
self.async_schedule_update_ha_state()
if self._topic[CONF_SPEED_STATE_TOPIC] is not None:
yield from mqtt.async_subscribe(
await mqtt.async_subscribe(
self.hass, self._topic[CONF_SPEED_STATE_TOPIC], speed_received,
self._qos)
self._speed = SPEED_OFF
@ -206,7 +205,7 @@ class MqttFan(MqttAvailability, FanEntity):
self.async_schedule_update_ha_state()
if self._topic[CONF_OSCILLATION_STATE_TOPIC] is not None:
yield from mqtt.async_subscribe(
await mqtt.async_subscribe(
self.hass, self._topic[CONF_OSCILLATION_STATE_TOPIC],
oscillation_received, self._qos)
self._oscillation = False
@ -251,8 +250,7 @@ class MqttFan(MqttAvailability, FanEntity):
"""Return the oscillation state."""
return self._oscillation
@asyncio.coroutine
def async_turn_on(self, speed: str = None, **kwargs) -> None:
async def async_turn_on(self, speed: str = None, **kwargs) -> None:
"""Turn on the entity.
This method is a coroutine.
@ -261,10 +259,9 @@ class MqttFan(MqttAvailability, FanEntity):
self.hass, self._topic[CONF_COMMAND_TOPIC],
self._payload[STATE_ON], self._qos, self._retain)
if speed:
yield from self.async_set_speed(speed)
await self.async_set_speed(speed)
@asyncio.coroutine
def async_turn_off(self, **kwargs) -> None:
async def async_turn_off(self, **kwargs) -> None:
"""Turn off the entity.
This method is a coroutine.
@ -273,8 +270,7 @@ class MqttFan(MqttAvailability, FanEntity):
self.hass, self._topic[CONF_COMMAND_TOPIC],
self._payload[STATE_OFF], self._qos, self._retain)
@asyncio.coroutine
def async_set_speed(self, speed: str) -> None:
async def async_set_speed(self, speed: str) -> None:
"""Set the speed of the fan.
This method is a coroutine.
@ -299,8 +295,7 @@ class MqttFan(MqttAvailability, FanEntity):
self._speed = speed
self.async_schedule_update_ha_state()
@asyncio.coroutine
def async_oscillate(self, oscillating: bool) -> None:
async def async_oscillate(self, oscillating: bool) -> None:
"""Set oscillation.
This method is a coroutine.

View File

@ -0,0 +1,324 @@
"""
Support for Template fans.
For more details about this platform, please refer to the documentation
https://home-assistant.io/components/fan.template/
"""
import logging
import voluptuous as vol
from homeassistant.core import callback
from homeassistant.const import (
CONF_FRIENDLY_NAME, CONF_VALUE_TEMPLATE, CONF_ENTITY_ID,
STATE_ON, STATE_OFF, MATCH_ALL, EVENT_HOMEASSISTANT_START,
STATE_UNKNOWN)
from homeassistant.exceptions import TemplateError
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.config_validation import PLATFORM_SCHEMA
from homeassistant.helpers.entity import ToggleEntity
from homeassistant.components.fan import (SPEED_LOW, SPEED_MEDIUM,
SPEED_HIGH, SUPPORT_SET_SPEED,
SUPPORT_OSCILLATE, FanEntity,
ATTR_SPEED, ATTR_OSCILLATING,
ENTITY_ID_FORMAT)
from homeassistant.helpers.entity import async_generate_entity_id
from homeassistant.helpers.script import Script
_LOGGER = logging.getLogger(__name__)
CONF_FANS = 'fans'
CONF_SPEED_LIST = 'speeds'
CONF_SPEED_TEMPLATE = 'speed_template'
CONF_OSCILLATING_TEMPLATE = 'oscillating_template'
CONF_ON_ACTION = 'turn_on'
CONF_OFF_ACTION = 'turn_off'
CONF_SET_SPEED_ACTION = 'set_speed'
CONF_SET_OSCILLATING_ACTION = 'set_oscillating'
_VALID_STATES = [STATE_ON, STATE_OFF]
_VALID_OSC = [True, False]
FAN_SCHEMA = vol.Schema({
vol.Optional(CONF_FRIENDLY_NAME): cv.string,
vol.Required(CONF_VALUE_TEMPLATE): cv.template,
vol.Optional(CONF_SPEED_TEMPLATE): cv.template,
vol.Optional(CONF_OSCILLATING_TEMPLATE): cv.template,
vol.Required(CONF_ON_ACTION): cv.SCRIPT_SCHEMA,
vol.Required(CONF_OFF_ACTION): cv.SCRIPT_SCHEMA,
vol.Optional(CONF_SET_SPEED_ACTION): cv.SCRIPT_SCHEMA,
vol.Optional(CONF_SET_OSCILLATING_ACTION): cv.SCRIPT_SCHEMA,
vol.Optional(
CONF_SPEED_LIST,
default=[SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH]
): cv.ensure_list,
vol.Optional(CONF_ENTITY_ID): cv.entity_ids
})
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_FANS): vol.Schema({cv.slug: FAN_SCHEMA}),
})
async def async_setup_platform(
hass, config, async_add_devices, discovery_info=None
):
"""Set up the Template Fans."""
fans = []
for device, device_config in config[CONF_FANS].items():
friendly_name = device_config.get(CONF_FRIENDLY_NAME, device)
state_template = device_config[CONF_VALUE_TEMPLATE]
speed_template = device_config.get(CONF_SPEED_TEMPLATE)
oscillating_template = device_config.get(
CONF_OSCILLATING_TEMPLATE
)
on_action = device_config[CONF_ON_ACTION]
off_action = device_config[CONF_OFF_ACTION]
set_speed_action = device_config.get(CONF_SET_SPEED_ACTION)
set_oscillating_action = device_config.get(CONF_SET_OSCILLATING_ACTION)
speed_list = device_config[CONF_SPEED_LIST]
entity_ids = set()
manual_entity_ids = device_config.get(CONF_ENTITY_ID)
for template in (state_template, speed_template, oscillating_template):
if template is None:
continue
template.hass = hass
if entity_ids == MATCH_ALL or manual_entity_ids is not None:
continue
template_entity_ids = template.extract_entities()
if template_entity_ids == MATCH_ALL:
entity_ids = MATCH_ALL
else:
entity_ids |= set(template_entity_ids)
if manual_entity_ids is not None:
entity_ids = manual_entity_ids
elif entity_ids != MATCH_ALL:
entity_ids = list(entity_ids)
fans.append(
TemplateFan(
hass, device, friendly_name,
state_template, speed_template, oscillating_template,
on_action, off_action, set_speed_action,
set_oscillating_action, speed_list, entity_ids
)
)
async_add_devices(fans)
class TemplateFan(FanEntity):
"""A template fan component."""
def __init__(self, hass, device_id, friendly_name,
state_template, speed_template, oscillating_template,
on_action, off_action, set_speed_action,
set_oscillating_action, speed_list, entity_ids):
"""Initialize the fan."""
self.hass = hass
self.entity_id = async_generate_entity_id(
ENTITY_ID_FORMAT, device_id, hass=hass)
self._name = friendly_name
self._template = state_template
self._speed_template = speed_template
self._oscillating_template = oscillating_template
self._supported_features = 0
self._on_script = Script(hass, on_action)
self._off_script = Script(hass, off_action)
self._set_speed_script = None
if set_speed_action:
self._set_speed_script = Script(hass, set_speed_action)
self._set_oscillating_script = None
if set_oscillating_action:
self._set_oscillating_script = Script(hass, set_oscillating_action)
self._state = STATE_OFF
self._speed = None
self._oscillating = None
self._template.hass = self.hass
if self._speed_template:
self._speed_template.hass = self.hass
self._supported_features |= SUPPORT_SET_SPEED
if self._oscillating_template:
self._oscillating_template.hass = self.hass
self._supported_features |= SUPPORT_OSCILLATE
self._entities = entity_ids
# List of valid speeds
self._speed_list = speed_list
@property
def name(self):
"""Return the display name of this fan."""
return self._name
@property
def supported_features(self) -> int:
"""Flag supported features."""
return self._supported_features
@property
def speed_list(self: ToggleEntity) -> list:
"""Get the list of available speeds."""
return self._speed_list
@property
def is_on(self):
"""Return true if device is on."""
return self._state == STATE_ON
@property
def speed(self):
"""Return the current speed."""
return self._speed
@property
def oscillating(self):
"""Return the oscillation state."""
return self._oscillating
@property
def should_poll(self):
"""Return the polling state."""
return False
# pylint: disable=arguments-differ
async def async_turn_on(self, speed: str = None) -> None:
"""Turn on the fan."""
await self._on_script.async_run()
self._state = STATE_ON
if speed is not None:
await self.async_set_speed(speed)
# pylint: disable=arguments-differ
async def async_turn_off(self) -> None:
"""Turn off the fan."""
await self._off_script.async_run()
self._state = STATE_OFF
async def async_set_speed(self, speed: str) -> None:
"""Set the speed of the fan."""
if self._set_speed_script is None:
return
if speed in self._speed_list:
self._speed = speed
await self._set_speed_script.async_run({ATTR_SPEED: speed})
else:
_LOGGER.error(
'Received invalid speed: %s. ' +
'Expected: %s.',
speed, self._speed_list)
async def async_oscillate(self, oscillating: bool) -> None:
"""Set oscillation of the fan."""
if self._set_oscillating_script is None:
return
await self._set_oscillating_script.async_run(
{ATTR_OSCILLATING: oscillating}
)
self._oscillating = oscillating
async def async_added_to_hass(self):
"""Register callbacks."""
@callback
def template_fan_state_listener(entity, old_state, new_state):
"""Handle target device state changes."""
self.async_schedule_update_ha_state(True)
@callback
def template_fan_startup(event):
"""Update template on startup."""
self.hass.helpers.event.async_track_state_change(
self._entities, template_fan_state_listener)
self.async_schedule_update_ha_state(True)
self.hass.bus.async_listen_once(
EVENT_HOMEASSISTANT_START, template_fan_startup)
async def async_update(self):
"""Update the state from the template."""
# Update state
try:
state = self._template.async_render()
except TemplateError as ex:
_LOGGER.error(ex)
state = None
self._state = None
# Validate state
if state in _VALID_STATES:
self._state = state
elif state == STATE_UNKNOWN:
self._state = None
else:
_LOGGER.error(
'Received invalid fan is_on state: %s. ' +
'Expected: %s.',
state, ', '.join(_VALID_STATES))
self._state = None
# Update speed if 'speed_template' is configured
if self._speed_template is not None:
try:
speed = self._speed_template.async_render()
except TemplateError as ex:
_LOGGER.error(ex)
speed = None
self._state = None
# Validate speed
if speed in self._speed_list:
self._speed = speed
elif speed == STATE_UNKNOWN:
self._speed = None
else:
_LOGGER.error(
'Received invalid speed: %s. ' +
'Expected: %s.',
speed, self._speed_list)
self._speed = None
# Update oscillating if 'oscillating_template' is configured
if self._oscillating_template is not None:
try:
oscillating = self._oscillating_template.async_render()
except TemplateError as ex:
_LOGGER.error(ex)
self._state = None
# Validate osc
if oscillating == 'True' or oscillating is True:
self._oscillating = True
elif oscillating == 'False' or oscillating is False:
self._oscillating = False
elif oscillating == STATE_UNKNOWN:
self._oscillating = None
else:
_LOGGER.error(
'Received invalid oscillating: %s. ' +
'Expected: True/False.', oscillating)
self._oscillating = None

View File

@ -16,15 +16,16 @@ import voluptuous as vol
import jinja2
import homeassistant.helpers.config_validation as cv
from homeassistant.components.http import HomeAssistantView
from homeassistant.components.http.view import HomeAssistantView
from homeassistant.components.http.const import KEY_AUTHENTICATED
from homeassistant.components import websocket_api
from homeassistant.config import find_config_file, load_yaml_config_file
from homeassistant.const import CONF_NAME, EVENT_THEMES_UPDATED
from homeassistant.core import callback
from homeassistant.helpers.translation import async_get_translations
from homeassistant.loader import bind_hass
REQUIREMENTS = ['home-assistant-frontend==20180426.0']
REQUIREMENTS = ['home-assistant-frontend==20180509.0']
DOMAIN = 'frontend'
DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log']
@ -94,6 +95,10 @@ SERVICE_RELOAD_THEMES = 'reload_themes'
SERVICE_SET_THEME_SCHEMA = vol.Schema({
vol.Required(CONF_NAME): cv.string,
})
WS_TYPE_GET_PANELS = 'get_panels'
SCHEMA_GET_PANELS = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
vol.Required('type'): WS_TYPE_GET_PANELS,
})
class AbstractPanel:
@ -291,6 +296,8 @@ def add_manifest_json_key(key, val):
@asyncio.coroutine
def async_setup(hass, config):
"""Set up the serving of the frontend."""
hass.components.websocket_api.async_register_command(
WS_TYPE_GET_PANELS, websocket_handle_get_panels, SCHEMA_GET_PANELS)
hass.http.register_view(ManifestJSONView)
conf = config.get(DOMAIN, {})
@ -597,3 +604,19 @@ def _is_latest(js_option, request):
useragent = request.headers.get('User-Agent')
return useragent and hass_frontend.version(useragent)
@callback
def websocket_handle_get_panels(hass, connection, msg):
"""Handle get panels command.
Async friendly.
"""
panels = {
panel:
connection.hass.data[DATA_PANELS][panel].to_response(
connection.hass, connection.request)
for panel in connection.hass.data[DATA_PANELS]}
connection.to_write.put_nowait(websocket_api.result_message(
msg['id'], panels))

View File

@ -70,8 +70,7 @@ def request_sync(hass):
hass.services.call(DOMAIN, SERVICE_REQUEST_SYNC)
@asyncio.coroutine
def async_setup(hass: HomeAssistant, yaml_config: Dict[str, Any]):
async def async_setup(hass: HomeAssistant, yaml_config: Dict[str, Any]):
"""Activate Google Actions component."""
config = yaml_config.get(DOMAIN, {})
agent_user_id = config.get(CONF_AGENT_USER_ID)
@ -79,20 +78,19 @@ def async_setup(hass: HomeAssistant, yaml_config: Dict[str, Any]):
hass.http.register_view(GoogleAssistantAuthView(hass, config))
async_register_http(hass, config)
@asyncio.coroutine
def request_sync_service_handler(call):
async def request_sync_service_handler(call):
"""Handle request sync service calls."""
websession = async_get_clientsession(hass)
try:
with async_timeout.timeout(5, loop=hass.loop):
res = yield from websession.post(
res = await websession.post(
REQUEST_SYNC_BASE_URL,
params={'key': api_key},
json={'agent_user_id': agent_user_id})
_LOGGER.info("Submitted request_sync request to Google")
res.raise_for_status()
except aiohttp.ClientResponseError:
body = yield from res.read()
body = await res.read()
_LOGGER.error(
'request_sync request failed: %d %s', res.status, body)
except (asyncio.TimeoutError, aiohttp.ClientError):

View File

@ -1,6 +1,5 @@
"""Google Assistant OAuth View."""
import asyncio
import logging
# Typing imports
@ -44,8 +43,7 @@ class GoogleAssistantAuthView(HomeAssistantView):
self.client_id = cfg.get(CONF_CLIENT_ID)
self.access_token = cfg.get(CONF_ACCESS_TOKEN)
@asyncio.coroutine
def get(self, request: Request) -> Response:
async def get(self, request: Request) -> Response:
"""Handle oauth token request."""
query = request.query
redirect_uri = query.get('redirect_uri')

View File

@ -4,7 +4,6 @@ Support for Google Actions Smart Home Control.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/google_assistant/
"""
import asyncio
import logging
from aiohttp.hdrs import AUTHORIZATION
@ -77,14 +76,13 @@ class GoogleAssistantView(HomeAssistantView):
self.access_token = access_token
self.gass_config = gass_config
@asyncio.coroutine
def post(self, request: Request) -> Response:
async def post(self, request: Request) -> Response:
"""Handle Google Assistant requests."""
auth = request.headers.get(AUTHORIZATION, None)
if 'Bearer {}'.format(self.access_token) != auth:
return self.json_message("missing authorization", status_code=401)
message = yield from request.json() # type: dict
result = yield from async_handle_message(
message = await request.json() # type: dict
result = await async_handle_message(
request.app['hass'], self.gass_config, message)
return self.json(result)

View File

@ -245,34 +245,31 @@ def get_entity_ids(hass, entity_id, domain_filter=None):
if ent_id.startswith(domain_filter)]
@asyncio.coroutine
def async_setup(hass, config):
async def async_setup(hass, config):
"""Set up all groups found defined in the configuration."""
component = hass.data.get(DOMAIN)
if component is None:
component = hass.data[DOMAIN] = EntityComponent(_LOGGER, DOMAIN, hass)
yield from _async_process_config(hass, config, component)
await _async_process_config(hass, config, component)
@asyncio.coroutine
def reload_service_handler(service):
async def reload_service_handler(service):
"""Remove all user-defined groups and load new ones from config."""
auto = list(filter(lambda e: not e.user_defined, component.entities))
conf = yield from component.async_prepare_reload()
conf = await component.async_prepare_reload()
if conf is None:
return
yield from _async_process_config(hass, conf, component)
await _async_process_config(hass, conf, component)
yield from component.async_add_entities(auto)
await component.async_add_entities(auto)
hass.services.async_register(
DOMAIN, SERVICE_RELOAD, reload_service_handler,
schema=RELOAD_SERVICE_SCHEMA)
@asyncio.coroutine
def groups_service_handler(service):
async def groups_service_handler(service):
"""Handle dynamic group service functions."""
object_id = service.data[ATTR_OBJECT_ID]
entity_id = ENTITY_ID_FORMAT.format(object_id)
@ -287,7 +284,7 @@ def async_setup(hass, config):
ATTR_VISIBLE, ATTR_ICON, ATTR_VIEW, ATTR_CONTROL
) if service.data.get(attr) is not None}
yield from Group.async_create_group(
await Group.async_create_group(
hass, service.data.get(ATTR_NAME, object_id),
object_id=object_id,
entity_ids=entity_ids,
@ -308,11 +305,11 @@ def async_setup(hass, config):
if ATTR_ADD_ENTITIES in service.data:
delta = service.data[ATTR_ADD_ENTITIES]
entity_ids = set(group.tracking) | set(delta)
yield from group.async_update_tracked_entity_ids(entity_ids)
await group.async_update_tracked_entity_ids(entity_ids)
if ATTR_ENTITIES in service.data:
entity_ids = service.data[ATTR_ENTITIES]
yield from group.async_update_tracked_entity_ids(entity_ids)
await group.async_update_tracked_entity_ids(entity_ids)
if ATTR_NAME in service.data:
group.name = service.data[ATTR_NAME]
@ -335,13 +332,13 @@ def async_setup(hass, config):
need_update = True
if need_update:
yield from group.async_update_ha_state()
await group.async_update_ha_state()
return
# remove group
if service.service == SERVICE_REMOVE:
yield from component.async_remove_entity(entity_id)
await component.async_remove_entity(entity_id)
hass.services.async_register(
DOMAIN, SERVICE_SET, groups_service_handler,
@ -351,8 +348,7 @@ def async_setup(hass, config):
DOMAIN, SERVICE_REMOVE, groups_service_handler,
schema=REMOVE_SERVICE_SCHEMA)
@asyncio.coroutine
def visibility_service_handler(service):
async def visibility_service_handler(service):
"""Change visibility of a group."""
visible = service.data.get(ATTR_VISIBLE)
@ -363,7 +359,7 @@ def async_setup(hass, config):
tasks.append(group.async_update_ha_state())
if tasks:
yield from asyncio.wait(tasks, loop=hass.loop)
await asyncio.wait(tasks, loop=hass.loop)
hass.services.async_register(
DOMAIN, SERVICE_SET_VISIBILITY, visibility_service_handler,
@ -372,8 +368,7 @@ def async_setup(hass, config):
return True
@asyncio.coroutine
def _async_process_config(hass, config, component):
async def _async_process_config(hass, config, component):
"""Process group configuration."""
for object_id, conf in config.get(DOMAIN, {}).items():
name = conf.get(CONF_NAME, object_id)
@ -384,7 +379,7 @@ def _async_process_config(hass, config, component):
# Don't create tasks and await them all. The order is important as
# groups get a number based on creation order.
yield from Group.async_create_group(
await Group.async_create_group(
hass, name, entity_ids, icon=icon, view=view,
control=control, object_id=object_id)
@ -428,10 +423,9 @@ class Group(Entity):
hass.loop).result()
@staticmethod
@asyncio.coroutine
def async_create_group(hass, name, entity_ids=None, user_defined=True,
visible=True, icon=None, view=False, control=None,
object_id=None):
async def async_create_group(hass, name, entity_ids=None,
user_defined=True, visible=True, icon=None,
view=False, control=None, object_id=None):
"""Initialize a group.
This method must be run in the event loop.
@ -453,7 +447,7 @@ class Group(Entity):
component = hass.data[DOMAIN] = \
EntityComponent(_LOGGER, DOMAIN, hass)
yield from component.async_add_entities([group], True)
await component.async_add_entities([group], True)
return group
@ -520,17 +514,16 @@ class Group(Entity):
self.async_update_tracked_entity_ids(entity_ids), self.hass.loop
).result()
@asyncio.coroutine
def async_update_tracked_entity_ids(self, entity_ids):
async def async_update_tracked_entity_ids(self, entity_ids):
"""Update the member entity IDs.
This method must be run in the event loop.
"""
yield from self.async_stop()
await self.async_stop()
self.tracking = tuple(ent_id.lower() for ent_id in entity_ids)
self.group_on, self.group_off = None, None
yield from self.async_update_ha_state(True)
await self.async_update_ha_state(True)
self.async_start()
@callback
@ -544,8 +537,7 @@ class Group(Entity):
self.hass, self.tracking, self._async_state_changed_listener
)
@asyncio.coroutine
def async_stop(self):
async def async_stop(self):
"""Unregister the group from Home Assistant.
This method must be run in the event loop.
@ -554,27 +546,24 @@ class Group(Entity):
self._async_unsub_state_changed()
self._async_unsub_state_changed = None
@asyncio.coroutine
def async_update(self):
async def async_update(self):
"""Query all members and determine current group state."""
self._state = STATE_UNKNOWN
self._async_update_group_state()
@asyncio.coroutine
def async_added_to_hass(self):
async def async_added_to_hass(self):
"""Callback when added to HASS."""
if self.tracking:
self.async_start()
@asyncio.coroutine
def async_will_remove_from_hass(self):
async def async_will_remove_from_hass(self):
"""Callback when removed from HASS."""
if self._async_unsub_state_changed:
self._async_unsub_state_changed()
self._async_unsub_state_changed = None
@asyncio.coroutine
def _async_state_changed_listener(self, entity_id, old_state, new_state):
async def _async_state_changed_listener(self, entity_id, old_state,
new_state):
"""Respond to a member state changing.
This method must be run in the event loop.
@ -584,7 +573,7 @@ class Group(Entity):
return
self._async_update_group_state(new_state)
yield from self.async_update_ha_state()
await self.async_update_ha_state()
@property
def _tracking_states(self):

View File

@ -4,7 +4,6 @@ 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/
"""
import asyncio
from collections import defaultdict
from datetime import timedelta
from itertools import groupby
@ -259,8 +258,7 @@ def get_state(hass, utc_point_in_time, entity_id, run=None):
return states[0] if states else None
@asyncio.coroutine
def async_setup(hass, config):
async def async_setup(hass, config):
"""Set up the history hooks."""
filters = Filters()
conf = config.get(DOMAIN, {})
@ -275,7 +273,7 @@ def async_setup(hass, config):
use_include_order = conf.get(CONF_ORDER)
hass.http.register_view(HistoryPeriodView(filters, use_include_order))
yield from hass.components.frontend.async_register_built_in_panel(
await hass.components.frontend.async_register_built_in_panel(
'history', 'history', 'mdi:poll-box')
return True
@ -293,8 +291,7 @@ class HistoryPeriodView(HomeAssistantView):
self.filters = filters
self.use_include_order = use_include_order
@asyncio.coroutine
def get(self, request, datetime=None):
async def get(self, request, datetime=None):
"""Return history over a period of time."""
timer_start = time.perf_counter()
if datetime:
@ -330,7 +327,7 @@ class HistoryPeriodView(HomeAssistantView):
hass = request.app['hass']
result = yield from hass.async_add_job(
result = await hass.async_add_job(
get_significant_states, hass, start_time, end_time,
entity_ids, self.filters, include_start_time_state)
result = list(result.values())
@ -353,8 +350,7 @@ class HistoryPeriodView(HomeAssistantView):
sorted_result.extend(result)
result = sorted_result
response = yield from hass.async_add_job(self.json, result)
return response
return await hass.async_add_job(self.json, result)
class Filters(object):

View File

@ -4,7 +4,6 @@ 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/
"""
import asyncio
import logging
import voluptuous as vol
@ -39,8 +38,7 @@ CONFIG_SCHEMA = vol.Schema({
}, extra=vol.ALLOW_EXTRA)
@asyncio.coroutine
def async_setup(hass, config):
async def async_setup(hass, config):
"""Load graph configurations."""
component = EntityComponent(
_LOGGER, DOMAIN, hass)
@ -51,7 +49,7 @@ def async_setup(hass, config):
graph = HistoryGraphEntity(name, cfg)
graphs.append(graph)
yield from component.async_add_entities(graphs)
await component.async_add_entities(graphs)
return True

View File

@ -14,7 +14,8 @@ from homeassistant.components.cover import (
from homeassistant.const import (
ATTR_SUPPORTED_FEATURES, ATTR_UNIT_OF_MEASUREMENT,
ATTR_DEVICE_CLASS, CONF_IP_ADDRESS, CONF_PORT, TEMP_CELSIUS,
TEMP_FAHRENHEIT, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP)
TEMP_FAHRENHEIT, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP,
DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_ILLUMINANCE, DEVICE_CLASS_TEMPERATURE)
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entityfilter import FILTER_SCHEMA
from homeassistant.util import get_local_ip
@ -22,15 +23,20 @@ from homeassistant.util.decorator import Registry
from .const import (
DOMAIN, HOMEKIT_FILE, CONF_AUTO_START, CONF_ENTITY_CONFIG, CONF_FILTER,
DEFAULT_PORT, DEFAULT_AUTO_START, SERVICE_HOMEKIT_START,
DEVICE_CLASS_CO2, DEVICE_CLASS_LIGHT, DEVICE_CLASS_HUMIDITY,
DEVICE_CLASS_PM25, DEVICE_CLASS_TEMPERATURE)
DEVICE_CLASS_CO2, DEVICE_CLASS_PM25)
from .util import (
validate_entity_config, show_setup_message)
TYPES = Registry()
_LOGGER = logging.getLogger(__name__)
REQUIREMENTS = ['HAP-python==1.1.9']
REQUIREMENTS = ['HAP-python==2.0.0']
# #### Driver Status ####
STATUS_READY = 0
STATUS_RUNNING = 1
STATUS_STOPPED = 2
STATUS_WAIT = 3
CONFIG_SCHEMA = vol.Schema({
@ -57,7 +63,7 @@ async def async_setup(hass, config):
entity_config = conf[CONF_ENTITY_CONFIG]
homekit = HomeKit(hass, port, ip_address, entity_filter, entity_config)
homekit.setup()
await hass.async_add_job(homekit.setup)
if auto_start:
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, homekit.start)
@ -65,8 +71,10 @@ async def async_setup(hass, config):
def handle_homekit_service_start(service):
"""Handle start HomeKit service call."""
if homekit.started:
_LOGGER.warning('HomeKit is already running')
if homekit.status != STATUS_READY:
_LOGGER.warning(
'HomeKit is not ready. Either it is already running or has '
'been stopped.')
return
homekit.start()
@ -118,10 +126,10 @@ def get_accessory(hass, state, aid, config):
unit = state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
device_class = state.attributes.get(ATTR_DEVICE_CLASS)
if device_class == DEVICE_CLASS_TEMPERATURE or unit == TEMP_CELSIUS \
or unit == TEMP_FAHRENHEIT:
if device_class == DEVICE_CLASS_TEMPERATURE or \
unit in (TEMP_CELSIUS, TEMP_FAHRENHEIT):
a_type = 'TemperatureSensor'
elif device_class == DEVICE_CLASS_HUMIDITY or unit == '%':
elif device_class == DEVICE_CLASS_HUMIDITY and unit == '%':
a_type = 'HumiditySensor'
elif device_class == DEVICE_CLASS_PM25 \
or DEVICE_CLASS_PM25 in state.entity_id:
@ -129,12 +137,10 @@ def get_accessory(hass, state, aid, config):
elif device_class == DEVICE_CLASS_CO2 \
or DEVICE_CLASS_CO2 in state.entity_id:
a_type = 'CarbonDioxideSensor'
elif device_class == DEVICE_CLASS_LIGHT or unit == 'lm' or \
unit == 'lux':
elif device_class == DEVICE_CLASS_ILLUMINANCE or unit in ('lm', 'lx'):
a_type = 'LightSensor'
elif state.domain == 'switch' or state.domain == 'remote' \
or state.domain == 'input_boolean' or state.domain == 'script':
elif state.domain in ('switch', 'remote', 'input_boolean', 'script'):
a_type = 'Switch'
if a_type is None:
@ -162,7 +168,7 @@ class HomeKit():
self._ip_address = ip_address
self._filter = entity_filter
self._config = entity_config
self.started = False
self.status = STATUS_READY
self.bridge = None
self.driver = None
@ -191,9 +197,9 @@ class HomeKit():
def start(self, *args):
"""Start the accessory driver."""
if self.started:
if self.status != STATUS_READY:
return
self.started = True
self.status = STATUS_WAIT
# pylint: disable=unused-variable
from . import ( # noqa F401
@ -202,19 +208,20 @@ class HomeKit():
for state in self.hass.states.all():
self.add_bridge_accessory(state)
self.bridge.set_broker(self.driver)
self.bridge.set_driver(self.driver)
if not self.bridge.paired:
show_setup_message(self.hass, self.bridge)
_LOGGER.debug('Driver start')
self.driver.start()
self.hass.add_job(self.driver.start)
self.status = STATUS_RUNNING
def stop(self, *args):
"""Stop the accessory driver."""
if not self.started:
if self.status != STATUS_RUNNING:
return
self.status = STATUS_STOPPED
_LOGGER.debug('Driver stop')
if self.driver and self.driver.run_sentinel:
self.driver.stop()
self.hass.add_job(self.driver.stop)

View File

@ -4,18 +4,20 @@ from functools import wraps
from inspect import getmodule
import logging
from pyhap.accessory import Accessory, Bridge, Category
from pyhap.accessory import Accessory, Bridge
from pyhap.accessory_driver import AccessoryDriver
from pyhap.const import CATEGORY_OTHER
from homeassistant.const import __version__
from homeassistant.core import callback as ha_callback
from homeassistant.core import 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
from .const import (
DEBOUNCE_TIMEOUT, BRIDGE_MODEL, BRIDGE_NAME, MANUFACTURER,
SERV_ACCESSORY_INFO, CHAR_MANUFACTURER,
CHAR_MODEL, CHAR_NAME, CHAR_SERIAL_NUMBER)
DEBOUNCE_TIMEOUT, BRIDGE_MODEL, BRIDGE_NAME,
BRIDGE_SERIAL_NUMBER, MANUFACTURER)
from .util import (
show_setup_message, dismiss_setup_message)
@ -59,55 +61,20 @@ def debounce(func):
return wrapper
def add_preload_service(acc, service, chars=None):
"""Define and return a service to be available for the accessory."""
from pyhap.loader import get_serv_loader, get_char_loader
service = get_serv_loader().get(service)
if chars:
chars = chars if isinstance(chars, list) else [chars]
for char_name in chars:
char = get_char_loader().get(char_name)
service.add_characteristic(char)
acc.add_service(service)
return service
def setup_char(char_name, service, value=None, properties=None, callback=None):
"""Helper function to return fully configured characteristic."""
char = service.get_characteristic(char_name)
if value:
char.value = value
if properties:
char.override_properties(properties)
if callback:
char.setter_callback = callback
return char
def set_accessory_info(acc, name, model, manufacturer=MANUFACTURER,
serial_number='0000'):
"""Set the default accessory information."""
service = acc.get_service(SERV_ACCESSORY_INFO)
service.get_characteristic(CHAR_NAME).set_value(name)
service.get_characteristic(CHAR_MODEL).set_value(model)
service.get_characteristic(CHAR_MANUFACTURER).set_value(manufacturer)
service.get_characteristic(CHAR_SERIAL_NUMBER).set_value(serial_number)
class HomeAccessory(Accessory):
"""Adapter class for Accessory."""
def __init__(self, hass, name, entity_id, aid, category):
def __init__(self, hass, name, entity_id, aid, category=CATEGORY_OTHER):
"""Initialize a Accessory object."""
super().__init__(name, aid=aid)
set_accessory_info(self, name, model=entity_id)
self.category = getattr(Category, category, Category.OTHER)
domain = split_entity_id(entity_id)[0].replace("_", " ").title()
self.set_info_service(
firmware_revision=__version__, manufacturer=MANUFACTURER,
model=domain, serial_number=entity_id)
self.category = category
self.entity_id = entity_id
self.hass = hass
def _set_services(self):
add_preload_service(self, SERV_ACCESSORY_INFO)
def run(self):
"""Method called by accessory after driver is started."""
state = self.hass.states.get(self.entity_id)
@ -137,12 +104,11 @@ class HomeBridge(Bridge):
def __init__(self, hass, name=BRIDGE_NAME):
"""Initialize a Bridge object."""
super().__init__(name)
set_accessory_info(self, name, model=BRIDGE_MODEL)
self.set_info_service(
firmware_revision=__version__, manufacturer=MANUFACTURER,
model=BRIDGE_MODEL, serial_number=BRIDGE_SERIAL_NUMBER)
self.hass = hass
def _set_services(self):
add_preload_service(self, SERV_ACCESSORY_INFO)
def setup_message(self):
"""Prevent print of pyhap setup message to terminal."""
pass

View File

@ -18,20 +18,10 @@ DEFAULT_PORT = 51827
SERVICE_HOMEKIT_START = 'start'
# #### STRING CONSTANTS ####
BRIDGE_MODEL = 'homekit.bridge'
BRIDGE_NAME = 'Home Assistant'
MANUFACTURER = 'HomeAssistant'
# #### Categories ####
CATEGORY_ALARM_SYSTEM = 'ALARM_SYSTEM'
CATEGORY_GARAGE_DOOR_OPENER = 'GARAGE_DOOR_OPENER'
CATEGORY_LIGHT = 'LIGHTBULB'
CATEGORY_LOCK = 'DOOR_LOCK'
CATEGORY_SENSOR = 'SENSOR'
CATEGORY_SWITCH = 'SWITCH'
CATEGORY_THERMOSTAT = 'THERMOSTAT'
CATEGORY_WINDOW_COVERING = 'WINDOW_COVERING'
BRIDGE_MODEL = 'Bridge'
BRIDGE_NAME = 'Home Assistant Bridge'
BRIDGE_SERIAL_NUMBER = 'homekit.bridge'
MANUFACTURER = 'Home Assistant'
# #### Services ####
SERV_ACCESSORY_INFO = 'AccessoryInformation'
@ -55,7 +45,6 @@ SERV_THERMOSTAT = 'Thermostat'
SERV_WINDOW_COVERING = 'WindowCovering'
# CurrentPosition, TargetPosition, PositionState
# #### Characteristics ####
CHAR_AIR_PARTICULATE_DENSITY = 'AirParticulateDensity'
CHAR_AIR_QUALITY = 'AirQuality'
@ -74,6 +63,7 @@ CHAR_CURRENT_POSITION = 'CurrentPosition' # Int | [0, 100]
CHAR_CURRENT_HUMIDITY = 'CurrentRelativeHumidity' # percent
CHAR_CURRENT_SECURITY_STATE = 'SecuritySystemCurrentState'
CHAR_CURRENT_TEMPERATURE = 'CurrentTemperature'
CHAR_FIRMWARE_REVISION = 'FirmwareRevision'
CHAR_HEATING_THRESHOLD_TEMPERATURE = 'HeatingThresholdTemperature'
CHAR_HUE = 'Hue' # arcdegress | [0, 360]
CHAR_LEAK_DETECTED = 'LeakDetected'

View File

@ -1,6 +1,8 @@
"""Class to hold all cover accessories."""
import logging
from pyhap.const import CATEGORY_WINDOW_COVERING, CATEGORY_GARAGE_DOOR_OPENER
from homeassistant.components.cover import (
ATTR_CURRENT_POSITION, ATTR_POSITION, DOMAIN, SUPPORT_STOP)
from homeassistant.const import (
@ -9,12 +11,11 @@ from homeassistant.const import (
ATTR_SUPPORTED_FEATURES)
from . import TYPES
from .accessories import HomeAccessory, add_preload_service, setup_char
from .accessories import HomeAccessory, debounce
from .const import (
CATEGORY_WINDOW_COVERING, SERV_WINDOW_COVERING,
CHAR_CURRENT_POSITION, CHAR_TARGET_POSITION, CHAR_POSITION_STATE,
CATEGORY_GARAGE_DOOR_OPENER, SERV_GARAGE_DOOR_OPENER,
CHAR_CURRENT_DOOR_STATE, CHAR_TARGET_DOOR_STATE)
SERV_WINDOW_COVERING, CHAR_CURRENT_POSITION,
CHAR_TARGET_POSITION, CHAR_POSITION_STATE,
SERV_GARAGE_DOOR_OPENER, CHAR_CURRENT_DOOR_STATE, CHAR_TARGET_DOOR_STATE)
_LOGGER = logging.getLogger(__name__)
@ -32,12 +33,11 @@ class GarageDoorOpener(HomeAccessory):
super().__init__(*args, category=CATEGORY_GARAGE_DOOR_OPENER)
self.flag_target_state = False
serv_garage_door = add_preload_service(self, SERV_GARAGE_DOOR_OPENER)
self.char_current_state = setup_char(
CHAR_CURRENT_DOOR_STATE, serv_garage_door, value=0)
self.char_target_state = setup_char(
CHAR_TARGET_DOOR_STATE, serv_garage_door, value=0,
callback=self.set_state)
serv_garage_door = self.add_preload_service(SERV_GARAGE_DOOR_OPENER)
self.char_current_state = serv_garage_door.configure_char(
CHAR_CURRENT_DOOR_STATE, value=0)
self.char_target_state = serv_garage_door.configure_char(
CHAR_TARGET_DOOR_STATE, value=0, setter_callback=self.set_state)
def set_state(self, value):
"""Change garage state if call came from HomeKit."""
@ -74,13 +74,13 @@ class WindowCovering(HomeAccessory):
super().__init__(*args, category=CATEGORY_WINDOW_COVERING)
self.homekit_target = None
serv_cover = add_preload_service(self, SERV_WINDOW_COVERING)
self.char_current_position = setup_char(
CHAR_CURRENT_POSITION, serv_cover, value=0)
self.char_target_position = setup_char(
CHAR_TARGET_POSITION, serv_cover, value=0,
callback=self.move_cover)
serv_cover = self.add_preload_service(SERV_WINDOW_COVERING)
self.char_current_position = serv_cover.configure_char(
CHAR_CURRENT_POSITION, value=0)
self.char_target_position = serv_cover.configure_char(
CHAR_TARGET_POSITION, value=0, setter_callback=self.move_cover)
@debounce
def move_cover(self, value):
"""Move cover to value if call came from HomeKit."""
_LOGGER.debug('%s: Set position to %d', self.entity_id, value)
@ -115,15 +115,15 @@ class WindowCoveringBasic(HomeAccessory):
.attributes.get(ATTR_SUPPORTED_FEATURES)
self.supports_stop = features & SUPPORT_STOP
serv_cover = add_preload_service(self, SERV_WINDOW_COVERING)
self.char_current_position = setup_char(
CHAR_CURRENT_POSITION, serv_cover, value=0)
self.char_target_position = setup_char(
CHAR_TARGET_POSITION, serv_cover, value=0,
callback=self.move_cover)
self.char_position_state = setup_char(
CHAR_POSITION_STATE, serv_cover, value=2)
serv_cover = self.add_preload_service(SERV_WINDOW_COVERING)
self.char_current_position = serv_cover.configure_char(
CHAR_CURRENT_POSITION, value=0)
self.char_target_position = serv_cover.configure_char(
CHAR_TARGET_POSITION, value=0, setter_callback=self.move_cover)
self.char_position_state = serv_cover.configure_char(
CHAR_POSITION_STATE, value=2)
@debounce
def move_cover(self, value):
"""Move cover to value if call came from HomeKit."""
_LOGGER.debug('%s: Set position to %d', self.entity_id, value)

View File

@ -1,16 +1,17 @@
"""Class to hold all light accessories."""
import logging
from pyhap.const import CATEGORY_LIGHTBULB
from homeassistant.components.light import (
ATTR_HS_COLOR, ATTR_COLOR_TEMP, ATTR_BRIGHTNESS, ATTR_MIN_MIREDS,
ATTR_MAX_MIREDS, SUPPORT_COLOR, SUPPORT_COLOR_TEMP, SUPPORT_BRIGHTNESS)
from homeassistant.const import ATTR_SUPPORTED_FEATURES, STATE_ON, STATE_OFF
from . import TYPES
from .accessories import (
HomeAccessory, add_preload_service, debounce, setup_char)
from .accessories import HomeAccessory, debounce
from .const import (
CATEGORY_LIGHT, SERV_LIGHTBULB, CHAR_COLOR_TEMPERATURE,
SERV_LIGHTBULB, CHAR_COLOR_TEMPERATURE,
CHAR_BRIGHTNESS, CHAR_HUE, CHAR_ON, CHAR_SATURATION)
_LOGGER = logging.getLogger(__name__)
@ -27,7 +28,7 @@ class Light(HomeAccessory):
def __init__(self, *args, config):
"""Initialize a new Light accessory object."""
super().__init__(*args, category=CATEGORY_LIGHT)
super().__init__(*args, category=CATEGORY_LIGHTBULB)
self._flag = {CHAR_ON: False, CHAR_BRIGHTNESS: False,
CHAR_HUE: False, CHAR_SATURATION: False,
CHAR_COLOR_TEMPERATURE: False, RGB_COLOR: False}
@ -46,30 +47,28 @@ class Light(HomeAccessory):
self._hue = None
self._saturation = None
serv_light = add_preload_service(self, SERV_LIGHTBULB, self.chars)
self.char_on = setup_char(
CHAR_ON, serv_light, value=self._state, callback=self.set_state)
serv_light = self.add_preload_service(SERV_LIGHTBULB, self.chars)
self.char_on = serv_light.configure_char(
CHAR_ON, value=self._state, setter_callback=self.set_state)
if CHAR_BRIGHTNESS in self.chars:
self.char_brightness = setup_char(
CHAR_BRIGHTNESS, serv_light, value=0,
callback=self.set_brightness)
self.char_brightness = serv_light.configure_char(
CHAR_BRIGHTNESS, value=0, setter_callback=self.set_brightness)
if CHAR_COLOR_TEMPERATURE in self.chars:
min_mireds = self.hass.states.get(self.entity_id) \
.attributes.get(ATTR_MIN_MIREDS, 153)
max_mireds = self.hass.states.get(self.entity_id) \
.attributes.get(ATTR_MAX_MIREDS, 500)
self.char_color_temperature = setup_char(
CHAR_COLOR_TEMPERATURE, serv_light, value=min_mireds,
self.char_color_temperature = serv_light.configure_char(
CHAR_COLOR_TEMPERATURE, value=min_mireds,
properties={'minValue': min_mireds, 'maxValue': max_mireds},
callback=self.set_color_temperature)
setter_callback=self.set_color_temperature)
if CHAR_HUE in self.chars:
self.char_hue = setup_char(
CHAR_HUE, serv_light, value=0, callback=self.set_hue)
self.char_hue = serv_light.configure_char(
CHAR_HUE, value=0, setter_callback=self.set_hue)
if CHAR_SATURATION in self.chars:
self.char_saturation = setup_char(
CHAR_SATURATION, serv_light, value=75,
callback=self.set_saturation)
self.char_saturation = serv_light.configure_char(
CHAR_SATURATION, value=75, setter_callback=self.set_saturation)
def set_state(self, value):
"""Set state if call came from HomeKit."""

View File

@ -1,13 +1,15 @@
"""Class to hold all lock accessories."""
import logging
from pyhap.const import CATEGORY_DOOR_LOCK
from homeassistant.components.lock import (
ATTR_ENTITY_ID, STATE_LOCKED, STATE_UNLOCKED, STATE_UNKNOWN)
from . import TYPES
from .accessories import HomeAccessory, add_preload_service, setup_char
from .accessories import HomeAccessory
from .const import (
CATEGORY_LOCK, SERV_LOCK, CHAR_LOCK_CURRENT_STATE, CHAR_LOCK_TARGET_STATE)
SERV_LOCK, CHAR_LOCK_CURRENT_STATE, CHAR_LOCK_TARGET_STATE)
_LOGGER = logging.getLogger(__name__)
@ -29,16 +31,16 @@ class Lock(HomeAccessory):
def __init__(self, *args, config):
"""Initialize a Lock accessory object."""
super().__init__(*args, category=CATEGORY_LOCK)
super().__init__(*args, category=CATEGORY_DOOR_LOCK)
self.flag_target_state = False
serv_lock_mechanism = add_preload_service(self, SERV_LOCK)
self.char_current_state = setup_char(
CHAR_LOCK_CURRENT_STATE, serv_lock_mechanism,
serv_lock_mechanism = self.add_preload_service(SERV_LOCK)
self.char_current_state = serv_lock_mechanism.configure_char(
CHAR_LOCK_CURRENT_STATE,
value=HASS_TO_HOMEKIT[STATE_UNKNOWN])
self.char_target_state = setup_char(
CHAR_LOCK_TARGET_STATE, serv_lock_mechanism,
value=HASS_TO_HOMEKIT[STATE_LOCKED], callback=self.set_state)
self.char_target_state = serv_lock_mechanism.configure_char(
CHAR_LOCK_TARGET_STATE, value=HASS_TO_HOMEKIT[STATE_LOCKED],
setter_callback=self.set_state)
def set_state(self, value):
"""Set lock state to value if call came from HomeKit."""

View File

@ -1,26 +1,31 @@
"""Class to hold all alarm control panel accessories."""
import logging
from pyhap.const import CATEGORY_ALARM_SYSTEM
from homeassistant.const import (
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME,
STATE_ALARM_ARMED_NIGHT, STATE_ALARM_DISARMED,
ATTR_ENTITY_ID, ATTR_CODE)
STATE_ALARM_TRIGGERED, ATTR_ENTITY_ID, ATTR_CODE)
from . import TYPES
from .accessories import HomeAccessory, add_preload_service, setup_char
from .accessories import HomeAccessory
from .const import (
CATEGORY_ALARM_SYSTEM, SERV_SECURITY_SYSTEM,
CHAR_CURRENT_SECURITY_STATE, CHAR_TARGET_SECURITY_STATE)
SERV_SECURITY_SYSTEM, CHAR_CURRENT_SECURITY_STATE,
CHAR_TARGET_SECURITY_STATE)
_LOGGER = logging.getLogger(__name__)
HASS_TO_HOMEKIT = {STATE_ALARM_DISARMED: 3, STATE_ALARM_ARMED_HOME: 0,
STATE_ALARM_ARMED_AWAY: 1, STATE_ALARM_ARMED_NIGHT: 2}
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_DISARMED: 'alarm_disarm',
STATE_ALARM_ARMED_HOME: 'alarm_arm_home',
STATE_TO_SERVICE = {STATE_ALARM_ARMED_HOME: 'alarm_arm_home',
STATE_ALARM_ARMED_AWAY: 'alarm_arm_away',
STATE_ALARM_ARMED_NIGHT: 'alarm_arm_night'}
STATE_ALARM_ARMED_NIGHT: 'alarm_arm_night',
STATE_ALARM_DISARMED: 'alarm_disarm'}
@TYPES.register('SecuritySystem')
@ -33,12 +38,12 @@ class SecuritySystem(HomeAccessory):
self._alarm_code = config.get(ATTR_CODE)
self.flag_target_state = False
serv_alarm = add_preload_service(self, SERV_SECURITY_SYSTEM)
self.char_current_state = setup_char(
CHAR_CURRENT_SECURITY_STATE, serv_alarm, value=3)
self.char_target_state = setup_char(
CHAR_TARGET_SECURITY_STATE, serv_alarm, value=3,
callback=self.set_security_state)
serv_alarm = self.add_preload_service(SERV_SECURITY_SYSTEM)
self.char_current_state = serv_alarm.configure_char(
CHAR_CURRENT_SECURITY_STATE, value=3)
self.char_target_state = serv_alarm.configure_char(
CHAR_TARGET_SECURITY_STATE, value=3,
setter_callback=self.set_security_state)
def set_security_state(self, value):
"""Move security state to value if call came from HomeKit."""
@ -62,7 +67,8 @@ class SecuritySystem(HomeAccessory):
_LOGGER.debug('%s: Updated current state to %s (%d)',
self.entity_id, hass_state, current_security_state)
if not self.flag_target_state:
# SecuritySystemTargetState does not support triggered
if not self.flag_target_state and \
hass_state != STATE_ALARM_TRIGGERED:
self.char_target_state.set_value(current_security_state)
if self.char_target_state.value == self.char_current_state.value:
self.flag_target_state = False
self.flag_target_state = False

View File

@ -1,14 +1,16 @@
"""Class to hold all sensor accessories."""
import logging
from pyhap.const import CATEGORY_SENSOR
from homeassistant.const import (
ATTR_UNIT_OF_MEASUREMENT, TEMP_CELSIUS,
ATTR_DEVICE_CLASS, STATE_ON, STATE_HOME)
from . import TYPES
from .accessories import HomeAccessory, add_preload_service, setup_char
from .accessories import HomeAccessory
from .const import (
CATEGORY_SENSOR, SERV_HUMIDITY_SENSOR, SERV_TEMPERATURE_SENSOR,
SERV_HUMIDITY_SENSOR, SERV_TEMPERATURE_SENSOR,
CHAR_CURRENT_HUMIDITY, CHAR_CURRENT_TEMPERATURE, PROP_CELSIUS,
SERV_AIR_QUALITY_SENSOR, CHAR_AIR_QUALITY, CHAR_AIR_PARTICULATE_DENSITY,
CHAR_CARBON_DIOXIDE_LEVEL, CHAR_CARBON_DIOXIDE_PEAK_LEVEL,
@ -52,10 +54,9 @@ class TemperatureSensor(HomeAccessory):
def __init__(self, *args, config):
"""Initialize a TemperatureSensor accessory object."""
super().__init__(*args, category=CATEGORY_SENSOR)
serv_temp = add_preload_service(self, SERV_TEMPERATURE_SENSOR)
self.char_temp = setup_char(
CHAR_CURRENT_TEMPERATURE, serv_temp, value=0,
properties=PROP_CELSIUS)
serv_temp = self.add_preload_service(SERV_TEMPERATURE_SENSOR)
self.char_temp = serv_temp.configure_char(
CHAR_CURRENT_TEMPERATURE, value=0, properties=PROP_CELSIUS)
self.unit = None
def update_state(self, new_state):
@ -76,9 +77,9 @@ class HumiditySensor(HomeAccessory):
def __init__(self, *args, config):
"""Initialize a HumiditySensor accessory object."""
super().__init__(*args, category=CATEGORY_SENSOR)
serv_humidity = add_preload_service(self, SERV_HUMIDITY_SENSOR)
self.char_humidity = setup_char(
CHAR_CURRENT_HUMIDITY, serv_humidity, value=0)
serv_humidity = self.add_preload_service(SERV_HUMIDITY_SENSOR)
self.char_humidity = serv_humidity.configure_char(
CHAR_CURRENT_HUMIDITY, value=0)
def update_state(self, new_state):
"""Update accessory after state change."""
@ -97,12 +98,12 @@ class AirQualitySensor(HomeAccessory):
"""Initialize a AirQualitySensor accessory object."""
super().__init__(*args, category=CATEGORY_SENSOR)
serv_air_quality = add_preload_service(self, SERV_AIR_QUALITY_SENSOR,
[CHAR_AIR_PARTICULATE_DENSITY])
self.char_quality = setup_char(
CHAR_AIR_QUALITY, serv_air_quality, value=0)
self.char_density = setup_char(
CHAR_AIR_PARTICULATE_DENSITY, serv_air_quality, value=0)
serv_air_quality = self.add_preload_service(
SERV_AIR_QUALITY_SENSOR, [CHAR_AIR_PARTICULATE_DENSITY])
self.char_quality = serv_air_quality.configure_char(
CHAR_AIR_QUALITY, value=0)
self.char_density = serv_air_quality.configure_char(
CHAR_AIR_PARTICULATE_DENSITY, value=0)
def update_state(self, new_state):
"""Update accessory after state change."""
@ -121,14 +122,14 @@ class CarbonDioxideSensor(HomeAccessory):
"""Initialize a CarbonDioxideSensor accessory object."""
super().__init__(*args, category=CATEGORY_SENSOR)
serv_co2 = add_preload_service(self, SERV_CARBON_DIOXIDE_SENSOR, [
serv_co2 = self.add_preload_service(SERV_CARBON_DIOXIDE_SENSOR, [
CHAR_CARBON_DIOXIDE_LEVEL, CHAR_CARBON_DIOXIDE_PEAK_LEVEL])
self.char_co2 = setup_char(
CHAR_CARBON_DIOXIDE_LEVEL, serv_co2, value=0)
self.char_peak = setup_char(
CHAR_CARBON_DIOXIDE_PEAK_LEVEL, serv_co2, value=0)
self.char_detected = setup_char(
CHAR_CARBON_DIOXIDE_DETECTED, serv_co2, value=0)
self.char_co2 = serv_co2.configure_char(
CHAR_CARBON_DIOXIDE_LEVEL, value=0)
self.char_peak = serv_co2.configure_char(
CHAR_CARBON_DIOXIDE_PEAK_LEVEL, value=0)
self.char_detected = serv_co2.configure_char(
CHAR_CARBON_DIOXIDE_DETECTED, value=0)
def update_state(self, new_state):
"""Update accessory after state change."""
@ -149,9 +150,9 @@ class LightSensor(HomeAccessory):
"""Initialize a LightSensor accessory object."""
super().__init__(*args, category=CATEGORY_SENSOR)
serv_light = add_preload_service(self, SERV_LIGHT_SENSOR)
self.char_light = setup_char(
CHAR_CURRENT_AMBIENT_LIGHT_LEVEL, serv_light, value=0)
serv_light = self.add_preload_service(SERV_LIGHT_SENSOR)
self.char_light = serv_light.configure_char(
CHAR_CURRENT_AMBIENT_LIGHT_LEVEL, value=0)
def update_state(self, new_state):
"""Update accessory after state change."""
@ -174,8 +175,8 @@ class BinarySensor(HomeAccessory):
if device_class in BINARY_SENSOR_SERVICE_MAP \
else BINARY_SENSOR_SERVICE_MAP[DEVICE_CLASS_OCCUPANCY]
service = add_preload_service(self, service_char[0])
self.char_detected = setup_char(service_char[1], service, value=0)
service = self.add_preload_service(service_char[0])
self.char_detected = service.configure_char(service_char[1], value=0)
def update_state(self, new_state):
"""Update accessory after state change."""

View File

@ -1,13 +1,15 @@
"""Class to hold all switch accessories."""
import logging
from pyhap.const import CATEGORY_SWITCH
from homeassistant.const import (
ATTR_ENTITY_ID, SERVICE_TURN_ON, SERVICE_TURN_OFF, STATE_ON)
from homeassistant.core import split_entity_id
from . import TYPES
from .accessories import HomeAccessory, add_preload_service, setup_char
from .const import CATEGORY_SWITCH, SERV_SWITCH, CHAR_ON
from .accessories import HomeAccessory
from .const import SERV_SWITCH, CHAR_ON
_LOGGER = logging.getLogger(__name__)
@ -22,9 +24,9 @@ class Switch(HomeAccessory):
self._domain = split_entity_id(self.entity_id)[0]
self.flag_target_state = False
serv_switch = add_preload_service(self, SERV_SWITCH)
self.char_on = setup_char(
CHAR_ON, serv_switch, value=False, callback=self.set_state)
serv_switch = self.add_preload_service(SERV_SWITCH)
self.char_on = serv_switch.configure_char(
CHAR_ON, value=False, setter_callback=self.set_state)
def set_state(self, value):
"""Move switch state to value if call came from HomeKit."""

View File

@ -1,21 +1,22 @@
"""Class to hold all thermostat accessories."""
import logging
from pyhap.const import CATEGORY_THERMOSTAT
from homeassistant.components.climate import (
ATTR_CURRENT_TEMPERATURE, ATTR_TEMPERATURE,
ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW,
ATTR_OPERATION_MODE, ATTR_OPERATION_LIST,
STATE_HEAT, STATE_COOL, STATE_AUTO,
STATE_HEAT, STATE_COOL, STATE_AUTO, SUPPORT_ON_OFF,
SUPPORT_TARGET_TEMPERATURE_HIGH, SUPPORT_TARGET_TEMPERATURE_LOW)
from homeassistant.const import (
ATTR_SUPPORTED_FEATURES, ATTR_UNIT_OF_MEASUREMENT,
ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, ATTR_UNIT_OF_MEASUREMENT,
STATE_OFF, TEMP_CELSIUS, TEMP_FAHRENHEIT)
from . import TYPES
from .accessories import (
HomeAccessory, add_preload_service, debounce, setup_char)
from .accessories import HomeAccessory, debounce
from .const import (
CATEGORY_THERMOSTAT, SERV_THERMOSTAT, CHAR_CURRENT_HEATING_COOLING,
SERV_THERMOSTAT, CHAR_CURRENT_HEATING_COOLING,
CHAR_TARGET_HEATING_COOLING, CHAR_CURRENT_TEMPERATURE,
CHAR_TARGET_TEMPERATURE, CHAR_TEMP_DISPLAY_UNITS,
CHAR_COOLING_THRESHOLD_TEMPERATURE, CHAR_HEATING_THRESHOLD_TEMPERATURE)
@ -41,6 +42,7 @@ class Thermostat(HomeAccessory):
"""Initialize a Thermostat accessory object."""
super().__init__(*args, category=CATEGORY_THERMOSTAT)
self._unit = TEMP_CELSIUS
self.support_power_state = False
self.heat_cool_flag_target_state = False
self.temperature_flag_target_state = False
self.coolingthresh_flag_target_state = False
@ -50,42 +52,43 @@ class Thermostat(HomeAccessory):
self.chars = []
features = self.hass.states.get(self.entity_id) \
.attributes.get(ATTR_SUPPORTED_FEATURES)
if features & SUPPORT_ON_OFF:
self.support_power_state = True
if features & SUPPORT_TEMP_RANGE:
self.chars.extend((CHAR_COOLING_THRESHOLD_TEMPERATURE,
CHAR_HEATING_THRESHOLD_TEMPERATURE))
serv_thermostat = add_preload_service(
self, SERV_THERMOSTAT, self.chars)
serv_thermostat = self.add_preload_service(SERV_THERMOSTAT, self.chars)
# Current and target mode characteristics
self.char_current_heat_cool = setup_char(
CHAR_CURRENT_HEATING_COOLING, serv_thermostat, value=0)
self.char_target_heat_cool = setup_char(
CHAR_TARGET_HEATING_COOLING, serv_thermostat, value=0,
callback=self.set_heat_cool)
self.char_current_heat_cool = serv_thermostat.configure_char(
CHAR_CURRENT_HEATING_COOLING, value=0)
self.char_target_heat_cool = serv_thermostat.configure_char(
CHAR_TARGET_HEATING_COOLING, value=0,
setter_callback=self.set_heat_cool)
# Current and target temperature characteristics
self.char_current_temp = setup_char(
CHAR_CURRENT_TEMPERATURE, serv_thermostat, value=21.0)
self.char_target_temp = setup_char(
CHAR_TARGET_TEMPERATURE, serv_thermostat, value=21.0,
callback=self.set_target_temperature)
self.char_current_temp = serv_thermostat.configure_char(
CHAR_CURRENT_TEMPERATURE, value=21.0)
self.char_target_temp = serv_thermostat.configure_char(
CHAR_TARGET_TEMPERATURE, value=21.0,
setter_callback=self.set_target_temperature)
# Display units characteristic
self.char_display_units = setup_char(
CHAR_TEMP_DISPLAY_UNITS, serv_thermostat, value=0)
self.char_display_units = serv_thermostat.configure_char(
CHAR_TEMP_DISPLAY_UNITS, value=0)
# If the device supports it: high and low temperature characteristics
self.char_cooling_thresh_temp = None
self.char_heating_thresh_temp = None
if CHAR_COOLING_THRESHOLD_TEMPERATURE in self.chars:
self.char_cooling_thresh_temp = setup_char(
CHAR_COOLING_THRESHOLD_TEMPERATURE, serv_thermostat,
value=23.0, callback=self.set_cooling_threshold)
self.char_cooling_thresh_temp = serv_thermostat.configure_char(
CHAR_COOLING_THRESHOLD_TEMPERATURE, value=23.0,
setter_callback=self.set_cooling_threshold)
if CHAR_HEATING_THRESHOLD_TEMPERATURE in self.chars:
self.char_heating_thresh_temp = setup_char(
CHAR_HEATING_THRESHOLD_TEMPERATURE, serv_thermostat,
value=19.0, callback=self.set_heating_threshold)
self.char_heating_thresh_temp = serv_thermostat.configure_char(
CHAR_HEATING_THRESHOLD_TEMPERATURE, value=19.0,
setter_callback=self.set_heating_threshold)
def set_heat_cool(self, value):
"""Move operation mode to value if call came from HomeKit."""
@ -93,6 +96,13 @@ class Thermostat(HomeAccessory):
_LOGGER.debug('%s: Set heat-cool to %d', self.entity_id, value)
self.heat_cool_flag_target_state = True
hass_value = HC_HOMEKIT_TO_HASS[value]
if self.support_power_state is True:
params = {ATTR_ENTITY_ID: self.entity_id}
if hass_value == STATE_OFF:
self.hass.services.call('climate', 'turn_off', params)
return
else:
self.hass.services.call('climate', 'turn_on', params)
self.hass.components.climate.set_operation_mode(
operation_mode=hass_value, entity_id=self.entity_id)
@ -178,15 +188,19 @@ class Thermostat(HomeAccessory):
# Update target operation mode
operation_mode = new_state.attributes.get(ATTR_OPERATION_MODE)
if operation_mode \
and operation_mode in HC_HASS_TO_HOMEKIT:
if self.support_power_state is True and new_state.state == STATE_OFF:
self.char_target_heat_cool.set_value(
HC_HASS_TO_HOMEKIT[STATE_OFF])
elif operation_mode and operation_mode in HC_HASS_TO_HOMEKIT:
if not self.heat_cool_flag_target_state:
self.char_target_heat_cool.set_value(
HC_HASS_TO_HOMEKIT[operation_mode])
self.heat_cool_flag_target_state = False
# Set current operation mode based on temperatures and target mode
if operation_mode == STATE_HEAT:
if self.support_power_state is True and new_state.state == STATE_OFF:
current_operation_mode = STATE_OFF
elif operation_mode == STATE_HEAT:
if isinstance(target_temp, float) and current_temp < target_temp:
current_operation_mode = STATE_HEAT
else:

View File

@ -20,7 +20,7 @@ from homeassistant.helpers.entity import Entity
import homeassistant.helpers.config_validation as cv
from homeassistant.loader import bind_hass
REQUIREMENTS = ['pyhomematic==0.1.41']
REQUIREMENTS = ['pyhomematic==0.1.42']
DOMAIN = 'homematic'
_LOGGER = logging.getLogger(__name__)

View File

@ -5,143 +5,239 @@ For more details about this component, please refer to the documentation at
https://home-assistant.io/components/homematicip_cloud/
"""
import asyncio
import logging
from socket import timeout
import voluptuous as vol
from homeassistant.core import callback
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.dispatcher import (dispatcher_send,
async_dispatcher_connect)
from homeassistant.helpers.discovery import load_platform
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.discovery import async_load_platform
from homeassistant.helpers.entity import Entity
from homeassistant.core import callback
REQUIREMENTS = ['homematicip==0.8']
REQUIREMENTS = ['homematicip==0.9.2.4']
_LOGGER = logging.getLogger(__name__)
DOMAIN = 'homematicip_cloud'
COMPONENTS = [
'sensor'
]
CONF_NAME = 'name'
CONF_ACCESSPOINT = 'accesspoint'
CONF_AUTHTOKEN = 'authtoken'
CONFIG_SCHEMA = vol.Schema({
vol.Optional(DOMAIN): [vol.Schema({
vol.Optional(CONF_NAME, default=''): cv.string,
vol.Optional(DOMAIN, default=[]): vol.All(cv.ensure_list, [vol.Schema({
vol.Optional(CONF_NAME): vol.Any(cv.string),
vol.Required(CONF_ACCESSPOINT): cv.string,
vol.Required(CONF_AUTHTOKEN): cv.string,
})],
})]),
}, extra=vol.ALLOW_EXTRA)
EVENT_HOME_CHANGED = 'homematicip_home_changed'
EVENT_DEVICE_CHANGED = 'homematicip_device_changed'
EVENT_GROUP_CHANGED = 'homematicip_group_changed'
EVENT_SECURITY_CHANGED = 'homematicip_security_changed'
EVENT_JOURNAL_CHANGED = 'homematicip_journal_changed'
HMIP_ACCESS_POINT = 'Access Point'
HMIP_HUB = 'HmIP-HUB'
ATTR_HOME_ID = 'home_id'
ATTR_HOME_LABEL = 'home_label'
ATTR_HOME_NAME = 'home_name'
ATTR_DEVICE_ID = 'device_id'
ATTR_DEVICE_LABEL = 'device_label'
ATTR_STATUS_UPDATE = 'status_update'
ATTR_FIRMWARE_STATE = 'firmware_state'
ATTR_UNREACHABLE = 'unreachable'
ATTR_LOW_BATTERY = 'low_battery'
ATTR_MODEL_TYPE = 'model_type'
ATTR_GROUP_TYPE = 'group_type'
ATTR_DEVICE_RSSI = 'device_rssi'
ATTR_DUTY_CYCLE = 'duty_cycle'
ATTR_CONNECTED = 'connected'
ATTR_SABOTAGE = 'sabotage'
ATTR_RSSI = 'rssi'
ATTR_TYPE = 'type'
ATTR_OPERATION_LOCK = 'operation_lock'
def setup(hass, config):
async def async_setup(hass, config):
"""Set up the HomematicIP component."""
# pylint: disable=import-error, no-name-in-module
from homematicip.home import Home
from homematicip.base.base_connection import HmipConnectionError
hass.data.setdefault(DOMAIN, {})
homes = hass.data[DOMAIN]
accesspoints = config.get(DOMAIN, [])
def _update_event(events):
"""Handle incoming HomeMaticIP events."""
for event in events:
etype = event['eventType']
edata = event['data']
if etype == 'DEVICE_CHANGED':
dispatcher_send(hass, EVENT_DEVICE_CHANGED, edata.id)
elif etype == 'GROUP_CHANGED':
dispatcher_send(hass, EVENT_GROUP_CHANGED, edata.id)
elif etype == 'HOME_CHANGED':
dispatcher_send(hass, EVENT_HOME_CHANGED, edata.id)
elif etype == 'JOURNAL_CHANGED':
dispatcher_send(hass, EVENT_SECURITY_CHANGED, edata.id)
return True
for device in accesspoints:
name = device.get(CONF_NAME)
accesspoint = device.get(CONF_ACCESSPOINT)
authtoken = device.get(CONF_AUTHTOKEN)
home = Home()
if name.lower() == 'none':
name = ''
home.label = name
for conf in accesspoints:
_websession = async_get_clientsession(hass)
_hmip = HomematicipConnector(hass, conf, _websession)
try:
home.set_auth_token(authtoken)
home.init(accesspoint)
if home.get_current_state():
_LOGGER.info("Connection to HMIP established")
else:
_LOGGER.warning("Connection to HMIP could not be established")
return False
except timeout:
_LOGGER.warning("Connection to HMIP could not be established")
await _hmip.init()
except HmipConnectionError:
_LOGGER.error('Failed to connect to the HomematicIP server, %s.',
conf.get(CONF_ACCESSPOINT))
return False
homes[home.id] = home
home.onEvent += _update_event
home.enable_events()
_LOGGER.info('HUB name: %s, id: %s', home.label, home.id)
for component in ['sensor']:
load_platform(hass, component, DOMAIN, {'homeid': home.id}, config)
home = _hmip.home
home.name = conf.get(CONF_NAME)
home.label = HMIP_ACCESS_POINT
home.modelType = HMIP_HUB
hass.data[DOMAIN][home.id] = home
_LOGGER.info('Connected to the HomematicIP server, %s.',
conf.get(CONF_ACCESSPOINT))
homeid = {ATTR_HOME_ID: home.id}
for component in COMPONENTS:
hass.async_add_job(async_load_platform(hass, component, DOMAIN,
homeid, config))
hass.loop.create_task(_hmip.connect())
return True
class HomematicipConnector:
"""Manages HomematicIP http and websocket connection."""
def __init__(self, hass, config, websession):
"""Initialize HomematicIP cloud connection."""
from homematicip.async.home import AsyncHome
self._hass = hass
self._ws_close_requested = False
self._retry_task = None
self._tries = 0
self._accesspoint = config.get(CONF_ACCESSPOINT)
_authtoken = config.get(CONF_AUTHTOKEN)
self.home = AsyncHome(hass.loop, websession)
self.home.set_auth_token(_authtoken)
self.home.on_update(self.async_update)
self._accesspoint_connected = True
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self.close())
async def init(self):
"""Initialize connection."""
await self.home.init(self._accesspoint)
await self.home.get_current_state()
@callback
def async_update(self, *args, **kwargs):
"""Async update the home device.
Triggered when the hmip HOME_CHANGED event has fired.
There are several occasions for this event to happen.
We are only interested to check whether the access point
is still connected. If not, device state changes cannot
be forwarded to hass. So if access point is disconnected all devices
are set to unavailable.
"""
if not self.home.connected:
_LOGGER.error(
"HMIP access point has lost connection with the cloud")
self._accesspoint_connected = False
self.set_all_to_unavailable()
elif not self._accesspoint_connected:
# Explicitly getting an update as device states might have
# changed during access point disconnect."""
job = self._hass.async_add_job(self.get_state())
job.add_done_callback(self.get_state_finished)
async def get_state(self):
"""Update hmip state and tell hass."""
await self.home.get_current_state()
self.update_all()
def get_state_finished(self, future):
"""Execute when get_state coroutine has finished."""
from homematicip.base.base_connection import HmipConnectionError
try:
future.result()
except HmipConnectionError:
# Somehow connection could not recover. Will disconnect and
# so reconnect loop is taking over.
_LOGGER.error(
"updating state after himp access point reconnect failed.")
self._hass.async_add_job(self.home.disable_events())
def set_all_to_unavailable(self):
"""Set all devices to unavailable and tell Hass."""
for device in self.home.devices:
device.unreach = True
self.update_all()
def update_all(self):
"""Signal all devices to update their state."""
for device in self.home.devices:
device.fire_update_event()
async def _handle_connection(self):
"""Handle websocket connection."""
from homematicip.base.base_connection import HmipConnectionError
await self.home.get_current_state()
hmip_events = await self.home.enable_events()
try:
await hmip_events
except HmipConnectionError:
return
async def connect(self):
"""Start websocket connection."""
self._tries = 0
while True:
await self._handle_connection()
if self._ws_close_requested:
break
self._ws_close_requested = False
self._tries += 1
try:
self._retry_task = self._hass.async_add_job(asyncio.sleep(
2 ** min(9, self._tries), loop=self._hass.loop))
await self._retry_task
except asyncio.CancelledError:
break
_LOGGER.info('Reconnect (%s) to the HomematicIP cloud server.',
self._tries)
async def close(self):
"""Close the websocket connection."""
self._ws_close_requested = True
if self._retry_task is not None:
self._retry_task.cancel()
await self.home.disable_events()
_LOGGER.info("Closed connection to HomematicIP cloud server.")
class HomematicipGenericDevice(Entity):
"""Representation of an HomematicIP generic device."""
def __init__(self, home, device):
def __init__(self, home, device, post=None):
"""Initialize the generic device."""
self._home = home
self._device = device
self.post = post
_LOGGER.info('Setting up %s (%s)', self.name,
self._device.modelType)
async def async_added_to_hass(self):
"""Register callbacks."""
async_dispatcher_connect(
self.hass, EVENT_DEVICE_CHANGED, self._device_changed)
self._device.on_update(self._device_changed)
@callback
def _device_changed(self, deviceid):
def _device_changed(self, json, **kwargs):
"""Handle device state changes."""
if deviceid is None or deviceid == self._device.id:
_LOGGER.debug('Event device %s', self._device.label)
self.async_schedule_update_ha_state()
def _name(self, addon=''):
"""Return the name of the device."""
name = ''
if self._home.label != '':
name += self._home.label + ' '
name += self._device.label
if addon != '':
name += ' ' + addon
return name
_LOGGER.debug('Event %s (%s)', self.name, self._device.modelType)
self.async_schedule_update_ha_state()
@property
def name(self):
"""Return the name of the generic device."""
return self._name()
name = self._device.label
if self._home.name is not None:
name = "{} {}".format(self._home.name, name)
if self.post is not None:
name = "{} {}".format(name, self.post)
return name
@property
def should_poll(self):
@ -153,24 +249,10 @@ class HomematicipGenericDevice(Entity):
"""Device available."""
return not self._device.unreach
def _generic_state_attributes(self):
"""Return the state attributes of the generic device."""
laststatus = ''
if self._device.lastStatusUpdate is not None:
laststatus = self._device.lastStatusUpdate.isoformat()
return {
ATTR_HOME_LABEL: self._home.label,
ATTR_DEVICE_LABEL: self._device.label,
ATTR_HOME_ID: self._device.homeId,
ATTR_DEVICE_ID: self._device.id.lower(),
ATTR_STATUS_UPDATE: laststatus,
ATTR_FIRMWARE_STATE: self._device.updateState.lower(),
ATTR_LOW_BATTERY: self._device.lowBat,
ATTR_RSSI: self._device.rssiDeviceValue,
ATTR_TYPE: self._device.modelType
}
@property
def device_state_attributes(self):
"""Return the state attributes of the generic device."""
return self._generic_state_attributes()
return {
ATTR_LOW_BATTERY: self._device.lowBat,
ATTR_MODEL_TYPE: self._device.modelType
}

View File

@ -32,17 +32,19 @@ def setup_auth(app, trusted_networks, api_password):
if (HTTP_HEADER_HA_AUTH in request.headers and
hmac.compare_digest(
api_password, request.headers[HTTP_HEADER_HA_AUTH])):
api_password.encode('utf-8'),
request.headers[HTTP_HEADER_HA_AUTH].encode('utf-8'))):
# A valid auth header has been set
authenticated = True
elif (DATA_API_PASSWORD in request.query and
hmac.compare_digest(api_password,
request.query[DATA_API_PASSWORD])):
hmac.compare_digest(
api_password.encode('utf-8'),
request.query[DATA_API_PASSWORD].encode('utf-8'))):
authenticated = True
elif (hdrs.AUTHORIZATION in request.headers and
validate_authorization_header(api_password, request)):
await async_validate_auth_header(api_password, request)):
authenticated = True
elif _is_trusted_ip(request, trusted_networks):
@ -70,23 +72,38 @@ def _is_trusted_ip(request, trusted_networks):
def validate_password(request, api_password):
"""Test if password is valid."""
return hmac.compare_digest(
api_password, request.app['hass'].http.api_password)
api_password.encode('utf-8'),
request.app['hass'].http.api_password.encode('utf-8'))
def validate_authorization_header(api_password, request):
async def async_validate_auth_header(api_password, request):
"""Test an authorization header if valid password."""
if hdrs.AUTHORIZATION not in request.headers:
return False
auth_type, auth = request.headers.get(hdrs.AUTHORIZATION).split(' ', 1)
auth_type, auth_val = request.headers.get(hdrs.AUTHORIZATION).split(' ', 1)
if auth_type != 'Basic':
if auth_type == 'Basic':
decoded = base64.b64decode(auth_val).decode('utf-8')
try:
username, password = decoded.split(':', 1)
except ValueError:
# If no ':' in decoded
return False
if username != 'homeassistant':
return False
return hmac.compare_digest(api_password.encode('utf-8'),
password.encode('utf-8'))
if auth_type != 'Bearer':
return False
decoded = base64.b64decode(auth).decode('utf-8')
username, password = decoded.split(':', 1)
if username != 'homeassistant':
hass = request.app['hass']
access_token = hass.auth.async_get_access_token(auth_val)
if access_token is None:
return False
return hmac.compare_digest(api_password, password)
request['hass_user'] = access_token.refresh_token.user
return True

View File

@ -0,0 +1,29 @@
{
"config": {
"abort": {
"all_configured": "\u0412\u0441\u0438\u0447\u043a\u0438 \u0431\u0430\u0437\u043e\u0432\u0438 \u0441\u0442\u0430\u043d\u0446\u0438\u0438 Philips Hue \u0441\u0430 \u0432\u0435\u0447\u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0438",
"already_configured": "\u0411\u0430\u0437\u043e\u0432\u0430\u0442\u0430 \u0441\u0442\u0430\u043d\u0446\u0438\u044f \u0435 \u0432\u0435\u0447\u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0430",
"cannot_connect": "\u041d\u0435 \u043c\u043e\u0436\u0430 \u0434\u0430 \u0441\u0435 \u0441\u0432\u044a\u0440\u0436\u0435 \u0441 \u0431\u0430\u0437\u043e\u0432\u0430\u0442\u0430 \u0441\u0442\u0430\u043d\u0446\u0438\u044f",
"discover_timeout": "\u041d\u0435\u0432\u044a\u0437\u043c\u043e\u0436\u043d\u043e \u0435 \u0434\u0430 \u0431\u044a\u0434\u0430\u0442 \u043e\u0442\u043a\u0440\u0438\u0442\u0438 \u0431\u0430\u0437\u043e\u0432\u0438 \u0441\u0442\u0430\u043d\u0446\u0438\u0438 \u043d\u0430 Philips Hue",
"no_bridges": "\u041d\u0435 \u0441\u0430 \u043e\u0442\u043a\u0440\u0438\u0442\u0438 \u0431\u0430\u0437\u043e\u0432\u0438 \u0441\u0442\u0430\u043d\u0446\u0438\u0438 \u043d\u0430 Philips Hue",
"unknown": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430"
},
"error": {
"linking": "\u041f\u043e\u044f\u0432\u0438 \u0441\u0435 \u043d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435\u0442\u043e.",
"register_failed": "\u0420\u0435\u0433\u0438\u0441\u0442\u0440\u0430\u0446\u0438\u044f\u0442\u0430 \u043d\u0435 \u0431\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u0430, \u043c\u043e\u043b\u044f \u043e\u043f\u0438\u0442\u0430\u0439\u0442\u0435 \u043e\u0442\u043d\u043e\u0432\u043e"
},
"step": {
"init": {
"data": {
"host": "\u0425\u043e\u0441\u0442"
},
"title": "\u0418\u0437\u0431\u0435\u0440\u0435\u0442\u0435 \u0431\u0430\u0437\u043e\u0432\u0430 \u0441\u0442\u0430\u043d\u0446\u0438\u044f Philips Hue"
},
"link": {
"description": "\u041d\u0430\u0442\u0438\u0441\u043d\u0435\u0442\u0435 \u0431\u0443\u0442\u043e\u043d\u0430 \u043d\u0430 \u0431\u0430\u0437\u043e\u0432\u0430\u0442\u0430 \u0441\u0442\u0430\u043d\u0446\u0438\u044f, \u0437\u0430 \u0434\u0430 \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u0430\u0442\u0435 Philips Hue \u0441 Home Assistant. \n\n![\u041c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435 \u043d\u0430 \u0431\u0443\u0442\u043e\u043d\u0430 \u043d\u0430 \u0431\u0430\u0437\u043e\u0432\u0430\u0442\u0430 \u0441\u0442\u0430\u043d\u0446\u0438\u044f](/static/images/config_philips_hue.jpg)",
"title": "\u0421\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435 \u043d\u0430 \u0445\u044a\u0431"
}
},
"title": "\u0411\u0430\u0437\u043e\u0432\u0430 \u0441\u0442\u0430\u043d\u0446\u0438\u044f Philips Hue"
}
}

View File

@ -0,0 +1,29 @@
{
"config": {
"abort": {
"all_configured": "Mae holl bontydd Philips Hue eisoes wedi eu ffurfweddu",
"already_configured": "Pont eisoes wedi'i ffurfweddu",
"cannot_connect": "Methu cysylltu i'r bont",
"discover_timeout": "Methu darganfod pontydd Hue",
"no_bridges": "Dim pontydd Philips Hue wedi'i ddarganfod",
"unknown": "Digwyddodd gwall anhysbys"
},
"error": {
"linking": "Digwyddodd gwall cysylltu anhysbys.",
"register_failed": "Wedi methu \u00e2 chofrestru, pl\u00eds ceisiwch eto"
},
"step": {
"init": {
"data": {
"host": "Gwesteiwr"
},
"title": "Dewiswch bont Hue"
},
"link": {
"description": "Pwyswch y botwm ar y bont i gofrestru Philips Hue gyda Cynorthwydd Cartref.\n\n![Lleoliad botwm ar bont](/static/images/config_philips_hue.jpg)",
"title": "Hwb cyswllt"
}
},
"title": "Pont Phillips Hue"
}
}

View File

@ -0,0 +1,19 @@
{
"config": {
"abort": {
"no_bridges": "Ingen Philips Hue bridge fundet"
},
"step": {
"init": {
"data": {
"host": "V\u00e6rt"
},
"title": "V\u00e6lg Hue bridge"
},
"link": {
"title": "Link Hub"
}
},
"title": "Philips Hue Bridge"
}
}

View File

@ -2,8 +2,11 @@
"config": {
"abort": {
"all_configured": "Alle Philips Hue Bridges sind bereits konfiguriert",
"already_configured": "Bridge ist bereits konfiguriert",
"cannot_connect": "Verbindung zur Bridge nicht m\u00f6glich",
"discover_timeout": "Nicht in der Lage Hue Bridges zu entdecken",
"no_bridges": "Philips Hue Bridges entdeckt"
"no_bridges": "Keine Philips Hue Bridges entdeckt",
"unknown": "Unbekannter Fehler ist aufgetreten"
},
"error": {
"linking": "Unbekannter Link-Fehler aufgetreten.",

View File

@ -2,8 +2,11 @@
"config": {
"abort": {
"all_configured": "All Philips Hue bridges are already configured",
"already_configured": "Bridge is already configured",
"cannot_connect": "Unable to connect to the bridge",
"discover_timeout": "Unable to discover Hue bridges",
"no_bridges": "No Philips Hue bridges discovered"
"no_bridges": "No Philips Hue bridges discovered",
"unknown": "Unknown error occurred"
},
"error": {
"linking": "Unknown linking error occurred.",

View File

@ -0,0 +1,11 @@
{
"config": {
"abort": {
"unknown": "Se produjo un error desconocido"
},
"error": {
"linking": "Se produjo un error de enlace desconocido.",
"register_failed": "No se pudo registrar, intente de nuevo"
}
}
}

View File

@ -0,0 +1,28 @@
{
"config": {
"abort": {
"all_configured": "M\u00e1r minden Philips Hue bridge konfigur\u00e1lt",
"already_configured": "A bridge m\u00e1r konfigur\u00e1lt",
"cannot_connect": "Nem siker\u00fclt csatlakozni a bridge-hez.",
"discover_timeout": "Nem tal\u00e1ltam a Hue bridget",
"no_bridges": "Nem tal\u00e1ltam Philips Hue bridget",
"unknown": "Ismeretlen hiba t\u00f6rt\u00e9nt"
},
"error": {
"linking": "Ismeretlen \u00f6sszekapcsol\u00e1si hiba t\u00f6rt\u00e9nt.",
"register_failed": "Regisztr\u00e1ci\u00f3 nem siker\u00fclt, k\u00e9rem pr\u00f3b\u00e1lja \u00fajra"
},
"step": {
"init": {
"data": {
"host": "H\u00e1zigazda (Host)"
},
"title": "V\u00e1lassz Hue bridge-t"
},
"link": {
"title": "Kapcsol\u00f3d\u00e1s a hubhoz"
}
},
"title": "Philips Hue Bridge"
}
}

View File

@ -0,0 +1,10 @@
{
"config": {
"abort": {
"all_configured": "Tutti i bridge Philips Hue sono gi\u00e0 configurati",
"discover_timeout": "Impossibile trovare i bridge Hue",
"no_bridges": "Nessun bridge Hue di Philips trovato"
},
"title": "Philips Hue Bridge"
}
}

View File

@ -2,8 +2,11 @@
"config": {
"abort": {
"all_configured": "\ubaa8\ub4e0 \ud544\ub9bd\uc2a4 Hue \ube0c\ub9bf\uc9c0\uac00 \uc774\ubbf8 \uc124\uc815\ub41c \uc0c1\ud0dc\uc785\ub2c8\ub2e4",
"already_configured": "\ube0c\ub9bf\uc9c0\uac00 \uc774\ubbf8 \uc124\uc815\ub41c \uc0c1\ud0dc\uc785\ub2c8\ub2e4",
"cannot_connect": "\ube0c\ub9ac\uc9c0\uc5d0 \uc5f0\uacb0\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4",
"discover_timeout": "Hue \ube0c\ub9bf\uc9c0\ub97c \ucc3e\uc744 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4",
"no_bridges": "\ubc1c\uacac\ub41c \ud544\ub9bd\uc2a4 Hue \ube0c\ub9bf\uc9c0\uac00 \uc5c6\uc2b5\ub2c8\ub2e4"
"no_bridges": "\ubc1c\uacac\ub41c \ud544\ub9bd\uc2a4 Hue \ube0c\ub9bf\uc9c0\uac00 \uc5c6\uc2b5\ub2c8\ub2e4",
"unknown": "\uc54c \uc218\uc5c6\ub294 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4"
},
"error": {
"linking": "\uc54c \uc218\uc5c6\ub294 \uc5f0\uacb0 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4.",

View File

@ -0,0 +1,29 @@
{
"config": {
"abort": {
"all_configured": "All Philips Hue Bridge si scho\u00a0konfigur\u00e9iert",
"already_configured": "Bridge ass scho konfigur\u00e9iert",
"cannot_connect": "Keng Verbindung mat der bridge m\u00e9iglech",
"discover_timeout": "Keng Hue bridge fonnt",
"no_bridges": "Keng Philips Hue Bridge fonnt",
"unknown": "Onbekannten Feeler opgetrueden"
},
"error": {
"linking": "Onbekannte Liaisoun's Feeler opgetrueden",
"register_failed": "Feeler beim registr\u00e9ieren, prob\u00e9iert w.e.g. nach emol"
},
"step": {
"init": {
"data": {
"host": "Host"
},
"title": "Hue Bridge auswielen"
},
"link": {
"description": "Dr\u00e9ckt de Kn\u00e4ppchen un der Bridge fir den Philips Hue mam Home Assistant ze registr\u00e9ieren.\n\n![Kn\u00e4ppchen un der Bridge](/static/images/config_philips_hue.jpg)",
"title": "Link Hub"
}
},
"title": "Philips Hue Bridge"
}
}

View File

@ -2,8 +2,11 @@
"config": {
"abort": {
"all_configured": "Alle Philips Hue bridges zijn al geconfigureerd",
"already_configured": "Bridge is al geconfigureerd",
"cannot_connect": "Kan geen verbinding maken met bridge",
"discover_timeout": "Hue bridges kunnen niet worden gevonden",
"no_bridges": "Geen Philips Hue bridges ontdekt"
"no_bridges": "Geen Philips Hue bridges ontdekt",
"unknown": "Onbekende fout opgetreden"
},
"error": {
"linking": "Er is een onbekende verbindingsfout opgetreden.",
@ -17,7 +20,7 @@
"title": "Kies Hue bridge"
},
"link": {
"description": "Druk op de knop van de bridge om Philips Hue te registreren met de Home Assistant. ![Locatie van de knop op bridge] (/static/images/config_philips_hue.jpg)",
"description": "Druk op de knop van de bridge om Philips Hue te registreren met Home Assistant. ![Locatie van de knop op bridge] (/static/images/config_philips_hue.jpg)",
"title": "Link Hub"
}
},

View File

@ -2,8 +2,11 @@
"config": {
"abort": {
"all_configured": "Alle Philips Hue Bridger er allerede konfigurert",
"already_configured": "Bridge er allerede konfigurert",
"cannot_connect": "Kan ikke koble til Bridge",
"discover_timeout": "Kunne ikke oppdage Hue Bridger",
"no_bridges": "Ingen Philips Hue Bridger oppdaget"
"no_bridges": "Ingen Philips Hue Bridger oppdaget",
"unknown": "Ukjent feil oppstod"
},
"error": {
"linking": "Ukjent koblingsfeil oppstod.",

View File

@ -2,8 +2,11 @@
"config": {
"abort": {
"all_configured": "Wszystkie mostki Hue s\u0105 ju\u017c skonfigurowane",
"already_configured": "Mostek jest ju\u017c skonfigurowany",
"cannot_connect": "Nie mo\u017cna po\u0142\u0105czy\u0107 si\u0119 z mostkiem",
"discover_timeout": "Nie mo\u017cna wykry\u0107 \u017cadnych mostk\u00f3w Hue",
"no_bridges": "Nie wykryto \u017cadnych mostk\u00f3w Hue"
"no_bridges": "Nie wykryto \u017cadnych mostk\u00f3w Hue",
"unknown": "Wyst\u0105pi\u0142 nieznany b\u0142\u0105d"
},
"error": {
"linking": "Wyst\u0105pi\u0142 nieznany b\u0142\u0105d w trakcie \u0142\u0105czenia.",

View File

@ -0,0 +1,5 @@
{
"config": {
"title": ""
}
}

Some files were not shown because too many files have changed in this diff Show More