Merge pull request #15330 from home-assistant/rc

0.73
This commit is contained in:
Paulus Schoutsen 2018-07-06 16:41:09 -04:00 committed by GitHub
commit a1d8b0e9b3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
305 changed files with 4859 additions and 2124 deletions

View File

@ -192,7 +192,7 @@ omit =
homeassistant/components/mychevy.py homeassistant/components/mychevy.py
homeassistant/components/*/mychevy.py homeassistant/components/*/mychevy.py
homeassistant/components/mysensors.py homeassistant/components/mysensors/*
homeassistant/components/*/mysensors.py homeassistant/components/*/mysensors.py
homeassistant/components/neato.py homeassistant/components/neato.py

3
.gitignore vendored
View File

@ -107,3 +107,6 @@ desktop.ini
# Secrets # Secrets
.lokalise_token .lokalise_token
# monkeytype
monkeytype.sqlite3

View File

@ -1,26 +1,27 @@
"""Provide an authentication layer for Home Assistant.""" """Provide an authentication layer for Home Assistant."""
import asyncio import asyncio
import binascii import binascii
from collections import OrderedDict
from datetime import datetime, timedelta
import os
import importlib import importlib
import logging import logging
import os
import uuid import uuid
from collections import OrderedDict
from datetime import datetime, timedelta
import attr import attr
import voluptuous as vol import voluptuous as vol
from voluptuous.humanize import humanize_error from voluptuous.humanize import humanize_error
from homeassistant import data_entry_flow, requirements from homeassistant import data_entry_flow, requirements
from homeassistant.core import callback
from homeassistant.const import CONF_TYPE, CONF_NAME, CONF_ID from homeassistant.const import CONF_TYPE, CONF_NAME, CONF_ID
from homeassistant.util.decorator import Registry from homeassistant.core import callback
from homeassistant.util import dt as dt_util from homeassistant.util import dt as dt_util
from homeassistant.util.decorator import Registry
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
STORAGE_VERSION = 1
STORAGE_KEY = 'auth'
AUTH_PROVIDERS = Registry() AUTH_PROVIDERS = Registry()
@ -121,23 +122,12 @@ class User:
is_owner = attr.ib(type=bool, default=False) is_owner = attr.ib(type=bool, default=False)
is_active = attr.ib(type=bool, default=False) is_active = attr.ib(type=bool, default=False)
name = attr.ib(type=str, default=None) 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. # List of credentials of a user.
credentials = attr.ib(type=list, default=attr.Factory(list)) credentials = attr.ib(type=list, default=attr.Factory(list), cmp=False)
# Tokens associated with a user. # Tokens associated with a user.
refresh_tokens = attr.ib(type=dict, default=attr.Factory(dict)) refresh_tokens = attr.ib(type=dict, default=attr.Factory(dict), cmp=False)
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) @attr.s(slots=True)
@ -152,7 +142,7 @@ class RefreshToken:
default=ACCESS_TOKEN_EXPIRATION) default=ACCESS_TOKEN_EXPIRATION)
token = attr.ib(type=str, token = attr.ib(type=str,
default=attr.Factory(lambda: generate_secret(64))) default=attr.Factory(lambda: generate_secret(64)))
access_tokens = attr.ib(type=list, default=attr.Factory(list)) access_tokens = attr.ib(type=list, default=attr.Factory(list), cmp=False)
@attr.s(slots=True) @attr.s(slots=True)
@ -168,9 +158,10 @@ class AccessToken:
default=attr.Factory(generate_secret)) default=attr.Factory(generate_secret))
@property @property
def expires(self): def expired(self):
"""Return datetime when this token expires.""" """Return if this token has expired."""
return self.created_at + self.refresh_token.access_token_expiration expires = self.created_at + self.refresh_token.access_token_expiration
return dt_util.utcnow() > expires
@attr.s(slots=True) @attr.s(slots=True)
@ -281,7 +272,24 @@ class AuthManager:
self.login_flow = data_entry_flow.FlowManager( self.login_flow = data_entry_flow.FlowManager(
hass, self._async_create_login_flow, hass, self._async_create_login_flow,
self._async_finish_login_flow) self._async_finish_login_flow)
self.access_tokens = {} self._access_tokens = {}
@property
def active(self):
"""Return if any auth providers are registered."""
return bool(self._providers)
@property
def support_legacy(self):
"""
Return if legacy_api_password auth providers are registered.
Should be removed when we removed legacy_api_password auth providers.
"""
for provider_type, _ in self._providers:
if provider_type == 'legacy_api_password':
return True
return False
@property @property
def async_auth_providers(self): def async_auth_providers(self):
@ -317,13 +325,22 @@ class AuthManager:
def async_create_access_token(self, refresh_token): def async_create_access_token(self, refresh_token):
"""Create a new access token.""" """Create a new access token."""
access_token = AccessToken(refresh_token) access_token = AccessToken(refresh_token)
self.access_tokens[access_token.token] = access_token self._access_tokens[access_token.token] = access_token
return access_token return access_token
@callback @callback
def async_get_access_token(self, token): def async_get_access_token(self, token):
"""Get an access token.""" """Get an access token."""
return self.access_tokens.get(token) tkn = self._access_tokens.get(token)
if tkn is None:
return None
if tkn.expired:
self._access_tokens.pop(token)
return None
return tkn
async def async_create_client(self, name, *, redirect_uris=None, async def async_create_client(self, name, *, redirect_uris=None,
no_secret=False): no_secret=False):
@ -331,6 +348,16 @@ class AuthManager:
return await self._store.async_create_client( return await self._store.async_create_client(
name, redirect_uris, no_secret) name, redirect_uris, no_secret)
async def async_get_or_create_client(self, name, *, redirect_uris=None,
no_secret=False):
"""Find a client, if not exists, create a new one."""
for client in await self._store.async_get_clients():
if client.name == name:
return client
return await self._store.async_create_client(
name, redirect_uris, no_secret)
async def async_get_client(self, client_id): async def async_get_client(self, client_id):
"""Get a client.""" """Get a client."""
return await self._store.async_get_client(client_id) return await self._store.async_get_client(client_id)
@ -374,29 +401,36 @@ class AuthStore:
def __init__(self, hass): def __init__(self, hass):
"""Initialize the auth store.""" """Initialize the auth store."""
self.hass = hass self.hass = hass
self.users = None self._users = None
self.clients = None self._clients = None
self._load_lock = asyncio.Lock(loop=hass.loop) self._store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY)
async def credentials_for_provider(self, provider_type, provider_id): async def credentials_for_provider(self, provider_type, provider_id):
"""Return credentials for specific auth provider type and id.""" """Return credentials for specific auth provider type and id."""
if self.users is None: if self._users is None:
await self.async_load() await self.async_load()
return [ return [
credentials credentials
for user in self.users.values() for user in self._users.values()
for credentials in user.credentials for credentials in user.credentials
if (credentials.auth_provider_type == provider_type and if (credentials.auth_provider_type == provider_type and
credentials.auth_provider_id == provider_id) credentials.auth_provider_id == provider_id)
] ]
async def async_get_user(self, user_id): async def async_get_users(self):
"""Retrieve a user.""" """Retrieve all users."""
if self.users is None: if self._users is None:
await self.async_load() await self.async_load()
return self.users.get(user_id) return list(self._users.values())
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): async def async_get_or_create_user(self, credentials, auth_provider):
"""Get or create a new user for given credentials. """Get or create a new user for given credentials.
@ -404,7 +438,7 @@ class AuthStore:
If link_user is passed in, the credentials will be linked to the passed If link_user is passed in, the credentials will be linked to the passed
in user if the credentials are new. in user if the credentials are new.
""" """
if self.users is None: if self._users is None:
await self.async_load() await self.async_load()
# New credentials, store in user # New credentials, store in user
@ -412,7 +446,7 @@ class AuthStore:
info = await auth_provider.async_user_meta_for_credentials( info = await auth_provider.async_user_meta_for_credentials(
credentials) credentials)
# Make owner and activate user if it's the first user. # Make owner and activate user if it's the first user.
if self.users: if self._users:
is_owner = False is_owner = False
is_active = False is_active = False
else: else:
@ -424,11 +458,11 @@ class AuthStore:
is_active=is_active, is_active=is_active,
name=info.get('name'), name=info.get('name'),
) )
self.users[new_user.id] = new_user self._users[new_user.id] = new_user
await self.async_link_user(new_user, credentials) await self.async_link_user(new_user, credentials)
return new_user return new_user
for user in self.users.values(): for user in self._users.values():
for creds in user.credentials: for creds in user.credentials:
if (creds.auth_provider_type == credentials.auth_provider_type if (creds.auth_provider_type == credentials.auth_provider_type
and creds.auth_provider_id == and creds.auth_provider_id ==
@ -445,11 +479,19 @@ class AuthStore:
async def async_remove_user(self, user): async def async_remove_user(self, user):
"""Remove a user.""" """Remove a user."""
self.users.pop(user.id) self._users.pop(user.id)
await self.async_save() await self.async_save()
async def async_create_refresh_token(self, user, client_id): async def async_create_refresh_token(self, user, client_id):
"""Create a new token for a user.""" """Create a new token for a user."""
local_user = await self.async_get_user(user.id)
if local_user is None:
raise ValueError('Invalid user')
local_client = await self.async_get_client(client_id)
if local_client is None:
raise ValueError('Invalid client_id')
refresh_token = RefreshToken(user, client_id) refresh_token = RefreshToken(user, client_id)
user.refresh_tokens[refresh_token.token] = refresh_token user.refresh_tokens[refresh_token.token] = refresh_token
await self.async_save() await self.async_save()
@ -457,10 +499,10 @@ class AuthStore:
async def async_get_refresh_token(self, token): async def async_get_refresh_token(self, token):
"""Get refresh token by token.""" """Get refresh token by token."""
if self.users is None: if self._users is None:
await self.async_load() await self.async_load()
for user in self.users.values(): for user in self._users.values():
refresh_token = user.refresh_tokens.get(token) refresh_token = user.refresh_tokens.get(token)
if refresh_token is not None: if refresh_token is not None:
return refresh_token return refresh_token
@ -469,7 +511,7 @@ class AuthStore:
async def async_create_client(self, name, redirect_uris, no_secret): async def async_create_client(self, name, redirect_uris, no_secret):
"""Create a new client.""" """Create a new client."""
if self.clients is None: if self._clients is None:
await self.async_load() await self.async_load()
kwargs = { kwargs = {
@ -481,23 +523,148 @@ class AuthStore:
kwargs['secret'] = None kwargs['secret'] = None
client = Client(**kwargs) client = Client(**kwargs)
self.clients[client.id] = client self._clients[client.id] = client
await self.async_save() await self.async_save()
return client return client
async def async_get_client(self, client_id): async def async_get_clients(self):
"""Get a client.""" """Return all clients."""
if self.clients is None: if self._clients is None:
await self.async_load() await self.async_load()
return self.clients.get(client_id) return list(self._clients.values())
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): async def async_load(self):
"""Load the users.""" """Load the users."""
async with self._load_lock: data = await self._store.async_load()
self.users = {}
self.clients = {} # Make sure that we're not overriding data if 2 loads happened at the
# same time
if self._users is not None:
return
if data is None:
self._users = {}
self._clients = {}
return
users = {
user_dict['id']: User(**user_dict) for user_dict in data['users']
}
for cred_dict in data['credentials']:
users[cred_dict['user_id']].credentials.append(Credentials(
id=cred_dict['id'],
is_new=False,
auth_provider_type=cred_dict['auth_provider_type'],
auth_provider_id=cred_dict['auth_provider_id'],
data=cred_dict['data'],
))
refresh_tokens = {}
for rt_dict in data['refresh_tokens']:
token = RefreshToken(
id=rt_dict['id'],
user=users[rt_dict['user_id']],
client_id=rt_dict['client_id'],
created_at=dt_util.parse_datetime(rt_dict['created_at']),
access_token_expiration=timedelta(
seconds=rt_dict['access_token_expiration']),
token=rt_dict['token'],
)
refresh_tokens[token.id] = token
users[rt_dict['user_id']].refresh_tokens[token.token] = token
for ac_dict in data['access_tokens']:
refresh_token = refresh_tokens[ac_dict['refresh_token_id']]
token = AccessToken(
refresh_token=refresh_token,
created_at=dt_util.parse_datetime(ac_dict['created_at']),
token=ac_dict['token'],
)
refresh_token.access_tokens.append(token)
clients = {
cl_dict['id']: Client(**cl_dict) for cl_dict in data['clients']
}
self._users = users
self._clients = clients
async def async_save(self): async def async_save(self):
"""Save users.""" """Save users."""
pass users = [
{
'id': user.id,
'is_owner': user.is_owner,
'is_active': user.is_active,
'name': user.name,
}
for user in self._users.values()
]
credentials = [
{
'id': credential.id,
'user_id': user.id,
'auth_provider_type': credential.auth_provider_type,
'auth_provider_id': credential.auth_provider_id,
'data': credential.data,
}
for user in self._users.values()
for credential in user.credentials
]
refresh_tokens = [
{
'id': refresh_token.id,
'user_id': user.id,
'client_id': refresh_token.client_id,
'created_at': refresh_token.created_at.isoformat(),
'access_token_expiration':
refresh_token.access_token_expiration.total_seconds(),
'token': refresh_token.token,
}
for user in self._users.values()
for refresh_token in user.refresh_tokens.values()
]
access_tokens = [
{
'id': user.id,
'refresh_token_id': refresh_token.id,
'created_at': access_token.created_at.isoformat(),
'token': access_token.token,
}
for user in self._users.values()
for refresh_token in user.refresh_tokens.values()
for access_token in refresh_token.access_tokens
]
clients = [
{
'id': client.id,
'name': client.name,
'secret': client.secret,
'redirect_uris': client.redirect_uris,
}
for client in self._clients.values()
]
data = {
'users': users,
'clients': clients,
'credentials': credentials,
'access_tokens': access_tokens,
'refresh_tokens': refresh_tokens,
}
await self._store.async_save(data, delay=1)

View File

@ -8,10 +8,10 @@ import voluptuous as vol
from homeassistant import auth, data_entry_flow from homeassistant import auth, data_entry_flow
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
from homeassistant.util import json
PATH_DATA = '.users.json' STORAGE_VERSION = 1
STORAGE_KEY = 'auth_provider.homeassistant'
CONFIG_SCHEMA = auth.AUTH_PROVIDER_SCHEMA.extend({ CONFIG_SCHEMA = auth.AUTH_PROVIDER_SCHEMA.extend({
}, extra=vol.PREVENT_EXTRA) }, extra=vol.PREVENT_EXTRA)
@ -31,14 +31,22 @@ class InvalidUser(HomeAssistantError):
class Data: class Data:
"""Hold the user data.""" """Hold the user data."""
def __init__(self, path, data): def __init__(self, hass):
"""Initialize the user data store.""" """Initialize the user data store."""
self.path = path self.hass = hass
self._store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY)
self._data = None
async def async_load(self):
"""Load stored data."""
data = await self._store.async_load()
if data is None: if data is None:
data = { data = {
'salt': auth.generate_secret(), 'salt': auth.generate_secret(),
'users': [] 'users': []
} }
self._data = data self._data = data
@property @property
@ -99,14 +107,9 @@ class Data:
else: else:
raise InvalidUser raise InvalidUser
def save(self): async def async_save(self):
"""Save data.""" """Save data."""
json.save_json(self.path, self._data) await self._store.async_save(self._data)
def load_data(path):
"""Load auth data."""
return Data(path, json.load_json(path, None))
@auth.AUTH_PROVIDERS.register('homeassistant') @auth.AUTH_PROVIDERS.register('homeassistant')
@ -121,12 +124,10 @@ class HassAuthProvider(auth.AuthProvider):
async def async_validate_login(self, username, password): async def async_validate_login(self, username, password):
"""Helper to validate a username and password.""" """Helper to validate a username and password."""
def validate(): data = Data(self.hass)
"""Validate creds.""" await data.async_load()
data = self._auth_data() await self.hass.async_add_executor_job(
data.validate_login(username, password) data.validate_login, username, password)
await self.hass.async_add_job(validate)
async def async_get_or_create_credentials(self, flow_result): async def async_get_or_create_credentials(self, flow_result):
"""Get credentials based on the flow result.""" """Get credentials based on the flow result."""
@ -141,10 +142,6 @@ class HassAuthProvider(auth.AuthProvider):
'username': username 'username': username
}) })
def _auth_data(self):
"""Return the auth provider data."""
return load_data(self.hass.config.path(PATH_DATA))
class LoginFlow(data_entry_flow.FlowHandler): class LoginFlow(data_entry_flow.FlowHandler):
"""Handler for the login flow.""" """Handler for the login flow."""

View File

@ -0,0 +1,104 @@
"""
Support Legacy API password auth provider.
It will be removed when auth system production ready
"""
from collections import OrderedDict
import hmac
import voluptuous as vol
from homeassistant.exceptions import HomeAssistantError
from homeassistant import auth, data_entry_flow
from homeassistant.core import callback
USER_SCHEMA = vol.Schema({
vol.Required('username'): str,
})
CONFIG_SCHEMA = auth.AUTH_PROVIDER_SCHEMA.extend({
}, extra=vol.PREVENT_EXTRA)
LEGACY_USER = 'homeassistant'
class InvalidAuthError(HomeAssistantError):
"""Raised when submitting invalid authentication."""
@auth.AUTH_PROVIDERS.register('legacy_api_password')
class LegacyApiPasswordAuthProvider(auth.AuthProvider):
"""Example auth provider based on hardcoded usernames and passwords."""
DEFAULT_TITLE = 'Legacy API Password'
async def async_credential_flow(self):
"""Return a flow to login."""
return LoginFlow(self)
@callback
def async_validate_login(self, password):
"""Helper to validate a username and password."""
if not hasattr(self.hass, 'http'):
raise ValueError('http component is not loaded')
if self.hass.http.api_password is None:
raise ValueError('http component is not configured using'
' api_password')
if not hmac.compare_digest(self.hass.http.api_password.encode('utf-8'),
password.encode('utf-8')):
raise InvalidAuthError
async def async_get_or_create_credentials(self, flow_result):
"""Return LEGACY_USER always."""
for credential in await self.async_credentials():
if credential.data['username'] == LEGACY_USER:
return credential
return self.async_create_credentials({
'username': LEGACY_USER
})
async def async_user_meta_for_credentials(self, credentials):
"""
Set name as LEGACY_USER always.
Will be used to populate info when creating a new user.
"""
return {'name': LEGACY_USER}
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['password'])
except InvalidAuthError:
errors['base'] = 'invalid_auth'
if not errors:
return self.async_create_entry(
title=self._auth_provider.name,
data={}
)
schema = OrderedDict()
schema['password'] = str
return self.async_show_form(
step_id='init',
data_schema=vol.Schema(schema),
errors=errors,
)

View File

@ -123,7 +123,6 @@ async def async_from_config_dict(config: Dict[str, Any],
components.update(hass.config_entries.async_domains()) components.update(hass.config_entries.async_domains())
# setup components # setup components
# pylint: disable=not-an-iterable
res = await 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. "

View File

@ -154,7 +154,6 @@ def async_setup(hass, config):
return True return True
# pylint: disable=no-self-use
class AlarmControlPanel(Entity): class AlarmControlPanel(Entity):
"""An abstract class for alarm control devices.""" """An abstract class for alarm control devices."""

View File

@ -20,7 +20,7 @@ from homeassistant.const import (
from homeassistant.components.mqtt import ( from homeassistant.components.mqtt import (
CONF_AVAILABILITY_TOPIC, CONF_STATE_TOPIC, CONF_COMMAND_TOPIC, CONF_AVAILABILITY_TOPIC, CONF_STATE_TOPIC, CONF_COMMAND_TOPIC,
CONF_PAYLOAD_AVAILABLE, CONF_PAYLOAD_NOT_AVAILABLE, CONF_QOS, CONF_PAYLOAD_AVAILABLE, CONF_PAYLOAD_NOT_AVAILABLE, CONF_QOS,
MqttAvailability) CONF_RETAIN, MqttAvailability)
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -54,6 +54,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
config.get(CONF_STATE_TOPIC), config.get(CONF_STATE_TOPIC),
config.get(CONF_COMMAND_TOPIC), config.get(CONF_COMMAND_TOPIC),
config.get(CONF_QOS), config.get(CONF_QOS),
config.get(CONF_RETAIN),
config.get(CONF_PAYLOAD_DISARM), config.get(CONF_PAYLOAD_DISARM),
config.get(CONF_PAYLOAD_ARM_HOME), config.get(CONF_PAYLOAD_ARM_HOME),
config.get(CONF_PAYLOAD_ARM_AWAY), config.get(CONF_PAYLOAD_ARM_AWAY),
@ -66,9 +67,9 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
class MqttAlarm(MqttAvailability, alarm.AlarmControlPanel): class MqttAlarm(MqttAvailability, alarm.AlarmControlPanel):
"""Representation of a MQTT alarm status.""" """Representation of a MQTT alarm status."""
def __init__(self, name, state_topic, command_topic, qos, payload_disarm, def __init__(self, name, state_topic, command_topic, qos, retain,
payload_arm_home, payload_arm_away, code, availability_topic, payload_disarm, payload_arm_home, payload_arm_away, code,
payload_available, payload_not_available): availability_topic, payload_available, payload_not_available):
"""Init the MQTT Alarm Control Panel.""" """Init the MQTT Alarm Control Panel."""
super().__init__(availability_topic, qos, payload_available, super().__init__(availability_topic, qos, payload_available,
payload_not_available) payload_not_available)
@ -77,6 +78,7 @@ class MqttAlarm(MqttAvailability, alarm.AlarmControlPanel):
self._state_topic = state_topic self._state_topic = state_topic
self._command_topic = command_topic self._command_topic = command_topic
self._qos = qos self._qos = qos
self._retain = retain
self._payload_disarm = payload_disarm self._payload_disarm = payload_disarm
self._payload_arm_home = payload_arm_home self._payload_arm_home = payload_arm_home
self._payload_arm_away = payload_arm_away self._payload_arm_away = payload_arm_away
@ -134,7 +136,8 @@ class MqttAlarm(MqttAvailability, alarm.AlarmControlPanel):
if not self._validate_code(code, 'disarming'): if not self._validate_code(code, 'disarming'):
return return
mqtt.async_publish( mqtt.async_publish(
self.hass, self._command_topic, self._payload_disarm, self._qos) self.hass, self._command_topic, self._payload_disarm, self._qos,
self._retain)
@asyncio.coroutine @asyncio.coroutine
def async_alarm_arm_home(self, code=None): def async_alarm_arm_home(self, code=None):
@ -145,7 +148,8 @@ class MqttAlarm(MqttAvailability, alarm.AlarmControlPanel):
if not self._validate_code(code, 'arming home'): if not self._validate_code(code, 'arming home'):
return return
mqtt.async_publish( mqtt.async_publish(
self.hass, self._command_topic, self._payload_arm_home, self._qos) self.hass, self._command_topic, self._payload_arm_home, self._qos,
self._retain)
@asyncio.coroutine @asyncio.coroutine
def async_alarm_arm_away(self, code=None): def async_alarm_arm_away(self, code=None):
@ -156,7 +160,8 @@ class MqttAlarm(MqttAvailability, alarm.AlarmControlPanel):
if not self._validate_code(code, 'arming away'): if not self._validate_code(code, 'arming away'):
return return
mqtt.async_publish( mqtt.async_publish(
self.hass, self._command_topic, self._payload_arm_away, self._qos) self.hass, self._command_topic, self._payload_arm_away, self._qos,
self._retain)
def _validate_code(self, code, state): def _validate_code(self, code, state):
"""Validate given code.""" """Validate given code."""

View File

@ -107,7 +107,6 @@ class _DisplayCategory(object):
THERMOSTAT = "THERMOSTAT" THERMOSTAT = "THERMOSTAT"
# Indicates the endpoint is a television. # Indicates the endpoint is a television.
# pylint: disable=invalid-name
TV = "TV" TV = "TV"
@ -1474,9 +1473,6 @@ async def async_api_set_thermostat_mode(hass, config, request, entity):
mode = mode if isinstance(mode, str) else mode['value'] mode = mode if isinstance(mode, str) else mode['value']
operation_list = entity.attributes.get(climate.ATTR_OPERATION_LIST) operation_list = entity.attributes.get(climate.ATTR_OPERATION_LIST)
# Work around a pylint false positive due to
# https://github.com/PyCQA/pylint/issues/1830
# pylint: disable=stop-iteration-return
ha_mode = next( ha_mode = next(
(k for k, v in API_THERMOSTAT_MODES.items() if v == mode), (k for k, v in API_THERMOSTAT_MODES.items() if v == mode),
None None

View File

@ -81,7 +81,6 @@ class APIEventStream(HomeAssistantView):
async def get(self, request): async 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
hass = request.app['hass'] hass = request.app['hass']
stop_obj = object() stop_obj = object()
to_write = asyncio.Queue(loop=hass.loop) to_write = asyncio.Queue(loop=hass.loop)

View File

@ -16,7 +16,7 @@ from homeassistant.const import (
from homeassistant.helpers.event import track_time_interval from homeassistant.helpers.event import track_time_interval
from homeassistant.helpers.dispatcher import dispatcher_send from homeassistant.helpers.dispatcher import dispatcher_send
REQUIREMENTS = ['pyarlo==0.1.7'] REQUIREMENTS = ['pyarlo==0.1.8']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)

View File

@ -16,7 +16,6 @@ _LOGGER = logging.getLogger(__name__)
DOMAIN = 'bbb_gpio' DOMAIN = 'bbb_gpio'
# pylint: disable=no-member
def setup(hass, config): def setup(hass, config):
"""Set up the BeagleBone Black GPIO component.""" """Set up the BeagleBone Black GPIO component."""
# pylint: disable=import-error # pylint: disable=import-error
@ -34,41 +33,39 @@ def setup(hass, config):
return True return True
# noqa: F821
def setup_output(pin): def setup_output(pin):
"""Set up a GPIO as output.""" """Set up a GPIO as output."""
# pylint: disable=import-error,undefined-variable # pylint: disable=import-error
import Adafruit_BBIO.GPIO as GPIO import Adafruit_BBIO.GPIO as GPIO
GPIO.setup(pin, GPIO.OUT) GPIO.setup(pin, GPIO.OUT)
def setup_input(pin, pull_mode): def setup_input(pin, pull_mode):
"""Set up a GPIO as input.""" """Set up a GPIO as input."""
# pylint: disable=import-error,undefined-variable # pylint: disable=import-error
import Adafruit_BBIO.GPIO as GPIO import Adafruit_BBIO.GPIO as GPIO
GPIO.setup(pin, GPIO.IN, # noqa: F821 GPIO.setup(pin, GPIO.IN,
GPIO.PUD_DOWN if pull_mode == 'DOWN' # noqa: F821 GPIO.PUD_DOWN if pull_mode == 'DOWN'
else GPIO.PUD_UP) # noqa: F821 else GPIO.PUD_UP)
def write_output(pin, value): def write_output(pin, value):
"""Write a value to a GPIO.""" """Write a value to a GPIO."""
# pylint: disable=import-error,undefined-variable # pylint: disable=import-error
import Adafruit_BBIO.GPIO as GPIO import Adafruit_BBIO.GPIO as GPIO
GPIO.output(pin, value) GPIO.output(pin, value)
def read_input(pin): def read_input(pin):
"""Read a value from a GPIO.""" """Read a value from a GPIO."""
# pylint: disable=import-error,undefined-variable # pylint: disable=import-error
import Adafruit_BBIO.GPIO as GPIO import Adafruit_BBIO.GPIO as GPIO
return GPIO.input(pin) is GPIO.HIGH return GPIO.input(pin) is GPIO.HIGH
def edge_detect(pin, event_callback, bounce): def edge_detect(pin, event_callback, bounce):
"""Add detection for RISING and FALLING events.""" """Add detection for RISING and FALLING events."""
# pylint: disable=import-error,undefined-variable # pylint: disable=import-error
import Adafruit_BBIO.GPIO as GPIO import Adafruit_BBIO.GPIO as GPIO
GPIO.add_event_detect( GPIO.add_event_detect(
pin, GPIO.BOTH, callback=event_callback, bouncetime=bounce) pin, GPIO.BOTH, callback=event_callback, bouncetime=bounce)

View File

@ -67,7 +67,6 @@ async def async_unload_entry(hass, entry):
return await hass.data[DOMAIN].async_unload_entry(entry) return await hass.data[DOMAIN].async_unload_entry(entry)
# pylint: disable=no-self-use
class BinarySensorDevice(Entity): class BinarySensorDevice(Entity):
"""Represent a binary sensor.""" """Represent a binary sensor."""

View File

@ -124,11 +124,11 @@ class BMWConnectedDriveSensor(BinarySensorDevice):
result['check_control_messages'] = check_control_messages result['check_control_messages'] = check_control_messages
elif self._attribute == 'charging_status': elif self._attribute == 'charging_status':
result['charging_status'] = vehicle_state.charging_status.value result['charging_status'] = vehicle_state.charging_status.value
# pylint: disable=W0212 # pylint: disable=protected-access
result['last_charging_end_result'] = \ result['last_charging_end_result'] = \
vehicle_state._attributes['lastChargingEndResult'] vehicle_state._attributes['lastChargingEndResult']
if self._attribute == 'connection_status': if self._attribute == 'connection_status':
# pylint: disable=W0212 # pylint: disable=protected-access
result['connection_status'] = \ result['connection_status'] = \
vehicle_state._attributes['connectionStatus'] vehicle_state._attributes['connectionStatus']
@ -166,7 +166,7 @@ class BMWConnectedDriveSensor(BinarySensorDevice):
# device class plug: On means device is plugged in, # device class plug: On means device is plugged in,
# Off means device is unplugged # Off means device is unplugged
if self._attribute == 'connection_status': if self._attribute == 'connection_status':
# pylint: disable=W0212 # pylint: disable=protected-access
self._state = (vehicle_state._attributes['connectionStatus'] == self._state = (vehicle_state._attributes['connectionStatus'] ==
'CONNECTED') 'CONNECTED')

View File

@ -14,7 +14,8 @@ from homeassistant.components.binary_sensor import (
from homeassistant.components.digital_ocean import ( from homeassistant.components.digital_ocean import (
CONF_DROPLETS, ATTR_CREATED_AT, ATTR_DROPLET_ID, ATTR_DROPLET_NAME, CONF_DROPLETS, ATTR_CREATED_AT, ATTR_DROPLET_ID, ATTR_DROPLET_NAME,
ATTR_FEATURES, ATTR_IPV4_ADDRESS, ATTR_IPV6_ADDRESS, ATTR_MEMORY, ATTR_FEATURES, ATTR_IPV4_ADDRESS, ATTR_IPV6_ADDRESS, ATTR_MEMORY,
ATTR_REGION, ATTR_VCPUS, DATA_DIGITAL_OCEAN) ATTR_REGION, ATTR_VCPUS, CONF_ATTRIBUTION, DATA_DIGITAL_OCEAN)
from homeassistant.const import ATTR_ATTRIBUTION
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -75,6 +76,7 @@ class DigitalOceanBinarySensor(BinarySensorDevice):
def device_state_attributes(self): def device_state_attributes(self):
"""Return the state attributes of the Digital Ocean droplet.""" """Return the state attributes of the Digital Ocean droplet."""
return { return {
ATTR_ATTRIBUTION: CONF_ATTRIBUTION,
ATTR_CREATED_AT: self.data.created_at, ATTR_CREATED_AT: self.data.created_at,
ATTR_DROPLET_ID: self.data.id, ATTR_DROPLET_ID: self.data.id,
ATTR_DROPLET_NAME: self.data.name, ATTR_DROPLET_NAME: self.data.name,

View File

@ -16,7 +16,7 @@ from homeassistant.const import (
from homeassistant.components.binary_sensor import ( from homeassistant.components.binary_sensor import (
BinarySensorDevice, PLATFORM_SCHEMA) BinarySensorDevice, PLATFORM_SCHEMA)
REQUIREMENTS = ['https://github.com/soldag/pyflic/archive/0.4.zip#pyflic==0.4'] REQUIREMENTS = ['pyflic-homeassistant==0.4.dev0']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)

View File

@ -39,7 +39,6 @@ class GC100BinarySensor(BinarySensorDevice):
def __init__(self, name, port_addr, gc100): def __init__(self, name, port_addr, gc100):
"""Initialize the GC100 binary sensor.""" """Initialize the GC100 binary sensor."""
# pylint: disable=no-member
self._name = name or DEVICE_DEFAULT_NAME self._name = name or DEVICE_DEFAULT_NAME
self._port_addr = port_addr self._port_addr = port_addr
self._gc100 = gc100 self._gc100 = gc100

View File

@ -8,7 +8,7 @@ https://home-assistant.io/components/binary_sensor.isy994/
import asyncio import asyncio
import logging import logging
from datetime import timedelta from datetime import timedelta
from typing import Callable # noqa from typing import Callable
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.components.binary_sensor import BinarySensorDevice, DOMAIN from homeassistant.components.binary_sensor import BinarySensorDevice, DOMAIN

View File

@ -29,7 +29,8 @@ async def async_setup_platform(
async_add_devices=async_add_devices) async_add_devices=async_add_devices)
class MySensorsBinarySensor(mysensors.MySensorsEntity, BinarySensorDevice): class MySensorsBinarySensor(
mysensors.device.MySensorsEntity, BinarySensorDevice):
"""Representation of a MySensors Binary Sensor child node.""" """Representation of a MySensors Binary Sensor child node."""
@property @property

View File

@ -31,12 +31,10 @@ CAMERA_BINARY_TYPES = {
STRUCTURE_BINARY_TYPES = { STRUCTURE_BINARY_TYPES = {
'away': None, 'away': None,
# 'security_state', # pending python-nest update
} }
STRUCTURE_BINARY_STATE_MAP = { STRUCTURE_BINARY_STATE_MAP = {
'away': {'away': True, 'home': False}, 'away': {'away': True, 'home': False},
'security_state': {'deter': True, 'ok': False},
} }
_BINARY_TYPES_DEPRECATED = [ _BINARY_TYPES_DEPRECATED = [
@ -135,7 +133,7 @@ class NestBinarySensor(NestSensorDevice, BinarySensorDevice):
value = getattr(self.device, self.variable) value = getattr(self.device, self.variable)
if self.variable in STRUCTURE_BINARY_TYPES: if self.variable in STRUCTURE_BINARY_TYPES:
self._state = bool(STRUCTURE_BINARY_STATE_MAP self._state = bool(STRUCTURE_BINARY_STATE_MAP
[self.variable][value]) [self.variable].get(value))
else: else:
self._state = bool(value) self._state = bool(value)

View File

@ -0,0 +1,127 @@
"""
Integration with the Rachio Iro sprinkler system controller.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/binary_sensor.rachio/
"""
from abc import abstractmethod
import logging
from homeassistant.components.binary_sensor import BinarySensorDevice
from homeassistant.components.rachio import (DOMAIN as DOMAIN_RACHIO,
KEY_DEVICE_ID,
KEY_STATUS,
KEY_SUBTYPE,
SIGNAL_RACHIO_CONTROLLER_UPDATE,
STATUS_OFFLINE,
STATUS_ONLINE,
SUBTYPE_OFFLINE,
SUBTYPE_ONLINE,)
from homeassistant.helpers.dispatcher import dispatcher_connect
DEPENDENCIES = ['rachio']
_LOGGER = logging.getLogger(__name__)
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Rachio binary sensors."""
devices = []
for controller in hass.data[DOMAIN_RACHIO].controllers:
devices.append(RachioControllerOnlineBinarySensor(hass, controller))
add_devices(devices)
_LOGGER.info("%d Rachio binary sensor(s) added", len(devices))
class RachioControllerBinarySensor(BinarySensorDevice):
"""Represent a binary sensor that reflects a Rachio state."""
def __init__(self, hass, controller, poll=True):
"""Set up a new Rachio controller binary sensor."""
self._controller = controller
if poll:
self._state = self._poll_update()
else:
self._state = None
dispatcher_connect(hass, SIGNAL_RACHIO_CONTROLLER_UPDATE,
self._handle_any_update)
@property
def should_poll(self) -> bool:
"""Declare that this entity pushes its state to HA."""
return False
@property
def is_on(self) -> bool:
"""Return whether the sensor has a 'true' value."""
return self._state
def _handle_any_update(self, *args, **kwargs) -> None:
"""Determine whether an update event applies to this device."""
if args[0][KEY_DEVICE_ID] != self._controller.controller_id:
# For another device
return
# For this device
self._handle_update()
@abstractmethod
def _poll_update(self, data=None) -> bool:
"""Request the state from the API."""
pass
@abstractmethod
def _handle_update(self, *args, **kwargs) -> None:
"""Handle an update to the state of this sensor."""
pass
class RachioControllerOnlineBinarySensor(RachioControllerBinarySensor):
"""Represent a binary sensor that reflects if the controller is online."""
def __init__(self, hass, controller):
"""Set up a new Rachio controller online binary sensor."""
super().__init__(hass, controller, poll=False)
self._state = self._poll_update(controller.init_data)
@property
def name(self) -> str:
"""Return the name of this sensor including the controller name."""
return "{} online".format(self._controller.name)
@property
def device_class(self) -> str:
"""Return the class of this device, from component DEVICE_CLASSES."""
return 'connectivity'
@property
def icon(self) -> str:
"""Return the name of an icon for this sensor."""
return 'mdi:wifi-strength-4' if self.is_on\
else 'mdi:wifi-strength-off-outline'
def _poll_update(self, data=None) -> bool:
"""Request the state from the API."""
if data is None:
data = self._controller.rachio.device.get(
self._controller.controller_id)[1]
if data[KEY_STATUS] == STATUS_ONLINE:
return True
elif data[KEY_STATUS] == STATUS_OFFLINE:
return False
else:
_LOGGER.warning('"%s" reported in unknown state "%s"', self.name,
data[KEY_STATUS])
def _handle_update(self, *args, **kwargs) -> None:
"""Handle an update to the state of this sensor."""
if args[0][KEY_SUBTYPE] == SUBTYPE_ONLINE:
self._state = True
elif args[0][KEY_SUBTYPE] == SUBTYPE_OFFLINE:
self._state = False
self.schedule_update_ha_state()

View File

@ -58,7 +58,6 @@ class RPiGPIOBinarySensor(BinarySensorDevice):
def __init__(self, name, port, pull_mode, bouncetime, invert_logic): def __init__(self, name, port, pull_mode, bouncetime, invert_logic):
"""Initialize the RPi binary sensor.""" """Initialize the RPi binary sensor."""
# pylint: disable=no-member
self._name = name or DEVICE_DEFAULT_NAME self._name = name or DEVICE_DEFAULT_NAME
self._port = port self._port = port
self._pull_mode = pull_mode self._pull_mode = pull_mode

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.3'] REQUIREMENTS = ['numpy==1.14.5']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)

View File

@ -13,7 +13,6 @@ DEPENDENCIES = ['wemo']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
# pylint: disable=too-many-function-args
def setup_platform(hass, config, add_devices_callback, discovery_info=None): def setup_platform(hass, config, add_devices_callback, discovery_info=None):
"""Register discovered WeMo binary sensors.""" """Register discovered WeMo binary sensors."""
import pywemo.discovery as discovery import pywemo.discovery as discovery

View File

@ -89,9 +89,7 @@ class GoogleCalendarData(object):
params['timeMin'] = start_date.isoformat('T') params['timeMin'] = start_date.isoformat('T')
params['timeMax'] = end_date.isoformat('T') params['timeMax'] = end_date.isoformat('T')
# pylint: disable=no-member
events = await hass.async_add_job(service.events) events = await hass.async_add_job(service.events)
# pylint: enable=no-member
result = await hass.async_add_job(events.list(**params).execute) result = await hass.async_add_job(events.list(**params).execute)
items = result.get('items', []) items = result.get('items', [])
@ -111,7 +109,7 @@ class GoogleCalendarData(object):
service, params = self._prepare_query() service, params = self._prepare_query()
params['timeMin'] = dt.now().isoformat('T') params['timeMin'] = dt.now().isoformat('T')
events = service.events() # pylint: disable=no-member events = service.events()
result = events.list(**params).execute() result = events.list(**params).execute()
items = result.get('items', []) items = result.get('items', [])

View File

@ -322,6 +322,7 @@ class Camera(Entity):
except asyncio.CancelledError: except asyncio.CancelledError:
_LOGGER.debug("Stream closed by frontend.") _LOGGER.debug("Stream closed by frontend.")
response = None response = None
raise
finally: finally:
if response is not None: if response is not None:

View File

@ -10,12 +10,13 @@ from datetime import timedelta
from homeassistant.components.camera import Camera from homeassistant.components.camera import Camera
from homeassistant.components.neato import ( from homeassistant.components.neato import (
NEATO_MAP_DATA, NEATO_ROBOTS, NEATO_LOGIN) NEATO_MAP_DATA, NEATO_ROBOTS, NEATO_LOGIN)
from homeassistant.util import Throttle
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
DEPENDENCIES = ['neato'] DEPENDENCIES = ['neato']
SCAN_INTERVAL = timedelta(minutes=10)
def setup_platform(hass, config, add_devices, discovery_info=None): def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Neato Camera.""" """Set up the Neato Camera."""
@ -45,7 +46,6 @@ class NeatoCleaningMap(Camera):
self.update() self.update()
return self._image return self._image
@Throttle(timedelta(seconds=60))
def update(self): def update(self):
"""Check the contents of the map list.""" """Check the contents of the map list."""
self.neato.update_robots() self.neato.update_robots()

View File

@ -233,6 +233,7 @@ class ProxyCamera(Camera):
_LOGGER.debug("Stream closed by frontend.") _LOGGER.debug("Stream closed by frontend.")
req.close() req.close()
response = None response = None
raise
finally: finally:
if response is not None: if response is not None:

View File

@ -67,8 +67,6 @@ async def async_setup_platform(hass, config, async_add_devices,
] ]
for cam in config.get(CONF_CAMERAS, []): for cam in config.get(CONF_CAMERAS, []):
# https://github.com/PyCQA/pylint/issues/1830
# pylint: disable=stop-iteration-return
camera = next( camera = next(
(dc for dc in discovered_cameras (dc for dc in discovered_cameras
if dc[CONF_IMAGE_NAME] == cam[CONF_IMAGE_NAME]), None) if dc[CONF_IMAGE_NAME] == cam[CONF_IMAGE_NAME]), None)

View File

@ -104,27 +104,25 @@ class XiaomiCamera(Camera):
dirs = [d for d in ftp.nlst() if '.' not in d] dirs = [d for d in ftp.nlst() if '.' not in d]
if not dirs: if not dirs:
if self._model == MODEL_YI: _LOGGER.warning("There don't appear to be any folders")
_LOGGER.warning("There don't appear to be any uploaded videos") return False
return False
elif self._model == MODEL_XIAOFANG:
_LOGGER.warning("There don't appear to be any folders")
return False
first_dir = dirs[-1] first_dir = dirs[-1]
try: try:
ftp.cwd(first_dir) ftp.cwd(first_dir)
except error_perm as exc: except error_perm as exc:
_LOGGER.error('Unable to find path: %s - %s', first_dir, exc) _LOGGER.error('Unable to find path: %s - %s', first_dir, exc)
return False return False
if self._model == MODEL_XIAOFANG:
dirs = [d for d in ftp.nlst() if '.' not in d] dirs = [d for d in ftp.nlst() if '.' not in d]
if not dirs: if not dirs:
_LOGGER.warning("There don't appear to be any uploaded videos") _LOGGER.warning("There don't appear to be any uploaded videos")
return False return False
latest_dir = dirs[-1] latest_dir = dirs[-1]
ftp.cwd(latest_dir) ftp.cwd(latest_dir)
videos = [v for v in ftp.nlst() if '.tmp' not in v] videos = [v for v in ftp.nlst() if '.tmp' not in v]
if not videos: if not videos:
_LOGGER.info('Video folder "%s" is empty; delaying', latest_dir) _LOGGER.info('Video folder "%s" is empty; delaying', latest_dir)

View File

@ -77,7 +77,7 @@ class YiCamera(Camera):
"""Retrieve the latest video file from the customized Yi FTP server.""" """Retrieve the latest video file from the customized Yi FTP server."""
from aioftp import Client, StatusCodeError from aioftp import Client, StatusCodeError
ftp = Client() ftp = Client(loop=self.hass.loop)
try: try:
await ftp.connect(self.host) await ftp.connect(self.host)
await ftp.login(self.user, self.passwd) await ftp.login(self.user, self.passwd)

View File

@ -0,0 +1,15 @@
{
"config": {
"abort": {
"no_devices_found": "V s\u00edti nebyly nalezena \u017e\u00e1dn\u00e1 za\u0159\u00edzen\u00ed Google Cast.",
"single_instance_allowed": "Pouze jedin\u00e1 konfigurace Google Cast je nezbytn\u00e1."
},
"step": {
"confirm": {
"description": "Chcete nastavit Google Cast?",
"title": "Google Cast"
}
},
"title": "Google Cast"
}
}

View File

@ -0,0 +1,14 @@
{
"config": {
"abort": {
"no_devices_found": "Keine Google Cast Ger\u00e4te im Netzwerk gefunden."
},
"step": {
"confirm": {
"description": "M\u00f6chten Sie Google Cast einrichten?",
"title": ""
}
},
"title": ""
}
}

View File

@ -0,0 +1,14 @@
{
"config": {
"abort": {
"no_devices_found": "Nem tal\u00e1lhat\u00f3k Google Cast eszk\u00f6z\u00f6k a h\u00e1l\u00f3zaton."
},
"step": {
"confirm": {
"description": "Be szeretn\u00e9d \u00e1ll\u00edtani a Google Cast szolg\u00e1ltat\u00e1st?",
"title": "Google Cast"
}
},
"title": "Google Cast"
}
}

View File

@ -0,0 +1,15 @@
{
"config": {
"abort": {
"no_devices_found": "Nessun dispositivo Google Cast trovato in rete.",
"single_instance_allowed": "\u00c8 necessaria una sola configurazione di Google Cast."
},
"step": {
"confirm": {
"description": "Vuoi configurare Google Cast?",
"title": "Google Cast"
}
},
"title": "Google Cast"
}
}

View File

@ -0,0 +1,15 @@
{
"config": {
"abort": {
"no_devices_found": "Keng Google Cast Apparater am Netzwierk fonnt.",
"single_instance_allowed": "N\u00ebmmen eng eenzeg Konfiguratioun vun Google Cast ass n\u00e9ideg."
},
"step": {
"confirm": {
"description": "Soll Google Cast konfigur\u00e9iert ginn?",
"title": "Google Cast"
}
},
"title": "Google Cast"
}
}

View File

@ -0,0 +1,15 @@
{
"config": {
"abort": {
"no_devices_found": "Geen Google Cast-apparaten gevonden op het netwerk.",
"single_instance_allowed": "Er is slechts \u00e9\u00e9n configuratie van Google Cast nodig."
},
"step": {
"confirm": {
"description": "Wilt u Google Cast instellen?",
"title": "Google Cast"
}
},
"title": "Google Cast"
}
}

View File

@ -0,0 +1,15 @@
{
"config": {
"abort": {
"no_devices_found": "V omre\u017eju niso najdene naprave Google Cast.",
"single_instance_allowed": "Potrebna je samo ena konfiguracija Google Cast-a."
},
"step": {
"confirm": {
"description": "Ali \u017eelite nastaviti Google Cast?",
"title": "Google Cast"
}
},
"title": "Google Cast"
}
}

View File

@ -0,0 +1,15 @@
{
"config": {
"abort": {
"no_devices_found": "\u5728\u7db2\u8def\u4e0a\u627e\u4e0d\u5230 Google Cast \u8a2d\u5099\u3002",
"single_instance_allowed": "\u50c5\u9700\u8a2d\u5b9a\u4e00\u6b21 Google Cast \u5373\u53ef\u3002"
},
"step": {
"confirm": {
"description": "\u662f\u5426\u8981\u8a2d\u5b9a Google Cast\uff1f",
"title": "Google Cast"
}
},
"title": "Google Cast"
}
}

View File

@ -470,7 +470,6 @@ async def async_unload_entry(hass, entry):
class ClimateDevice(Entity): class ClimateDevice(Entity):
"""Representation of a climate device.""" """Representation of a climate device."""
# pylint: disable=no-self-use
@property @property
def state(self): def state(self):
"""Return the current state.""" """Return the current state."""

View File

@ -53,7 +53,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
add_devices(devices) add_devices(devices)
# pylint: disable=import-error, no-name-in-module # pylint: disable=import-error
class EQ3BTSmartThermostat(ClimateDevice): class EQ3BTSmartThermostat(ClimateDevice):
"""Representation of an eQ-3 Bluetooth Smart thermostat.""" """Representation of an eQ-3 Bluetooth Smart thermostat."""

View File

@ -263,7 +263,6 @@ class GenericThermostat(ClimateDevice):
@property @property
def min_temp(self): def min_temp(self):
"""Return the minimum temperature.""" """Return the minimum temperature."""
# pylint: disable=no-member
if self._min_temp: if self._min_temp:
return self._min_temp return self._min_temp
@ -273,7 +272,6 @@ class GenericThermostat(ClimateDevice):
@property @property
def max_temp(self): def max_temp(self):
"""Return the maximum temperature.""" """Return the maximum temperature."""
# pylint: disable=no-member
if self._max_temp: if self._max_temp:
return self._max_temp return self._max_temp

View File

@ -34,7 +34,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
}) })
# pylint: disable=unused-variable
def setup_platform(hass, config, add_devices, discovery_info=None): def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the heatmiser thermostat.""" """Set up the heatmiser thermostat."""
from heatmiserV3 import heatmiser, connection from heatmiserV3 import heatmiser, connection

View File

@ -0,0 +1,130 @@
"""
Support for Homekit climate devices.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/climate.homekit_controller/
"""
import logging
from homeassistant.components.homekit_controller import (
HomeKitEntity, KNOWN_ACCESSORIES)
from homeassistant.components.climate import (
ClimateDevice, STATE_HEAT, STATE_COOL, STATE_IDLE,
SUPPORT_TARGET_TEMPERATURE, SUPPORT_OPERATION_MODE)
from homeassistant.const import TEMP_CELSIUS, STATE_OFF, ATTR_TEMPERATURE
DEPENDENCIES = ['homekit_controller']
_LOGGER = logging.getLogger(__name__)
# Map of Homekit operation modes to hass modes
MODE_HOMEKIT_TO_HASS = {
0: STATE_OFF,
1: STATE_HEAT,
2: STATE_COOL,
}
# Map of hass operation modes to homekit modes
MODE_HASS_TO_HOMEKIT = {v: k for k, v in MODE_HOMEKIT_TO_HASS.items()}
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up Homekit climate."""
if discovery_info is not None:
accessory = hass.data[KNOWN_ACCESSORIES][discovery_info['serial']]
add_devices([HomeKitClimateDevice(accessory, discovery_info)], True)
class HomeKitClimateDevice(HomeKitEntity, ClimateDevice):
"""Representation of a Homekit climate device."""
def __init__(self, *args):
"""Initialise the device."""
super().__init__(*args)
self._state = None
self._current_mode = None
self._valid_modes = []
self._current_temp = None
self._target_temp = None
def update_characteristics(self, characteristics):
"""Synchronise device state with Home Assistant."""
# pylint: disable=import-error
from homekit import CharacteristicsTypes as ctypes
for characteristic in characteristics:
ctype = characteristic['type']
if ctype == ctypes.HEATING_COOLING_CURRENT:
self._state = MODE_HOMEKIT_TO_HASS.get(
characteristic['value'])
if ctype == ctypes.HEATING_COOLING_TARGET:
self._chars['target_mode'] = characteristic['iid']
self._features |= SUPPORT_OPERATION_MODE
self._current_mode = MODE_HOMEKIT_TO_HASS.get(
characteristic['value'])
self._valid_modes = [MODE_HOMEKIT_TO_HASS.get(
mode) for mode in characteristic['valid-values']]
elif ctype == ctypes.TEMPERATURE_CURRENT:
self._current_temp = characteristic['value']
elif ctype == ctypes.TEMPERATURE_TARGET:
self._chars['target_temp'] = characteristic['iid']
self._features |= SUPPORT_TARGET_TEMPERATURE
self._target_temp = characteristic['value']
def set_temperature(self, **kwargs):
"""Set new target temperature."""
temp = kwargs.get(ATTR_TEMPERATURE)
characteristics = [{'aid': self._aid,
'iid': self._chars['target_temp'],
'value': temp}]
self.put_characteristics(characteristics)
def set_operation_mode(self, operation_mode):
"""Set new target operation mode."""
characteristics = [{'aid': self._aid,
'iid': self._chars['target_mode'],
'value': MODE_HASS_TO_HOMEKIT[operation_mode]}]
self.put_characteristics(characteristics)
@property
def state(self):
"""Return the current state."""
# If the device reports its operating mode as off, it sometimes doesn't
# report a new state.
if self._current_mode == STATE_OFF:
return STATE_OFF
if self._state == STATE_OFF and self._current_mode != STATE_OFF:
return STATE_IDLE
return self._state
@property
def current_temperature(self):
"""Return the current temperature."""
return self._current_temp
@property
def target_temperature(self):
"""Return the temperature we try to reach."""
return self._target_temp
@property
def current_operation(self):
"""Return current operation ie. heat, cool, idle."""
return self._current_mode
@property
def operation_list(self):
"""Return the list of available operation modes."""
return self._valid_modes
@property
def supported_features(self):
"""Return the list of supported features."""
return self._features
@property
def temperature_unit(self):
"""Return the unit of measurement."""
return TEMP_CELSIUS

View File

@ -129,6 +129,9 @@ PLATFORM_SCHEMA = SCHEMA_BASE.extend({
@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 MQTT climate devices.""" """Set up the MQTT climate devices."""
if discovery_info is not None:
config = PLATFORM_SCHEMA(discovery_info)
template_keys = ( template_keys = (
CONF_POWER_STATE_TEMPLATE, CONF_POWER_STATE_TEMPLATE,
CONF_MODE_STATE_TEMPLATE, CONF_MODE_STATE_TEMPLATE,
@ -635,11 +638,9 @@ class MqttClimate(MqttAvailability, ClimateDevice):
@property @property
def min_temp(self): def min_temp(self):
"""Return the minimum temperature.""" """Return the minimum temperature."""
# pylint: disable=no-member
return self._min_temp return self._min_temp
@property @property
def max_temp(self): def max_temp(self):
"""Return the maximum temperature.""" """Return the maximum temperature."""
# pylint: disable=no-member
return self._max_temp return self._max_temp

View File

@ -26,9 +26,8 @@ DICT_MYS_TO_HA = {
'Off': STATE_OFF, 'Off': STATE_OFF,
} }
SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_TARGET_TEMPERATURE_HIGH | FAN_LIST = ['Auto', 'Min', 'Normal', 'Max']
SUPPORT_TARGET_TEMPERATURE_LOW | SUPPORT_FAN_MODE | OPERATION_LIST = [STATE_OFF, STATE_AUTO, STATE_COOL, STATE_HEAT]
SUPPORT_OPERATION_MODE)
async def async_setup_platform( async def async_setup_platform(
@ -39,13 +38,24 @@ async def async_setup_platform(
async_add_devices=async_add_devices) async_add_devices=async_add_devices)
class MySensorsHVAC(mysensors.MySensorsEntity, ClimateDevice): class MySensorsHVAC(mysensors.device.MySensorsEntity, ClimateDevice):
"""Representation of a MySensors HVAC.""" """Representation of a MySensors HVAC."""
@property @property
def supported_features(self): def supported_features(self):
"""Return the list of supported features.""" """Return the list of supported features."""
return SUPPORT_FLAGS features = SUPPORT_OPERATION_MODE
set_req = self.gateway.const.SetReq
if set_req.V_HVAC_SPEED in self._values:
features = features | SUPPORT_FAN_MODE
if (set_req.V_HVAC_SETPOINT_COOL in self._values and
set_req.V_HVAC_SETPOINT_HEAT in self._values):
features = (
features | SUPPORT_TARGET_TEMPERATURE_HIGH |
SUPPORT_TARGET_TEMPERATURE_LOW)
else:
features = features | SUPPORT_TARGET_TEMPERATURE
return features
@property @property
def assumed_state(self): def assumed_state(self):
@ -103,7 +113,7 @@ class MySensorsHVAC(mysensors.MySensorsEntity, ClimateDevice):
@property @property
def operation_list(self): def operation_list(self):
"""List of available operation modes.""" """List of available operation modes."""
return [STATE_OFF, STATE_AUTO, STATE_COOL, STATE_HEAT] return OPERATION_LIST
@property @property
def current_fan_mode(self): def current_fan_mode(self):
@ -113,7 +123,7 @@ class MySensorsHVAC(mysensors.MySensorsEntity, ClimateDevice):
@property @property
def fan_list(self): def fan_list(self):
"""List of available fan modes.""" """List of available fan modes."""
return ['Auto', 'Min', 'Normal', 'Max'] return FAN_LIST
async def async_set_temperature(self, **kwargs): async def async_set_temperature(self, **kwargs):
"""Set new target temperature.""" """Set new target temperature."""

View File

@ -5,15 +5,15 @@ For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/climate.zwave/ https://home-assistant.io/components/climate.zwave/
""" """
# Because we do not compile openzwave on CI # Because we do not compile openzwave on CI
# pylint: disable=import-error
import logging import logging
from homeassistant.components.climate import ( from homeassistant.components.climate import (
DOMAIN, ClimateDevice, SUPPORT_TARGET_TEMPERATURE, SUPPORT_FAN_MODE, DOMAIN, ClimateDevice, STATE_AUTO, STATE_COOL, STATE_HEAT,
SUPPORT_TARGET_TEMPERATURE, SUPPORT_FAN_MODE,
SUPPORT_OPERATION_MODE, SUPPORT_SWING_MODE) SUPPORT_OPERATION_MODE, SUPPORT_SWING_MODE)
from homeassistant.components.zwave import ZWaveDeviceEntity from homeassistant.components.zwave import ZWaveDeviceEntity
from homeassistant.components.zwave import async_setup_platform # noqa # pylint: disable=unused-import from homeassistant.components.zwave import async_setup_platform # noqa # pylint: disable=unused-import
from homeassistant.const import ( from homeassistant.const import (
TEMP_CELSIUS, TEMP_FAHRENHEIT, ATTR_TEMPERATURE) STATE_OFF, TEMP_CELSIUS, TEMP_FAHRENHEIT, ATTR_TEMPERATURE)
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -32,6 +32,15 @@ DEVICE_MAPPINGS = {
REMOTEC_ZXT_120_THERMOSTAT: WORKAROUND_ZXT_120 REMOTEC_ZXT_120_THERMOSTAT: WORKAROUND_ZXT_120
} }
STATE_MAPPINGS = {
'Off': STATE_OFF,
'Heat': STATE_HEAT,
'Heat Mode': STATE_HEAT,
'Heat (Default)': STATE_HEAT,
'Cool': STATE_COOL,
'Auto': STATE_AUTO,
}
def get_device(hass, values, **kwargs): def get_device(hass, values, **kwargs):
"""Create Z-Wave entity device.""" """Create Z-Wave entity device."""
@ -49,6 +58,7 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice):
self._current_temperature = None self._current_temperature = None
self._current_operation = None self._current_operation = None
self._operation_list = None self._operation_list = None
self._operation_mapping = None
self._operating_state = None self._operating_state = None
self._current_fan_mode = None self._current_fan_mode = None
self._fan_list = None self._fan_list = None
@ -87,10 +97,21 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice):
"""Handle the data changes for node values.""" """Handle the data changes for node values."""
# Operation Mode # Operation Mode
if self.values.mode: if self.values.mode:
self._current_operation = self.values.mode.data self._operation_list = []
self._operation_mapping = {}
operation_list = self.values.mode.data_items operation_list = self.values.mode.data_items
if operation_list: if operation_list:
self._operation_list = list(operation_list) for mode in operation_list:
ha_mode = STATE_MAPPINGS.get(mode)
if ha_mode and ha_mode not in self._operation_mapping:
self._operation_mapping[ha_mode] = mode
self._operation_list.append(ha_mode)
continue
self._operation_list.append(mode)
current_mode = self.values.mode.data
self._current_operation = next(
(key for key, value in self._operation_mapping.items()
if value == current_mode), current_mode)
_LOGGER.debug("self._operation_list=%s", self._operation_list) _LOGGER.debug("self._operation_list=%s", self._operation_list)
_LOGGER.debug("self._current_operation=%s", self._current_operation) _LOGGER.debug("self._current_operation=%s", self._current_operation)
@ -206,7 +227,8 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice):
def set_operation_mode(self, operation_mode): def set_operation_mode(self, operation_mode):
"""Set new target operation mode.""" """Set new target operation mode."""
if self.values.mode: if self.values.mode:
self.values.mode.data = operation_mode self.values.mode.data = self._operation_mapping.get(
operation_mode, operation_mode)
def set_swing_mode(self, swing_mode): def set_swing_mode(self, swing_mode):
"""Set new target swing mode.""" """Set new target swing mode."""

View File

@ -198,7 +198,6 @@ async def async_setup(hass, config):
class CoverDevice(Entity): class CoverDevice(Entity):
"""Representation a cover.""" """Representation a cover."""
# pylint: disable=no-self-use
@property @property
def current_cover_position(self): def current_cover_position(self):
"""Return current position of cover. """Return current position of cover.

View File

@ -24,7 +24,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
class DemoCover(CoverDevice): class DemoCover(CoverDevice):
"""Representation of a demo cover.""" """Representation of a demo cover."""
# pylint: disable=no-self-use
def __init__(self, hass, name, position=None, tilt_position=None, def __init__(self, hass, name, position=None, tilt_position=None,
device_class=None, supported_features=None): device_class=None, supported_features=None):
"""Initialize the cover.""" """Initialize the cover."""

View File

@ -73,7 +73,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
class GaradgetCover(CoverDevice): class GaradgetCover(CoverDevice):
"""Representation of a Garadget cover.""" """Representation of a Garadget cover."""
# pylint: disable=no-self-use
def __init__(self, hass, args): def __init__(self, hass, args):
"""Initialize the cover.""" """Initialize the cover."""
self.particle_url = 'https://api.particle.io' self.particle_url = 'https://api.particle.io'

View File

@ -5,7 +5,7 @@ For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/cover.isy994/ https://home-assistant.io/components/cover.isy994/
""" """
import logging import logging
from typing import Callable # noqa from typing import Callable
from homeassistant.components.cover import CoverDevice, DOMAIN from homeassistant.components.cover import CoverDevice, DOMAIN
from homeassistant.components.isy994 import (ISY994_NODES, ISY994_PROGRAMS, from homeassistant.components.isy994 import (ISY994_NODES, ISY994_PROGRAMS,

View File

@ -17,7 +17,7 @@ async def async_setup_platform(
async_add_devices=async_add_devices) async_add_devices=async_add_devices)
class MySensorsCover(mysensors.MySensorsEntity, CoverDevice): class MySensorsCover(mysensors.device.MySensorsEntity, CoverDevice):
"""Representation of the value of a MySensors Cover child node.""" """Representation of the value of a MySensors Cover child node."""
@property @property

View File

@ -72,7 +72,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
class OpenGarageCover(CoverDevice): class OpenGarageCover(CoverDevice):
"""Representation of a OpenGarage cover.""" """Representation of a OpenGarage cover."""
# pylint: disable=no-self-use
def __init__(self, hass, args): def __init__(self, hass, args):
"""Initialize the cover.""" """Initialize the cover."""
self.opengarage_url = 'http://{}:{}'.format( self.opengarage_url = 'http://{}:{}'.format(

View File

@ -21,6 +21,10 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
_id = shade.object_id() + shade.name() _id = shade.object_id() + shade.name()
if _id not in hass.data[DOMAIN]['unique_ids']: if _id not in hass.data[DOMAIN]['unique_ids']:
add_devices([WinkCoverDevice(shade, hass)]) add_devices([WinkCoverDevice(shade, hass)])
for shade in pywink.get_shade_groups():
_id = shade.object_id() + shade.name()
if _id not in hass.data[DOMAIN]['unique_ids']:
add_devices([WinkCoverDevice(shade, hass)])
for door in pywink.get_garage_doors(): for door in pywink.get_garage_doors():
_id = door.object_id() + door.name() _id = door.object_id() + door.name()
if _id not in hass.data[DOMAIN]['unique_ids']: if _id not in hass.data[DOMAIN]['unique_ids']:

View File

@ -42,7 +42,6 @@ class ZwaveRollershutter(zwave.ZWaveDeviceEntity, CoverDevice):
def __init__(self, hass, values, invert_buttons): def __init__(self, hass, values, invert_buttons):
"""Initialize the Z-Wave rollershutter.""" """Initialize the Z-Wave rollershutter."""
ZWaveDeviceEntity.__init__(self, values, DOMAIN) ZWaveDeviceEntity.__init__(self, values, DOMAIN)
# pylint: disable=no-member
self._network = hass.data[zwave.const.DATA_NETWORK] self._network = hass.data[zwave.const.DATA_NETWORK]
self._open_id = None self._open_id = None
self._close_id = None self._close_id = None

View File

@ -22,7 +22,8 @@
}, },
"options": { "options": {
"data": { "data": {
"allow_clip_sensor": "Povolit import virtu\u00e1ln\u00edch \u010didel" "allow_clip_sensor": "Povolit import virtu\u00e1ln\u00edch \u010didel",
"allow_deconz_groups": "Povolit import skupin deCONZ "
}, },
"title": "Dal\u0161\u00ed mo\u017enosti konfigurace pro deCONZ" "title": "Dal\u0161\u00ed mo\u017enosti konfigurace pro deCONZ"
} }

View File

@ -19,8 +19,14 @@
"link": { "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\"", "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": "Mit deCONZ verbinden"
},
"options": {
"data": {
"allow_clip_sensor": "Import virtueller Sensoren zulassen",
"allow_deconz_groups": "Import von deCONZ-Gruppen zulassen"
}
} }
}, },
"title": "deCONZ" "title": "deCONZ Zigbee Gateway"
} }
} }

View File

@ -22,7 +22,8 @@
}, },
"options": { "options": {
"data": { "data": {
"allow_clip_sensor": "Erlaabt den Import vun virtuellen Sensoren" "allow_clip_sensor": "Erlaabt den Import vun virtuellen Sensoren",
"allow_deconz_groups": "Erlaabt den Import vun deCONZ Gruppen"
}, },
"title": "Extra Konfiguratiouns Optiounen fir deCONZ" "title": "Extra Konfiguratiouns Optiounen fir deCONZ"
} }

View File

@ -19,6 +19,13 @@
"link": { "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\"", "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": "Koppel met deCONZ"
},
"options": {
"data": {
"allow_clip_sensor": "Sta het importeren van virtuele sensoren toe",
"allow_deconz_groups": "Sta de import van deCONZ-groepen toe"
},
"title": "Extra configuratieopties voor deCONZ"
} }
}, },
"title": "deCONZ" "title": "deCONZ"

View File

@ -22,7 +22,8 @@
}, },
"options": { "options": {
"data": { "data": {
"allow_clip_sensor": "Dovoli uvoz virtualnih senzorjev" "allow_clip_sensor": "Dovoli uvoz virtualnih senzorjev",
"allow_deconz_groups": "Dovoli uvoz deCONZ skupin"
}, },
"title": "Dodatne mo\u017enosti konfiguracije za deCONZ" "title": "Dodatne mo\u017enosti konfiguracije za deCONZ"
} }

View File

@ -22,7 +22,8 @@
}, },
"options": { "options": {
"data": { "data": {
"allow_clip_sensor": "\u5141\u8a31\u532f\u5165\u865b\u64ec\u611f\u61c9\u5668" "allow_clip_sensor": "\u5141\u8a31\u532f\u5165\u865b\u64ec\u611f\u61c9\u5668",
"allow_deconz_groups": "\u5141\u8a31\u532f\u5165 deCONZ \u7fa4\u7d44"
}, },
"title": "deCONZ \u9644\u52a0\u8a2d\u5b9a\u9078\u9805" "title": "deCONZ \u9644\u52a0\u8a2d\u5b9a\u9078\u9805"
} }

View File

@ -22,7 +22,7 @@ from .const import (
CONF_ALLOW_CLIP_SENSOR, CONFIG_FILE, DATA_DECONZ_EVENT, CONF_ALLOW_CLIP_SENSOR, CONFIG_FILE, DATA_DECONZ_EVENT,
DATA_DECONZ_ID, DATA_DECONZ_UNSUB, DOMAIN, _LOGGER) DATA_DECONZ_ID, DATA_DECONZ_UNSUB, DOMAIN, _LOGGER)
REQUIREMENTS = ['pydeconz==38'] REQUIREMENTS = ['pydeconz==39']
CONFIG_SCHEMA = vol.Schema({ CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({ DOMAIN: vol.Schema({

View File

@ -163,9 +163,6 @@ class DeconzFlowHandler(data_entry_flow.FlowHandler):
if CONF_API_KEY not in import_config: if CONF_API_KEY not in import_config:
return await self.async_step_link() return await self.async_step_link()
self.deconz_config[CONF_ALLOW_CLIP_SENSOR] = True user_input = {CONF_ALLOW_CLIP_SENSOR: True,
self.deconz_config[CONF_ALLOW_DECONZ_GROUPS] = True CONF_ALLOW_DECONZ_GROUPS: True}
return self.async_create_entry( return await self.async_step_options(user_input=user_input)
title='deCONZ-' + self.deconz_config[CONF_BRIDGEID],
data=self.deconz_config
)

View File

@ -50,7 +50,6 @@ class CiscoDeviceScanner(DeviceScanner):
self.success_init = self._update_info() self.success_init = self._update_info()
_LOGGER.info('cisco_ios scanner initialized') _LOGGER.info('cisco_ios scanner initialized')
# pylint: disable=no-self-use
def get_device_name(self, device): def get_device_name(self, device):
"""Get the firmware doesn't save the name of the wireless device.""" """Get the firmware doesn't save the name of the wireless device."""
return None return None

View File

@ -22,7 +22,7 @@ from homeassistant.components.device_tracker import (
from homeassistant.const import ( from homeassistant.const import (
CONF_HOST, CONF_PORT) CONF_HOST, CONF_PORT)
REQUIREMENTS = ['aiofreepybox==0.0.3'] REQUIREMENTS = ['aiofreepybox==0.0.4']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)

View File

@ -7,7 +7,7 @@ https://home-assistant.io/components/device_tracker.gpslogger/
import logging import logging
from hmac import compare_digest from hmac import compare_digest
from aiohttp.web import Request, HTTPUnauthorized # NOQA from aiohttp.web import Request, HTTPUnauthorized
import voluptuous as vol import voluptuous as vol
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv

View File

@ -61,7 +61,6 @@ class LinksysAPDeviceScanner(DeviceScanner):
return self.last_results return self.last_results
# pylint: disable=no-self-use
def get_device_name(self, device): def get_device_name(self, device):
""" """
Return the name (if known) of the device. Return the name (if known) of the device.

View File

@ -14,7 +14,7 @@ from homeassistant.components.device_tracker import (
from homeassistant.const import ( from homeassistant.const import (
CONF_HOST, CONF_PASSWORD, CONF_USERNAME, CONF_PORT) CONF_HOST, CONF_PASSWORD, CONF_USERNAME, CONF_PORT)
REQUIREMENTS = ['librouteros==1.0.5'] REQUIREMENTS = ['librouteros==2.1.0']
MTK_DEFAULT_API_PORT = '8728' MTK_DEFAULT_API_PORT = '8728'

View File

@ -23,13 +23,13 @@ async def async_setup_scanner(hass, config, async_see, discovery_info=None):
id(device.gateway), device.node_id, device.child_id, id(device.gateway), device.node_id, device.child_id,
device.value_type) device.value_type)
async_dispatcher_connect( async_dispatcher_connect(
hass, mysensors.SIGNAL_CALLBACK.format(*dev_id), hass, mysensors.const.SIGNAL_CALLBACK.format(*dev_id),
device.async_update_callback) device.async_update_callback)
return True return True
class MySensorsDeviceScanner(mysensors.MySensorsDevice): class MySensorsDeviceScanner(mysensors.device.MySensorsDevice):
"""Represent a MySensors scanner.""" """Represent a MySensors scanner."""
def __init__(self, async_see, *args): def __init__(self, async_see, *args):

View File

@ -74,8 +74,6 @@ class SnmpScanner(DeviceScanner):
return [client['mac'] for client in self.last_results return [client['mac'] for client in self.last_results
if client.get('mac')] if client.get('mac')]
# Suppressing no-self-use warning
# pylint: disable=R0201
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 None if we don't know."""
# We have no names # We have no names
@ -106,7 +104,6 @@ class SnmpScanner(DeviceScanner):
if errindication: if errindication:
_LOGGER.error("SNMPLIB error: %s", errindication) _LOGGER.error("SNMPLIB error: %s", errindication)
return return
# pylint: disable=no-member
if errstatus: if errstatus:
_LOGGER.error("SNMP error: %s at %s", errstatus.prettyPrint(), _LOGGER.error("SNMP error: %s at %s", errstatus.prettyPrint(),
errindex and restable[int(errindex) - 1][0] or '?') errindex and restable[int(errindex) - 1][0] or '?')

View File

@ -68,7 +68,6 @@ class TplinkDeviceScanner(DeviceScanner):
self._update_info() self._update_info()
return self.last_results return self.last_results
# pylint: disable=no-self-use
def get_device_name(self, device): def get_device_name(self, device):
"""Get firmware doesn't save the name of the wireless device.""" """Get firmware doesn't save the name of the wireless device."""
return None return None
@ -103,7 +102,6 @@ class Tplink2DeviceScanner(TplinkDeviceScanner):
self._update_info() self._update_info()
return self.last_results.keys() return self.last_results.keys()
# pylint: disable=no-self-use
def get_device_name(self, device): def get_device_name(self, device):
"""Get firmware doesn't save the name of the wireless device.""" """Get firmware doesn't save the name of the wireless device."""
return self.last_results.get(device) return self.last_results.get(device)
@ -164,7 +162,6 @@ class Tplink3DeviceScanner(TplinkDeviceScanner):
self._log_out() self._log_out()
return self.last_results.keys() return self.last_results.keys()
# pylint: disable=no-self-use
def get_device_name(self, device): def get_device_name(self, device):
"""Get the firmware doesn't save the name of the wireless device. """Get the firmware doesn't save the name of the wireless device.
@ -273,7 +270,6 @@ class Tplink4DeviceScanner(TplinkDeviceScanner):
self._update_info() self._update_info()
return self.last_results return self.last_results
# pylint: disable=no-self-use
def get_device_name(self, device): def get_device_name(self, device):
"""Get the name of the wireless device.""" """Get the name of the wireless device."""
return None return None
@ -349,7 +345,6 @@ class Tplink5DeviceScanner(TplinkDeviceScanner):
self._update_info() self._update_info()
return self.last_results.keys() return self.last_results.keys()
# pylint: disable=no-self-use
def get_device_name(self, device): def get_device_name(self, device):
"""Get firmware doesn't save the name of the wireless device.""" """Get firmware doesn't save the name of the wireless device."""
return None return None

View File

@ -27,6 +27,7 @@ ATTR_MEMORY = 'memory'
ATTR_REGION = 'region' ATTR_REGION = 'region'
ATTR_VCPUS = 'vcpus' ATTR_VCPUS = 'vcpus'
CONF_ATTRIBUTION = 'Data provided by Digital Ocean'
CONF_DROPLETS = 'droplets' CONF_DROPLETS = 'droplets'
DATA_DIGITAL_OCEAN = 'data_do' DATA_DIGITAL_OCEAN = 'data_do'

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.4.1'] REQUIREMENTS = ['netdisco==1.5.0']
DOMAIN = 'discovery' DOMAIN = 'discovery'

View File

@ -105,7 +105,6 @@ def setup(hass, config):
Will automatically load thermostat and sensor components to support Will automatically load thermostat and sensor components to support
devices discovered on the network. devices discovered on the network.
""" """
# pylint: disable=import-error
global NETWORK global NETWORK
if 'ecobee' in _CONFIGURING: if 'ecobee' in _CONFIGURING:

View File

@ -91,9 +91,11 @@ def setup(hass, yaml_config):
server_port=config.listen_port, server_port=config.listen_port,
api_password=None, api_password=None,
ssl_certificate=None, ssl_certificate=None,
ssl_peer_certificate=None,
ssl_key=None, ssl_key=None,
cors_origins=None, cors_origins=None,
use_x_forwarded_for=False, use_x_forwarded_for=False,
trusted_proxies=[],
trusted_networks=[], trusted_networks=[],
login_threshold=0, login_threshold=0,
is_ban_enabled=False is_ban_enabled=False

View File

@ -26,7 +26,7 @@ from homeassistant.helpers.translation import async_get_translations
from homeassistant.loader import bind_hass from homeassistant.loader import bind_hass
from homeassistant.util.yaml import load_yaml from homeassistant.util.yaml import load_yaml
REQUIREMENTS = ['home-assistant-frontend==20180625.0'] REQUIREMENTS = ['home-assistant-frontend==20180704.0']
DOMAIN = 'frontend' DOMAIN = 'frontend'
DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log'] DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log']
@ -200,8 +200,8 @@ def add_manifest_json_key(key, val):
async def async_setup(hass, config): async def async_setup(hass, config):
"""Set up the serving of the frontend.""" """Set up the serving of the frontend."""
if list(hass.auth.async_auth_providers): if hass.auth.active:
client = await hass.auth.async_create_client( client = await hass.auth.async_get_or_create_client(
'Home Assistant Frontend', 'Home Assistant Frontend',
redirect_uris=['/'], redirect_uris=['/'],
no_secret=True, no_secret=True,

View File

@ -31,7 +31,7 @@ CONFIG_SCHEMA = vol.Schema({
}, extra=vol.ALLOW_EXTRA) }, extra=vol.ALLOW_EXTRA)
# pylint: disable=no-member, import-self # pylint: disable=no-member
def setup(hass, base_config): def setup(hass, base_config):
"""Set up the gc100 component.""" """Set up the gc100 component."""
import gc100 import gc100

View File

@ -197,7 +197,7 @@ def setup_services(hass, track_new_found_calendars, calendar_service):
def _scan_for_calendars(service): def _scan_for_calendars(service):
"""Scan for new calendars.""" """Scan for new calendars."""
service = calendar_service.get() service = calendar_service.get()
cal_list = service.calendarList() # pylint: disable=no-member cal_list = service.calendarList()
calendars = cal_list.list().execute()['items'] calendars = cal_list.list().execute()['items']
for calendar in calendars: for calendar in calendars:
calendar['track'] = track_new_found_calendars calendar['track'] = track_new_found_calendars

View File

@ -13,9 +13,8 @@ import async_timeout
import voluptuous as vol import voluptuous as vol
# Typing imports # Typing imports
# pylint: disable=using-constant-test,unused-import,ungrouped-imports from homeassistant.core import HomeAssistant
from homeassistant.core import HomeAssistant # NOQA from typing import Dict, Any
from typing import Dict, Any # NOQA
from homeassistant.const import CONF_NAME from homeassistant.const import CONF_NAME
from homeassistant.helpers import config_validation as cv from homeassistant.helpers import config_validation as cv

View File

@ -3,12 +3,11 @@
import logging import logging
# Typing imports # Typing imports
# pylint: disable=using-constant-test,unused-import,ungrouped-imports
# if False: # if False:
from aiohttp.web import Request, Response # NOQA from aiohttp.web import Request, Response
from typing import Dict, Any # NOQA from typing import Dict, Any
from homeassistant.core import HomeAssistant # NOQA from homeassistant.core import HomeAssistant
from homeassistant.components.http import HomeAssistantView from homeassistant.components.http import HomeAssistantView
from homeassistant.const import ( from homeassistant.const import (
HTTP_BAD_REQUEST, HTTP_BAD_REQUEST,

View File

@ -7,10 +7,10 @@ https://home-assistant.io/components/google_assistant/
import logging import logging
from aiohttp.hdrs import AUTHORIZATION from aiohttp.hdrs import AUTHORIZATION
from aiohttp.web import Request, Response # NOQA from aiohttp.web import Request, Response
# Typing imports # Typing imports
# pylint: disable=using-constant-test,unused-import,ungrouped-imports # pylint: disable=unused-import
from homeassistant.components.http import HomeAssistantView from homeassistant.components.http import HomeAssistantView
from homeassistant.core import HomeAssistant, callback # NOQA from homeassistant.core import HomeAssistant, callback # NOQA
from homeassistant.helpers.entity import Entity # NOQA from homeassistant.helpers.entity import Entity # NOQA

View File

@ -4,7 +4,7 @@ from itertools import product
import logging import logging
# Typing imports # Typing imports
# pylint: disable=using-constant-test,unused-import,ungrouped-imports # pylint: disable=unused-import
# if False: # if False:
from aiohttp.web import Request, Response # NOQA from aiohttp.web import Request, Response # NOQA
from typing import Dict, Tuple, Any, Optional # NOQA from typing import Dict, Tuple, Any, Optional # NOQA

View File

@ -107,8 +107,8 @@ def get_accessory(hass, driver, state, aid, config):
a_type = 'Thermostat' a_type = 'Thermostat'
elif state.domain == 'cover': elif state.domain == 'cover':
features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
device_class = state.attributes.get(ATTR_DEVICE_CLASS) device_class = state.attributes.get(ATTR_DEVICE_CLASS)
features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
if device_class == 'garage' and \ if device_class == 'garage' and \
features & (cover.SUPPORT_OPEN | cover.SUPPORT_CLOSE): features & (cover.SUPPORT_OPEN | cover.SUPPORT_CLOSE):
@ -134,8 +134,8 @@ def get_accessory(hass, driver, state, aid, config):
a_type = 'MediaPlayer' a_type = 'MediaPlayer'
elif state.domain == 'sensor': elif state.domain == 'sensor':
unit = state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
device_class = state.attributes.get(ATTR_DEVICE_CLASS) device_class = state.attributes.get(ATTR_DEVICE_CLASS)
unit = state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
if device_class == DEVICE_CLASS_TEMPERATURE or \ if device_class == DEVICE_CLASS_TEMPERATURE or \
unit in (TEMP_CELSIUS, TEMP_FAHRENHEIT): unit in (TEMP_CELSIUS, TEMP_FAHRENHEIT):

View File

@ -8,7 +8,8 @@ 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 pyhap.const import CATEGORY_OTHER
from homeassistant.const import __version__ from homeassistant.const import (
__version__, ATTR_BATTERY_CHARGING, ATTR_BATTERY_LEVEL)
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.core import split_entity_id
from homeassistant.helpers.event import ( from homeassistant.helpers.event import (
@ -16,10 +17,11 @@ from homeassistant.helpers.event import (
from homeassistant.util import dt as dt_util from homeassistant.util import dt as dt_util
from .const import ( from .const import (
BRIDGE_MODEL, BRIDGE_NAME, BRIDGE_SERIAL_NUMBER, BRIDGE_MODEL, BRIDGE_NAME, BRIDGE_SERIAL_NUMBER, CHAR_BATTERY_LEVEL,
DEBOUNCE_TIMEOUT, MANUFACTURER) CHAR_CHARGING_STATE, CHAR_STATUS_LOW_BATTERY, DEBOUNCE_TIMEOUT,
MANUFACTURER, SERV_BATTERY_SERVICE)
from .util import ( from .util import (
show_setup_message, dismiss_setup_message) convert_to_float, show_setup_message, dismiss_setup_message)
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -67,6 +69,23 @@ class HomeAccessory(Accessory):
self.entity_id = entity_id self.entity_id = entity_id
self.hass = hass self.hass = hass
self.debounce = {} self.debounce = {}
self._support_battery_level = False
self._support_battery_charging = True
"""Add battery service if available"""
battery_level = self.hass.states.get(self.entity_id).attributes \
.get(ATTR_BATTERY_LEVEL)
if battery_level is None:
return
_LOGGER.debug('%s: Found battery level attribute', self.entity_id)
self._support_battery_level = True
serv_battery = self.add_preload_service(SERV_BATTERY_SERVICE)
self._char_battery = serv_battery.configure_char(
CHAR_BATTERY_LEVEL, value=0)
self._char_charging = serv_battery.configure_char(
CHAR_CHARGING_STATE, value=2)
self._char_low_battery = serv_battery.configure_char(
CHAR_STATUS_LOW_BATTERY, value=0)
async def run(self): async def run(self):
"""Method called by accessory after driver is started. """Method called by accessory after driver is started.
@ -85,8 +104,32 @@ class HomeAccessory(Accessory):
_LOGGER.debug('New_state: %s', new_state) _LOGGER.debug('New_state: %s', new_state)
if new_state is None: if new_state is None:
return return
if self._support_battery_level:
self.hass.async_add_job(self.update_battery, new_state)
self.hass.async_add_job(self.update_state, new_state) self.hass.async_add_job(self.update_state, new_state)
def update_battery(self, new_state):
"""Update battery service if available.
Only call this function if self._support_battery_level is True.
"""
battery_level = convert_to_float(
new_state.attributes.get(ATTR_BATTERY_LEVEL))
self._char_battery.set_value(battery_level)
self._char_low_battery.set_value(battery_level < 20)
_LOGGER.debug('%s: Updated battery level to %d', self.entity_id,
battery_level)
if not self._support_battery_charging:
return
charging = new_state.attributes.get(ATTR_BATTERY_CHARGING)
if charging is None:
self._support_battery_charging = False
return
hk_charging = 1 if charging is True else 0
self._char_charging.set_value(hk_charging)
_LOGGER.debug('%s: Updated battery charging to %d', self.entity_id,
hk_charging)
def update_state(self, new_state): def update_state(self, new_state):
"""Method called on state change to update HomeKit value. """Method called on state change to update HomeKit value.

View File

@ -38,6 +38,7 @@ TYPE_SWITCH = 'switch'
# #### Services #### # #### Services ####
SERV_ACCESSORY_INFO = 'AccessoryInformation' SERV_ACCESSORY_INFO = 'AccessoryInformation'
SERV_AIR_QUALITY_SENSOR = 'AirQualitySensor' SERV_AIR_QUALITY_SENSOR = 'AirQualitySensor'
SERV_BATTERY_SERVICE = 'BatteryService'
SERV_CARBON_DIOXIDE_SENSOR = 'CarbonDioxideSensor' SERV_CARBON_DIOXIDE_SENSOR = 'CarbonDioxideSensor'
SERV_CARBON_MONOXIDE_SENSOR = 'CarbonMonoxideSensor' SERV_CARBON_MONOXIDE_SENSOR = 'CarbonMonoxideSensor'
SERV_CONTACT_SENSOR = 'ContactSensor' SERV_CONTACT_SENSOR = 'ContactSensor'
@ -62,11 +63,13 @@ SERV_WINDOW_COVERING = 'WindowCovering'
CHAR_ACTIVE = 'Active' CHAR_ACTIVE = 'Active'
CHAR_AIR_PARTICULATE_DENSITY = 'AirParticulateDensity' CHAR_AIR_PARTICULATE_DENSITY = 'AirParticulateDensity'
CHAR_AIR_QUALITY = 'AirQuality' CHAR_AIR_QUALITY = 'AirQuality'
CHAR_BATTERY_LEVEL = 'BatteryLevel'
CHAR_BRIGHTNESS = 'Brightness' CHAR_BRIGHTNESS = 'Brightness'
CHAR_CARBON_DIOXIDE_DETECTED = 'CarbonDioxideDetected' CHAR_CARBON_DIOXIDE_DETECTED = 'CarbonDioxideDetected'
CHAR_CARBON_DIOXIDE_LEVEL = 'CarbonDioxideLevel' CHAR_CARBON_DIOXIDE_LEVEL = 'CarbonDioxideLevel'
CHAR_CARBON_DIOXIDE_PEAK_LEVEL = 'CarbonDioxidePeakLevel' CHAR_CARBON_DIOXIDE_PEAK_LEVEL = 'CarbonDioxidePeakLevel'
CHAR_CARBON_MONOXIDE_DETECTED = 'CarbonMonoxideDetected' CHAR_CARBON_MONOXIDE_DETECTED = 'CarbonMonoxideDetected'
CHAR_CHARGING_STATE = 'ChargingState'
CHAR_COLOR_TEMPERATURE = 'ColorTemperature' CHAR_COLOR_TEMPERATURE = 'ColorTemperature'
CHAR_CONTACT_SENSOR_STATE = 'ContactSensorState' CHAR_CONTACT_SENSOR_STATE = 'ContactSensorState'
CHAR_COOLING_THRESHOLD_TEMPERATURE = 'CoolingThresholdTemperature' CHAR_COOLING_THRESHOLD_TEMPERATURE = 'CoolingThresholdTemperature'
@ -96,6 +99,7 @@ CHAR_ROTATION_DIRECTION = 'RotationDirection'
CHAR_SATURATION = 'Saturation' CHAR_SATURATION = 'Saturation'
CHAR_SERIAL_NUMBER = 'SerialNumber' CHAR_SERIAL_NUMBER = 'SerialNumber'
CHAR_SMOKE_DETECTED = 'SmokeDetected' CHAR_SMOKE_DETECTED = 'SmokeDetected'
CHAR_STATUS_LOW_BATTERY = 'StatusLowBattery'
CHAR_SWING_MODE = 'SwingMode' CHAR_SWING_MODE = 'SwingMode'
CHAR_TARGET_DOOR_STATE = 'TargetDoorState' CHAR_TARGET_DOOR_STATE = 'TargetDoorState'
CHAR_TARGET_HEATING_COOLING = 'TargetHeatingCoolingState' CHAR_TARGET_HEATING_COOLING = 'TargetHeatingCoolingState'

View File

@ -57,9 +57,6 @@ class Fan(HomeAccessory):
def set_state(self, value): def set_state(self, value):
"""Set state if call came from HomeKit.""" """Set state if call came from HomeKit."""
if self._state == value:
return
_LOGGER.debug('%s: Set state to %d', self.entity_id, value) _LOGGER.debug('%s: Set state to %d', self.entity_id, value)
self._flag[CHAR_ACTIVE] = True self._flag[CHAR_ACTIVE] = True
service = SERVICE_TURN_ON if value == 1 else SERVICE_TURN_OFF service = SERVICE_TURN_ON if value == 1 else SERVICE_TURN_OFF

View File

@ -5,8 +5,10 @@ from pyhap.const import CATEGORY_ALARM_SYSTEM
from homeassistant.components.alarm_control_panel import DOMAIN from homeassistant.components.alarm_control_panel import DOMAIN
from homeassistant.const import ( from homeassistant.const import (
ATTR_ENTITY_ID, ATTR_CODE, STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, ATTR_ENTITY_ID, ATTR_CODE, SERVICE_ALARM_ARM_AWAY, SERVICE_ALARM_ARM_HOME,
STATE_ALARM_ARMED_NIGHT, STATE_ALARM_TRIGGERED, STATE_ALARM_DISARMED) SERVICE_ALARM_ARM_NIGHT, SERVICE_ALARM_DISARM, STATE_ALARM_ARMED_AWAY,
STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT, STATE_ALARM_TRIGGERED,
STATE_ALARM_DISARMED)
from . import TYPES from . import TYPES
from .accessories import HomeAccessory from .accessories import HomeAccessory
@ -22,10 +24,11 @@ HASS_TO_HOMEKIT = {STATE_ALARM_ARMED_HOME: 0,
STATE_ALARM_DISARMED: 3, STATE_ALARM_DISARMED: 3,
STATE_ALARM_TRIGGERED: 4} 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_ARMED_HOME: 'alarm_arm_home', STATE_TO_SERVICE = {
STATE_ALARM_ARMED_AWAY: 'alarm_arm_away', STATE_ALARM_ARMED_AWAY: SERVICE_ALARM_ARM_AWAY,
STATE_ALARM_ARMED_NIGHT: 'alarm_arm_night', STATE_ALARM_ARMED_HOME: SERVICE_ALARM_ARM_HOME,
STATE_ALARM_DISARMED: 'alarm_disarm'} STATE_ALARM_ARMED_NIGHT: SERVICE_ALARM_ARM_NIGHT,
STATE_ALARM_DISARMED: SERVICE_ALARM_DISARM}
@TYPES.register('SecuritySystem') @TYPES.register('SecuritySystem')

View File

@ -3,7 +3,7 @@ import logging
from pyhap.const import CATEGORY_OUTLET, CATEGORY_SWITCH from pyhap.const import CATEGORY_OUTLET, CATEGORY_SWITCH
from homeassistant.components.switch import DOMAIN as SWITCH from homeassistant.components.switch import DOMAIN
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
@ -37,7 +37,7 @@ class Outlet(HomeAccessory):
self.flag_target_state = True self.flag_target_state = True
params = {ATTR_ENTITY_ID: self.entity_id} params = {ATTR_ENTITY_ID: self.entity_id}
service = SERVICE_TURN_ON if value else SERVICE_TURN_OFF service = SERVICE_TURN_ON if value else SERVICE_TURN_OFF
self.hass.services.call(SWITCH, service, params) self.hass.services.call(DOMAIN, service, params)
def update_state(self, new_state): def update_state(self, new_state):
"""Update switch state after state changed.""" """Update switch state after state changed."""

View File

@ -23,6 +23,7 @@ HOMEKIT_DIR = '.homekit'
HOMEKIT_ACCESSORY_DISPATCH = { HOMEKIT_ACCESSORY_DISPATCH = {
'lightbulb': 'light', 'lightbulb': 'light',
'outlet': 'switch', 'outlet': 'switch',
'thermostat': 'climate',
} }
KNOWN_ACCESSORIES = "{}-accessories".format(DOMAIN) KNOWN_ACCESSORIES = "{}-accessories".format(DOMAIN)
@ -219,8 +220,12 @@ class HomeKitEntity(Entity):
"""Synchronise a HomeKit device state with Home Assistant.""" """Synchronise a HomeKit device state with Home Assistant."""
raise NotImplementedError raise NotImplementedError
def put_characteristics(self, characteristics):
"""Control a HomeKit device state from Home Assistant."""
body = json.dumps({'characteristics': characteristics})
self._securecon.put('/characteristics', body)
# pylint: too-many-function-args
def setup(hass, config): def setup(hass, config):
"""Set up for Homekit devices.""" """Set up for Homekit devices."""
def discovery_dispatch(service, discovery_info): def discovery_dispatch(service, discovery_info):

View File

@ -20,7 +20,7 @@ import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
from homeassistant.loader import bind_hass from homeassistant.loader import bind_hass
REQUIREMENTS = ['pyhomematic==0.1.43'] REQUIREMENTS = ['pyhomematic==0.1.44']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -148,6 +148,7 @@ CONF_PATH = 'path'
CONF_CALLBACK_IP = 'callback_ip' CONF_CALLBACK_IP = 'callback_ip'
CONF_CALLBACK_PORT = 'callback_port' CONF_CALLBACK_PORT = 'callback_port'
CONF_RESOLVENAMES = 'resolvenames' CONF_RESOLVENAMES = 'resolvenames'
CONF_JSONPORT = 'jsonport'
CONF_VARIABLES = 'variables' CONF_VARIABLES = 'variables'
CONF_DEVICES = 'devices' CONF_DEVICES = 'devices'
CONF_PRIMARY = 'primary' CONF_PRIMARY = 'primary'
@ -155,6 +156,7 @@ CONF_PRIMARY = 'primary'
DEFAULT_LOCAL_IP = '0.0.0.0' DEFAULT_LOCAL_IP = '0.0.0.0'
DEFAULT_LOCAL_PORT = 0 DEFAULT_LOCAL_PORT = 0
DEFAULT_RESOLVENAMES = False DEFAULT_RESOLVENAMES = False
DEFAULT_JSONPORT = 80
DEFAULT_PORT = 2001 DEFAULT_PORT = 2001
DEFAULT_PATH = '' DEFAULT_PATH = ''
DEFAULT_USERNAME = 'Admin' DEFAULT_USERNAME = 'Admin'
@ -178,6 +180,7 @@ CONFIG_SCHEMA = vol.Schema({
vol.Optional(CONF_PATH, default=DEFAULT_PATH): cv.string, vol.Optional(CONF_PATH, default=DEFAULT_PATH): cv.string,
vol.Optional(CONF_RESOLVENAMES, default=DEFAULT_RESOLVENAMES): vol.Optional(CONF_RESOLVENAMES, default=DEFAULT_RESOLVENAMES):
vol.In(CONF_RESOLVENAMES_OPTIONS), vol.In(CONF_RESOLVENAMES_OPTIONS),
vol.Optional(CONF_JSONPORT, default=DEFAULT_JSONPORT): cv.port,
vol.Optional(CONF_USERNAME, default=DEFAULT_USERNAME): cv.string, vol.Optional(CONF_USERNAME, default=DEFAULT_USERNAME): cv.string,
vol.Optional(CONF_PASSWORD, default=DEFAULT_PASSWORD): cv.string, vol.Optional(CONF_PASSWORD, default=DEFAULT_PASSWORD): cv.string,
vol.Optional(CONF_CALLBACK_IP): cv.string, vol.Optional(CONF_CALLBACK_IP): cv.string,
@ -299,6 +302,7 @@ def setup(hass, config):
'port': rconfig.get(CONF_PORT), 'port': rconfig.get(CONF_PORT),
'path': rconfig.get(CONF_PATH), 'path': rconfig.get(CONF_PATH),
'resolvenames': rconfig.get(CONF_RESOLVENAMES), 'resolvenames': rconfig.get(CONF_RESOLVENAMES),
'jsonport': rconfig.get(CONF_JSONPORT),
'username': rconfig.get(CONF_USERNAME), 'username': rconfig.get(CONF_USERNAME),
'password': rconfig.get(CONF_PASSWORD), 'password': rconfig.get(CONF_PASSWORD),
'callbackip': rconfig.get(CONF_CALLBACK_IP), 'callbackip': rconfig.get(CONF_CALLBACK_IP),

View File

@ -40,33 +40,29 @@ CONF_SERVER_HOST = 'server_host'
CONF_SERVER_PORT = 'server_port' CONF_SERVER_PORT = 'server_port'
CONF_BASE_URL = 'base_url' CONF_BASE_URL = 'base_url'
CONF_SSL_CERTIFICATE = 'ssl_certificate' CONF_SSL_CERTIFICATE = 'ssl_certificate'
CONF_SSL_PEER_CERTIFICATE = 'ssl_peer_certificate'
CONF_SSL_KEY = 'ssl_key' CONF_SSL_KEY = 'ssl_key'
CONF_CORS_ORIGINS = 'cors_allowed_origins' CONF_CORS_ORIGINS = 'cors_allowed_origins'
CONF_USE_X_FORWARDED_FOR = 'use_x_forwarded_for' CONF_USE_X_FORWARDED_FOR = 'use_x_forwarded_for'
CONF_TRUSTED_PROXIES = 'trusted_proxies'
CONF_TRUSTED_NETWORKS = 'trusted_networks' CONF_TRUSTED_NETWORKS = 'trusted_networks'
CONF_LOGIN_ATTEMPTS_THRESHOLD = 'login_attempts_threshold' CONF_LOGIN_ATTEMPTS_THRESHOLD = 'login_attempts_threshold'
CONF_IP_BAN_ENABLED = 'ip_ban_enabled' CONF_IP_BAN_ENABLED = 'ip_ban_enabled'
# TLS configuration follows the best-practice guidelines specified here: # TLS configuration follows the best-practice guidelines specified here:
# https://wiki.mozilla.org/Security/Server_Side_TLS # https://wiki.mozilla.org/Security/Server_Side_TLS
# Intermediate guidelines are followed. # Modern guidelines are followed.
SSL_VERSION = ssl.PROTOCOL_SSLv23 SSL_VERSION = ssl.PROTOCOL_TLS # pylint: disable=no-member
SSL_OPTS = ssl.OP_NO_SSLv2 | ssl.OP_NO_SSLv3 SSL_OPTS = ssl.OP_NO_SSLv2 | ssl.OP_NO_SSLv3 | \
ssl.OP_NO_TLSv1 | ssl.OP_NO_TLSv1_1 | \
ssl.OP_CIPHER_SERVER_PREFERENCE
if hasattr(ssl, 'OP_NO_COMPRESSION'): if hasattr(ssl, 'OP_NO_COMPRESSION'):
SSL_OPTS |= ssl.OP_NO_COMPRESSION SSL_OPTS |= ssl.OP_NO_COMPRESSION
CIPHERS = "ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:" \ CIPHERS = "ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:" \
"ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:" \
"ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:" \ "ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:" \
"ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:" \ "ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:" \
"DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:" \ "ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256"
"ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:" \
"ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:" \
"ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES256-SHA384:" \
"ECDHE-ECDSA-AES256-SHA:ECDHE-RSA-AES256-SHA:" \
"DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:" \
"DHE-RSA-AES256-SHA:ECDHE-ECDSA-DES-CBC3-SHA:" \
"ECDHE-RSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:AES128-GCM-SHA256:" \
"AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:" \
"AES256-SHA:DES-CBC3-SHA:!DSS"
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -80,10 +76,13 @@ HTTP_SCHEMA = vol.Schema({
vol.Optional(CONF_SERVER_PORT, default=SERVER_PORT): cv.port, vol.Optional(CONF_SERVER_PORT, default=SERVER_PORT): cv.port,
vol.Optional(CONF_BASE_URL): cv.string, vol.Optional(CONF_BASE_URL): cv.string,
vol.Optional(CONF_SSL_CERTIFICATE): cv.isfile, vol.Optional(CONF_SSL_CERTIFICATE): cv.isfile,
vol.Optional(CONF_SSL_PEER_CERTIFICATE): cv.isfile,
vol.Optional(CONF_SSL_KEY): cv.isfile, vol.Optional(CONF_SSL_KEY): cv.isfile,
vol.Optional(CONF_CORS_ORIGINS, default=[]): vol.Optional(CONF_CORS_ORIGINS, default=[]):
vol.All(cv.ensure_list, [cv.string]), vol.All(cv.ensure_list, [cv.string]),
vol.Optional(CONF_USE_X_FORWARDED_FOR, default=False): cv.boolean, vol.Optional(CONF_USE_X_FORWARDED_FOR, default=False): cv.boolean,
vol.Optional(CONF_TRUSTED_PROXIES, default=[]):
vol.All(cv.ensure_list, [ip_network]),
vol.Optional(CONF_TRUSTED_NETWORKS, default=[]): vol.Optional(CONF_TRUSTED_NETWORKS, default=[]):
vol.All(cv.ensure_list, [ip_network]), vol.All(cv.ensure_list, [ip_network]),
vol.Optional(CONF_LOGIN_ATTEMPTS_THRESHOLD, vol.Optional(CONF_LOGIN_ATTEMPTS_THRESHOLD,
@ -108,9 +107,11 @@ async def async_setup(hass, config):
server_host = conf[CONF_SERVER_HOST] server_host = conf[CONF_SERVER_HOST]
server_port = conf[CONF_SERVER_PORT] server_port = conf[CONF_SERVER_PORT]
ssl_certificate = conf.get(CONF_SSL_CERTIFICATE) ssl_certificate = conf.get(CONF_SSL_CERTIFICATE)
ssl_peer_certificate = conf.get(CONF_SSL_PEER_CERTIFICATE)
ssl_key = conf.get(CONF_SSL_KEY) ssl_key = conf.get(CONF_SSL_KEY)
cors_origins = conf[CONF_CORS_ORIGINS] cors_origins = conf[CONF_CORS_ORIGINS]
use_x_forwarded_for = conf[CONF_USE_X_FORWARDED_FOR] use_x_forwarded_for = conf[CONF_USE_X_FORWARDED_FOR]
trusted_proxies = conf[CONF_TRUSTED_PROXIES]
trusted_networks = conf[CONF_TRUSTED_NETWORKS] trusted_networks = conf[CONF_TRUSTED_NETWORKS]
is_ban_enabled = conf[CONF_IP_BAN_ENABLED] is_ban_enabled = conf[CONF_IP_BAN_ENABLED]
login_threshold = conf[CONF_LOGIN_ATTEMPTS_THRESHOLD] login_threshold = conf[CONF_LOGIN_ATTEMPTS_THRESHOLD]
@ -125,9 +126,11 @@ async def async_setup(hass, config):
server_port=server_port, server_port=server_port,
api_password=api_password, api_password=api_password,
ssl_certificate=ssl_certificate, ssl_certificate=ssl_certificate,
ssl_peer_certificate=ssl_peer_certificate,
ssl_key=ssl_key, ssl_key=ssl_key,
cors_origins=cors_origins, cors_origins=cors_origins,
use_x_forwarded_for=use_x_forwarded_for, use_x_forwarded_for=use_x_forwarded_for,
trusted_proxies=trusted_proxies,
trusted_networks=trusted_networks, trusted_networks=trusted_networks,
login_threshold=login_threshold, login_threshold=login_threshold,
is_ban_enabled=is_ban_enabled is_ban_enabled=is_ban_enabled
@ -166,21 +169,37 @@ async def async_setup(hass, config):
class HomeAssistantHTTP(object): class HomeAssistantHTTP(object):
"""HTTP server for Home Assistant.""" """HTTP server for Home Assistant."""
def __init__(self, hass, api_password, ssl_certificate, def __init__(self, hass, api_password,
ssl_certificate, ssl_peer_certificate,
ssl_key, server_host, server_port, cors_origins, ssl_key, server_host, server_port, cors_origins,
use_x_forwarded_for, trusted_networks, use_x_forwarded_for, trusted_proxies, trusted_networks,
login_threshold, is_ban_enabled): login_threshold, is_ban_enabled):
"""Initialize the HTTP Home Assistant server.""" """Initialize the HTTP Home Assistant server."""
app = self.app = web.Application( app = self.app = web.Application(
middlewares=[staticresource_middleware]) middlewares=[staticresource_middleware])
# This order matters # This order matters
setup_real_ip(app, use_x_forwarded_for) setup_real_ip(app, use_x_forwarded_for, trusted_proxies)
if is_ban_enabled: if is_ban_enabled:
setup_bans(hass, app, login_threshold) setup_bans(hass, app, login_threshold)
setup_auth(app, trusted_networks, api_password) if hass.auth.active:
if hass.auth.support_legacy:
_LOGGER.warning("Experimental auth api enabled and "
"legacy_api_password support enabled. Please "
"use access_token instead api_password, "
"although you can still use legacy "
"api_password")
else:
_LOGGER.warning("Experimental auth api enabled. Please use "
"access_token instead api_password.")
elif api_password is None:
_LOGGER.warning("You have been advised to set http.api_password.")
setup_auth(app, trusted_networks, hass.auth.active,
support_legacy=hass.auth.support_legacy,
api_password=api_password)
if cors_origins: if cors_origins:
setup_cors(app, cors_origins) setup_cors(app, cors_origins)
@ -190,6 +209,7 @@ class HomeAssistantHTTP(object):
self.hass = hass self.hass = hass
self.api_password = api_password self.api_password = api_password
self.ssl_certificate = ssl_certificate self.ssl_certificate = ssl_certificate
self.ssl_peer_certificate = ssl_peer_certificate
self.ssl_key = ssl_key self.ssl_key = ssl_key
self.server_host = server_host self.server_host = server_host
self.server_port = server_port self.server_port = server_port
@ -287,8 +307,12 @@ class HomeAssistantHTTP(object):
except OSError as error: except OSError as error:
_LOGGER.error("Could not read SSL certificate from %s: %s", _LOGGER.error("Could not read SSL certificate from %s: %s",
self.ssl_certificate, error) self.ssl_certificate, error)
context = None
return return
if self.ssl_peer_certificate:
context.verify_mode = ssl.CERT_REQUIRED
context.load_verify_locations(cafile=self.ssl_peer_certificate)
else: else:
context = None context = None

View File

@ -17,37 +17,44 @@ _LOGGER = logging.getLogger(__name__)
@callback @callback
def setup_auth(app, trusted_networks, api_password): def setup_auth(app, trusted_networks, use_auth,
support_legacy=False, api_password=None):
"""Create auth middleware for the app.""" """Create auth middleware for the app."""
@middleware @middleware
async def auth_middleware(request, handler): async def auth_middleware(request, handler):
"""Authenticate as middleware.""" """Authenticate as middleware."""
# If no password set, just always set authenticated=True
if api_password is None:
request[KEY_AUTHENTICATED] = True
return await handler(request)
# Check authentication
authenticated = False authenticated = False
if (HTTP_HEADER_HA_AUTH in request.headers and if use_auth and (HTTP_HEADER_HA_AUTH in request.headers or
hmac.compare_digest( DATA_API_PASSWORD in request.query):
api_password.encode('utf-8'), _LOGGER.warning('Please use access_token instead api_password.')
request.headers[HTTP_HEADER_HA_AUTH].encode('utf-8'))):
legacy_auth = (not use_auth or support_legacy) and api_password
if (hdrs.AUTHORIZATION in request.headers and
await async_validate_auth_header(
request, api_password if legacy_auth else None)):
# it included both use_auth and api_password Basic auth
authenticated = True
elif (legacy_auth and HTTP_HEADER_HA_AUTH in request.headers and
hmac.compare_digest(
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 (legacy_auth and DATA_API_PASSWORD in request.query and
hmac.compare_digest( hmac.compare_digest(
api_password.encode('utf-8'), api_password.encode('utf-8'),
request.query[DATA_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 _is_trusted_ip(request, trusted_networks):
await async_validate_auth_header(api_password, request)):
authenticated = True authenticated = True
elif _is_trusted_ip(request, trusted_networks): elif not use_auth and api_password is None:
# If neither password nor auth_providers set,
# just always set authenticated=True
authenticated = True authenticated = True
request[KEY_AUTHENTICATED] = authenticated request[KEY_AUTHENTICATED] = authenticated
@ -76,8 +83,12 @@ def validate_password(request, api_password):
request.app['hass'].http.api_password.encode('utf-8')) request.app['hass'].http.api_password.encode('utf-8'))
async def async_validate_auth_header(api_password, request): async def async_validate_auth_header(request, api_password=None):
"""Test an authorization header if valid password.""" """
Test authorization header against access token.
Basic auth_type is legacy code, should be removed with api_password.
"""
if hdrs.AUTHORIZATION not in request.headers: if hdrs.AUTHORIZATION not in request.headers:
return False return False
@ -88,7 +99,16 @@ async def async_validate_auth_header(api_password, request):
# If no space in authorization header # If no space in authorization header
return False return False
if auth_type == 'Basic': if auth_type == 'Bearer':
hass = request.app['hass']
access_token = hass.auth.async_get_access_token(auth_val)
if access_token is None:
return False
request['hass_user'] = access_token.refresh_token.user
return True
elif auth_type == 'Basic' and api_password is not None:
decoded = base64.b64decode(auth_val).decode('utf-8') decoded = base64.b64decode(auth_val).decode('utf-8')
try: try:
username, password = decoded.split(':', 1) username, password = decoded.split(':', 1)
@ -102,13 +122,5 @@ async def async_validate_auth_header(api_password, request):
return hmac.compare_digest(api_password.encode('utf-8'), return hmac.compare_digest(api_password.encode('utf-8'),
password.encode('utf-8')) password.encode('utf-8'))
if auth_type != 'Bearer': else:
return False return False
hass = request.app['hass']
access_token = hass.auth.async_get_access_token(auth_val)
if access_token is None:
return False
request['hass_user'] = access_token.refresh_token.user
return True

View File

@ -11,18 +11,25 @@ from .const import KEY_REAL_IP
@callback @callback
def setup_real_ip(app, use_x_forwarded_for): def setup_real_ip(app, use_x_forwarded_for, trusted_proxies):
"""Create IP Ban middleware for the app.""" """Create IP Ban middleware for the app."""
@middleware @middleware
async def real_ip_middleware(request, handler): async def real_ip_middleware(request, handler):
"""Real IP middleware.""" """Real IP middleware."""
if (use_x_forwarded_for and connected_ip = ip_address(
X_FORWARDED_FOR in request.headers): request.transport.get_extra_info('peername')[0])
request[KEY_REAL_IP] = ip_address( request[KEY_REAL_IP] = connected_ip
request.headers.get(X_FORWARDED_FOR).split(',')[0])
else: # Only use the XFF header if enabled, present, and from a trusted proxy
request[KEY_REAL_IP] = \ try:
ip_address(request.transport.get_extra_info('peername')[0]) if (use_x_forwarded_for and
X_FORWARDED_FOR in request.headers and
any(connected_ip in trusted_proxy
for trusted_proxy in trusted_proxies)):
request[KEY_REAL_IP] = ip_address(
request.headers.get(X_FORWARDED_FOR).split(', ')[-1])
except ValueError:
pass
return await handler(request) return await handler(request)

View File

@ -18,7 +18,6 @@ class CachingStaticResource(StaticResource):
filename = URL(request.match_info['filename']).path filename = URL(request.match_info['filename']).path
try: try:
# PyLint is wrong about resolve not being a member. # PyLint is wrong about resolve not being a member.
# pylint: disable=no-member
filepath = self._directory.joinpath(filename).resolve() filepath = self._directory.joinpath(filename).resolve()
if not self._follow_symlinks: if not self._follow_symlinks:
filepath.relative_to(self._directory) filepath.relative_to(self._directory)

View File

@ -24,6 +24,6 @@
"title": "Hub verbinden" "title": "Hub verbinden"
} }
}, },
"title": "Philips Hue Bridge" "title": ""
} }
} }

View File

@ -24,6 +24,6 @@
"title": "\u0421\u0432\u044f\u0437\u044c \u0441 \u0445\u0430\u0431\u043e\u043c" "title": "\u0421\u0432\u044f\u0437\u044c \u0441 \u0445\u0430\u0431\u043e\u043c"
} }
}, },
"title": "\u0428\u043b\u044e\u0437 Philips Hue" "title": "Philips Hue"
} }
} }

View File

@ -124,24 +124,16 @@ class HueBridge(object):
(group for group in self.api.groups.values() (group for group in self.api.groups.values()
if group.name == group_name), None) if group.name == group_name), None)
# The same scene name can exist in multiple groups. # Additional scene logic to handle duplicate scene names across groups
# In this case, activate first scene that contains the scene = next(
# the exact same light IDs as the group (scene for scene in self.api.scenes.values()
scenes = [] if scene.name == scene_name
for scene in self.api.scenes.values(): and group is not None
if scene.name == scene_name: and sorted(scene.lights) == sorted(group.lights)),
scenes.append(scene) None)
if len(scenes) == 1:
scene_id = scenes[0].id
else:
group_lights = sorted(group.lights)
for scene in scenes:
if group_lights == scene.lights:
scene_id = scene.id
break
# If we can't find it, fetch latest info. # If we can't find it, fetch latest info.
if not updated and (group is None or scene_id is None): if not updated and (group is None or scene is None):
await self.api.groups.update() await self.api.groups.update()
await self.api.scenes.update() await self.api.scenes.update()
await self.hue_activate_scene(call, updated=True) await self.hue_activate_scene(call, updated=True)
@ -151,11 +143,11 @@ class HueBridge(object):
LOGGER.warning('Unable to find group %s', group_name) LOGGER.warning('Unable to find group %s', group_name)
return return
if scene_id is None: if scene is None:
LOGGER.warning('Unable to find scene %s', scene_name) LOGGER.warning('Unable to find scene %s', scene_name)
return return
await group.set_action(scene=scene_id) await group.set_action(scene=scene.id)
async def get_bridge(hass, host, username=None): async def get_bridge(hass, host, username=None):

View File

@ -16,7 +16,7 @@ from homeassistant.components.image_processing import (
from homeassistant.core import split_entity_id from homeassistant.core import split_entity_id
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['numpy==1.14.3'] REQUIREMENTS = ['numpy==1.14.5']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -152,7 +152,6 @@ class OpenCVImageProcessor(ImageProcessingEntity):
import cv2 # pylint: disable=import-error import cv2 # pylint: disable=import-error
import numpy import numpy
# pylint: disable=no-member
cv_image = cv2.imdecode( cv_image = cv2.imdecode(
numpy.asarray(bytearray(image)), cv2.IMREAD_UNCHANGED) numpy.asarray(bytearray(image)), cv2.IMREAD_UNCHANGED)
@ -168,7 +167,6 @@ class OpenCVImageProcessor(ImageProcessingEntity):
else: else:
path = classifier path = classifier
# pylint: disable=no-member
cascade = cv2.CascadeClassifier(path) cascade = cv2.CascadeClassifier(path)
detections = cascade.detectMultiScale( detections = cascade.detectMultiScale(

View File

@ -17,7 +17,7 @@ import homeassistant.helpers.config_validation as cv
from homeassistant.helpers import discovery from homeassistant.helpers import discovery
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
REQUIREMENTS = ['insteonplm==0.10.0'] REQUIREMENTS = ['insteonplm==0.11.3']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -300,7 +300,8 @@ class IPDB(object):
OpenClosedRelay) OpenClosedRelay)
from insteonplm.states.dimmable import (DimmableSwitch, from insteonplm.states.dimmable import (DimmableSwitch,
DimmableSwitch_Fan) DimmableSwitch_Fan,
DimmableRemote)
from insteonplm.states.sensor import (VariableSensor, from insteonplm.states.sensor import (VariableSensor,
OnOffSensor, OnOffSensor,
@ -328,6 +329,7 @@ class IPDB(object):
State(DimmableSwitch_Fan, 'fan'), State(DimmableSwitch_Fan, 'fan'),
State(DimmableSwitch, 'light'), State(DimmableSwitch, 'light'),
State(DimmableRemote, 'binary_sensor'),
State(X10DimmableSwitch, 'light'), State(X10DimmableSwitch, 'light'),
State(X10OnOffSwitch, 'switch'), State(X10OnOffSwitch, 'switch'),

View File

@ -181,7 +181,6 @@ def devices_with_push():
def enabled_push_ids(): def enabled_push_ids():
"""Return a list of push enabled target push IDs.""" """Return a list of push enabled target push IDs."""
push_ids = list() push_ids = list()
# pylint: disable=unused-variable
for device in CONFIG_FILE[ATTR_DEVICES].values(): for device in CONFIG_FILE[ATTR_DEVICES].values():
if device.get(ATTR_PUSH_ID) is not None: if device.get(ATTR_PUSH_ID) is not None:
push_ids.append(device.get(ATTR_PUSH_ID)) push_ids.append(device.get(ATTR_PUSH_ID))
@ -203,7 +202,6 @@ def device_name_for_push_id(push_id):
def setup(hass, config): def setup(hass, config):
"""Set up the iOS component.""" """Set up the iOS component."""
# pylint: disable=import-error
global CONFIG_FILE global CONFIG_FILE
global CONFIG_FILE_PATH global CONFIG_FILE_PATH

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