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/*/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/*/bmw_connected_drive.py
homeassistant/components/android_ip_webcam.py homeassistant/components/android_ip_webcam.py
@ -166,6 +166,9 @@ omit =
homeassistant/components/mailgun.py homeassistant/components/mailgun.py
homeassistant/components/*/mailgun.py homeassistant/components/*/mailgun.py
homeassistant/components/matrix.py
homeassistant/components/*/matrix.py
homeassistant/components/maxcube.py homeassistant/components/maxcube.py
homeassistant/components/*/maxcube.py homeassistant/components/*/maxcube.py
@ -208,6 +211,9 @@ omit =
homeassistant/components/raincloud.py homeassistant/components/raincloud.py
homeassistant/components/*/raincloud.py homeassistant/components/*/raincloud.py
homeassistant/components/rainmachine.py
homeassistant/components/*/rainmachine.py
homeassistant/components/raspihats.py homeassistant/components/raspihats.py
homeassistant/components/*/raspihats.py homeassistant/components/*/raspihats.py
@ -516,7 +522,6 @@ omit =
homeassistant/components/notify/lannouncer.py homeassistant/components/notify/lannouncer.py
homeassistant/components/notify/llamalab_automate.py homeassistant/components/notify/llamalab_automate.py
homeassistant/components/notify/mastodon.py homeassistant/components/notify/mastodon.py
homeassistant/components/notify/matrix.py
homeassistant/components/notify/message_bird.py homeassistant/components/notify/message_bird.py
homeassistant/components/notify/mycroft.py homeassistant/components/notify/mycroft.py
homeassistant/components/notify/nfandroidtv.py homeassistant/components/notify/nfandroidtv.py
@ -574,6 +579,7 @@ omit =
homeassistant/components/sensor/discogs.py homeassistant/components/sensor/discogs.py
homeassistant/components/sensor/dnsip.py homeassistant/components/sensor/dnsip.py
homeassistant/components/sensor/dovado.py homeassistant/components/sensor/dovado.py
homeassistant/components/sensor/domain_expiry.py
homeassistant/components/sensor/dte_energy_bridge.py homeassistant/components/sensor/dte_energy_bridge.py
homeassistant/components/sensor/dublin_bus_transport.py homeassistant/components/sensor/dublin_bus_transport.py
homeassistant/components/sensor/dwd_weather_warnings.py homeassistant/components/sensor/dwd_weather_warnings.py
@ -615,6 +621,7 @@ omit =
homeassistant/components/sensor/lyft.py homeassistant/components/sensor/lyft.py
homeassistant/components/sensor/metoffice.py homeassistant/components/sensor/metoffice.py
homeassistant/components/sensor/miflora.py homeassistant/components/sensor/miflora.py
homeassistant/components/sensor/mitemp_bt.py
homeassistant/components/sensor/modem_callerid.py homeassistant/components/sensor/modem_callerid.py
homeassistant/components/sensor/mopar.py homeassistant/components/sensor/mopar.py
homeassistant/components/sensor/mqtt_room.py homeassistant/components/sensor/mqtt_room.py
@ -635,6 +642,7 @@ omit =
homeassistant/components/sensor/plex.py homeassistant/components/sensor/plex.py
homeassistant/components/sensor/pocketcasts.py homeassistant/components/sensor/pocketcasts.py
homeassistant/components/sensor/pollen.py homeassistant/components/sensor/pollen.py
homeassistant/components/sensor/postnl.py
homeassistant/components/sensor/pushbullet.py homeassistant/components/sensor/pushbullet.py
homeassistant/components/sensor/pvoutput.py homeassistant/components/sensor/pvoutput.py
homeassistant/components/sensor/pyload.py homeassistant/components/sensor/pyload.py
@ -656,6 +664,7 @@ omit =
homeassistant/components/sensor/sma.py homeassistant/components/sensor/sma.py
homeassistant/components/sensor/snmp.py homeassistant/components/sensor/snmp.py
homeassistant/components/sensor/sochain.py homeassistant/components/sensor/sochain.py
homeassistant/components/sensor/socialblade.py
homeassistant/components/sensor/sonarr.py homeassistant/components/sensor/sonarr.py
homeassistant/components/sensor/speedtest.py homeassistant/components/sensor/speedtest.py
homeassistant/components/sensor/spotcrime.py homeassistant/components/sensor/spotcrime.py
@ -710,7 +719,6 @@ omit =
homeassistant/components/switch/orvibo.py homeassistant/components/switch/orvibo.py
homeassistant/components/switch/pulseaudio_loopback.py homeassistant/components/switch/pulseaudio_loopback.py
homeassistant/components/switch/rainbird.py homeassistant/components/switch/rainbird.py
homeassistant/components/switch/rainmachine.py
homeassistant/components/switch/rest.py homeassistant/components/switch/rest.py
homeassistant/components/switch/rpi_rf.py homeassistant/components/switch/rpi_rf.py
homeassistant/components/switch/snmp.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/history_graph.py @andrey-git
homeassistant/components/light/tplink.py @rytilahti homeassistant/components/light/tplink.py @rytilahti
homeassistant/components/light/yeelight.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/emby.py @mezz64
homeassistant/components/media_player/kodi.py @armills 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/mediaroom.py @dgomes
homeassistant/components/media_player/monoprice.py @etsinko homeassistant/components/media_player/monoprice.py @etsinko
homeassistant/components/media_player/sonos.py @amelchio 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/sensor/waqi.py @andrey-git
homeassistant/components/switch/rainmachine.py @bachya homeassistant/components/switch/rainmachine.py @bachya
homeassistant/components/switch/tplink.py @rytilahti homeassistant/components/switch/tplink.py @rytilahti
homeassistant/components/vacuum/roomba.py @pschmitt
homeassistant/components/xiaomi_aqara.py @danielhiversen @syssi homeassistant/components/xiaomi_aqara.py @danielhiversen @syssi
homeassistant/components/*/axis.py @kane610 homeassistant/components/*/axis.py @kane610
@ -90,6 +94,8 @@ homeassistant/components/*/hive.py @Rendili @KJonline
homeassistant/components/homekit/* @cdce8p homeassistant/components/homekit/* @cdce8p
homeassistant/components/knx.py @Julius2342 homeassistant/components/knx.py @Julius2342
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/*/qwikswitch.py @kellerza homeassistant/components/*/qwikswitch.py @kellerza
homeassistant/components/*/rfxtrx.py @danielhiversen 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 import voluptuous as vol
from homeassistant import ( from homeassistant import (
core, config as conf_util, config_entries, loader, core, config as conf_util, config_entries, components as core_components)
components as core_components)
from homeassistant.components import persistent_notification from homeassistant.components import persistent_notification
from homeassistant.const import EVENT_HOMEASSISTANT_CLOSE from homeassistant.const import EVENT_HOMEASSISTANT_CLOSE
from homeassistant.setup import async_setup_component from homeassistant.setup import async_setup_component
@ -67,16 +66,15 @@ def from_config_dict(config: Dict[str, Any],
return hass return hass
@asyncio.coroutine async def async_from_config_dict(config: Dict[str, Any],
def async_from_config_dict(config: Dict[str, Any], hass: core.HomeAssistant,
hass: core.HomeAssistant, config_dir: Optional[str] = None,
config_dir: Optional[str] = None, enable_log: bool = True,
enable_log: bool = True, verbose: bool = False,
verbose: bool = False, skip_pip: bool = False,
skip_pip: bool = False, log_rotate_days: Any = None,
log_rotate_days: Any = None, log_file: Any = None,
log_file: Any = None, log_no_color: bool = False) \
log_no_color: bool = False) \
-> Optional[core.HomeAssistant]: -> Optional[core.HomeAssistant]:
"""Try to configure Home Assistant from a configuration dictionary. """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, {}) core_config = config.get(core.DOMAIN, {})
try: 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: except vol.Invalid as ex:
conf_util.async_log_exception(ex, 'homeassistant', core_config, hass) conf_util.async_log_exception(ex, 'homeassistant', core_config, hass)
return None 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 hass.config.skip_pip = skip_pip
if skip_pip: if skip_pip:
_LOGGER.warning("Skipping pip installation of required modules. " _LOGGER.warning("Skipping pip installation of required modules. "
"This may cause issues") "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. # Make a copy because we are mutating it.
config = OrderedDict(config) config = OrderedDict(config)
# Merge packages # Merge packages
conf_util.merge_packages_config( 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 # Ensure we have no None values after merge
for key, value in config.items(): for key, value in config.items():
@ -120,7 +115,7 @@ def async_from_config_dict(config: Dict[str, Any],
config[key] = {} config[key] = {}
hass.config_entries = config_entries.ConfigEntries(hass, config) 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] # Filter out the repeating and common config section [homeassistant]
components = set(key.split(' ')[0] for key in config.keys() 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 # setup components
# pylint: disable=not-an-iterable # 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: if not res:
_LOGGER.error("Home Assistant core failed to initialize. " _LOGGER.error("Home Assistant core failed to initialize. "
"further initialization aborted") "further initialization aborted")
return hass return hass
yield from persistent_notification.async_setup(hass, config) await persistent_notification.async_setup(hass, config)
_LOGGER.info("Home Assistant core initialized") _LOGGER.info("Home Assistant core initialized")
@ -145,7 +140,7 @@ def async_from_config_dict(config: Dict[str, Any],
continue continue
hass.async_add_job(async_setup_component(hass, component, config)) 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 # stage 2
for component in components: for component in components:
@ -153,7 +148,7 @@ def async_from_config_dict(config: Dict[str, Any],
continue continue
hass.async_add_job(async_setup_component(hass, component, config)) 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() stop = time()
_LOGGER.info("Home Assistant initialized in %.2fs", stop-start) _LOGGER.info("Home Assistant initialized in %.2fs", stop-start)
@ -187,14 +182,13 @@ def from_config_file(config_path: str,
return hass return hass
@asyncio.coroutine async def async_from_config_file(config_path: str,
def async_from_config_file(config_path: str, hass: core.HomeAssistant,
hass: core.HomeAssistant, verbose: bool = False,
verbose: bool = False, skip_pip: bool = True,
skip_pip: bool = True, log_rotate_days: Any = None,
log_rotate_days: Any = None, log_file: Any = None,
log_file: Any = None, log_no_color: bool = False):
log_no_color: bool = False):
"""Read the configuration file and try to start all the functionality. """Read the configuration file and try to start all the functionality.
Will add functionality to 'hass' parameter. 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 # Set config dir to directory holding config file
config_dir = os.path.abspath(os.path.dirname(config_path)) config_dir = os.path.abspath(os.path.dirname(config_path))
hass.config.config_dir = config_dir 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, async_enable_logging(hass, verbose, log_rotate_days, log_file,
log_no_color) log_no_color)
try: try:
config_dict = yield from hass.async_add_job( config_dict = await hass.async_add_job(
conf_util.load_yaml_config_file, config_path) conf_util.load_yaml_config_file, config_path)
except HomeAssistantError as err: except HomeAssistantError as err:
_LOGGER.error("Error loading %s: %s", config_path, err) _LOGGER.error("Error loading %s: %s", config_path, err)
@ -217,7 +211,7 @@ def async_from_config_file(config_path: str,
finally: finally:
clear_secret_cache() 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) config_dict, hass, enable_log=False, skip_pip=skip_pip)
return hass return hass
@ -294,11 +288,10 @@ def async_enable_logging(hass: core.HomeAssistant,
async_handler = AsyncHandler(hass.loop, err_handler) async_handler = AsyncHandler(hass.loop, err_handler)
@asyncio.coroutine async def async_stop_async_handler(event):
def async_stop_async_handler(event):
"""Cleanup async handler.""" """Cleanup async handler."""
logging.getLogger('').removeHandler(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( hass.bus.async_listen_once(
EVENT_HOMEASSISTANT_CLOSE, async_stop_async_handler) EVENT_HOMEASSISTANT_CLOSE, async_stop_async_handler)
@ -323,15 +316,14 @@ def mount_local_lib_path(config_dir: str) -> str:
return deps_dir return deps_dir
@asyncio.coroutine async def async_mount_local_lib_path(config_dir: str,
def async_mount_local_lib_path(config_dir: str, loop: asyncio.AbstractEventLoop) -> str:
loop: asyncio.AbstractEventLoop) -> str:
"""Add local library to Python Path. """Add local library to Python Path.
This function is a coroutine. This function is a coroutine.
""" """
deps_dir = os.path.join(config_dir, 'deps') 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: if lib_dir not in sys.path:
sys.path.insert(0, lib_dir) sys.path.insert(0, lib_dir)
return deps_dir return deps_dir

View File

@ -81,7 +81,7 @@ TRIGGER_SCHEMA = vol.Schema({
ABODE_PLATFORMS = [ ABODE_PLATFORMS = [
'alarm_control_panel', 'binary_sensor', 'lock', 'switch', 'cover', '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 from homeassistant.helpers.aiohttp_client import async_get_clientsession
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['pyalarmdotcom==0.3.1'] REQUIREMENTS = ['pyalarmdotcom==0.3.2']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -93,6 +93,13 @@ class AlarmDotCom(alarm.AlarmControlPanel):
return STATE_ALARM_ARMED_AWAY return STATE_ALARM_ARMED_AWAY
return STATE_UNKNOWN return STATE_UNKNOWN
@property
def device_state_attributes(self):
"""Return the state attributes."""
return {
'sensor_status': self._alarm.sensor_status
}
@asyncio.coroutine @asyncio.coroutine
def async_alarm_disarm(self, code=None): def async_alarm_disarm(self, code=None):
"""Send disarm command.""" """Send disarm command."""

View File

@ -76,8 +76,7 @@ class APIEventStream(HomeAssistantView):
url = URL_API_STREAM url = URL_API_STREAM
name = "api:stream" name = "api:stream"
@asyncio.coroutine async def get(self, request):
def get(self, request):
"""Provide a streaming interface for the event bus.""" """Provide a streaming interface for the event bus."""
# pylint: disable=no-self-use # pylint: disable=no-self-use
hass = request.app['hass'] hass = request.app['hass']
@ -88,8 +87,7 @@ class APIEventStream(HomeAssistantView):
if restrict: if restrict:
restrict = restrict.split(',') + [EVENT_HOMEASSISTANT_STOP] restrict = restrict.split(',') + [EVENT_HOMEASSISTANT_STOP]
@asyncio.coroutine async def forward_events(event):
def forward_events(event):
"""Forward events to the open request.""" """Forward events to the open request."""
if event.event_type == EVENT_TIME_CHANGED: if event.event_type == EVENT_TIME_CHANGED:
return return
@ -104,11 +102,11 @@ class APIEventStream(HomeAssistantView):
else: else:
data = json.dumps(event, cls=rem.JSONEncoder) data = json.dumps(event, cls=rem.JSONEncoder)
yield from to_write.put(data) await to_write.put(data)
response = web.StreamResponse() response = web.StreamResponse()
response.content_type = 'text/event-stream' 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) 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)) _LOGGER.debug('STREAM %s ATTACHED', id(stop_obj))
# Fire off one message so browsers fire open event right away # 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: while True:
try: try:
with async_timeout.timeout(STREAM_PING_INTERVAL, with async_timeout.timeout(STREAM_PING_INTERVAL,
loop=hass.loop): loop=hass.loop):
payload = yield from to_write.get() payload = await to_write.get()
if payload is stop_obj: if payload is stop_obj:
break break
@ -130,9 +128,9 @@ class APIEventStream(HomeAssistantView):
msg = "data: {}\n\n".format(payload) msg = "data: {}\n\n".format(payload)
_LOGGER.debug('STREAM %s WRITING %s', id(stop_obj), _LOGGER.debug('STREAM %s WRITING %s', id(stop_obj),
msg.strip()) msg.strip())
yield from response.write(msg.encode("UTF-8")) await response.write(msg.encode("UTF-8"))
except asyncio.TimeoutError: except asyncio.TimeoutError:
yield from to_write.put(STREAM_PING_PAYLOAD) await to_write.put(STREAM_PING_PAYLOAD)
except asyncio.CancelledError: except asyncio.CancelledError:
_LOGGER.debug('STREAM %s ABORT', id(stop_obj)) _LOGGER.debug('STREAM %s ABORT', id(stop_obj))
@ -200,12 +198,11 @@ class APIEntityStateView(HomeAssistantView):
return self.json(state) return self.json(state)
return self.json_message('Entity not found', HTTP_NOT_FOUND) return self.json_message('Entity not found', HTTP_NOT_FOUND)
@asyncio.coroutine async def post(self, request, entity_id):
def post(self, request, entity_id):
"""Update state of entity.""" """Update state of entity."""
hass = request.app['hass'] hass = request.app['hass']
try: try:
data = yield from request.json() data = await request.json()
except ValueError: except ValueError:
return self.json_message('Invalid JSON specified', return self.json_message('Invalid JSON specified',
HTTP_BAD_REQUEST) HTTP_BAD_REQUEST)
@ -257,10 +254,9 @@ class APIEventView(HomeAssistantView):
url = '/api/events/{event_type}' url = '/api/events/{event_type}'
name = "api:event" name = "api:event"
@asyncio.coroutine async def post(self, request, event_type):
def post(self, request, event_type):
"""Fire events.""" """Fire events."""
body = yield from request.text() body = await request.text()
try: try:
event_data = json.loads(body) if body else None event_data = json.loads(body) if body else None
except ValueError: except ValueError:
@ -292,10 +288,9 @@ class APIServicesView(HomeAssistantView):
url = URL_API_SERVICES url = URL_API_SERVICES
name = "api:services" name = "api:services"
@asyncio.coroutine async def get(self, request):
def get(self, request):
"""Get registered services.""" """Get registered services."""
services = yield from async_services_json(request.app['hass']) services = await async_services_json(request.app['hass'])
return self.json(services) return self.json(services)
@ -305,14 +300,13 @@ class APIDomainServicesView(HomeAssistantView):
url = "/api/services/{domain}/{service}" url = "/api/services/{domain}/{service}"
name = "api:domain-services" name = "api:domain-services"
@asyncio.coroutine async def post(self, request, domain, service):
def post(self, request, domain, service):
"""Call a service. """Call a service.
Returns a list of changed states. Returns a list of changed states.
""" """
hass = request.app['hass'] hass = request.app['hass']
body = yield from request.text() body = await request.text()
try: try:
data = json.loads(body) if body else None data = json.loads(body) if body else None
except ValueError: except ValueError:
@ -320,7 +314,7 @@ class APIDomainServicesView(HomeAssistantView):
HTTP_BAD_REQUEST) HTTP_BAD_REQUEST)
with AsyncTrackStates(hass) as changed_states: 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) return self.json(changed_states)
@ -343,11 +337,10 @@ class APITemplateView(HomeAssistantView):
url = URL_API_TEMPLATE url = URL_API_TEMPLATE
name = "api:template" name = "api:template"
@asyncio.coroutine async def post(self, request):
def post(self, request):
"""Render a template.""" """Render a template."""
try: try:
data = yield from request.json() data = await request.json()
tpl = template.Template(data['template'], request.app['hass']) tpl = template.Template(data['template'], request.app['hass'])
return tpl.async_render(data.get('variables')) return tpl.async_render(data.get('variables'))
except (ValueError, TemplateError) as ex: except (ValueError, TemplateError) as ex:
@ -366,10 +359,9 @@ class APIErrorLog(HomeAssistantView):
return await self.file(request, request.app['hass'].data[DATA_LOGGING]) return await self.file(request, request.app['hass'].data[DATA_LOGGING])
@asyncio.coroutine async def async_services_json(hass):
def async_services_json(hass):
"""Generate services data to JSONify.""" """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} return [{"domain": key, "services": value}
for key, value in descriptions.items()] 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 import asyncio
from functools import partial from functools import partial
import importlib
import logging import logging
import voluptuous as vol 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 import ToggleEntity
from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.restore_state import async_get_last_state from homeassistant.helpers.restore_state import async_get_last_state
from homeassistant.loader import get_platform
from homeassistant.util.dt import utcnow from homeassistant.util.dt import utcnow
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
@ -58,12 +58,14 @@ _LOGGER = logging.getLogger(__name__)
def _platform_validator(config): def _platform_validator(config):
"""Validate it is a valid platform.""" """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 platform.TRIGGER_SCHEMA(config)
return config
return getattr(platform, 'TRIGGER_SCHEMA')(config)
_TRIGGER_SCHEMA = vol.All( _TRIGGER_SCHEMA = vol.All(
@ -71,7 +73,7 @@ _TRIGGER_SCHEMA = vol.All(
[ [
vol.All( vol.All(
vol.Schema({ vol.Schema({
vol.Required(CONF_PLATFORM): cv.platform_validator(DOMAIN) vol.Required(CONF_PLATFORM): str
}, extra=vol.ALLOW_EXTRA), }, extra=vol.ALLOW_EXTRA),
_platform_validator _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): async def async_setup(hass, config):
"""Track states and offer events for binary sensors.""" """Track states and offer events for binary sensors."""
component = EntityComponent( component = hass.data[DOMAIN] = EntityComponent(
logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL) logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL)
await component.async_setup(config) await component.async_setup(config)
return True 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 # pylint: disable=no-self-use
class BinarySensorDevice(Entity): class BinarySensorDevice(Entity):
"""Represent a binary sensor.""" """Represent a binary sensor."""

View File

@ -11,7 +11,6 @@ import voluptuous as vol
from homeassistant.components.binary_sensor import ( from homeassistant.components.binary_sensor import (
BinarySensorDevice, PLATFORM_SCHEMA) BinarySensorDevice, PLATFORM_SCHEMA)
from homeassistant.const import CONF_MONITORED_CONDITIONS from homeassistant.const import CONF_MONITORED_CONDITIONS
from homeassistant.loader import get_component
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -31,7 +30,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
def setup_platform(hass, config, add_devices, discovery_info=None): def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the available BloomSky weather binary sensors.""" """Set up the available BloomSky weather binary sensors."""
bloomsky = get_component('bloomsky') bloomsky = hass.components.bloomsky
# Default needed in case of discovery # Default needed in case of discovery
sensors = config.get(CONF_MONITORED_CONDITIONS, SENSOR_TYPES) 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.binary_sensor import BinarySensorDevice
from homeassistant.components.deconz import ( 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.const import ATTR_BATTERY_LEVEL
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
DEPENDENCIES = ['deconz'] DEPENDENCIES = ['deconz']
async def async_setup_platform(hass, config, async_add_devices, async def async_setup_platform(hass, config, async_add_devices,
discovery_info=None): 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.""" """Set up the deCONZ binary sensor."""
if discovery_info is None: @callback
return 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 async_add_sensor(hass.data[DATA_DECONZ].sensors.values())
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)
class DeconzBinarySensor(BinarySensorDevice): class DeconzBinarySensor(BinarySensorDevice):

View File

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

View File

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

View File

@ -14,7 +14,7 @@ from homeassistant.components.binary_sensor import (
from homeassistant.const import CONF_NAME from homeassistant.const import CONF_NAME
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['tapsaff==0.1.3'] REQUIREMENTS = ['tapsaff==0.2.0']
_LOGGER = logging.getLogger(__name__) _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.helpers.event import async_track_state_change
from homeassistant.util import utcnow from homeassistant.util import utcnow
REQUIREMENTS = ['numpy==1.14.2'] REQUIREMENTS = ['numpy==1.14.3']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)

View File

@ -7,7 +7,6 @@ https://home-assistant.io/components/binary_sensor.wemo/
import logging import logging
from homeassistant.components.binary_sensor import BinarySensorDevice from homeassistant.components.binary_sensor import BinarySensorDevice
from homeassistant.loader import get_component
DEPENDENCIES = ['wemo'] DEPENDENCIES = ['wemo']
@ -25,18 +24,18 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
device = discovery.device_from_description(location, mac) device = discovery.device_from_description(location, mac)
if device: if device:
add_devices_callback([WemoBinarySensor(device)]) add_devices_callback([WemoBinarySensor(hass, device)])
class WemoBinarySensor(BinarySensorDevice): class WemoBinarySensor(BinarySensorDevice):
"""Representation a WeMo binary sensor.""" """Representation a WeMo binary sensor."""
def __init__(self, device): def __init__(self, hass, device):
"""Initialize the WeMo sensor.""" """Initialize the WeMo sensor."""
self.wemo = device self.wemo = device
self._state = None self._state = None
wemo = get_component('wemo') wemo = hass.components.wemo
wemo.SUBSCRIPTION_REGISTRY.register(self.wemo) wemo.SUBSCRIPTION_REGISTRY.register(self.wemo)
wemo.SUBSCRIPTION_REGISTRY.on(self.wemo, None, self._update_callback) 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__) _LOGGER = logging.getLogger(__name__)
REQUIREMENTS = ['holidays==0.9.4'] REQUIREMENTS = ['holidays==0.9.5']
# List of all countries currently supported by holidays # List of all countries currently supported by holidays
# There seems to be no way to get the list out at runtime # There seems to be no way to get the list out at runtime
ALL_COUNTRIES = ['Australia', 'AU', 'Austria', 'AT', 'Belgium', 'BE', 'Canada', ALL_COUNTRIES = ['Argentina', 'AR', 'Australia', 'AU', 'Austria', 'AT',
'CA', 'Colombia', 'CO', 'Czech', 'CZ', 'Denmark', 'DK', 'Belgium', 'BE', 'Canada', 'CA', 'Colombia', 'CO', 'Czech',
'England', 'EuropeanCentralBank', 'ECB', 'TAR', 'Finland', 'CZ', 'Denmark', 'DK', 'England', 'EuropeanCentralBank',
'FI', 'France', 'FRA', 'Germany', 'DE', 'Ireland', 'ECB', 'TAR', 'Finland', 'FI', 'France', 'FRA', 'Germany',
'Isle of Man', 'Italy', 'IT', 'Japan', 'JP', 'Mexico', 'MX', 'DE', 'Hungary', 'HU', 'Ireland', 'Isle of Man', 'Italy',
'Netherlands', 'NL', 'NewZealand', 'NZ', 'Northern Ireland', 'IT', 'Japan', 'JP', 'Mexico', 'MX', 'Netherlands', 'NL',
'NewZealand', 'NZ', 'Northern Ireland',
'Norway', 'NO', 'Polish', 'PL', 'Portugal', 'PT', 'Norway', 'NO', 'Polish', 'PL', 'Portugal', 'PT',
'PortugalExt', 'PTE', 'Scotland', 'Slovenia', 'SI', 'PortugalExt', 'PTE', 'Scotland', 'Slovenia', 'SI',
'Slovakia', 'SK', 'South Africa', 'ZA', 'Spain', 'ES', '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 (_, gateway) in hass.data[PY_XIAOMI_GATEWAY].gateways.items():
for device in gateway.devices['binary_sensor']: for device in gateway.devices['binary_sensor']:
model = device['model'] 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)) 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)) devices.append(XiaomiDoorSensor(device, gateway))
elif model == 'sensor_wleak.aq1': elif model == 'sensor_wleak.aq1':
devices.append(XiaomiWaterLeakSensor(device, gateway)) devices.append(XiaomiWaterLeakSensor(device, gateway))
elif model == 'smoke': elif model in ['smoke', 'sensor_smoke']:
devices.append(XiaomiSmokeSensor(device, gateway)) devices.append(XiaomiSmokeSensor(device, gateway))
elif model == 'natgas': elif model in ['natgas', 'sensor_natgas']:
devices.append(XiaomiNatgasSensor(device, gateway)) devices.append(XiaomiNatgasSensor(device, gateway))
elif model in ['switch', 'sensor_switch.aq2', 'sensor_switch.aq3']: elif model in ['switch', 'sensor_switch',
devices.append(XiaomiButton(device, 'Switch', 'status', '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)) hass, gateway))
elif model == '86sw1': elif model in ['86sw1', 'sensor_86sw1.aq1']:
devices.append(XiaomiButton(device, 'Wall Switch', 'channel_0', devices.append(XiaomiButton(device, 'Wall Switch', 'channel_0',
hass, gateway)) hass, gateway))
elif model == '86sw2': elif model in ['86sw2', 'sensor_86sw2.aq1']:
devices.append(XiaomiButton(device, 'Wall Switch (Left)', devices.append(XiaomiButton(device, 'Wall Switch (Left)',
'channel_0', hass, gateway)) 'channel_0', hass, gateway))
devices.append(XiaomiButton(device, 'Wall Switch (Right)', devices.append(XiaomiButton(device, 'Wall Switch (Right)',
'channel_1', hass, gateway)) 'channel_1', hass, gateway))
devices.append(XiaomiButton(device, 'Wall Switch (Both)', devices.append(XiaomiButton(device, 'Wall Switch (Both)',
'dual_channel', hass, gateway)) 'dual_channel', hass, gateway))
elif model == 'cube': elif model in ['cube', 'sensor_cube']:
devices.append(XiaomiCube(device, hass, gateway)) devices.append(XiaomiCube(device, hass, gateway))
add_devices(devices) add_devices(devices)
@ -129,8 +134,12 @@ class XiaomiMotionSensor(XiaomiBinarySensor):
"""Initialize the XiaomiMotionSensor.""" """Initialize the XiaomiMotionSensor."""
self._hass = hass self._hass = hass
self._no_motion_since = 0 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, XiaomiBinarySensor.__init__(self, device, 'Motion Sensor', xiaomi_hub,
'status', 'motion') data_key, 'motion')
@property @property
def device_state_attributes(self): 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: if discovery_info is None:
return return
from zigpy.zcl.clusters.general import OnOff
from zigpy.zcl.clusters.security import IasZone 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 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']: if discovery_info['new_join']:
await cluster.bind() await cluster.bind()
ieee = cluster.endpoint.device.application.ieee 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_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): class BinarySensor(zha.Entity, BinarySensorDevice):
"""THe ZHA Binary Sensor.""" """The ZHA Binary Sensor."""
_domain = DOMAIN _domain = DOMAIN
@ -102,3 +137,113 @@ class BinarySensor(zha.Entity, BinarySensorDevice):
state = result.get('zone_status', self._state) state = result.get('zone_status', self._state)
if isinstance(state, (int, uint16_t)): if isinstance(state, (int, uint16_t)):
self._state = result.get('zone_status', self._state) & 3 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/ https://home-assistant.io/components/camera/
""" """
import asyncio import asyncio
import base64
import collections import collections
from contextlib import suppress from contextlib import suppress
from datetime import timedelta from datetime import timedelta
@ -13,20 +14,20 @@ import logging
import hashlib import hashlib
from random import SystemRandom from random import SystemRandom
import aiohttp import attr
from aiohttp import web from aiohttp import web
import async_timeout import async_timeout
import voluptuous as vol import voluptuous as vol
from homeassistant.core import callback 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.exceptions import HomeAssistantError
from homeassistant.loader import bind_hass 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 import Entity
from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa
from homeassistant.components.http import HomeAssistantView, KEY_AUTHENTICATED from homeassistant.components.http import HomeAssistantView, KEY_AUTHENTICATED
from homeassistant.components import websocket_api
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
DOMAIN = 'camera' DOMAIN = 'camera'
@ -53,6 +54,9 @@ ENTITY_IMAGE_URL = '/api/camera_proxy/{0}?token={1}'
TOKEN_CHANGE_INTERVAL = timedelta(minutes=5) TOKEN_CHANGE_INTERVAL = timedelta(minutes=5)
_RND = SystemRandom() _RND = SystemRandom()
FALLBACK_STREAM_INTERVAL = 1 # seconds
MIN_STREAM_INTERVAL = 0.5 # seconds
CAMERA_SERVICE_SCHEMA = vol.Schema({ CAMERA_SERVICE_SCHEMA = vol.Schema({
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, 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 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 @bind_hass
def enable_motion_detection(hass, entity_id=None): def enable_motion_detection(hass, entity_id=None):
@ -89,43 +107,40 @@ def async_snapshot(hass, filename, entity_id=None):
@bind_hass @bind_hass
@asyncio.coroutine async def async_get_image(hass, entity_id, timeout=10):
def async_get_image(hass, entity_id, timeout=10):
"""Fetch an image from a camera entity.""" """Fetch an image from a camera entity."""
websession = async_get_clientsession(hass) component = hass.data.get(DOMAIN)
state = hass.states.get(entity_id)
if state is None: if component is None:
raise HomeAssistantError( raise HomeAssistantError('Camera component not setup')
"No entity '{0}' for grab an image".format(entity_id))
url = "{0}{1}".format( camera = component.get_entity(entity_id)
hass.config.api.base_url,
state.attributes.get(ATTR_ENTITY_PICTURE)
)
try: if camera is None:
raise HomeAssistantError('Camera not found')
with suppress(asyncio.CancelledError, asyncio.TimeoutError):
with async_timeout.timeout(timeout, loop=hass.loop): with async_timeout.timeout(timeout, loop=hass.loop):
response = yield from websession.get(url) image = await camera.async_camera_image()
if response.status != 200: if image:
raise HomeAssistantError("Error {0} on {1}".format( return Image(camera.content_type, image)
response.status, url))
image = yield from response.read() raise HomeAssistantError('Unable to get image')
return image
except (asyncio.TimeoutError, aiohttp.ClientError):
raise HomeAssistantError("Can't connect to {0}".format(url))
@asyncio.coroutine @asyncio.coroutine
def async_setup(hass, config): def async_setup(hass, config):
"""Set up the camera component.""" """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(CameraImageView(component))
hass.http.register_view(CameraMjpegStream(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) yield from component.async_setup(config)
@ -252,19 +267,21 @@ class Camera(Entity):
""" """
return self.hass.async_add_job(self.camera_image) return self.hass.async_add_job(self.camera_image)
@asyncio.coroutine async def handle_async_still_stream(self, request, interval):
def handle_async_mjpeg_stream(self, request):
"""Generate an HTTP MJPEG stream from camera images. """Generate an HTTP MJPEG stream from camera images.
This method must be run in the event loop. 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; ' response.content_type = ('multipart/x-mixed-replace; '
'boundary=--frameboundary') '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.""" """Write image to stream."""
await response.write(bytes( await response.write(bytes(
'--frameboundary\r\n' '--frameboundary\r\n'
@ -277,21 +294,21 @@ class Camera(Entity):
try: try:
while True: while True:
img_bytes = yield from self.async_camera_image() img_bytes = await self.async_camera_image()
if not img_bytes: if not img_bytes:
break break
if img_bytes and img_bytes != last_image: 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, # Chrome seems to always ignore first picture,
# print it twice. # print it twice.
if last_image is None: if last_image is None:
yield from write(img_bytes) await write_to_mjpeg_stream(img_bytes)
last_image = img_bytes last_image = img_bytes
yield from asyncio.sleep(.5) await asyncio.sleep(interval)
except asyncio.CancelledError: except asyncio.CancelledError:
_LOGGER.debug("Stream closed by frontend.") _LOGGER.debug("Stream closed by frontend.")
@ -299,7 +316,17 @@ class Camera(Entity):
finally: finally:
if response is not None: 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 @property
def state(self): def state(self):
@ -329,20 +356,20 @@ class Camera(Entity):
@property @property
def state_attributes(self): def state_attributes(self):
"""Return the camera state attributes.""" """Return the camera state attributes."""
attr = { attrs = {
'access_token': self.access_tokens[-1], 'access_token': self.access_tokens[-1],
} }
if self.model: if self.model:
attr['model_name'] = self.model attrs['model_name'] = self.model
if self.brand: if self.brand:
attr['brand'] = self.brand attrs['brand'] = self.brand
if self.motion_detection_enabled: if self.motion_detection_enabled:
attr['motion_detection'] = self.motion_detection_enabled attrs['motion_detection'] = self.motion_detection_enabled
return attr return attrs
@callback @callback
def async_update_token(self): def async_update_token(self):
@ -411,7 +438,40 @@ class CameraMjpegStream(CameraView):
url = '/api/camera_proxy_stream/{entity_id}' url = '/api/camera_proxy_stream/{entity_id}'
name = 'api:camera:stream' name = 'api:camera:stream'
@asyncio.coroutine async def handle(self, request, camera):
def handle(self, request, camera): """Serve camera stream, possibly with interval."""
"""Serve camera image.""" interval = request.query.get('interval')
yield from camera.handle_async_mjpeg_stream(request) 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 import requests
from homeassistant.components.camera import Camera from homeassistant.components.camera import Camera
from homeassistant.loader import get_component
DEPENDENCIES = ['bloomsky'] DEPENDENCIES = ['bloomsky']
@ -17,7 +16,7 @@ DEPENDENCIES = ['bloomsky']
# pylint: disable=unused-argument # pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None): def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up access to BloomSky cameras.""" """Set up access to BloomSky cameras."""
bloomsky = get_component('bloomsky') bloomsky = hass.components.bloomsky
for device in bloomsky.BLOOMSKY.devices.values(): for device in bloomsky.BLOOMSKY.devices.values():
add_devices([BloomSkyCamera(bloomsky.BLOOMSKY, device)]) add_devices([BloomSkyCamera(bloomsky.BLOOMSKY, device)])

View File

@ -11,31 +11,44 @@ import os
import voluptuous as vol import voluptuous as vol
from homeassistant.const import CONF_NAME 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 from homeassistant.helpers import config_validation as cv
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
CONF_FILE_PATH = 'file_path' CONF_FILE_PATH = 'file_path'
DEFAULT_NAME = 'Local File' DEFAULT_NAME = 'Local File'
SERVICE_UPDATE_FILE_PATH = 'local_file_update_file_path'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_FILE_PATH): cv.string, vol.Required(CONF_FILE_PATH): cv.string,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): 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): def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Camera that works with local files.""" """Set up the Camera that works with local files."""
file_path = config[CONF_FILE_PATH] file_path = config[CONF_FILE_PATH]
camera = LocalFile(config[CONF_NAME], file_path)
# check filepath given is readable def update_file_path_service(call):
if not os.access(file_path, os.R_OK): """Update the file path."""
_LOGGER.warning("Could not read camera %s image from file: %s", file_path = call.data.get(CONF_FILE_PATH)
config[CONF_NAME], 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): class LocalFile(Camera):
@ -46,6 +59,7 @@ class LocalFile(Camera):
super().__init__() super().__init__()
self._name = name self._name = name
self.check_file_path_access(file_path)
self._file_path = file_path self._file_path = file_path
# Set content type of local file # Set content type of local file
content, _ = mimetypes.guess_type(file_path) content, _ = mimetypes.guess_type(file_path)
@ -61,7 +75,26 @@ class LocalFile(Camera):
_LOGGER.warning("Could not read camera %s image from file: %s", _LOGGER.warning("Could not read camera %s image from file: %s",
self._name, self._file_path) 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 @property
def name(self): def name(self):
"""Return the name of this camera.""" """Return the name of this camera."""
return self._name 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.const import CONF_VERIFY_SSL
from homeassistant.components.netatmo import CameraData from homeassistant.components.netatmo import CameraData
from homeassistant.components.camera import (Camera, PLATFORM_SCHEMA) from homeassistant.components.camera import (Camera, PLATFORM_SCHEMA)
from homeassistant.loader import get_component
from homeassistant.helpers import config_validation as cv from homeassistant.helpers import config_validation as cv
DEPENDENCIES = ['netatmo'] DEPENDENCIES = ['netatmo']
@ -33,7 +32,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
# pylint: disable=unused-argument # pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None): def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up access to Netatmo cameras.""" """Set up access to Netatmo cameras."""
netatmo = get_component('netatmo') netatmo = hass.components.netatmo
home = config.get(CONF_HOME) home = config.get(CONF_HOME)
verify_ssl = config.get(CONF_VERIFY_SSL, True) verify_ssl = config.get(CONF_VERIFY_SSL, True)
import lnetatmo import lnetatmo

View File

@ -24,6 +24,16 @@ snapshot:
description: Template of a Filename. Variable is entity_id. description: Template of a Filename. Variable is entity_id.
example: '/tmp/snapshot_{{ 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: onvif_ptz:
description: Pan/Tilt/Zoom service for ONVIF camera. description: Pan/Tilt/Zoom service for ONVIF camera.
fields: fields:
@ -39,4 +49,3 @@ onvif_ptz:
zoom: zoom:
description: "Zoom. Allowed values: ZOOM_IN, ZOOM_OUT" description: "Zoom. Allowed values: ZOOM_IN, ZOOM_OUT"
example: "ZOOM_IN" example: "ZOOM_IN"

View File

@ -13,7 +13,6 @@ from homeassistant.components.climate import (
STATE_HEAT, STATE_IDLE, ClimateDevice, PLATFORM_SCHEMA, STATE_HEAT, STATE_IDLE, ClimateDevice, PLATFORM_SCHEMA,
SUPPORT_TARGET_TEMPERATURE, SUPPORT_OPERATION_MODE, SUPPORT_AWAY_MODE) SUPPORT_TARGET_TEMPERATURE, SUPPORT_OPERATION_MODE, SUPPORT_AWAY_MODE)
from homeassistant.util import Throttle from homeassistant.util import Throttle
from homeassistant.loader import get_component
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
DEPENDENCIES = ['netatmo'] DEPENDENCIES = ['netatmo']
@ -42,7 +41,7 @@ SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_OPERATION_MODE |
def setup_platform(hass, config, add_devices, discovery_info=None): def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the NetAtmo Thermostat.""" """Set up the NetAtmo Thermostat."""
netatmo = get_component('netatmo') netatmo = hass.components.netatmo
device = config.get(CONF_RELAY) device = config.get(CONF_RELAY)
import lnetatmo 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.util import dt as dt_util
from homeassistant.components.alexa import smart_home as alexa_sh 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 helpers as ga_h
from homeassistant.components.google_assistant import const as ga_c
from . import http_api, iot from . import http_api, iot
from .const import CONFIG_DIR, DOMAIN, SERVERS from .const import CONFIG_DIR, DOMAIN, SERVERS
@ -52,7 +53,8 @@ ALEXA_ENTITY_SCHEMA = vol.Schema({
GOOGLE_ENTITY_SCHEMA = vol.Schema({ GOOGLE_ENTITY_SCHEMA = vol.Schema({
vol.Optional(CONF_NAME): cv.string, 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({ ASSISTANT_SCHEMA = vol.Schema({

View File

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

View File

@ -79,5 +79,7 @@ class TahomaCover(TahomaDevice, CoverDevice):
if self.tahoma_device.type == \ if self.tahoma_device.type == \
'io:RollerShutterWithLowSpeedManagementIOComponent': 'io:RollerShutterWithLowSpeedManagementIOComponent':
self.apply_action('setPosition', 'secured') self.apply_action('setPosition', 'secured')
elif self.tahoma_device.type == 'rts:BlindRTSComponent':
self.apply_action('my')
else: else:
self.apply_action('stopIdentify') 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": { "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": { "abort": {
"already_configured": "Bridge is already configured", "already_configured": "Bridge is already configured",
"no_bridges": "No deCONZ bridges discovered", "no_bridges": "No deCONZ bridges discovered",
"one_instance_only": "Component only supports one deCONZ instance" "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 import voluptuous as vol
from homeassistant.const import ( from homeassistant.const import (
CONF_API_KEY, CONF_HOST, CONF_PORT, EVENT_HOMEASSISTANT_STOP) CONF_API_KEY, CONF_EVENT, CONF_HOST,
from homeassistant.core import callback CONF_ID, CONF_PORT, EVENT_HOMEASSISTANT_STOP)
from homeassistant.helpers import ( from homeassistant.core import EventOrigin, callback
aiohttp_client, discovery, config_validation as cv) 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 from homeassistant.util.json import load_json
# Loading the config flow file will register the flow # Loading the config flow file will register the flow
from .config_flow import configured_hosts 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({ CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({ DOMAIN: vol.Schema({
@ -27,6 +32,8 @@ CONFIG_SCHEMA = vol.Schema({
}) })
}, extra=vol.ALLOW_EXTRA) }, extra=vol.ALLOW_EXTRA)
SERVICE_DECONZ = 'configure'
SERVICE_FIELD = 'field' SERVICE_FIELD = 'field'
SERVICE_ENTITY = 'entity' SERVICE_ENTITY = 'entity'
SERVICE_DATA = 'data' SERVICE_DATA = 'data'
@ -58,28 +65,27 @@ async def async_setup(hass, config):
return True return True
async def async_setup_entry(hass, entry): async def async_setup_entry(hass, config_entry):
"""Set up a deCONZ bridge for a config 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.
Load config, group, light and sensor data for server information. Load config, group, light and sensor data for server information.
Start websocket for push notification of state changes from deCONZ. Start websocket for push notification of state changes from deCONZ.
""" """
_LOGGER.debug("deCONZ config %s", deconz_config)
from pydeconz import DeconzSession 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) 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() result = await deconz.async_load_parameters()
if result is False: if result is False:
_LOGGER.error("Failed to communicate with deCONZ") _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[DOMAIN] = deconz
hass.data[DATA_DECONZ_ID] = {} hass.data[DATA_DECONZ_ID] = {}
hass.data[DATA_DECONZ_EVENT] = []
hass.data[DATA_DECONZ_UNSUB] = []
for component in ['binary_sensor', 'light', 'scene', 'sensor']: for component in ['binary_sensor', 'light', 'scene', 'sensor']:
hass.async_add_job(discovery.async_load_platform( hass.async_add_job(hass.config_entries.async_forward_entry_setup(
hass, component, DOMAIN, {}, config)) 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() deconz.start()
async def async_configure(call): async def async_configure(call):
@ -121,7 +142,7 @@ async def async_setup_deconz(hass, config, deconz_config):
return return
await deconz.async_put_state(field, data) await deconz.async_put_state(field, data)
hass.services.async_register( hass.services.async_register(
DOMAIN, 'configure', async_configure, schema=SERVICE_SCHEMA) DOMAIN, SERVICE_DECONZ, async_configure, schema=SERVICE_SCHEMA)
@callback @callback
def deconz_shutdown(event): 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) hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, deconz_shutdown)
return True 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' DOMAIN = 'deconz'
CONFIG_FILE = 'deconz.conf' CONFIG_FILE = 'deconz.conf'
DATA_DECONZ_EVENT = 'deconz_events'
DATA_DECONZ_ID = 'deconz_entities' 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 ( from homeassistant.helpers.event import (
async_track_point_in_utc_time, async_track_state_change) async_track_point_in_utc_time, async_track_state_change)
from homeassistant.helpers.sun import is_up, get_astral_event_next from homeassistant.helpers.sun import is_up, get_astral_event_next
from homeassistant.loader import get_component
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
DOMAIN = 'device_sun_light_trigger' DOMAIN = 'device_sun_light_trigger'
@ -48,9 +47,9 @@ CONFIG_SCHEMA = vol.Schema({
def async_setup(hass, config): def async_setup(hass, config):
"""Set up the triggers to control lights based on device presence.""" """Set up the triggers to control lights based on device presence."""
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
device_tracker = get_component('device_tracker') device_tracker = hass.components.device_tracker
group = get_component('group') group = hass.components.group
light = get_component('light') light = hass.components.light
conf = config[DOMAIN] conf = config[DOMAIN]
disable_turn_off = conf.get(CONF_DISABLE_TURN_OFF) disable_turn_off = conf.get(CONF_DISABLE_TURN_OFF)
light_group = conf.get(CONF_LIGHT_GROUP, light.ENTITY_ID_ALL_LIGHTS) 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( device_group = conf.get(
CONF_DEVICE_GROUP, device_tracker.ENTITY_ID_ALL_DEVICES) CONF_DEVICE_GROUP, device_tracker.ENTITY_ID_ALL_DEVICES)
device_entity_ids = group.get_entity_ids( device_entity_ids = group.get_entity_ids(
hass, device_group, device_tracker.DOMAIN) device_group, device_tracker.DOMAIN)
if not device_entity_ids: if not device_entity_ids:
logger.error("No devices found to track") logger.error("No devices found to track")
return False return False
# Get the light IDs from the specified group # 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: if not light_ids:
logger.error("No lights found to turn on") 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): def async_turn_on_before_sunset(light_id):
"""Turn on lights.""" """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 return
light.async_turn_on(hass, light_id, light.async_turn_on(light_id,
transition=LIGHT_TRANSITION_TIME.seconds, transition=LIGHT_TRANSITION_TIME.seconds,
profile=light_profile) profile=light_profile)
@ -129,7 +128,7 @@ def async_setup(hass, config):
@callback @callback
def check_light_on_dev_state_change(entity, old_state, new_state): def check_light_on_dev_state_change(entity, old_state, new_state):
"""Handle tracked device state changes.""" """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)) light_needed = not (lights_are_on or is_up(hass))
# These variables are needed for the elif check # These variables are needed for the elif check
@ -139,7 +138,7 @@ def async_setup(hass, config):
# Do we need lights? # Do we need lights?
if light_needed: if light_needed:
logger.info("Home coming event for %s. Turning lights on", entity) 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 # Are we in the time span were we would turn on the lights
# if someone would be home? # 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 # when the fading in started and turn it on if so
for index, light_id in enumerate(light_ids): for index, light_id in enumerate(light_ids):
if now > start_point + index * LIGHT_TRANSITION_TIME: if now > start_point + index * LIGHT_TRANSITION_TIME:
light.async_turn_on(hass, light_id) light.async_turn_on(light_id)
else: else:
# If this light didn't happen to be turned on yet so # If this light didn't happen to be turned on yet so
@ -169,12 +168,12 @@ def async_setup(hass, config):
@callback @callback
def turn_off_lights_when_all_leave(entity, old_state, new_state): def turn_off_lights_when_all_leave(entity, old_state, new_state):
"""Handle device group state change.""" """Handle device group state change."""
if not group.is_on(hass, light_group): if not group.is_on(light_group):
return return
logger.info( logger.info(
"Everyone has left but there are lights on. Turning them off") "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( async_track_state_change(
hass, device_group, turn_off_lights_when_all_leave, 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.core import callback
from homeassistant.loader import bind_hass from homeassistant.loader import bind_hass
from homeassistant.components import group, zone 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.config import load_yaml_config_file, async_log_exception
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import config_per_platform, discovery 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.restore_state import async_get_last_state
from homeassistant.helpers.typing import GPSType, ConfigType, HomeAssistantType from homeassistant.helpers.typing import GPSType, ConfigType, HomeAssistantType
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.loader import get_component
import homeassistant.util as util import homeassistant.util as util
from homeassistant.util.async_ import run_coroutine_threadsafe from homeassistant.util.async_ import run_coroutine_threadsafe
import homeassistant.util.dt as dt_util import homeassistant.util.dt as dt_util
@ -321,7 +321,7 @@ class DeviceTracker(object):
# During init, we ignore the group # During init, we ignore the group
if self.group and self.track_new: if self.group and self.track_new:
self.group.async_set_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, add=[device.entity_id]) name=GROUP_NAME_ALL_DEVICES, add=[device.entity_id])
self.hass.bus.async_fire(EVENT_NEW_DEVICE, { 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() entity_ids = [dev.entity_id for dev in self.devices.values()
if dev.track] if dev.track]
self.group = get_component('group') self.group = self.hass.components.group
self.group.async_set_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) name=GROUP_NAME_ALL_DEVICES, entity_ids=entity_ids)
@callback @callback
@ -541,7 +541,7 @@ class Device(Entity):
elif self.location_name: elif self.location_name:
self._state = self.location_name self._state = self.location_name
elif self.gps is not None and self.source_type == SOURCE_TYPE_GPS: 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) self.hass, self.gps[0], self.gps[1], self.gps_accuracy)
if zone_state is None: if zone_state is None:
self._state = STATE_NOT_HOME self._state = STATE_NOT_HOME

View File

@ -19,7 +19,7 @@ from homeassistant.util import slugify
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
REQUIREMENTS = ['locationsharinglib==1.2.1'] REQUIREMENTS = ['locationsharinglib==1.2.2']
CREDENTIALS_FILE = '.google_maps_location_sharing.cookies' CREDENTIALS_FILE = '.google_maps_location_sharing.cookies'
@ -79,5 +79,6 @@ class GoogleMapsScanner(object):
gps=(person.latitude, person.longitude), gps=(person.latitude, person.longitude),
picture=person.picture_url, picture=person.picture_url,
source_type=SOURCE_TYPE_GPS, source_type=SOURCE_TYPE_GPS,
gps_accuracy=person.accuracy,
attributes=attrs 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 For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/device_tracker.gpslogger/ https://home-assistant.io/components/device_tracker.gpslogger/
""" """
import asyncio
import logging import logging
from hmac import compare_digest from hmac import compare_digest
@ -22,6 +21,7 @@ from homeassistant.components.http import (
from homeassistant.components.device_tracker import ( # NOQA from homeassistant.components.device_tracker import ( # NOQA
DOMAIN, PLATFORM_SCHEMA DOMAIN, PLATFORM_SCHEMA
) )
from homeassistant.helpers.typing import HomeAssistantType, ConfigType
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -32,8 +32,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
}) })
@asyncio.coroutine async def async_setup_scanner(hass: HomeAssistantType, config: ConfigType,
def async_setup_scanner(hass, config, async_see, discovery_info=None): async_see, discovery_info=None):
"""Set up an endpoint for the GPSLogger application.""" """Set up an endpoint for the GPSLogger application."""
hass.http.register_view(GPSLoggerView(async_see, config)) hass.http.register_view(GPSLoggerView(async_see, config))
@ -54,8 +54,7 @@ class GPSLoggerView(HomeAssistantView):
# password is set # password is set
self.requires_auth = self._password is None self.requires_auth = self._password is None
@asyncio.coroutine async def get(self, request: Request):
def get(self, request: Request):
"""Handle for GPSLogger message received as GET.""" """Handle for GPSLogger message received as GET."""
hass = request.app['hass'] hass = request.app['hass']
data = request.query data = request.query

View File

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

View File

@ -12,21 +12,27 @@ import homeassistant.helpers.config_validation as cv
from homeassistant.components.device_tracker import ( from homeassistant.components.device_tracker import (
DOMAIN, PLATFORM_SCHEMA, DeviceScanner) DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
from homeassistant.const import ( 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__) _LOGGER = logging.getLogger(__name__)
DEFAULT_HOST = 'routerlogin.net' CONF_APS = 'accesspoints'
DEFAULT_USER = 'admin'
DEFAULT_PORT = 5000
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string, vol.Optional(CONF_HOST, default=''): cv.string,
vol.Optional(CONF_USERNAME, default=DEFAULT_USER): cv.string, vol.Optional(CONF_SSL, default=False): cv.boolean,
vol.Optional(CONF_USERNAME, default=''): cv.string,
vol.Required(CONF_PASSWORD): 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.""" """Validate the configuration and returns a Netgear scanner."""
info = config[DOMAIN] info = config[DOMAIN]
host = info.get(CONF_HOST) host = info.get(CONF_HOST)
ssl = info.get(CONF_SSL)
username = info.get(CONF_USERNAME) username = info.get(CONF_USERNAME)
password = info.get(CONF_PASSWORD) password = info.get(CONF_PASSWORD)
port = info.get(CONF_PORT) 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 return scanner if scanner.success_init else None
@ -46,16 +57,21 @@ def get_scanner(hass, config):
class NetgearDeviceScanner(DeviceScanner): class NetgearDeviceScanner(DeviceScanner):
"""Queries a Netgear wireless router using the SOAP-API.""" """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.""" """Initialize the scanner."""
import pynetgear import pynetgear
self.tracked_devices = devices
self.excluded_devices = excluded_devices
self.tracked_accesspoints = accesspoints
self.last_results = [] self.last_results = []
self._api = pynetgear.Netgear(password, host, username, port) self._api = pynetgear.Netgear(password, host, username, port, ssl)
_LOGGER.info("Logging in") _LOGGER.info("Logging in")
results = self._api.get_attached_devices() results = self.get_attached_devices()
self.success_init = results is not None 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.""" """Scan for new devices and return a list with found device IDs."""
self._update_info() 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): def get_device_name(self, device):
"""Return the name of the given device or None if we don't know.""" """Return the name of the given device or the MAC if we don't know."""
try: parts = device.split("_")
return next(result.name for result in self.last_results mac = parts[0]
if result.mac == device) ap_mac = None
except StopIteration: if len(parts) > 1:
return None 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): def _update_info(self):
"""Retrieve latest information from the Netgear router. """Retrieve latest information from the Netgear router.
@ -88,9 +139,21 @@ class NetgearDeviceScanner(DeviceScanner):
_LOGGER.info("Scanning") _LOGGER.info("Scanning")
results = self._api.get_attached_devices() results = self.get_attached_devices()
if results is None: if results is None:
_LOGGER.warning("Error scanning devices") _LOGGER.warning("Error scanning devices")
self.last_results = results or [] 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: try:
res = requests.post(url, data=data, timeout=5) res = requests.post(url, data=data, timeout=5)
except requests.exceptions.Timeout: except (requests.exceptions.ConnectionError, requests.exceptions.Timeout):
return return
if res.status_code == 200: 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 For more details about this component, please refer to the documentation at
https://home-assistant.io/components/dialogflow/ https://home-assistant.io/components/dialogflow/
""" """
import asyncio
import logging import logging
import voluptuous as vol import voluptuous as vol
@ -37,8 +36,7 @@ class DialogFlowError(HomeAssistantError):
"""Raised when a DialogFlow error happens.""" """Raised when a DialogFlow error happens."""
@asyncio.coroutine async def async_setup(hass, config):
def async_setup(hass, config):
"""Set up Dialogflow component.""" """Set up Dialogflow component."""
hass.http.register_view(DialogflowIntentsView) hass.http.register_view(DialogflowIntentsView)
@ -51,16 +49,15 @@ class DialogflowIntentsView(HomeAssistantView):
url = INTENTS_API_ENDPOINT url = INTENTS_API_ENDPOINT
name = 'api:dialogflow' name = 'api:dialogflow'
@asyncio.coroutine async def post(self, request):
def post(self, request):
"""Handle Dialogflow.""" """Handle Dialogflow."""
hass = request.app['hass'] hass = request.app['hass']
message = yield from request.json() message = await request.json()
_LOGGER.debug("Received Dialogflow request: %s", message) _LOGGER.debug("Received Dialogflow request: %s", message)
try: 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) return b'' if response is None else self.json(response)
except DialogFlowError as err: except DialogFlowError as err:
@ -93,8 +90,7 @@ def dialogflow_error_response(hass, message, error):
return dialogflow_response.as_dict() return dialogflow_response.as_dict()
@asyncio.coroutine async def async_handle_message(hass, message):
def async_handle_message(hass, message):
"""Handle a DialogFlow message.""" """Handle a DialogFlow message."""
req = message.get('result') req = message.get('result')
action_incomplete = req['actionIncomplete'] action_incomplete = req['actionIncomplete']
@ -110,7 +106,7 @@ def async_handle_message(hass, message):
raise DialogFlowError( raise DialogFlowError(
"You have not defined an action in your Dialogflow intent.") "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, hass, DOMAIN, action,
{key: {'value': value} for key, value {key: {'value': value} for key, value
in parameters.items()}) 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 from homeassistant.helpers.discovery import async_load_platform, async_discover
import homeassistant.util.dt as dt_util import homeassistant.util.dt as dt_util
REQUIREMENTS = ['netdisco==1.3.1'] REQUIREMENTS = ['netdisco==1.4.1']
DOMAIN = 'discovery' DOMAIN = 'discovery'
@ -79,6 +79,7 @@ SERVICE_HANDLERS = {
'bluesound': ('media_player', 'bluesound'), 'bluesound': ('media_player', 'bluesound'),
'songpal': ('media_player', 'songpal'), 'songpal': ('media_player', 'songpal'),
'kodi': ('media_player', 'kodi'), 'kodi': ('media_player', 'kodi'),
'volumio': ('media_player', 'volumio'),
} }
OPTIONAL_SERVICE_HANDLERS = { 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.helpers.event import async_track_point_in_utc_time
from homeassistant.util.dt import utcnow from homeassistant.util.dt import utcnow
REQUIREMENTS = ['pyeight==0.0.7'] REQUIREMENTS = ['pyeight==0.0.8']
_LOGGER = logging.getLogger(__name__) _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 homeassistant.util.json import load_json, save_json
from .hue_api import ( from .hue_api import (
HueUsernameView, HueAllLightsStateView, HueOneLightStateView, HueUsernameView, HueAllLightsStateView, HueOneLightStateView,
HueOneLightChangeView) HueOneLightChangeView, HueGroupView)
from .upnp import DescriptionXmlView, UPNPResponderThread from .upnp import DescriptionXmlView, UPNPResponderThread
DOMAIN = 'emulated_hue' DOMAIN = 'emulated_hue'
@ -104,6 +104,7 @@ def setup(hass, yaml_config):
server.register_view(HueAllLightsStateView(config)) server.register_view(HueAllLightsStateView(config))
server.register_view(HueOneLightStateView(config)) server.register_view(HueOneLightStateView(config))
server.register_view(HueOneLightChangeView(config)) server.register_view(HueOneLightChangeView(config))
server.register_view(HueGroupView(config))
upnp_listener = UPNPResponderThread( upnp_listener = UPNPResponderThread(
config.host_ip_addr, config.listen_port, config.host_ip_addr, config.listen_port,

View File

@ -51,6 +51,29 @@ class HueUsernameView(HomeAssistantView):
return self.json([{'success': {'username': '12345678901234567890'}}]) 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): class HueAllLightsStateView(HomeAssistantView):
"""Handle requests for getting and setting info about entities.""" """Handle requests for getting and setting info about entities."""

View File

@ -31,7 +31,7 @@ _LOGGER = logging.getLogger(__name__)
@asyncio.coroutine @asyncio.coroutine
def async_setup_platform(hass, config, async_add_devices, discovery_info=None): def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
"""Set up the INSTEON PLM device class for the hass platform.""" """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'] address = discovery_info['address']
device = plm.devices[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 For more details about this platform, please refer to the documentation
https://home-assistant.io/components/fan.mqtt/ https://home-assistant.io/components/fan.mqtt/
""" """
import asyncio
import logging import logging
import voluptuous as vol import voluptuous as vol
@ -19,6 +18,7 @@ from homeassistant.components.mqtt import (
CONF_PAYLOAD_AVAILABLE, CONF_PAYLOAD_NOT_AVAILABLE, CONF_QOS, CONF_RETAIN, CONF_PAYLOAD_AVAILABLE, CONF_PAYLOAD_NOT_AVAILABLE, CONF_QOS, CONF_RETAIN,
MqttAvailability) MqttAvailability)
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.typing import HomeAssistantType, ConfigType
from homeassistant.components.fan import (SPEED_LOW, SPEED_MEDIUM, from homeassistant.components.fan import (SPEED_LOW, SPEED_MEDIUM,
SPEED_HIGH, FanEntity, SPEED_HIGH, FanEntity,
SUPPORT_SET_SPEED, SUPPORT_OSCILLATE, SUPPORT_SET_SPEED, SUPPORT_OSCILLATE,
@ -77,8 +77,8 @@ PLATFORM_SCHEMA = mqtt.MQTT_RW_PLATFORM_SCHEMA.extend({
}).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema) }).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema)
@asyncio.coroutine async def async_setup_platform(hass: HomeAssistantType, config: ConfigType,
def async_setup_platform(hass, config, async_add_devices, discovery_info=None): async_add_devices, discovery_info=None):
"""Set up the MQTT fan platform.""" """Set up the MQTT fan platform."""
if discovery_info is not None: if discovery_info is not None:
config = PLATFORM_SCHEMA(discovery_info) config = PLATFORM_SCHEMA(discovery_info)
@ -149,10 +149,9 @@ class MqttFan(MqttAvailability, FanEntity):
self._supported_features |= (topic[CONF_SPEED_STATE_TOPIC] self._supported_features |= (topic[CONF_SPEED_STATE_TOPIC]
is not None and SUPPORT_SET_SPEED) is not None and SUPPORT_SET_SPEED)
@asyncio.coroutine async def async_added_to_hass(self):
def async_added_to_hass(self):
"""Subscribe to MQTT events.""" """Subscribe to MQTT events."""
yield from super().async_added_to_hass() await super().async_added_to_hass()
templates = {} templates = {}
for key, tpl in list(self._templates.items()): for key, tpl in list(self._templates.items()):
@ -173,7 +172,7 @@ class MqttFan(MqttAvailability, FanEntity):
self.async_schedule_update_ha_state() self.async_schedule_update_ha_state()
if self._topic[CONF_STATE_TOPIC] is not None: 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.hass, self._topic[CONF_STATE_TOPIC], state_received,
self._qos) self._qos)
@ -190,7 +189,7 @@ class MqttFan(MqttAvailability, FanEntity):
self.async_schedule_update_ha_state() self.async_schedule_update_ha_state()
if self._topic[CONF_SPEED_STATE_TOPIC] is not None: 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.hass, self._topic[CONF_SPEED_STATE_TOPIC], speed_received,
self._qos) self._qos)
self._speed = SPEED_OFF self._speed = SPEED_OFF
@ -206,7 +205,7 @@ class MqttFan(MqttAvailability, FanEntity):
self.async_schedule_update_ha_state() self.async_schedule_update_ha_state()
if self._topic[CONF_OSCILLATION_STATE_TOPIC] is not None: 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], self.hass, self._topic[CONF_OSCILLATION_STATE_TOPIC],
oscillation_received, self._qos) oscillation_received, self._qos)
self._oscillation = False self._oscillation = False
@ -251,8 +250,7 @@ class MqttFan(MqttAvailability, FanEntity):
"""Return the oscillation state.""" """Return the oscillation state."""
return self._oscillation return self._oscillation
@asyncio.coroutine async def async_turn_on(self, speed: str = None, **kwargs) -> None:
def async_turn_on(self, speed: str = None, **kwargs) -> None:
"""Turn on the entity. """Turn on the entity.
This method is a coroutine. This method is a coroutine.
@ -261,10 +259,9 @@ class MqttFan(MqttAvailability, FanEntity):
self.hass, self._topic[CONF_COMMAND_TOPIC], self.hass, self._topic[CONF_COMMAND_TOPIC],
self._payload[STATE_ON], self._qos, self._retain) self._payload[STATE_ON], self._qos, self._retain)
if speed: if speed:
yield from self.async_set_speed(speed) await self.async_set_speed(speed)
@asyncio.coroutine async def async_turn_off(self, **kwargs) -> None:
def async_turn_off(self, **kwargs) -> None:
"""Turn off the entity. """Turn off the entity.
This method is a coroutine. This method is a coroutine.
@ -273,8 +270,7 @@ class MqttFan(MqttAvailability, FanEntity):
self.hass, self._topic[CONF_COMMAND_TOPIC], self.hass, self._topic[CONF_COMMAND_TOPIC],
self._payload[STATE_OFF], self._qos, self._retain) self._payload[STATE_OFF], self._qos, self._retain)
@asyncio.coroutine async def async_set_speed(self, speed: str) -> None:
def async_set_speed(self, speed: str) -> None:
"""Set the speed of the fan. """Set the speed of the fan.
This method is a coroutine. This method is a coroutine.
@ -299,8 +295,7 @@ class MqttFan(MqttAvailability, FanEntity):
self._speed = speed self._speed = speed
self.async_schedule_update_ha_state() self.async_schedule_update_ha_state()
@asyncio.coroutine async def async_oscillate(self, oscillating: bool) -> None:
def async_oscillate(self, oscillating: bool) -> None:
"""Set oscillation. """Set oscillation.
This method is a coroutine. 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 jinja2
import homeassistant.helpers.config_validation as cv 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.http.const import KEY_AUTHENTICATED
from homeassistant.components import websocket_api
from homeassistant.config import find_config_file, load_yaml_config_file from homeassistant.config import find_config_file, load_yaml_config_file
from homeassistant.const import CONF_NAME, EVENT_THEMES_UPDATED from homeassistant.const import CONF_NAME, EVENT_THEMES_UPDATED
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.helpers.translation import async_get_translations from homeassistant.helpers.translation import async_get_translations
from homeassistant.loader import bind_hass from homeassistant.loader import bind_hass
REQUIREMENTS = ['home-assistant-frontend==20180426.0'] REQUIREMENTS = ['home-assistant-frontend==20180509.0']
DOMAIN = 'frontend' DOMAIN = 'frontend'
DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log'] DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log']
@ -94,6 +95,10 @@ SERVICE_RELOAD_THEMES = 'reload_themes'
SERVICE_SET_THEME_SCHEMA = vol.Schema({ SERVICE_SET_THEME_SCHEMA = vol.Schema({
vol.Required(CONF_NAME): cv.string, 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: class AbstractPanel:
@ -291,6 +296,8 @@ def add_manifest_json_key(key, val):
@asyncio.coroutine @asyncio.coroutine
def async_setup(hass, config): def async_setup(hass, config):
"""Set up the serving of the frontend.""" """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) hass.http.register_view(ManifestJSONView)
conf = config.get(DOMAIN, {}) conf = config.get(DOMAIN, {})
@ -597,3 +604,19 @@ def _is_latest(js_option, request):
useragent = request.headers.get('User-Agent') useragent = request.headers.get('User-Agent')
return useragent and hass_frontend.version(useragent) 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) hass.services.call(DOMAIN, SERVICE_REQUEST_SYNC)
@asyncio.coroutine async def async_setup(hass: HomeAssistant, yaml_config: Dict[str, Any]):
def async_setup(hass: HomeAssistant, yaml_config: Dict[str, Any]):
"""Activate Google Actions component.""" """Activate Google Actions component."""
config = yaml_config.get(DOMAIN, {}) config = yaml_config.get(DOMAIN, {})
agent_user_id = config.get(CONF_AGENT_USER_ID) 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)) hass.http.register_view(GoogleAssistantAuthView(hass, config))
async_register_http(hass, config) async_register_http(hass, config)
@asyncio.coroutine async def request_sync_service_handler(call):
def request_sync_service_handler(call):
"""Handle request sync service calls.""" """Handle request sync service calls."""
websession = async_get_clientsession(hass) websession = async_get_clientsession(hass)
try: try:
with async_timeout.timeout(5, loop=hass.loop): with async_timeout.timeout(5, loop=hass.loop):
res = yield from websession.post( res = await websession.post(
REQUEST_SYNC_BASE_URL, REQUEST_SYNC_BASE_URL,
params={'key': api_key}, params={'key': api_key},
json={'agent_user_id': agent_user_id}) json={'agent_user_id': agent_user_id})
_LOGGER.info("Submitted request_sync request to Google") _LOGGER.info("Submitted request_sync request to Google")
res.raise_for_status() res.raise_for_status()
except aiohttp.ClientResponseError: except aiohttp.ClientResponseError:
body = yield from res.read() body = await res.read()
_LOGGER.error( _LOGGER.error(
'request_sync request failed: %d %s', res.status, body) 'request_sync request failed: %d %s', res.status, body)
except (asyncio.TimeoutError, aiohttp.ClientError): except (asyncio.TimeoutError, aiohttp.ClientError):

View File

@ -1,6 +1,5 @@
"""Google Assistant OAuth View.""" """Google Assistant OAuth View."""
import asyncio
import logging import logging
# Typing imports # Typing imports
@ -44,8 +43,7 @@ class GoogleAssistantAuthView(HomeAssistantView):
self.client_id = cfg.get(CONF_CLIENT_ID) self.client_id = cfg.get(CONF_CLIENT_ID)
self.access_token = cfg.get(CONF_ACCESS_TOKEN) self.access_token = cfg.get(CONF_ACCESS_TOKEN)
@asyncio.coroutine async def get(self, request: Request) -> Response:
def get(self, request: Request) -> Response:
"""Handle oauth token request.""" """Handle oauth token request."""
query = request.query query = request.query
redirect_uri = query.get('redirect_uri') 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 For more details about this component, please refer to the documentation at
https://home-assistant.io/components/google_assistant/ https://home-assistant.io/components/google_assistant/
""" """
import asyncio
import logging import logging
from aiohttp.hdrs import AUTHORIZATION from aiohttp.hdrs import AUTHORIZATION
@ -77,14 +76,13 @@ class GoogleAssistantView(HomeAssistantView):
self.access_token = access_token self.access_token = access_token
self.gass_config = gass_config self.gass_config = gass_config
@asyncio.coroutine async def post(self, request: Request) -> Response:
def post(self, request: Request) -> Response:
"""Handle Google Assistant requests.""" """Handle Google Assistant requests."""
auth = request.headers.get(AUTHORIZATION, None) auth = request.headers.get(AUTHORIZATION, None)
if 'Bearer {}'.format(self.access_token) != auth: if 'Bearer {}'.format(self.access_token) != auth:
return self.json_message("missing authorization", status_code=401) return self.json_message("missing authorization", status_code=401)
message = yield from request.json() # type: dict message = await request.json() # type: dict
result = yield from async_handle_message( result = await async_handle_message(
request.app['hass'], self.gass_config, message) request.app['hass'], self.gass_config, message)
return self.json(result) 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)] if ent_id.startswith(domain_filter)]
@asyncio.coroutine async def async_setup(hass, config):
def async_setup(hass, config):
"""Set up all groups found defined in the configuration.""" """Set up all groups found defined in the configuration."""
component = hass.data.get(DOMAIN) component = hass.data.get(DOMAIN)
if component is None: if component is None:
component = hass.data[DOMAIN] = EntityComponent(_LOGGER, DOMAIN, hass) 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 async def reload_service_handler(service):
def reload_service_handler(service):
"""Remove all user-defined groups and load new ones from config.""" """Remove all user-defined groups and load new ones from config."""
auto = list(filter(lambda e: not e.user_defined, component.entities)) 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: if conf is None:
return 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( hass.services.async_register(
DOMAIN, SERVICE_RELOAD, reload_service_handler, DOMAIN, SERVICE_RELOAD, reload_service_handler,
schema=RELOAD_SERVICE_SCHEMA) schema=RELOAD_SERVICE_SCHEMA)
@asyncio.coroutine async def groups_service_handler(service):
def groups_service_handler(service):
"""Handle dynamic group service functions.""" """Handle dynamic group service functions."""
object_id = service.data[ATTR_OBJECT_ID] object_id = service.data[ATTR_OBJECT_ID]
entity_id = ENTITY_ID_FORMAT.format(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 ATTR_VISIBLE, ATTR_ICON, ATTR_VIEW, ATTR_CONTROL
) if service.data.get(attr) is not None} ) 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), hass, service.data.get(ATTR_NAME, object_id),
object_id=object_id, object_id=object_id,
entity_ids=entity_ids, entity_ids=entity_ids,
@ -308,11 +305,11 @@ def async_setup(hass, config):
if ATTR_ADD_ENTITIES in service.data: if ATTR_ADD_ENTITIES in service.data:
delta = service.data[ATTR_ADD_ENTITIES] delta = service.data[ATTR_ADD_ENTITIES]
entity_ids = set(group.tracking) | set(delta) 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: if ATTR_ENTITIES in service.data:
entity_ids = service.data[ATTR_ENTITIES] 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: if ATTR_NAME in service.data:
group.name = service.data[ATTR_NAME] group.name = service.data[ATTR_NAME]
@ -335,13 +332,13 @@ def async_setup(hass, config):
need_update = True need_update = True
if need_update: if need_update:
yield from group.async_update_ha_state() await group.async_update_ha_state()
return return
# remove group # remove group
if service.service == SERVICE_REMOVE: if service.service == SERVICE_REMOVE:
yield from component.async_remove_entity(entity_id) await component.async_remove_entity(entity_id)
hass.services.async_register( hass.services.async_register(
DOMAIN, SERVICE_SET, groups_service_handler, DOMAIN, SERVICE_SET, groups_service_handler,
@ -351,8 +348,7 @@ def async_setup(hass, config):
DOMAIN, SERVICE_REMOVE, groups_service_handler, DOMAIN, SERVICE_REMOVE, groups_service_handler,
schema=REMOVE_SERVICE_SCHEMA) schema=REMOVE_SERVICE_SCHEMA)
@asyncio.coroutine async def visibility_service_handler(service):
def visibility_service_handler(service):
"""Change visibility of a group.""" """Change visibility of a group."""
visible = service.data.get(ATTR_VISIBLE) visible = service.data.get(ATTR_VISIBLE)
@ -363,7 +359,7 @@ def async_setup(hass, config):
tasks.append(group.async_update_ha_state()) tasks.append(group.async_update_ha_state())
if tasks: if tasks:
yield from asyncio.wait(tasks, loop=hass.loop) await asyncio.wait(tasks, loop=hass.loop)
hass.services.async_register( hass.services.async_register(
DOMAIN, SERVICE_SET_VISIBILITY, visibility_service_handler, DOMAIN, SERVICE_SET_VISIBILITY, visibility_service_handler,
@ -372,8 +368,7 @@ def async_setup(hass, config):
return True return True
@asyncio.coroutine async def _async_process_config(hass, config, component):
def _async_process_config(hass, config, component):
"""Process group configuration.""" """Process group configuration."""
for object_id, conf in config.get(DOMAIN, {}).items(): for object_id, conf in config.get(DOMAIN, {}).items():
name = conf.get(CONF_NAME, object_id) 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 # Don't create tasks and await them all. The order is important as
# groups get a number based on creation order. # 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, hass, name, entity_ids, icon=icon, view=view,
control=control, object_id=object_id) control=control, object_id=object_id)
@ -428,10 +423,9 @@ class Group(Entity):
hass.loop).result() hass.loop).result()
@staticmethod @staticmethod
@asyncio.coroutine async def async_create_group(hass, name, entity_ids=None,
def async_create_group(hass, name, entity_ids=None, user_defined=True, user_defined=True, visible=True, icon=None,
visible=True, icon=None, view=False, control=None, view=False, control=None, object_id=None):
object_id=None):
"""Initialize a group. """Initialize a group.
This method must be run in the event loop. This method must be run in the event loop.
@ -453,7 +447,7 @@ class Group(Entity):
component = hass.data[DOMAIN] = \ component = hass.data[DOMAIN] = \
EntityComponent(_LOGGER, DOMAIN, hass) EntityComponent(_LOGGER, DOMAIN, hass)
yield from component.async_add_entities([group], True) await component.async_add_entities([group], True)
return group return group
@ -520,17 +514,16 @@ class Group(Entity):
self.async_update_tracked_entity_ids(entity_ids), self.hass.loop self.async_update_tracked_entity_ids(entity_ids), self.hass.loop
).result() ).result()
@asyncio.coroutine async def async_update_tracked_entity_ids(self, entity_ids):
def async_update_tracked_entity_ids(self, entity_ids):
"""Update the member entity IDs. """Update the member entity IDs.
This method must be run in the event loop. 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.tracking = tuple(ent_id.lower() for ent_id in entity_ids)
self.group_on, self.group_off = None, None 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() self.async_start()
@callback @callback
@ -544,8 +537,7 @@ class Group(Entity):
self.hass, self.tracking, self._async_state_changed_listener self.hass, self.tracking, self._async_state_changed_listener
) )
@asyncio.coroutine async def async_stop(self):
def async_stop(self):
"""Unregister the group from Home Assistant. """Unregister the group from Home Assistant.
This method must be run in the event loop. 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()
self._async_unsub_state_changed = None self._async_unsub_state_changed = None
@asyncio.coroutine async def async_update(self):
def async_update(self):
"""Query all members and determine current group state.""" """Query all members and determine current group state."""
self._state = STATE_UNKNOWN self._state = STATE_UNKNOWN
self._async_update_group_state() self._async_update_group_state()
@asyncio.coroutine async def async_added_to_hass(self):
def async_added_to_hass(self):
"""Callback when added to HASS.""" """Callback when added to HASS."""
if self.tracking: if self.tracking:
self.async_start() self.async_start()
@asyncio.coroutine async def async_will_remove_from_hass(self):
def async_will_remove_from_hass(self):
"""Callback when removed from HASS.""" """Callback when removed from HASS."""
if self._async_unsub_state_changed: if self._async_unsub_state_changed:
self._async_unsub_state_changed() self._async_unsub_state_changed()
self._async_unsub_state_changed = None self._async_unsub_state_changed = None
@asyncio.coroutine async def _async_state_changed_listener(self, entity_id, old_state,
def _async_state_changed_listener(self, entity_id, old_state, new_state): new_state):
"""Respond to a member state changing. """Respond to a member state changing.
This method must be run in the event loop. This method must be run in the event loop.
@ -584,7 +573,7 @@ class Group(Entity):
return return
self._async_update_group_state(new_state) self._async_update_group_state(new_state)
yield from self.async_update_ha_state() await self.async_update_ha_state()
@property @property
def _tracking_states(self): 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 For more details about this component, please refer to the documentation at
https://home-assistant.io/components/history/ https://home-assistant.io/components/history/
""" """
import asyncio
from collections import defaultdict from collections import defaultdict
from datetime import timedelta from datetime import timedelta
from itertools import groupby 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 return states[0] if states else None
@asyncio.coroutine async def async_setup(hass, config):
def async_setup(hass, config):
"""Set up the history hooks.""" """Set up the history hooks."""
filters = Filters() filters = Filters()
conf = config.get(DOMAIN, {}) conf = config.get(DOMAIN, {})
@ -275,7 +273,7 @@ def async_setup(hass, config):
use_include_order = conf.get(CONF_ORDER) use_include_order = conf.get(CONF_ORDER)
hass.http.register_view(HistoryPeriodView(filters, use_include_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') 'history', 'history', 'mdi:poll-box')
return True return True
@ -293,8 +291,7 @@ class HistoryPeriodView(HomeAssistantView):
self.filters = filters self.filters = filters
self.use_include_order = use_include_order self.use_include_order = use_include_order
@asyncio.coroutine async def get(self, request, datetime=None):
def get(self, request, datetime=None):
"""Return history over a period of time.""" """Return history over a period of time."""
timer_start = time.perf_counter() timer_start = time.perf_counter()
if datetime: if datetime:
@ -330,7 +327,7 @@ class HistoryPeriodView(HomeAssistantView):
hass = request.app['hass'] 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, get_significant_states, hass, start_time, end_time,
entity_ids, self.filters, include_start_time_state) entity_ids, self.filters, include_start_time_state)
result = list(result.values()) result = list(result.values())
@ -353,8 +350,7 @@ class HistoryPeriodView(HomeAssistantView):
sorted_result.extend(result) sorted_result.extend(result)
result = sorted_result result = sorted_result
response = yield from hass.async_add_job(self.json, result) return await hass.async_add_job(self.json, result)
return response
class Filters(object): 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 For more details about this component, please refer to the documentation at
https://home-assistant.io/components/history_graph/ https://home-assistant.io/components/history_graph/
""" """
import asyncio
import logging import logging
import voluptuous as vol import voluptuous as vol
@ -39,8 +38,7 @@ CONFIG_SCHEMA = vol.Schema({
}, extra=vol.ALLOW_EXTRA) }, extra=vol.ALLOW_EXTRA)
@asyncio.coroutine async def async_setup(hass, config):
def async_setup(hass, config):
"""Load graph configurations.""" """Load graph configurations."""
component = EntityComponent( component = EntityComponent(
_LOGGER, DOMAIN, hass) _LOGGER, DOMAIN, hass)
@ -51,7 +49,7 @@ def async_setup(hass, config):
graph = HistoryGraphEntity(name, cfg) graph = HistoryGraphEntity(name, cfg)
graphs.append(graph) graphs.append(graph)
yield from component.async_add_entities(graphs) await component.async_add_entities(graphs)
return True return True

View File

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

View File

@ -4,18 +4,20 @@ from functools import wraps
from inspect import getmodule from inspect import getmodule
import logging import logging
from pyhap.accessory import Accessory, Bridge, Category from pyhap.accessory import Accessory, Bridge
from pyhap.accessory_driver import AccessoryDriver 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 callback as ha_callback
from homeassistant.core import split_entity_id
from homeassistant.helpers.event import ( from homeassistant.helpers.event import (
async_track_state_change, track_point_in_utc_time) async_track_state_change, track_point_in_utc_time)
from homeassistant.util import dt as dt_util from homeassistant.util import dt as dt_util
from .const import ( from .const import (
DEBOUNCE_TIMEOUT, BRIDGE_MODEL, BRIDGE_NAME, MANUFACTURER, DEBOUNCE_TIMEOUT, BRIDGE_MODEL, BRIDGE_NAME,
SERV_ACCESSORY_INFO, CHAR_MANUFACTURER, BRIDGE_SERIAL_NUMBER, MANUFACTURER)
CHAR_MODEL, CHAR_NAME, CHAR_SERIAL_NUMBER)
from .util import ( from .util import (
show_setup_message, dismiss_setup_message) show_setup_message, dismiss_setup_message)
@ -59,55 +61,20 @@ def debounce(func):
return wrapper 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): class HomeAccessory(Accessory):
"""Adapter class for 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.""" """Initialize a Accessory object."""
super().__init__(name, aid=aid) super().__init__(name, aid=aid)
set_accessory_info(self, name, model=entity_id) domain = split_entity_id(entity_id)[0].replace("_", " ").title()
self.category = getattr(Category, category, Category.OTHER) self.set_info_service(
firmware_revision=__version__, manufacturer=MANUFACTURER,
model=domain, serial_number=entity_id)
self.category = category
self.entity_id = entity_id self.entity_id = entity_id
self.hass = hass self.hass = hass
def _set_services(self):
add_preload_service(self, SERV_ACCESSORY_INFO)
def run(self): def run(self):
"""Method called by accessory after driver is started.""" """Method called by accessory after driver is started."""
state = self.hass.states.get(self.entity_id) state = self.hass.states.get(self.entity_id)
@ -137,12 +104,11 @@ class HomeBridge(Bridge):
def __init__(self, hass, name=BRIDGE_NAME): def __init__(self, hass, name=BRIDGE_NAME):
"""Initialize a Bridge object.""" """Initialize a Bridge object."""
super().__init__(name) 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 self.hass = hass
def _set_services(self):
add_preload_service(self, SERV_ACCESSORY_INFO)
def setup_message(self): def setup_message(self):
"""Prevent print of pyhap setup message to terminal.""" """Prevent print of pyhap setup message to terminal."""
pass pass

View File

@ -18,20 +18,10 @@ DEFAULT_PORT = 51827
SERVICE_HOMEKIT_START = 'start' SERVICE_HOMEKIT_START = 'start'
# #### STRING CONSTANTS #### # #### STRING CONSTANTS ####
BRIDGE_MODEL = 'homekit.bridge' BRIDGE_MODEL = 'Bridge'
BRIDGE_NAME = 'Home Assistant' BRIDGE_NAME = 'Home Assistant Bridge'
MANUFACTURER = 'HomeAssistant' BRIDGE_SERIAL_NUMBER = 'homekit.bridge'
MANUFACTURER = 'Home Assistant'
# #### 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'
# #### Services #### # #### Services ####
SERV_ACCESSORY_INFO = 'AccessoryInformation' SERV_ACCESSORY_INFO = 'AccessoryInformation'
@ -55,7 +45,6 @@ SERV_THERMOSTAT = 'Thermostat'
SERV_WINDOW_COVERING = 'WindowCovering' SERV_WINDOW_COVERING = 'WindowCovering'
# CurrentPosition, TargetPosition, PositionState # CurrentPosition, TargetPosition, PositionState
# #### Characteristics #### # #### Characteristics ####
CHAR_AIR_PARTICULATE_DENSITY = 'AirParticulateDensity' CHAR_AIR_PARTICULATE_DENSITY = 'AirParticulateDensity'
CHAR_AIR_QUALITY = 'AirQuality' CHAR_AIR_QUALITY = 'AirQuality'
@ -74,6 +63,7 @@ CHAR_CURRENT_POSITION = 'CurrentPosition' # Int | [0, 100]
CHAR_CURRENT_HUMIDITY = 'CurrentRelativeHumidity' # percent CHAR_CURRENT_HUMIDITY = 'CurrentRelativeHumidity' # percent
CHAR_CURRENT_SECURITY_STATE = 'SecuritySystemCurrentState' CHAR_CURRENT_SECURITY_STATE = 'SecuritySystemCurrentState'
CHAR_CURRENT_TEMPERATURE = 'CurrentTemperature' CHAR_CURRENT_TEMPERATURE = 'CurrentTemperature'
CHAR_FIRMWARE_REVISION = 'FirmwareRevision'
CHAR_HEATING_THRESHOLD_TEMPERATURE = 'HeatingThresholdTemperature' CHAR_HEATING_THRESHOLD_TEMPERATURE = 'HeatingThresholdTemperature'
CHAR_HUE = 'Hue' # arcdegress | [0, 360] CHAR_HUE = 'Hue' # arcdegress | [0, 360]
CHAR_LEAK_DETECTED = 'LeakDetected' CHAR_LEAK_DETECTED = 'LeakDetected'

View File

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

View File

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

View File

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

View File

@ -1,26 +1,31 @@
"""Class to hold all alarm control panel accessories.""" """Class to hold all alarm control panel accessories."""
import logging import logging
from pyhap.const import CATEGORY_ALARM_SYSTEM
from homeassistant.const import ( from homeassistant.const import (
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME,
STATE_ALARM_ARMED_NIGHT, STATE_ALARM_DISARMED, STATE_ALARM_ARMED_NIGHT, STATE_ALARM_DISARMED,
ATTR_ENTITY_ID, ATTR_CODE) STATE_ALARM_TRIGGERED, ATTR_ENTITY_ID, ATTR_CODE)
from . import TYPES from . import TYPES
from .accessories import HomeAccessory, add_preload_service, setup_char from .accessories import HomeAccessory
from .const import ( from .const import (
CATEGORY_ALARM_SYSTEM, SERV_SECURITY_SYSTEM, SERV_SECURITY_SYSTEM, CHAR_CURRENT_SECURITY_STATE,
CHAR_CURRENT_SECURITY_STATE, CHAR_TARGET_SECURITY_STATE) CHAR_TARGET_SECURITY_STATE)
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
HASS_TO_HOMEKIT = {STATE_ALARM_DISARMED: 3, STATE_ALARM_ARMED_HOME: 0, HASS_TO_HOMEKIT = {STATE_ALARM_ARMED_HOME: 0,
STATE_ALARM_ARMED_AWAY: 1, STATE_ALARM_ARMED_NIGHT: 2} 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()} HOMEKIT_TO_HASS = {c: s for s, c in HASS_TO_HOMEKIT.items()}
STATE_TO_SERVICE = {STATE_ALARM_DISARMED: 'alarm_disarm', STATE_TO_SERVICE = {STATE_ALARM_ARMED_HOME: 'alarm_arm_home',
STATE_ALARM_ARMED_HOME: 'alarm_arm_home',
STATE_ALARM_ARMED_AWAY: 'alarm_arm_away', 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') @TYPES.register('SecuritySystem')
@ -33,12 +38,12 @@ class SecuritySystem(HomeAccessory):
self._alarm_code = config.get(ATTR_CODE) self._alarm_code = config.get(ATTR_CODE)
self.flag_target_state = False self.flag_target_state = False
serv_alarm = add_preload_service(self, SERV_SECURITY_SYSTEM) serv_alarm = self.add_preload_service(SERV_SECURITY_SYSTEM)
self.char_current_state = setup_char( self.char_current_state = serv_alarm.configure_char(
CHAR_CURRENT_SECURITY_STATE, serv_alarm, value=3) CHAR_CURRENT_SECURITY_STATE, value=3)
self.char_target_state = setup_char( self.char_target_state = serv_alarm.configure_char(
CHAR_TARGET_SECURITY_STATE, serv_alarm, value=3, CHAR_TARGET_SECURITY_STATE, value=3,
callback=self.set_security_state) setter_callback=self.set_security_state)
def set_security_state(self, value): def set_security_state(self, value):
"""Move security state to value if call came from HomeKit.""" """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)', _LOGGER.debug('%s: Updated current state to %s (%d)',
self.entity_id, hass_state, current_security_state) 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) 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.""" """Class to hold all sensor accessories."""
import logging import logging
from pyhap.const import CATEGORY_SENSOR
from homeassistant.const import ( from homeassistant.const import (
ATTR_UNIT_OF_MEASUREMENT, TEMP_CELSIUS, ATTR_UNIT_OF_MEASUREMENT, TEMP_CELSIUS,
ATTR_DEVICE_CLASS, STATE_ON, STATE_HOME) ATTR_DEVICE_CLASS, STATE_ON, STATE_HOME)
from . import TYPES from . import TYPES
from .accessories import HomeAccessory, add_preload_service, setup_char from .accessories import HomeAccessory
from .const import ( 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, CHAR_CURRENT_HUMIDITY, CHAR_CURRENT_TEMPERATURE, PROP_CELSIUS,
SERV_AIR_QUALITY_SENSOR, CHAR_AIR_QUALITY, CHAR_AIR_PARTICULATE_DENSITY, SERV_AIR_QUALITY_SENSOR, CHAR_AIR_QUALITY, CHAR_AIR_PARTICULATE_DENSITY,
CHAR_CARBON_DIOXIDE_LEVEL, CHAR_CARBON_DIOXIDE_PEAK_LEVEL, CHAR_CARBON_DIOXIDE_LEVEL, CHAR_CARBON_DIOXIDE_PEAK_LEVEL,
@ -52,10 +54,9 @@ class TemperatureSensor(HomeAccessory):
def __init__(self, *args, config): def __init__(self, *args, config):
"""Initialize a TemperatureSensor accessory object.""" """Initialize a TemperatureSensor accessory object."""
super().__init__(*args, category=CATEGORY_SENSOR) super().__init__(*args, category=CATEGORY_SENSOR)
serv_temp = add_preload_service(self, SERV_TEMPERATURE_SENSOR) serv_temp = self.add_preload_service(SERV_TEMPERATURE_SENSOR)
self.char_temp = setup_char( self.char_temp = serv_temp.configure_char(
CHAR_CURRENT_TEMPERATURE, serv_temp, value=0, CHAR_CURRENT_TEMPERATURE, value=0, properties=PROP_CELSIUS)
properties=PROP_CELSIUS)
self.unit = None self.unit = None
def update_state(self, new_state): def update_state(self, new_state):
@ -76,9 +77,9 @@ class HumiditySensor(HomeAccessory):
def __init__(self, *args, config): def __init__(self, *args, config):
"""Initialize a HumiditySensor accessory object.""" """Initialize a HumiditySensor accessory object."""
super().__init__(*args, category=CATEGORY_SENSOR) super().__init__(*args, category=CATEGORY_SENSOR)
serv_humidity = add_preload_service(self, SERV_HUMIDITY_SENSOR) serv_humidity = self.add_preload_service(SERV_HUMIDITY_SENSOR)
self.char_humidity = setup_char( self.char_humidity = serv_humidity.configure_char(
CHAR_CURRENT_HUMIDITY, serv_humidity, value=0) CHAR_CURRENT_HUMIDITY, value=0)
def update_state(self, new_state): def update_state(self, new_state):
"""Update accessory after state change.""" """Update accessory after state change."""
@ -97,12 +98,12 @@ class AirQualitySensor(HomeAccessory):
"""Initialize a AirQualitySensor accessory object.""" """Initialize a AirQualitySensor accessory object."""
super().__init__(*args, category=CATEGORY_SENSOR) super().__init__(*args, category=CATEGORY_SENSOR)
serv_air_quality = add_preload_service(self, SERV_AIR_QUALITY_SENSOR, serv_air_quality = self.add_preload_service(
[CHAR_AIR_PARTICULATE_DENSITY]) SERV_AIR_QUALITY_SENSOR, [CHAR_AIR_PARTICULATE_DENSITY])
self.char_quality = setup_char( self.char_quality = serv_air_quality.configure_char(
CHAR_AIR_QUALITY, serv_air_quality, value=0) CHAR_AIR_QUALITY, value=0)
self.char_density = setup_char( self.char_density = serv_air_quality.configure_char(
CHAR_AIR_PARTICULATE_DENSITY, serv_air_quality, value=0) CHAR_AIR_PARTICULATE_DENSITY, value=0)
def update_state(self, new_state): def update_state(self, new_state):
"""Update accessory after state change.""" """Update accessory after state change."""
@ -121,14 +122,14 @@ class CarbonDioxideSensor(HomeAccessory):
"""Initialize a CarbonDioxideSensor accessory object.""" """Initialize a CarbonDioxideSensor accessory object."""
super().__init__(*args, category=CATEGORY_SENSOR) 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]) CHAR_CARBON_DIOXIDE_LEVEL, CHAR_CARBON_DIOXIDE_PEAK_LEVEL])
self.char_co2 = setup_char( self.char_co2 = serv_co2.configure_char(
CHAR_CARBON_DIOXIDE_LEVEL, serv_co2, value=0) CHAR_CARBON_DIOXIDE_LEVEL, value=0)
self.char_peak = setup_char( self.char_peak = serv_co2.configure_char(
CHAR_CARBON_DIOXIDE_PEAK_LEVEL, serv_co2, value=0) CHAR_CARBON_DIOXIDE_PEAK_LEVEL, value=0)
self.char_detected = setup_char( self.char_detected = serv_co2.configure_char(
CHAR_CARBON_DIOXIDE_DETECTED, serv_co2, value=0) CHAR_CARBON_DIOXIDE_DETECTED, value=0)
def update_state(self, new_state): def update_state(self, new_state):
"""Update accessory after state change.""" """Update accessory after state change."""
@ -149,9 +150,9 @@ class LightSensor(HomeAccessory):
"""Initialize a LightSensor accessory object.""" """Initialize a LightSensor accessory object."""
super().__init__(*args, category=CATEGORY_SENSOR) super().__init__(*args, category=CATEGORY_SENSOR)
serv_light = add_preload_service(self, SERV_LIGHT_SENSOR) serv_light = self.add_preload_service(SERV_LIGHT_SENSOR)
self.char_light = setup_char( self.char_light = serv_light.configure_char(
CHAR_CURRENT_AMBIENT_LIGHT_LEVEL, serv_light, value=0) CHAR_CURRENT_AMBIENT_LIGHT_LEVEL, value=0)
def update_state(self, new_state): def update_state(self, new_state):
"""Update accessory after state change.""" """Update accessory after state change."""
@ -174,8 +175,8 @@ class BinarySensor(HomeAccessory):
if device_class in BINARY_SENSOR_SERVICE_MAP \ if device_class in BINARY_SENSOR_SERVICE_MAP \
else BINARY_SENSOR_SERVICE_MAP[DEVICE_CLASS_OCCUPANCY] else BINARY_SENSOR_SERVICE_MAP[DEVICE_CLASS_OCCUPANCY]
service = add_preload_service(self, service_char[0]) service = self.add_preload_service(service_char[0])
self.char_detected = setup_char(service_char[1], service, value=0) self.char_detected = service.configure_char(service_char[1], value=0)
def update_state(self, new_state): def update_state(self, new_state):
"""Update accessory after state change.""" """Update accessory after state change."""

View File

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

View File

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

View File

@ -20,7 +20,7 @@ from homeassistant.helpers.entity import Entity
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.loader import bind_hass from homeassistant.loader import bind_hass
REQUIREMENTS = ['pyhomematic==0.1.41'] REQUIREMENTS = ['pyhomematic==0.1.42']
DOMAIN = 'homematic' DOMAIN = 'homematic'
_LOGGER = logging.getLogger(__name__) _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/ https://home-assistant.io/components/homematicip_cloud/
""" """
import asyncio
import logging import logging
from socket import timeout
import voluptuous as vol import voluptuous as vol
from homeassistant.core import callback
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.dispatcher import (dispatcher_send, from homeassistant.const import EVENT_HOMEASSISTANT_STOP
async_dispatcher_connect) from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.discovery import load_platform from homeassistant.helpers.discovery import async_load_platform
from homeassistant.helpers.entity import Entity 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__) _LOGGER = logging.getLogger(__name__)
DOMAIN = 'homematicip_cloud' DOMAIN = 'homematicip_cloud'
COMPONENTS = [
'sensor'
]
CONF_NAME = 'name' CONF_NAME = 'name'
CONF_ACCESSPOINT = 'accesspoint' CONF_ACCESSPOINT = 'accesspoint'
CONF_AUTHTOKEN = 'authtoken' CONF_AUTHTOKEN = 'authtoken'
CONFIG_SCHEMA = vol.Schema({ CONFIG_SCHEMA = vol.Schema({
vol.Optional(DOMAIN): [vol.Schema({ vol.Optional(DOMAIN, default=[]): vol.All(cv.ensure_list, [vol.Schema({
vol.Optional(CONF_NAME, default=''): cv.string, vol.Optional(CONF_NAME): vol.Any(cv.string),
vol.Required(CONF_ACCESSPOINT): cv.string, vol.Required(CONF_ACCESSPOINT): cv.string,
vol.Required(CONF_AUTHTOKEN): cv.string, vol.Required(CONF_AUTHTOKEN): cv.string,
})], })]),
}, extra=vol.ALLOW_EXTRA) }, extra=vol.ALLOW_EXTRA)
EVENT_HOME_CHANGED = 'homematicip_home_changed' HMIP_ACCESS_POINT = 'Access Point'
EVENT_DEVICE_CHANGED = 'homematicip_device_changed' HMIP_HUB = 'HmIP-HUB'
EVENT_GROUP_CHANGED = 'homematicip_group_changed'
EVENT_SECURITY_CHANGED = 'homematicip_security_changed'
EVENT_JOURNAL_CHANGED = 'homematicip_journal_changed'
ATTR_HOME_ID = 'home_id' ATTR_HOME_ID = 'home_id'
ATTR_HOME_LABEL = 'home_label' ATTR_HOME_NAME = 'home_name'
ATTR_DEVICE_ID = 'device_id' ATTR_DEVICE_ID = 'device_id'
ATTR_DEVICE_LABEL = 'device_label' ATTR_DEVICE_LABEL = 'device_label'
ATTR_STATUS_UPDATE = 'status_update' ATTR_STATUS_UPDATE = 'status_update'
ATTR_FIRMWARE_STATE = 'firmware_state' ATTR_FIRMWARE_STATE = 'firmware_state'
ATTR_UNREACHABLE = 'unreachable'
ATTR_LOW_BATTERY = 'low_battery' 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_SABOTAGE = 'sabotage'
ATTR_RSSI = 'rssi' ATTR_OPERATION_LOCK = 'operation_lock'
ATTR_TYPE = 'type'
def setup(hass, config): async def async_setup(hass, config):
"""Set up the HomematicIP component.""" """Set up the HomematicIP component."""
# pylint: disable=import-error, no-name-in-module from homematicip.base.base_connection import HmipConnectionError
from homematicip.home import Home
hass.data.setdefault(DOMAIN, {}) hass.data.setdefault(DOMAIN, {})
homes = hass.data[DOMAIN]
accesspoints = config.get(DOMAIN, []) accesspoints = config.get(DOMAIN, [])
for conf in accesspoints:
def _update_event(events): _websession = async_get_clientsession(hass)
"""Handle incoming HomeMaticIP events.""" _hmip = HomematicipConnector(hass, conf, _websession)
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
try: try:
home.set_auth_token(authtoken) await _hmip.init()
home.init(accesspoint) except HmipConnectionError:
if home.get_current_state(): _LOGGER.error('Failed to connect to the HomematicIP server, %s.',
_LOGGER.info("Connection to HMIP established") conf.get(CONF_ACCESSPOINT))
else:
_LOGGER.warning("Connection to HMIP could not be established")
return False
except timeout:
_LOGGER.warning("Connection to HMIP could not be established")
return False 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']: home = _hmip.home
load_platform(hass, component, DOMAIN, {'homeid': home.id}, config) 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 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): class HomematicipGenericDevice(Entity):
"""Representation of an HomematicIP generic device.""" """Representation of an HomematicIP generic device."""
def __init__(self, home, device): def __init__(self, home, device, post=None):
"""Initialize the generic device.""" """Initialize the generic device."""
self._home = home self._home = home
self._device = device self._device = device
self.post = post
_LOGGER.info('Setting up %s (%s)', self.name,
self._device.modelType)
async def async_added_to_hass(self): async def async_added_to_hass(self):
"""Register callbacks.""" """Register callbacks."""
async_dispatcher_connect( self._device.on_update(self._device_changed)
self.hass, EVENT_DEVICE_CHANGED, self._device_changed)
@callback def _device_changed(self, json, **kwargs):
def _device_changed(self, deviceid):
"""Handle device state changes.""" """Handle device state changes."""
if deviceid is None or deviceid == self._device.id: _LOGGER.debug('Event %s (%s)', self.name, self._device.modelType)
_LOGGER.debug('Event device %s', self._device.label) self.async_schedule_update_ha_state()
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
@property @property
def name(self): def name(self):
"""Return the name of the generic device.""" """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 @property
def should_poll(self): def should_poll(self):
@ -153,24 +249,10 @@ class HomematicipGenericDevice(Entity):
"""Device available.""" """Device available."""
return not self._device.unreach 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 @property
def device_state_attributes(self): def device_state_attributes(self):
"""Return the state attributes of the generic device.""" """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 if (HTTP_HEADER_HA_AUTH in request.headers and
hmac.compare_digest( 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 # A valid auth header has been set
authenticated = True authenticated = True
elif (DATA_API_PASSWORD in request.query and elif (DATA_API_PASSWORD in request.query and
hmac.compare_digest(api_password, hmac.compare_digest(
request.query[DATA_API_PASSWORD])): api_password.encode('utf-8'),
request.query[DATA_API_PASSWORD].encode('utf-8'))):
authenticated = True authenticated = True
elif (hdrs.AUTHORIZATION in request.headers and elif (hdrs.AUTHORIZATION in request.headers and
validate_authorization_header(api_password, request)): await async_validate_auth_header(api_password, request)):
authenticated = True authenticated = True
elif _is_trusted_ip(request, trusted_networks): elif _is_trusted_ip(request, trusted_networks):
@ -70,23 +72,38 @@ def _is_trusted_ip(request, trusted_networks):
def validate_password(request, api_password): def validate_password(request, api_password):
"""Test if password is valid.""" """Test if password is valid."""
return hmac.compare_digest( 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.""" """Test an authorization header if valid password."""
if hdrs.AUTHORIZATION not in request.headers: if hdrs.AUTHORIZATION not in request.headers:
return False 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 return False
decoded = base64.b64decode(auth).decode('utf-8') hass = request.app['hass']
username, password = decoded.split(':', 1) access_token = hass.auth.async_get_access_token(auth_val)
if access_token is None:
if username != 'homeassistant':
return False 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": { "config": {
"abort": { "abort": {
"all_configured": "Alle Philips Hue Bridges sind bereits konfiguriert", "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", "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": { "error": {
"linking": "Unbekannter Link-Fehler aufgetreten.", "linking": "Unbekannter Link-Fehler aufgetreten.",

View File

@ -2,8 +2,11 @@
"config": { "config": {
"abort": { "abort": {
"all_configured": "All Philips Hue bridges are already configured", "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", "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": { "error": {
"linking": "Unknown linking error occurred.", "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": { "config": {
"abort": { "abort": {
"all_configured": "\ubaa8\ub4e0 \ud544\ub9bd\uc2a4 Hue \ube0c\ub9bf\uc9c0\uac00 \uc774\ubbf8 \uc124\uc815\ub41c \uc0c1\ud0dc\uc785\ub2c8\ub2e4", "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", "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": { "error": {
"linking": "\uc54c \uc218\uc5c6\ub294 \uc5f0\uacb0 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4.", "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": { "config": {
"abort": { "abort": {
"all_configured": "Alle Philips Hue bridges zijn al geconfigureerd", "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", "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": { "error": {
"linking": "Er is een onbekende verbindingsfout opgetreden.", "linking": "Er is een onbekende verbindingsfout opgetreden.",
@ -17,7 +20,7 @@
"title": "Kies Hue bridge" "title": "Kies Hue bridge"
}, },
"link": { "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" "title": "Link Hub"
} }
}, },

View File

@ -2,8 +2,11 @@
"config": { "config": {
"abort": { "abort": {
"all_configured": "Alle Philips Hue Bridger er allerede konfigurert", "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", "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": { "error": {
"linking": "Ukjent koblingsfeil oppstod.", "linking": "Ukjent koblingsfeil oppstod.",

View File

@ -2,8 +2,11 @@
"config": { "config": {
"abort": { "abort": {
"all_configured": "Wszystkie mostki Hue s\u0105 ju\u017c skonfigurowane", "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", "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": { "error": {
"linking": "Wyst\u0105pi\u0142 nieznany b\u0142\u0105d w trakcie \u0142\u0105czenia.", "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