mirror of
https://github.com/home-assistant/core.git
synced 2025-07-22 20:57:21 +00:00
commit
a1d8b0e9b3
@ -192,7 +192,7 @@ omit =
|
||||
homeassistant/components/mychevy.py
|
||||
homeassistant/components/*/mychevy.py
|
||||
|
||||
homeassistant/components/mysensors.py
|
||||
homeassistant/components/mysensors/*
|
||||
homeassistant/components/*/mysensors.py
|
||||
|
||||
homeassistant/components/neato.py
|
||||
|
3
.gitignore
vendored
3
.gitignore
vendored
@ -107,3 +107,6 @@ desktop.ini
|
||||
|
||||
# Secrets
|
||||
.lokalise_token
|
||||
|
||||
# monkeytype
|
||||
monkeytype.sqlite3
|
||||
|
@ -1,26 +1,27 @@
|
||||
"""Provide an authentication layer for Home Assistant."""
|
||||
import asyncio
|
||||
import binascii
|
||||
from collections import OrderedDict
|
||||
from datetime import datetime, timedelta
|
||||
import os
|
||||
import importlib
|
||||
import logging
|
||||
import os
|
||||
import uuid
|
||||
from collections import OrderedDict
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
import attr
|
||||
import voluptuous as vol
|
||||
from voluptuous.humanize import humanize_error
|
||||
|
||||
from homeassistant import data_entry_flow, requirements
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.const import CONF_TYPE, CONF_NAME, CONF_ID
|
||||
from homeassistant.util.decorator import Registry
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from homeassistant.util.decorator import Registry
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
STORAGE_VERSION = 1
|
||||
STORAGE_KEY = 'auth'
|
||||
|
||||
AUTH_PROVIDERS = Registry()
|
||||
|
||||
@ -121,23 +122,12 @@ class User:
|
||||
is_owner = attr.ib(type=bool, default=False)
|
||||
is_active = attr.ib(type=bool, default=False)
|
||||
name = attr.ib(type=str, default=None)
|
||||
# For persisting and see if saved?
|
||||
# store = attr.ib(type=AuthStore, default=None)
|
||||
|
||||
# List of credentials of a user.
|
||||
credentials = attr.ib(type=list, default=attr.Factory(list))
|
||||
credentials = attr.ib(type=list, default=attr.Factory(list), cmp=False)
|
||||
|
||||
# Tokens associated with a user.
|
||||
refresh_tokens = attr.ib(type=dict, default=attr.Factory(dict))
|
||||
|
||||
def as_dict(self):
|
||||
"""Convert user object to a dictionary."""
|
||||
return {
|
||||
'id': self.id,
|
||||
'is_owner': self.is_owner,
|
||||
'is_active': self.is_active,
|
||||
'name': self.name,
|
||||
}
|
||||
refresh_tokens = attr.ib(type=dict, default=attr.Factory(dict), cmp=False)
|
||||
|
||||
|
||||
@attr.s(slots=True)
|
||||
@ -152,7 +142,7 @@ class RefreshToken:
|
||||
default=ACCESS_TOKEN_EXPIRATION)
|
||||
token = attr.ib(type=str,
|
||||
default=attr.Factory(lambda: generate_secret(64)))
|
||||
access_tokens = attr.ib(type=list, default=attr.Factory(list))
|
||||
access_tokens = attr.ib(type=list, default=attr.Factory(list), cmp=False)
|
||||
|
||||
|
||||
@attr.s(slots=True)
|
||||
@ -168,9 +158,10 @@ class AccessToken:
|
||||
default=attr.Factory(generate_secret))
|
||||
|
||||
@property
|
||||
def expires(self):
|
||||
"""Return datetime when this token expires."""
|
||||
return self.created_at + self.refresh_token.access_token_expiration
|
||||
def expired(self):
|
||||
"""Return if this token has expired."""
|
||||
expires = self.created_at + self.refresh_token.access_token_expiration
|
||||
return dt_util.utcnow() > expires
|
||||
|
||||
|
||||
@attr.s(slots=True)
|
||||
@ -281,7 +272,24 @@ class AuthManager:
|
||||
self.login_flow = data_entry_flow.FlowManager(
|
||||
hass, self._async_create_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
|
||||
def async_auth_providers(self):
|
||||
@ -317,13 +325,22 @@ class AuthManager:
|
||||
def async_create_access_token(self, refresh_token):
|
||||
"""Create a new access token."""
|
||||
access_token = AccessToken(refresh_token)
|
||||
self.access_tokens[access_token.token] = access_token
|
||||
self._access_tokens[access_token.token] = access_token
|
||||
return access_token
|
||||
|
||||
@callback
|
||||
def async_get_access_token(self, token):
|
||||
"""Get an access token."""
|
||||
return self.access_tokens.get(token)
|
||||
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,
|
||||
no_secret=False):
|
||||
@ -331,6 +348,16 @@ class AuthManager:
|
||||
return await self._store.async_create_client(
|
||||
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):
|
||||
"""Get a client."""
|
||||
return await self._store.async_get_client(client_id)
|
||||
@ -374,29 +401,36 @@ class AuthStore:
|
||||
def __init__(self, hass):
|
||||
"""Initialize the auth store."""
|
||||
self.hass = hass
|
||||
self.users = None
|
||||
self.clients = None
|
||||
self._load_lock = asyncio.Lock(loop=hass.loop)
|
||||
self._users = None
|
||||
self._clients = None
|
||||
self._store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY)
|
||||
|
||||
async def credentials_for_provider(self, provider_type, provider_id):
|
||||
"""Return credentials for specific auth provider type and id."""
|
||||
if self.users is None:
|
||||
if self._users is None:
|
||||
await self.async_load()
|
||||
|
||||
return [
|
||||
credentials
|
||||
for user in self.users.values()
|
||||
for user in self._users.values()
|
||||
for credentials in user.credentials
|
||||
if (credentials.auth_provider_type == provider_type and
|
||||
credentials.auth_provider_id == provider_id)
|
||||
]
|
||||
|
||||
async def async_get_user(self, user_id):
|
||||
"""Retrieve a user."""
|
||||
if self.users is None:
|
||||
async def async_get_users(self):
|
||||
"""Retrieve all users."""
|
||||
if self._users is None:
|
||||
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):
|
||||
"""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
|
||||
in user if the credentials are new.
|
||||
"""
|
||||
if self.users is None:
|
||||
if self._users is None:
|
||||
await self.async_load()
|
||||
|
||||
# New credentials, store in user
|
||||
@ -412,7 +446,7 @@ class AuthStore:
|
||||
info = await auth_provider.async_user_meta_for_credentials(
|
||||
credentials)
|
||||
# Make owner and activate user if it's the first user.
|
||||
if self.users:
|
||||
if self._users:
|
||||
is_owner = False
|
||||
is_active = False
|
||||
else:
|
||||
@ -424,11 +458,11 @@ class AuthStore:
|
||||
is_active=is_active,
|
||||
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)
|
||||
return new_user
|
||||
|
||||
for user in self.users.values():
|
||||
for user in self._users.values():
|
||||
for creds in user.credentials:
|
||||
if (creds.auth_provider_type == credentials.auth_provider_type
|
||||
and creds.auth_provider_id ==
|
||||
@ -445,11 +479,19 @@ class AuthStore:
|
||||
|
||||
async def async_remove_user(self, user):
|
||||
"""Remove a user."""
|
||||
self.users.pop(user.id)
|
||||
self._users.pop(user.id)
|
||||
await self.async_save()
|
||||
|
||||
async def async_create_refresh_token(self, user, client_id):
|
||||
"""Create a new token for a user."""
|
||||
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)
|
||||
user.refresh_tokens[refresh_token.token] = refresh_token
|
||||
await self.async_save()
|
||||
@ -457,10 +499,10 @@ class AuthStore:
|
||||
|
||||
async def async_get_refresh_token(self, token):
|
||||
"""Get refresh token by token."""
|
||||
if self.users is None:
|
||||
if self._users is None:
|
||||
await self.async_load()
|
||||
|
||||
for user in self.users.values():
|
||||
for user in self._users.values():
|
||||
refresh_token = user.refresh_tokens.get(token)
|
||||
if refresh_token is not None:
|
||||
return refresh_token
|
||||
@ -469,7 +511,7 @@ class AuthStore:
|
||||
|
||||
async def async_create_client(self, name, redirect_uris, no_secret):
|
||||
"""Create a new client."""
|
||||
if self.clients is None:
|
||||
if self._clients is None:
|
||||
await self.async_load()
|
||||
|
||||
kwargs = {
|
||||
@ -481,23 +523,148 @@ class AuthStore:
|
||||
kwargs['secret'] = None
|
||||
|
||||
client = Client(**kwargs)
|
||||
self.clients[client.id] = client
|
||||
self._clients[client.id] = client
|
||||
await self.async_save()
|
||||
return client
|
||||
|
||||
async def async_get_client(self, client_id):
|
||||
"""Get a client."""
|
||||
if self.clients is None:
|
||||
async def async_get_clients(self):
|
||||
"""Return all clients."""
|
||||
if self._clients is None:
|
||||
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):
|
||||
"""Load the users."""
|
||||
async with self._load_lock:
|
||||
self.users = {}
|
||||
self.clients = {}
|
||||
data = await self._store.async_load()
|
||||
|
||||
# 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):
|
||||
"""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)
|
||||
|
@ -8,10 +8,10 @@ import voluptuous as vol
|
||||
|
||||
from homeassistant import auth, data_entry_flow
|
||||
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({
|
||||
}, extra=vol.PREVENT_EXTRA)
|
||||
@ -31,14 +31,22 @@ class InvalidUser(HomeAssistantError):
|
||||
class Data:
|
||||
"""Hold the user data."""
|
||||
|
||||
def __init__(self, path, data):
|
||||
def __init__(self, hass):
|
||||
"""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:
|
||||
data = {
|
||||
'salt': auth.generate_secret(),
|
||||
'users': []
|
||||
}
|
||||
|
||||
self._data = data
|
||||
|
||||
@property
|
||||
@ -99,14 +107,9 @@ class Data:
|
||||
else:
|
||||
raise InvalidUser
|
||||
|
||||
def save(self):
|
||||
async def async_save(self):
|
||||
"""Save data."""
|
||||
json.save_json(self.path, self._data)
|
||||
|
||||
|
||||
def load_data(path):
|
||||
"""Load auth data."""
|
||||
return Data(path, json.load_json(path, None))
|
||||
await self._store.async_save(self._data)
|
||||
|
||||
|
||||
@auth.AUTH_PROVIDERS.register('homeassistant')
|
||||
@ -121,12 +124,10 @@ class HassAuthProvider(auth.AuthProvider):
|
||||
|
||||
async def async_validate_login(self, username, password):
|
||||
"""Helper to validate a username and password."""
|
||||
def validate():
|
||||
"""Validate creds."""
|
||||
data = self._auth_data()
|
||||
data.validate_login(username, password)
|
||||
|
||||
await self.hass.async_add_job(validate)
|
||||
data = Data(self.hass)
|
||||
await data.async_load()
|
||||
await self.hass.async_add_executor_job(
|
||||
data.validate_login, username, password)
|
||||
|
||||
async def async_get_or_create_credentials(self, flow_result):
|
||||
"""Get credentials based on the flow result."""
|
||||
@ -141,10 +142,6 @@ class HassAuthProvider(auth.AuthProvider):
|
||||
'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):
|
||||
"""Handler for the login flow."""
|
||||
|
104
homeassistant/auth_providers/legacy_api_password.py
Normal file
104
homeassistant/auth_providers/legacy_api_password.py
Normal 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,
|
||||
)
|
@ -123,7 +123,6 @@ async def async_from_config_dict(config: Dict[str, Any],
|
||||
components.update(hass.config_entries.async_domains())
|
||||
|
||||
# setup components
|
||||
# pylint: disable=not-an-iterable
|
||||
res = await core_components.async_setup(hass, config)
|
||||
if not res:
|
||||
_LOGGER.error("Home Assistant core failed to initialize. "
|
||||
|
@ -154,7 +154,6 @@ def async_setup(hass, config):
|
||||
return True
|
||||
|
||||
|
||||
# pylint: disable=no-self-use
|
||||
class AlarmControlPanel(Entity):
|
||||
"""An abstract class for alarm control devices."""
|
||||
|
||||
|
@ -20,7 +20,7 @@ from homeassistant.const import (
|
||||
from homeassistant.components.mqtt import (
|
||||
CONF_AVAILABILITY_TOPIC, CONF_STATE_TOPIC, CONF_COMMAND_TOPIC,
|
||||
CONF_PAYLOAD_AVAILABLE, CONF_PAYLOAD_NOT_AVAILABLE, CONF_QOS,
|
||||
MqttAvailability)
|
||||
CONF_RETAIN, MqttAvailability)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
_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_COMMAND_TOPIC),
|
||||
config.get(CONF_QOS),
|
||||
config.get(CONF_RETAIN),
|
||||
config.get(CONF_PAYLOAD_DISARM),
|
||||
config.get(CONF_PAYLOAD_ARM_HOME),
|
||||
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):
|
||||
"""Representation of a MQTT alarm status."""
|
||||
|
||||
def __init__(self, name, state_topic, command_topic, qos, payload_disarm,
|
||||
payload_arm_home, payload_arm_away, code, availability_topic,
|
||||
payload_available, payload_not_available):
|
||||
def __init__(self, name, state_topic, command_topic, qos, retain,
|
||||
payload_disarm, payload_arm_home, payload_arm_away, code,
|
||||
availability_topic, payload_available, payload_not_available):
|
||||
"""Init the MQTT Alarm Control Panel."""
|
||||
super().__init__(availability_topic, qos, payload_available,
|
||||
payload_not_available)
|
||||
@ -77,6 +78,7 @@ class MqttAlarm(MqttAvailability, alarm.AlarmControlPanel):
|
||||
self._state_topic = state_topic
|
||||
self._command_topic = command_topic
|
||||
self._qos = qos
|
||||
self._retain = retain
|
||||
self._payload_disarm = payload_disarm
|
||||
self._payload_arm_home = payload_arm_home
|
||||
self._payload_arm_away = payload_arm_away
|
||||
@ -134,7 +136,8 @@ class MqttAlarm(MqttAvailability, alarm.AlarmControlPanel):
|
||||
if not self._validate_code(code, 'disarming'):
|
||||
return
|
||||
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
|
||||
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'):
|
||||
return
|
||||
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
|
||||
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'):
|
||||
return
|
||||
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):
|
||||
"""Validate given code."""
|
||||
|
@ -107,7 +107,6 @@ class _DisplayCategory(object):
|
||||
THERMOSTAT = "THERMOSTAT"
|
||||
|
||||
# Indicates the endpoint is a television.
|
||||
# pylint: disable=invalid-name
|
||||
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']
|
||||
|
||||
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(
|
||||
(k for k, v in API_THERMOSTAT_MODES.items() if v == mode),
|
||||
None
|
||||
|
@ -81,7 +81,6 @@ class APIEventStream(HomeAssistantView):
|
||||
|
||||
async def get(self, request):
|
||||
"""Provide a streaming interface for the event bus."""
|
||||
# pylint: disable=no-self-use
|
||||
hass = request.app['hass']
|
||||
stop_obj = object()
|
||||
to_write = asyncio.Queue(loop=hass.loop)
|
||||
|
@ -16,7 +16,7 @@ from homeassistant.const import (
|
||||
from homeassistant.helpers.event import track_time_interval
|
||||
from homeassistant.helpers.dispatcher import dispatcher_send
|
||||
|
||||
REQUIREMENTS = ['pyarlo==0.1.7']
|
||||
REQUIREMENTS = ['pyarlo==0.1.8']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
@ -16,7 +16,6 @@ _LOGGER = logging.getLogger(__name__)
|
||||
DOMAIN = 'bbb_gpio'
|
||||
|
||||
|
||||
# pylint: disable=no-member
|
||||
def setup(hass, config):
|
||||
"""Set up the BeagleBone Black GPIO component."""
|
||||
# pylint: disable=import-error
|
||||
@ -34,41 +33,39 @@ def setup(hass, config):
|
||||
return True
|
||||
|
||||
|
||||
# noqa: F821
|
||||
|
||||
def setup_output(pin):
|
||||
"""Set up a GPIO as output."""
|
||||
# pylint: disable=import-error,undefined-variable
|
||||
# pylint: disable=import-error
|
||||
import Adafruit_BBIO.GPIO as GPIO
|
||||
GPIO.setup(pin, GPIO.OUT)
|
||||
|
||||
|
||||
def setup_input(pin, pull_mode):
|
||||
"""Set up a GPIO as input."""
|
||||
# pylint: disable=import-error,undefined-variable
|
||||
# pylint: disable=import-error
|
||||
import Adafruit_BBIO.GPIO as GPIO
|
||||
GPIO.setup(pin, GPIO.IN, # noqa: F821
|
||||
GPIO.PUD_DOWN if pull_mode == 'DOWN' # noqa: F821
|
||||
else GPIO.PUD_UP) # noqa: F821
|
||||
GPIO.setup(pin, GPIO.IN,
|
||||
GPIO.PUD_DOWN if pull_mode == 'DOWN'
|
||||
else GPIO.PUD_UP)
|
||||
|
||||
|
||||
def write_output(pin, value):
|
||||
"""Write a value to a GPIO."""
|
||||
# pylint: disable=import-error,undefined-variable
|
||||
# pylint: disable=import-error
|
||||
import Adafruit_BBIO.GPIO as GPIO
|
||||
GPIO.output(pin, value)
|
||||
|
||||
|
||||
def read_input(pin):
|
||||
"""Read a value from a GPIO."""
|
||||
# pylint: disable=import-error,undefined-variable
|
||||
# pylint: disable=import-error
|
||||
import Adafruit_BBIO.GPIO as GPIO
|
||||
return GPIO.input(pin) is GPIO.HIGH
|
||||
|
||||
|
||||
def edge_detect(pin, event_callback, bounce):
|
||||
"""Add detection for RISING and FALLING events."""
|
||||
# pylint: disable=import-error,undefined-variable
|
||||
# pylint: disable=import-error
|
||||
import Adafruit_BBIO.GPIO as GPIO
|
||||
GPIO.add_event_detect(
|
||||
pin, GPIO.BOTH, callback=event_callback, bouncetime=bounce)
|
||||
|
@ -67,7 +67,6 @@ async def async_unload_entry(hass, entry):
|
||||
return await hass.data[DOMAIN].async_unload_entry(entry)
|
||||
|
||||
|
||||
# pylint: disable=no-self-use
|
||||
class BinarySensorDevice(Entity):
|
||||
"""Represent a binary sensor."""
|
||||
|
||||
|
@ -124,11 +124,11 @@ class BMWConnectedDriveSensor(BinarySensorDevice):
|
||||
result['check_control_messages'] = check_control_messages
|
||||
elif self._attribute == 'charging_status':
|
||||
result['charging_status'] = vehicle_state.charging_status.value
|
||||
# pylint: disable=W0212
|
||||
# pylint: disable=protected-access
|
||||
result['last_charging_end_result'] = \
|
||||
vehicle_state._attributes['lastChargingEndResult']
|
||||
if self._attribute == 'connection_status':
|
||||
# pylint: disable=W0212
|
||||
# pylint: disable=protected-access
|
||||
result['connection_status'] = \
|
||||
vehicle_state._attributes['connectionStatus']
|
||||
|
||||
@ -166,7 +166,7 @@ class BMWConnectedDriveSensor(BinarySensorDevice):
|
||||
# device class plug: On means device is plugged in,
|
||||
# Off means device is unplugged
|
||||
if self._attribute == 'connection_status':
|
||||
# pylint: disable=W0212
|
||||
# pylint: disable=protected-access
|
||||
self._state = (vehicle_state._attributes['connectionStatus'] ==
|
||||
'CONNECTED')
|
||||
|
||||
|
@ -14,7 +14,8 @@ from homeassistant.components.binary_sensor import (
|
||||
from homeassistant.components.digital_ocean import (
|
||||
CONF_DROPLETS, ATTR_CREATED_AT, ATTR_DROPLET_ID, ATTR_DROPLET_NAME,
|
||||
ATTR_FEATURES, ATTR_IPV4_ADDRESS, ATTR_IPV6_ADDRESS, ATTR_MEMORY,
|
||||
ATTR_REGION, ATTR_VCPUS, DATA_DIGITAL_OCEAN)
|
||||
ATTR_REGION, ATTR_VCPUS, CONF_ATTRIBUTION, DATA_DIGITAL_OCEAN)
|
||||
from homeassistant.const import ATTR_ATTRIBUTION
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@ -75,6 +76,7 @@ class DigitalOceanBinarySensor(BinarySensorDevice):
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes of the Digital Ocean droplet."""
|
||||
return {
|
||||
ATTR_ATTRIBUTION: CONF_ATTRIBUTION,
|
||||
ATTR_CREATED_AT: self.data.created_at,
|
||||
ATTR_DROPLET_ID: self.data.id,
|
||||
ATTR_DROPLET_NAME: self.data.name,
|
||||
|
@ -16,7 +16,7 @@ from homeassistant.const import (
|
||||
from homeassistant.components.binary_sensor import (
|
||||
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__)
|
||||
|
||||
|
@ -39,7 +39,6 @@ class GC100BinarySensor(BinarySensorDevice):
|
||||
|
||||
def __init__(self, name, port_addr, gc100):
|
||||
"""Initialize the GC100 binary sensor."""
|
||||
# pylint: disable=no-member
|
||||
self._name = name or DEVICE_DEFAULT_NAME
|
||||
self._port_addr = port_addr
|
||||
self._gc100 = gc100
|
||||
|
@ -8,7 +8,7 @@ https://home-assistant.io/components/binary_sensor.isy994/
|
||||
import asyncio
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
from typing import Callable # noqa
|
||||
from typing import Callable
|
||||
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice, DOMAIN
|
||||
|
@ -29,7 +29,8 @@ async def async_setup_platform(
|
||||
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."""
|
||||
|
||||
@property
|
||||
|
@ -31,12 +31,10 @@ CAMERA_BINARY_TYPES = {
|
||||
|
||||
STRUCTURE_BINARY_TYPES = {
|
||||
'away': None,
|
||||
# 'security_state', # pending python-nest update
|
||||
}
|
||||
|
||||
STRUCTURE_BINARY_STATE_MAP = {
|
||||
'away': {'away': True, 'home': False},
|
||||
'security_state': {'deter': True, 'ok': False},
|
||||
}
|
||||
|
||||
_BINARY_TYPES_DEPRECATED = [
|
||||
@ -135,7 +133,7 @@ class NestBinarySensor(NestSensorDevice, BinarySensorDevice):
|
||||
value = getattr(self.device, self.variable)
|
||||
if self.variable in STRUCTURE_BINARY_TYPES:
|
||||
self._state = bool(STRUCTURE_BINARY_STATE_MAP
|
||||
[self.variable][value])
|
||||
[self.variable].get(value))
|
||||
else:
|
||||
self._state = bool(value)
|
||||
|
||||
|
127
homeassistant/components/binary_sensor/rachio.py
Normal file
127
homeassistant/components/binary_sensor/rachio.py
Normal 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()
|
@ -58,7 +58,6 @@ class RPiGPIOBinarySensor(BinarySensorDevice):
|
||||
|
||||
def __init__(self, name, port, pull_mode, bouncetime, invert_logic):
|
||||
"""Initialize the RPi binary sensor."""
|
||||
# pylint: disable=no-member
|
||||
self._name = name or DEVICE_DEFAULT_NAME
|
||||
self._port = port
|
||||
self._pull_mode = pull_mode
|
||||
|
@ -23,7 +23,7 @@ from homeassistant.helpers.entity import generate_entity_id
|
||||
from homeassistant.helpers.event import async_track_state_change
|
||||
from homeassistant.util import utcnow
|
||||
|
||||
REQUIREMENTS = ['numpy==1.14.3']
|
||||
REQUIREMENTS = ['numpy==1.14.5']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
@ -13,7 +13,6 @@ DEPENDENCIES = ['wemo']
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# pylint: disable=too-many-function-args
|
||||
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||
"""Register discovered WeMo binary sensors."""
|
||||
import pywemo.discovery as discovery
|
||||
|
@ -89,9 +89,7 @@ class GoogleCalendarData(object):
|
||||
params['timeMin'] = start_date.isoformat('T')
|
||||
params['timeMax'] = end_date.isoformat('T')
|
||||
|
||||
# pylint: disable=no-member
|
||||
events = await hass.async_add_job(service.events)
|
||||
# pylint: enable=no-member
|
||||
result = await hass.async_add_job(events.list(**params).execute)
|
||||
|
||||
items = result.get('items', [])
|
||||
@ -111,7 +109,7 @@ class GoogleCalendarData(object):
|
||||
service, params = self._prepare_query()
|
||||
params['timeMin'] = dt.now().isoformat('T')
|
||||
|
||||
events = service.events() # pylint: disable=no-member
|
||||
events = service.events()
|
||||
result = events.list(**params).execute()
|
||||
|
||||
items = result.get('items', [])
|
||||
|
@ -322,6 +322,7 @@ class Camera(Entity):
|
||||
except asyncio.CancelledError:
|
||||
_LOGGER.debug("Stream closed by frontend.")
|
||||
response = None
|
||||
raise
|
||||
|
||||
finally:
|
||||
if response is not None:
|
||||
|
@ -10,12 +10,13 @@ from datetime import timedelta
|
||||
from homeassistant.components.camera import Camera
|
||||
from homeassistant.components.neato import (
|
||||
NEATO_MAP_DATA, NEATO_ROBOTS, NEATO_LOGIN)
|
||||
from homeassistant.util import Throttle
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEPENDENCIES = ['neato']
|
||||
|
||||
SCAN_INTERVAL = timedelta(minutes=10)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the Neato Camera."""
|
||||
@ -45,7 +46,6 @@ class NeatoCleaningMap(Camera):
|
||||
self.update()
|
||||
return self._image
|
||||
|
||||
@Throttle(timedelta(seconds=60))
|
||||
def update(self):
|
||||
"""Check the contents of the map list."""
|
||||
self.neato.update_robots()
|
||||
|
@ -233,6 +233,7 @@ class ProxyCamera(Camera):
|
||||
_LOGGER.debug("Stream closed by frontend.")
|
||||
req.close()
|
||||
response = None
|
||||
raise
|
||||
|
||||
finally:
|
||||
if response is not None:
|
||||
|
@ -67,8 +67,6 @@ async def async_setup_platform(hass, config, async_add_devices,
|
||||
]
|
||||
|
||||
for cam in config.get(CONF_CAMERAS, []):
|
||||
# https://github.com/PyCQA/pylint/issues/1830
|
||||
# pylint: disable=stop-iteration-return
|
||||
camera = next(
|
||||
(dc for dc in discovered_cameras
|
||||
if dc[CONF_IMAGE_NAME] == cam[CONF_IMAGE_NAME]), None)
|
||||
|
@ -104,27 +104,25 @@ class XiaomiCamera(Camera):
|
||||
|
||||
dirs = [d for d in ftp.nlst() if '.' not in d]
|
||||
if not dirs:
|
||||
if self._model == MODEL_YI:
|
||||
_LOGGER.warning("There don't appear to be any uploaded videos")
|
||||
return False
|
||||
elif self._model == MODEL_XIAOFANG:
|
||||
_LOGGER.warning("There don't appear to be any folders")
|
||||
return False
|
||||
_LOGGER.warning("There don't appear to be any folders")
|
||||
return False
|
||||
|
||||
first_dir = dirs[-1]
|
||||
try:
|
||||
ftp.cwd(first_dir)
|
||||
except error_perm as exc:
|
||||
_LOGGER.error('Unable to find path: %s - %s', first_dir, exc)
|
||||
return False
|
||||
first_dir = dirs[-1]
|
||||
try:
|
||||
ftp.cwd(first_dir)
|
||||
except error_perm as exc:
|
||||
_LOGGER.error('Unable to find path: %s - %s', first_dir, exc)
|
||||
return False
|
||||
|
||||
if self._model == MODEL_XIAOFANG:
|
||||
dirs = [d for d in ftp.nlst() if '.' not in d]
|
||||
if not dirs:
|
||||
_LOGGER.warning("There don't appear to be any uploaded videos")
|
||||
return False
|
||||
|
||||
latest_dir = dirs[-1]
|
||||
ftp.cwd(latest_dir)
|
||||
latest_dir = dirs[-1]
|
||||
ftp.cwd(latest_dir)
|
||||
|
||||
videos = [v for v in ftp.nlst() if '.tmp' not in v]
|
||||
if not videos:
|
||||
_LOGGER.info('Video folder "%s" is empty; delaying', latest_dir)
|
||||
|
@ -77,7 +77,7 @@ class YiCamera(Camera):
|
||||
"""Retrieve the latest video file from the customized Yi FTP server."""
|
||||
from aioftp import Client, StatusCodeError
|
||||
|
||||
ftp = Client()
|
||||
ftp = Client(loop=self.hass.loop)
|
||||
try:
|
||||
await ftp.connect(self.host)
|
||||
await ftp.login(self.user, self.passwd)
|
||||
|
15
homeassistant/components/cast/.translations/cs.json
Normal file
15
homeassistant/components/cast/.translations/cs.json
Normal 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"
|
||||
}
|
||||
}
|
14
homeassistant/components/cast/.translations/de.json
Normal file
14
homeassistant/components/cast/.translations/de.json
Normal 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": ""
|
||||
}
|
||||
}
|
14
homeassistant/components/cast/.translations/hu.json
Normal file
14
homeassistant/components/cast/.translations/hu.json
Normal 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"
|
||||
}
|
||||
}
|
15
homeassistant/components/cast/.translations/it.json
Normal file
15
homeassistant/components/cast/.translations/it.json
Normal 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"
|
||||
}
|
||||
}
|
15
homeassistant/components/cast/.translations/lb.json
Normal file
15
homeassistant/components/cast/.translations/lb.json
Normal 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"
|
||||
}
|
||||
}
|
15
homeassistant/components/cast/.translations/nl.json
Normal file
15
homeassistant/components/cast/.translations/nl.json
Normal 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"
|
||||
}
|
||||
}
|
15
homeassistant/components/cast/.translations/sl.json
Normal file
15
homeassistant/components/cast/.translations/sl.json
Normal 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"
|
||||
}
|
||||
}
|
15
homeassistant/components/cast/.translations/zh-Hant.json
Normal file
15
homeassistant/components/cast/.translations/zh-Hant.json
Normal 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"
|
||||
}
|
||||
}
|
@ -470,7 +470,6 @@ async def async_unload_entry(hass, entry):
|
||||
class ClimateDevice(Entity):
|
||||
"""Representation of a climate device."""
|
||||
|
||||
# pylint: disable=no-self-use
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the current state."""
|
||||
|
@ -53,7 +53,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
add_devices(devices)
|
||||
|
||||
|
||||
# pylint: disable=import-error, no-name-in-module
|
||||
# pylint: disable=import-error
|
||||
class EQ3BTSmartThermostat(ClimateDevice):
|
||||
"""Representation of an eQ-3 Bluetooth Smart thermostat."""
|
||||
|
||||
|
@ -263,7 +263,6 @@ class GenericThermostat(ClimateDevice):
|
||||
@property
|
||||
def min_temp(self):
|
||||
"""Return the minimum temperature."""
|
||||
# pylint: disable=no-member
|
||||
if self._min_temp:
|
||||
return self._min_temp
|
||||
|
||||
@ -273,7 +272,6 @@ class GenericThermostat(ClimateDevice):
|
||||
@property
|
||||
def max_temp(self):
|
||||
"""Return the maximum temperature."""
|
||||
# pylint: disable=no-member
|
||||
if self._max_temp:
|
||||
return self._max_temp
|
||||
|
||||
|
@ -34,7 +34,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
})
|
||||
|
||||
|
||||
# pylint: disable=unused-variable
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the heatmiser thermostat."""
|
||||
from heatmiserV3 import heatmiser, connection
|
||||
|
130
homeassistant/components/climate/homekit_controller.py
Normal file
130
homeassistant/components/climate/homekit_controller.py
Normal 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
|
@ -129,6 +129,9 @@ PLATFORM_SCHEMA = SCHEMA_BASE.extend({
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
"""Set up the MQTT climate devices."""
|
||||
if discovery_info is not None:
|
||||
config = PLATFORM_SCHEMA(discovery_info)
|
||||
|
||||
template_keys = (
|
||||
CONF_POWER_STATE_TEMPLATE,
|
||||
CONF_MODE_STATE_TEMPLATE,
|
||||
@ -635,11 +638,9 @@ class MqttClimate(MqttAvailability, ClimateDevice):
|
||||
@property
|
||||
def min_temp(self):
|
||||
"""Return the minimum temperature."""
|
||||
# pylint: disable=no-member
|
||||
return self._min_temp
|
||||
|
||||
@property
|
||||
def max_temp(self):
|
||||
"""Return the maximum temperature."""
|
||||
# pylint: disable=no-member
|
||||
return self._max_temp
|
||||
|
@ -26,9 +26,8 @@ DICT_MYS_TO_HA = {
|
||||
'Off': STATE_OFF,
|
||||
}
|
||||
|
||||
SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_TARGET_TEMPERATURE_HIGH |
|
||||
SUPPORT_TARGET_TEMPERATURE_LOW | SUPPORT_FAN_MODE |
|
||||
SUPPORT_OPERATION_MODE)
|
||||
FAN_LIST = ['Auto', 'Min', 'Normal', 'Max']
|
||||
OPERATION_LIST = [STATE_OFF, STATE_AUTO, STATE_COOL, STATE_HEAT]
|
||||
|
||||
|
||||
async def async_setup_platform(
|
||||
@ -39,13 +38,24 @@ async def async_setup_platform(
|
||||
async_add_devices=async_add_devices)
|
||||
|
||||
|
||||
class MySensorsHVAC(mysensors.MySensorsEntity, ClimateDevice):
|
||||
class MySensorsHVAC(mysensors.device.MySensorsEntity, ClimateDevice):
|
||||
"""Representation of a MySensors HVAC."""
|
||||
|
||||
@property
|
||||
def supported_features(self):
|
||||
"""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
|
||||
def assumed_state(self):
|
||||
@ -103,7 +113,7 @@ class MySensorsHVAC(mysensors.MySensorsEntity, ClimateDevice):
|
||||
@property
|
||||
def operation_list(self):
|
||||
"""List of available operation modes."""
|
||||
return [STATE_OFF, STATE_AUTO, STATE_COOL, STATE_HEAT]
|
||||
return OPERATION_LIST
|
||||
|
||||
@property
|
||||
def current_fan_mode(self):
|
||||
@ -113,7 +123,7 @@ class MySensorsHVAC(mysensors.MySensorsEntity, ClimateDevice):
|
||||
@property
|
||||
def fan_list(self):
|
||||
"""List of available fan modes."""
|
||||
return ['Auto', 'Min', 'Normal', 'Max']
|
||||
return FAN_LIST
|
||||
|
||||
async def async_set_temperature(self, **kwargs):
|
||||
"""Set new target temperature."""
|
||||
|
@ -5,15 +5,15 @@ For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/climate.zwave/
|
||||
"""
|
||||
# Because we do not compile openzwave on CI
|
||||
# pylint: disable=import-error
|
||||
import logging
|
||||
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)
|
||||
from homeassistant.components.zwave import ZWaveDeviceEntity
|
||||
from homeassistant.components.zwave import async_setup_platform # noqa # pylint: disable=unused-import
|
||||
from homeassistant.const import (
|
||||
TEMP_CELSIUS, TEMP_FAHRENHEIT, ATTR_TEMPERATURE)
|
||||
STATE_OFF, TEMP_CELSIUS, TEMP_FAHRENHEIT, ATTR_TEMPERATURE)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@ -32,6 +32,15 @@ DEVICE_MAPPINGS = {
|
||||
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):
|
||||
"""Create Z-Wave entity device."""
|
||||
@ -49,6 +58,7 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice):
|
||||
self._current_temperature = None
|
||||
self._current_operation = None
|
||||
self._operation_list = None
|
||||
self._operation_mapping = None
|
||||
self._operating_state = None
|
||||
self._current_fan_mode = None
|
||||
self._fan_list = None
|
||||
@ -87,10 +97,21 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice):
|
||||
"""Handle the data changes for node values."""
|
||||
# Operation 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
|
||||
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._current_operation=%s", self._current_operation)
|
||||
|
||||
@ -206,7 +227,8 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice):
|
||||
def set_operation_mode(self, operation_mode):
|
||||
"""Set new target operation 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):
|
||||
"""Set new target swing mode."""
|
||||
|
@ -198,7 +198,6 @@ async def async_setup(hass, config):
|
||||
class CoverDevice(Entity):
|
||||
"""Representation a cover."""
|
||||
|
||||
# pylint: disable=no-self-use
|
||||
@property
|
||||
def current_cover_position(self):
|
||||
"""Return current position of cover.
|
||||
|
@ -24,7 +24,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
class DemoCover(CoverDevice):
|
||||
"""Representation of a demo cover."""
|
||||
|
||||
# pylint: disable=no-self-use
|
||||
def __init__(self, hass, name, position=None, tilt_position=None,
|
||||
device_class=None, supported_features=None):
|
||||
"""Initialize the cover."""
|
||||
|
@ -73,7 +73,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
class GaradgetCover(CoverDevice):
|
||||
"""Representation of a Garadget cover."""
|
||||
|
||||
# pylint: disable=no-self-use
|
||||
def __init__(self, hass, args):
|
||||
"""Initialize the cover."""
|
||||
self.particle_url = 'https://api.particle.io'
|
||||
|
@ -5,7 +5,7 @@ For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/cover.isy994/
|
||||
"""
|
||||
import logging
|
||||
from typing import Callable # noqa
|
||||
from typing import Callable
|
||||
|
||||
from homeassistant.components.cover import CoverDevice, DOMAIN
|
||||
from homeassistant.components.isy994 import (ISY994_NODES, ISY994_PROGRAMS,
|
||||
|
@ -17,7 +17,7 @@ async def async_setup_platform(
|
||||
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."""
|
||||
|
||||
@property
|
||||
|
@ -72,7 +72,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
class OpenGarageCover(CoverDevice):
|
||||
"""Representation of a OpenGarage cover."""
|
||||
|
||||
# pylint: disable=no-self-use
|
||||
def __init__(self, hass, args):
|
||||
"""Initialize the cover."""
|
||||
self.opengarage_url = 'http://{}:{}'.format(
|
||||
|
@ -21,6 +21,10 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
_id = shade.object_id() + shade.name()
|
||||
if _id not in hass.data[DOMAIN]['unique_ids']:
|
||||
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():
|
||||
_id = door.object_id() + door.name()
|
||||
if _id not in hass.data[DOMAIN]['unique_ids']:
|
||||
|
@ -42,7 +42,6 @@ class ZwaveRollershutter(zwave.ZWaveDeviceEntity, CoverDevice):
|
||||
def __init__(self, hass, values, invert_buttons):
|
||||
"""Initialize the Z-Wave rollershutter."""
|
||||
ZWaveDeviceEntity.__init__(self, values, DOMAIN)
|
||||
# pylint: disable=no-member
|
||||
self._network = hass.data[zwave.const.DATA_NETWORK]
|
||||
self._open_id = None
|
||||
self._close_id = None
|
||||
|
@ -22,7 +22,8 @@
|
||||
},
|
||||
"options": {
|
||||
"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"
|
||||
}
|
||||
|
@ -19,8 +19,14 @@
|
||||
"link": {
|
||||
"description": "Entsperren Sie Ihr deCONZ-Gateway, um sich bei Home Assistant zu registrieren. \n\n 1. Gehen Sie zu den deCONZ-Systemeinstellungen \n 2. Dr\u00fccken Sie die Taste \"Gateway entsperren\"",
|
||||
"title": "Mit deCONZ verbinden"
|
||||
},
|
||||
"options": {
|
||||
"data": {
|
||||
"allow_clip_sensor": "Import virtueller Sensoren zulassen",
|
||||
"allow_deconz_groups": "Import von deCONZ-Gruppen zulassen"
|
||||
}
|
||||
}
|
||||
},
|
||||
"title": "deCONZ"
|
||||
"title": "deCONZ Zigbee Gateway"
|
||||
}
|
||||
}
|
@ -22,7 +22,8 @@
|
||||
},
|
||||
"options": {
|
||||
"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"
|
||||
}
|
||||
|
@ -19,6 +19,13 @@
|
||||
"link": {
|
||||
"description": "Ontgrendel je deCONZ gateway om te registreren met Home Assistant.\n\n1. Ga naar deCONZ systeeminstellingen\n2. Druk op de knop \"Gateway ontgrendelen\"",
|
||||
"title": "Koppel met deCONZ"
|
||||
},
|
||||
"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"
|
||||
|
@ -22,7 +22,8 @@
|
||||
},
|
||||
"options": {
|
||||
"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"
|
||||
}
|
||||
|
@ -22,7 +22,8 @@
|
||||
},
|
||||
"options": {
|
||||
"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"
|
||||
}
|
||||
|
@ -22,7 +22,7 @@ from .const import (
|
||||
CONF_ALLOW_CLIP_SENSOR, CONFIG_FILE, DATA_DECONZ_EVENT,
|
||||
DATA_DECONZ_ID, DATA_DECONZ_UNSUB, DOMAIN, _LOGGER)
|
||||
|
||||
REQUIREMENTS = ['pydeconz==38']
|
||||
REQUIREMENTS = ['pydeconz==39']
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.Schema({
|
||||
|
@ -163,9 +163,6 @@ class DeconzFlowHandler(data_entry_flow.FlowHandler):
|
||||
if CONF_API_KEY not in import_config:
|
||||
return await self.async_step_link()
|
||||
|
||||
self.deconz_config[CONF_ALLOW_CLIP_SENSOR] = True
|
||||
self.deconz_config[CONF_ALLOW_DECONZ_GROUPS] = True
|
||||
return self.async_create_entry(
|
||||
title='deCONZ-' + self.deconz_config[CONF_BRIDGEID],
|
||||
data=self.deconz_config
|
||||
)
|
||||
user_input = {CONF_ALLOW_CLIP_SENSOR: True,
|
||||
CONF_ALLOW_DECONZ_GROUPS: True}
|
||||
return await self.async_step_options(user_input=user_input)
|
||||
|
@ -50,7 +50,6 @@ class CiscoDeviceScanner(DeviceScanner):
|
||||
self.success_init = self._update_info()
|
||||
_LOGGER.info('cisco_ios scanner initialized')
|
||||
|
||||
# pylint: disable=no-self-use
|
||||
def get_device_name(self, device):
|
||||
"""Get the firmware doesn't save the name of the wireless device."""
|
||||
return None
|
||||
|
@ -22,7 +22,7 @@ from homeassistant.components.device_tracker import (
|
||||
from homeassistant.const import (
|
||||
CONF_HOST, CONF_PORT)
|
||||
|
||||
REQUIREMENTS = ['aiofreepybox==0.0.3']
|
||||
REQUIREMENTS = ['aiofreepybox==0.0.4']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
@ -7,7 +7,7 @@ https://home-assistant.io/components/device_tracker.gpslogger/
|
||||
import logging
|
||||
from hmac import compare_digest
|
||||
|
||||
from aiohttp.web import Request, HTTPUnauthorized # NOQA
|
||||
from aiohttp.web import Request, HTTPUnauthorized
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
@ -61,7 +61,6 @@ class LinksysAPDeviceScanner(DeviceScanner):
|
||||
|
||||
return self.last_results
|
||||
|
||||
# pylint: disable=no-self-use
|
||||
def get_device_name(self, device):
|
||||
"""
|
||||
Return the name (if known) of the device.
|
||||
|
@ -14,7 +14,7 @@ from homeassistant.components.device_tracker import (
|
||||
from homeassistant.const import (
|
||||
CONF_HOST, CONF_PASSWORD, CONF_USERNAME, CONF_PORT)
|
||||
|
||||
REQUIREMENTS = ['librouteros==1.0.5']
|
||||
REQUIREMENTS = ['librouteros==2.1.0']
|
||||
|
||||
MTK_DEFAULT_API_PORT = '8728'
|
||||
|
||||
|
@ -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,
|
||||
device.value_type)
|
||||
async_dispatcher_connect(
|
||||
hass, mysensors.SIGNAL_CALLBACK.format(*dev_id),
|
||||
hass, mysensors.const.SIGNAL_CALLBACK.format(*dev_id),
|
||||
device.async_update_callback)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class MySensorsDeviceScanner(mysensors.MySensorsDevice):
|
||||
class MySensorsDeviceScanner(mysensors.device.MySensorsDevice):
|
||||
"""Represent a MySensors scanner."""
|
||||
|
||||
def __init__(self, async_see, *args):
|
||||
|
@ -74,8 +74,6 @@ class SnmpScanner(DeviceScanner):
|
||||
return [client['mac'] for client in self.last_results
|
||||
if client.get('mac')]
|
||||
|
||||
# Suppressing no-self-use warning
|
||||
# pylint: disable=R0201
|
||||
def get_device_name(self, device):
|
||||
"""Return the name of the given device or None if we don't know."""
|
||||
# We have no names
|
||||
@ -106,7 +104,6 @@ class SnmpScanner(DeviceScanner):
|
||||
if errindication:
|
||||
_LOGGER.error("SNMPLIB error: %s", errindication)
|
||||
return
|
||||
# pylint: disable=no-member
|
||||
if errstatus:
|
||||
_LOGGER.error("SNMP error: %s at %s", errstatus.prettyPrint(),
|
||||
errindex and restable[int(errindex) - 1][0] or '?')
|
||||
|
@ -68,7 +68,6 @@ class TplinkDeviceScanner(DeviceScanner):
|
||||
self._update_info()
|
||||
return self.last_results
|
||||
|
||||
# pylint: disable=no-self-use
|
||||
def get_device_name(self, device):
|
||||
"""Get firmware doesn't save the name of the wireless device."""
|
||||
return None
|
||||
@ -103,7 +102,6 @@ class Tplink2DeviceScanner(TplinkDeviceScanner):
|
||||
self._update_info()
|
||||
return self.last_results.keys()
|
||||
|
||||
# pylint: disable=no-self-use
|
||||
def get_device_name(self, device):
|
||||
"""Get firmware doesn't save the name of the wireless device."""
|
||||
return self.last_results.get(device)
|
||||
@ -164,7 +162,6 @@ class Tplink3DeviceScanner(TplinkDeviceScanner):
|
||||
self._log_out()
|
||||
return self.last_results.keys()
|
||||
|
||||
# pylint: disable=no-self-use
|
||||
def get_device_name(self, device):
|
||||
"""Get the firmware doesn't save the name of the wireless device.
|
||||
|
||||
@ -273,7 +270,6 @@ class Tplink4DeviceScanner(TplinkDeviceScanner):
|
||||
self._update_info()
|
||||
return self.last_results
|
||||
|
||||
# pylint: disable=no-self-use
|
||||
def get_device_name(self, device):
|
||||
"""Get the name of the wireless device."""
|
||||
return None
|
||||
@ -349,7 +345,6 @@ class Tplink5DeviceScanner(TplinkDeviceScanner):
|
||||
self._update_info()
|
||||
return self.last_results.keys()
|
||||
|
||||
# pylint: disable=no-self-use
|
||||
def get_device_name(self, device):
|
||||
"""Get firmware doesn't save the name of the wireless device."""
|
||||
return None
|
||||
|
@ -27,6 +27,7 @@ ATTR_MEMORY = 'memory'
|
||||
ATTR_REGION = 'region'
|
||||
ATTR_VCPUS = 'vcpus'
|
||||
|
||||
CONF_ATTRIBUTION = 'Data provided by Digital Ocean'
|
||||
CONF_DROPLETS = 'droplets'
|
||||
|
||||
DATA_DIGITAL_OCEAN = 'data_do'
|
||||
|
@ -21,7 +21,7 @@ from homeassistant.helpers.event import async_track_point_in_utc_time
|
||||
from homeassistant.helpers.discovery import async_load_platform, async_discover
|
||||
import homeassistant.util.dt as dt_util
|
||||
|
||||
REQUIREMENTS = ['netdisco==1.4.1']
|
||||
REQUIREMENTS = ['netdisco==1.5.0']
|
||||
|
||||
DOMAIN = 'discovery'
|
||||
|
||||
|
@ -105,7 +105,6 @@ def setup(hass, config):
|
||||
Will automatically load thermostat and sensor components to support
|
||||
devices discovered on the network.
|
||||
"""
|
||||
# pylint: disable=import-error
|
||||
global NETWORK
|
||||
|
||||
if 'ecobee' in _CONFIGURING:
|
||||
|
@ -91,9 +91,11 @@ def setup(hass, yaml_config):
|
||||
server_port=config.listen_port,
|
||||
api_password=None,
|
||||
ssl_certificate=None,
|
||||
ssl_peer_certificate=None,
|
||||
ssl_key=None,
|
||||
cors_origins=None,
|
||||
use_x_forwarded_for=False,
|
||||
trusted_proxies=[],
|
||||
trusted_networks=[],
|
||||
login_threshold=0,
|
||||
is_ban_enabled=False
|
||||
|
@ -26,7 +26,7 @@ from homeassistant.helpers.translation import async_get_translations
|
||||
from homeassistant.loader import bind_hass
|
||||
from homeassistant.util.yaml import load_yaml
|
||||
|
||||
REQUIREMENTS = ['home-assistant-frontend==20180625.0']
|
||||
REQUIREMENTS = ['home-assistant-frontend==20180704.0']
|
||||
|
||||
DOMAIN = 'frontend'
|
||||
DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log']
|
||||
@ -200,8 +200,8 @@ def add_manifest_json_key(key, val):
|
||||
|
||||
async def async_setup(hass, config):
|
||||
"""Set up the serving of the frontend."""
|
||||
if list(hass.auth.async_auth_providers):
|
||||
client = await hass.auth.async_create_client(
|
||||
if hass.auth.active:
|
||||
client = await hass.auth.async_get_or_create_client(
|
||||
'Home Assistant Frontend',
|
||||
redirect_uris=['/'],
|
||||
no_secret=True,
|
||||
|
@ -31,7 +31,7 @@ CONFIG_SCHEMA = vol.Schema({
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
|
||||
# pylint: disable=no-member, import-self
|
||||
# pylint: disable=no-member
|
||||
def setup(hass, base_config):
|
||||
"""Set up the gc100 component."""
|
||||
import gc100
|
||||
|
@ -197,7 +197,7 @@ def setup_services(hass, track_new_found_calendars, calendar_service):
|
||||
def _scan_for_calendars(service):
|
||||
"""Scan for new calendars."""
|
||||
service = calendar_service.get()
|
||||
cal_list = service.calendarList() # pylint: disable=no-member
|
||||
cal_list = service.calendarList()
|
||||
calendars = cal_list.list().execute()['items']
|
||||
for calendar in calendars:
|
||||
calendar['track'] = track_new_found_calendars
|
||||
|
@ -13,9 +13,8 @@ import async_timeout
|
||||
import voluptuous as vol
|
||||
|
||||
# Typing imports
|
||||
# pylint: disable=using-constant-test,unused-import,ungrouped-imports
|
||||
from homeassistant.core import HomeAssistant # NOQA
|
||||
from typing import Dict, Any # NOQA
|
||||
from homeassistant.core import HomeAssistant
|
||||
from typing import Dict, Any
|
||||
|
||||
from homeassistant.const import CONF_NAME
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
|
@ -3,12 +3,11 @@
|
||||
import logging
|
||||
|
||||
# Typing imports
|
||||
# pylint: disable=using-constant-test,unused-import,ungrouped-imports
|
||||
# if False:
|
||||
from aiohttp.web import Request, Response # NOQA
|
||||
from typing import Dict, Any # NOQA
|
||||
from aiohttp.web import Request, Response
|
||||
from typing import Dict, Any
|
||||
|
||||
from homeassistant.core import HomeAssistant # NOQA
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.components.http import HomeAssistantView
|
||||
from homeassistant.const import (
|
||||
HTTP_BAD_REQUEST,
|
||||
|
@ -7,10 +7,10 @@ https://home-assistant.io/components/google_assistant/
|
||||
import logging
|
||||
|
||||
from aiohttp.hdrs import AUTHORIZATION
|
||||
from aiohttp.web import Request, Response # NOQA
|
||||
from aiohttp.web import Request, Response
|
||||
|
||||
# Typing imports
|
||||
# pylint: disable=using-constant-test,unused-import,ungrouped-imports
|
||||
# pylint: disable=unused-import
|
||||
from homeassistant.components.http import HomeAssistantView
|
||||
from homeassistant.core import HomeAssistant, callback # NOQA
|
||||
from homeassistant.helpers.entity import Entity # NOQA
|
||||
|
@ -4,7 +4,7 @@ from itertools import product
|
||||
import logging
|
||||
|
||||
# Typing imports
|
||||
# pylint: disable=using-constant-test,unused-import,ungrouped-imports
|
||||
# pylint: disable=unused-import
|
||||
# if False:
|
||||
from aiohttp.web import Request, Response # NOQA
|
||||
from typing import Dict, Tuple, Any, Optional # NOQA
|
||||
|
@ -107,8 +107,8 @@ def get_accessory(hass, driver, state, aid, config):
|
||||
a_type = 'Thermostat'
|
||||
|
||||
elif state.domain == 'cover':
|
||||
features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
|
||||
device_class = state.attributes.get(ATTR_DEVICE_CLASS)
|
||||
features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
|
||||
|
||||
if device_class == 'garage' and \
|
||||
features & (cover.SUPPORT_OPEN | cover.SUPPORT_CLOSE):
|
||||
@ -134,8 +134,8 @@ def get_accessory(hass, driver, state, aid, config):
|
||||
a_type = 'MediaPlayer'
|
||||
|
||||
elif state.domain == 'sensor':
|
||||
unit = state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
|
||||
device_class = state.attributes.get(ATTR_DEVICE_CLASS)
|
||||
unit = state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
|
||||
|
||||
if device_class == DEVICE_CLASS_TEMPERATURE or \
|
||||
unit in (TEMP_CELSIUS, TEMP_FAHRENHEIT):
|
||||
|
@ -8,7 +8,8 @@ from pyhap.accessory import Accessory, Bridge
|
||||
from pyhap.accessory_driver import AccessoryDriver
|
||||
from pyhap.const import CATEGORY_OTHER
|
||||
|
||||
from homeassistant.const import __version__
|
||||
from homeassistant.const import (
|
||||
__version__, ATTR_BATTERY_CHARGING, ATTR_BATTERY_LEVEL)
|
||||
from homeassistant.core import callback as ha_callback
|
||||
from homeassistant.core import split_entity_id
|
||||
from homeassistant.helpers.event import (
|
||||
@ -16,10 +17,11 @@ from homeassistant.helpers.event import (
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from .const import (
|
||||
BRIDGE_MODEL, BRIDGE_NAME, BRIDGE_SERIAL_NUMBER,
|
||||
DEBOUNCE_TIMEOUT, MANUFACTURER)
|
||||
BRIDGE_MODEL, BRIDGE_NAME, BRIDGE_SERIAL_NUMBER, CHAR_BATTERY_LEVEL,
|
||||
CHAR_CHARGING_STATE, CHAR_STATUS_LOW_BATTERY, DEBOUNCE_TIMEOUT,
|
||||
MANUFACTURER, SERV_BATTERY_SERVICE)
|
||||
from .util import (
|
||||
show_setup_message, dismiss_setup_message)
|
||||
convert_to_float, show_setup_message, dismiss_setup_message)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@ -67,6 +69,23 @@ class HomeAccessory(Accessory):
|
||||
self.entity_id = entity_id
|
||||
self.hass = hass
|
||||
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):
|
||||
"""Method called by accessory after driver is started.
|
||||
@ -85,8 +104,32 @@ class HomeAccessory(Accessory):
|
||||
_LOGGER.debug('New_state: %s', new_state)
|
||||
if new_state is None:
|
||||
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)
|
||||
|
||||
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):
|
||||
"""Method called on state change to update HomeKit value.
|
||||
|
||||
|
@ -38,6 +38,7 @@ TYPE_SWITCH = 'switch'
|
||||
# #### Services ####
|
||||
SERV_ACCESSORY_INFO = 'AccessoryInformation'
|
||||
SERV_AIR_QUALITY_SENSOR = 'AirQualitySensor'
|
||||
SERV_BATTERY_SERVICE = 'BatteryService'
|
||||
SERV_CARBON_DIOXIDE_SENSOR = 'CarbonDioxideSensor'
|
||||
SERV_CARBON_MONOXIDE_SENSOR = 'CarbonMonoxideSensor'
|
||||
SERV_CONTACT_SENSOR = 'ContactSensor'
|
||||
@ -62,11 +63,13 @@ SERV_WINDOW_COVERING = 'WindowCovering'
|
||||
CHAR_ACTIVE = 'Active'
|
||||
CHAR_AIR_PARTICULATE_DENSITY = 'AirParticulateDensity'
|
||||
CHAR_AIR_QUALITY = 'AirQuality'
|
||||
CHAR_BATTERY_LEVEL = 'BatteryLevel'
|
||||
CHAR_BRIGHTNESS = 'Brightness'
|
||||
CHAR_CARBON_DIOXIDE_DETECTED = 'CarbonDioxideDetected'
|
||||
CHAR_CARBON_DIOXIDE_LEVEL = 'CarbonDioxideLevel'
|
||||
CHAR_CARBON_DIOXIDE_PEAK_LEVEL = 'CarbonDioxidePeakLevel'
|
||||
CHAR_CARBON_MONOXIDE_DETECTED = 'CarbonMonoxideDetected'
|
||||
CHAR_CHARGING_STATE = 'ChargingState'
|
||||
CHAR_COLOR_TEMPERATURE = 'ColorTemperature'
|
||||
CHAR_CONTACT_SENSOR_STATE = 'ContactSensorState'
|
||||
CHAR_COOLING_THRESHOLD_TEMPERATURE = 'CoolingThresholdTemperature'
|
||||
@ -96,6 +99,7 @@ CHAR_ROTATION_DIRECTION = 'RotationDirection'
|
||||
CHAR_SATURATION = 'Saturation'
|
||||
CHAR_SERIAL_NUMBER = 'SerialNumber'
|
||||
CHAR_SMOKE_DETECTED = 'SmokeDetected'
|
||||
CHAR_STATUS_LOW_BATTERY = 'StatusLowBattery'
|
||||
CHAR_SWING_MODE = 'SwingMode'
|
||||
CHAR_TARGET_DOOR_STATE = 'TargetDoorState'
|
||||
CHAR_TARGET_HEATING_COOLING = 'TargetHeatingCoolingState'
|
||||
|
@ -57,9 +57,6 @@ class Fan(HomeAccessory):
|
||||
|
||||
def set_state(self, value):
|
||||
"""Set state if call came from HomeKit."""
|
||||
if self._state == value:
|
||||
return
|
||||
|
||||
_LOGGER.debug('%s: Set state to %d', self.entity_id, value)
|
||||
self._flag[CHAR_ACTIVE] = True
|
||||
service = SERVICE_TURN_ON if value == 1 else SERVICE_TURN_OFF
|
||||
|
@ -5,8 +5,10 @@ from pyhap.const import CATEGORY_ALARM_SYSTEM
|
||||
|
||||
from homeassistant.components.alarm_control_panel import DOMAIN
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID, ATTR_CODE, STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME,
|
||||
STATE_ALARM_ARMED_NIGHT, STATE_ALARM_TRIGGERED, STATE_ALARM_DISARMED)
|
||||
ATTR_ENTITY_ID, ATTR_CODE, SERVICE_ALARM_ARM_AWAY, SERVICE_ALARM_ARM_HOME,
|
||||
SERVICE_ALARM_ARM_NIGHT, SERVICE_ALARM_DISARM, STATE_ALARM_ARMED_AWAY,
|
||||
STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT, STATE_ALARM_TRIGGERED,
|
||||
STATE_ALARM_DISARMED)
|
||||
|
||||
from . import TYPES
|
||||
from .accessories import HomeAccessory
|
||||
@ -22,10 +24,11 @@ HASS_TO_HOMEKIT = {STATE_ALARM_ARMED_HOME: 0,
|
||||
STATE_ALARM_DISARMED: 3,
|
||||
STATE_ALARM_TRIGGERED: 4}
|
||||
HOMEKIT_TO_HASS = {c: s for s, c in HASS_TO_HOMEKIT.items()}
|
||||
STATE_TO_SERVICE = {STATE_ALARM_ARMED_HOME: 'alarm_arm_home',
|
||||
STATE_ALARM_ARMED_AWAY: 'alarm_arm_away',
|
||||
STATE_ALARM_ARMED_NIGHT: 'alarm_arm_night',
|
||||
STATE_ALARM_DISARMED: 'alarm_disarm'}
|
||||
STATE_TO_SERVICE = {
|
||||
STATE_ALARM_ARMED_AWAY: SERVICE_ALARM_ARM_AWAY,
|
||||
STATE_ALARM_ARMED_HOME: SERVICE_ALARM_ARM_HOME,
|
||||
STATE_ALARM_ARMED_NIGHT: SERVICE_ALARM_ARM_NIGHT,
|
||||
STATE_ALARM_DISARMED: SERVICE_ALARM_DISARM}
|
||||
|
||||
|
||||
@TYPES.register('SecuritySystem')
|
||||
|
@ -3,7 +3,7 @@ import logging
|
||||
|
||||
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 (
|
||||
ATTR_ENTITY_ID, SERVICE_TURN_ON, SERVICE_TURN_OFF, STATE_ON)
|
||||
from homeassistant.core import split_entity_id
|
||||
@ -37,7 +37,7 @@ class Outlet(HomeAccessory):
|
||||
self.flag_target_state = True
|
||||
params = {ATTR_ENTITY_ID: self.entity_id}
|
||||
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):
|
||||
"""Update switch state after state changed."""
|
||||
|
@ -23,6 +23,7 @@ HOMEKIT_DIR = '.homekit'
|
||||
HOMEKIT_ACCESSORY_DISPATCH = {
|
||||
'lightbulb': 'light',
|
||||
'outlet': 'switch',
|
||||
'thermostat': 'climate',
|
||||
}
|
||||
|
||||
KNOWN_ACCESSORIES = "{}-accessories".format(DOMAIN)
|
||||
@ -219,8 +220,12 @@ class HomeKitEntity(Entity):
|
||||
"""Synchronise a HomeKit device state with Home Assistant."""
|
||||
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):
|
||||
"""Set up for Homekit devices."""
|
||||
def discovery_dispatch(service, discovery_info):
|
||||
|
@ -20,7 +20,7 @@ import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.loader import bind_hass
|
||||
|
||||
REQUIREMENTS = ['pyhomematic==0.1.43']
|
||||
REQUIREMENTS = ['pyhomematic==0.1.44']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@ -148,6 +148,7 @@ CONF_PATH = 'path'
|
||||
CONF_CALLBACK_IP = 'callback_ip'
|
||||
CONF_CALLBACK_PORT = 'callback_port'
|
||||
CONF_RESOLVENAMES = 'resolvenames'
|
||||
CONF_JSONPORT = 'jsonport'
|
||||
CONF_VARIABLES = 'variables'
|
||||
CONF_DEVICES = 'devices'
|
||||
CONF_PRIMARY = 'primary'
|
||||
@ -155,6 +156,7 @@ CONF_PRIMARY = 'primary'
|
||||
DEFAULT_LOCAL_IP = '0.0.0.0'
|
||||
DEFAULT_LOCAL_PORT = 0
|
||||
DEFAULT_RESOLVENAMES = False
|
||||
DEFAULT_JSONPORT = 80
|
||||
DEFAULT_PORT = 2001
|
||||
DEFAULT_PATH = ''
|
||||
DEFAULT_USERNAME = 'Admin'
|
||||
@ -178,6 +180,7 @@ CONFIG_SCHEMA = vol.Schema({
|
||||
vol.Optional(CONF_PATH, default=DEFAULT_PATH): cv.string,
|
||||
vol.Optional(CONF_RESOLVENAMES, default=DEFAULT_RESOLVENAMES):
|
||||
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_PASSWORD, default=DEFAULT_PASSWORD): cv.string,
|
||||
vol.Optional(CONF_CALLBACK_IP): cv.string,
|
||||
@ -299,6 +302,7 @@ def setup(hass, config):
|
||||
'port': rconfig.get(CONF_PORT),
|
||||
'path': rconfig.get(CONF_PATH),
|
||||
'resolvenames': rconfig.get(CONF_RESOLVENAMES),
|
||||
'jsonport': rconfig.get(CONF_JSONPORT),
|
||||
'username': rconfig.get(CONF_USERNAME),
|
||||
'password': rconfig.get(CONF_PASSWORD),
|
||||
'callbackip': rconfig.get(CONF_CALLBACK_IP),
|
||||
|
@ -40,33 +40,29 @@ CONF_SERVER_HOST = 'server_host'
|
||||
CONF_SERVER_PORT = 'server_port'
|
||||
CONF_BASE_URL = 'base_url'
|
||||
CONF_SSL_CERTIFICATE = 'ssl_certificate'
|
||||
CONF_SSL_PEER_CERTIFICATE = 'ssl_peer_certificate'
|
||||
CONF_SSL_KEY = 'ssl_key'
|
||||
CONF_CORS_ORIGINS = 'cors_allowed_origins'
|
||||
CONF_USE_X_FORWARDED_FOR = 'use_x_forwarded_for'
|
||||
CONF_TRUSTED_PROXIES = 'trusted_proxies'
|
||||
CONF_TRUSTED_NETWORKS = 'trusted_networks'
|
||||
CONF_LOGIN_ATTEMPTS_THRESHOLD = 'login_attempts_threshold'
|
||||
CONF_IP_BAN_ENABLED = 'ip_ban_enabled'
|
||||
|
||||
# TLS configuration follows the best-practice guidelines specified here:
|
||||
# https://wiki.mozilla.org/Security/Server_Side_TLS
|
||||
# Intermediate guidelines are followed.
|
||||
SSL_VERSION = ssl.PROTOCOL_SSLv23
|
||||
SSL_OPTS = ssl.OP_NO_SSLv2 | ssl.OP_NO_SSLv3
|
||||
# Modern guidelines are followed.
|
||||
SSL_VERSION = ssl.PROTOCOL_TLS # pylint: disable=no-member
|
||||
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'):
|
||||
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-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:" \
|
||||
"DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:" \
|
||||
"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"
|
||||
"ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:" \
|
||||
"ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256"
|
||||
|
||||
_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_BASE_URL): cv.string,
|
||||
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_CORS_ORIGINS, default=[]):
|
||||
vol.All(cv.ensure_list, [cv.string]),
|
||||
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.All(cv.ensure_list, [ip_network]),
|
||||
vol.Optional(CONF_LOGIN_ATTEMPTS_THRESHOLD,
|
||||
@ -108,9 +107,11 @@ async def async_setup(hass, config):
|
||||
server_host = conf[CONF_SERVER_HOST]
|
||||
server_port = conf[CONF_SERVER_PORT]
|
||||
ssl_certificate = conf.get(CONF_SSL_CERTIFICATE)
|
||||
ssl_peer_certificate = conf.get(CONF_SSL_PEER_CERTIFICATE)
|
||||
ssl_key = conf.get(CONF_SSL_KEY)
|
||||
cors_origins = conf[CONF_CORS_ORIGINS]
|
||||
use_x_forwarded_for = conf[CONF_USE_X_FORWARDED_FOR]
|
||||
trusted_proxies = conf[CONF_TRUSTED_PROXIES]
|
||||
trusted_networks = conf[CONF_TRUSTED_NETWORKS]
|
||||
is_ban_enabled = conf[CONF_IP_BAN_ENABLED]
|
||||
login_threshold = conf[CONF_LOGIN_ATTEMPTS_THRESHOLD]
|
||||
@ -125,9 +126,11 @@ async def async_setup(hass, config):
|
||||
server_port=server_port,
|
||||
api_password=api_password,
|
||||
ssl_certificate=ssl_certificate,
|
||||
ssl_peer_certificate=ssl_peer_certificate,
|
||||
ssl_key=ssl_key,
|
||||
cors_origins=cors_origins,
|
||||
use_x_forwarded_for=use_x_forwarded_for,
|
||||
trusted_proxies=trusted_proxies,
|
||||
trusted_networks=trusted_networks,
|
||||
login_threshold=login_threshold,
|
||||
is_ban_enabled=is_ban_enabled
|
||||
@ -166,21 +169,37 @@ async def async_setup(hass, config):
|
||||
class HomeAssistantHTTP(object):
|
||||
"""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,
|
||||
use_x_forwarded_for, trusted_networks,
|
||||
use_x_forwarded_for, trusted_proxies, trusted_networks,
|
||||
login_threshold, is_ban_enabled):
|
||||
"""Initialize the HTTP Home Assistant server."""
|
||||
app = self.app = web.Application(
|
||||
middlewares=[staticresource_middleware])
|
||||
|
||||
# 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:
|
||||
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:
|
||||
setup_cors(app, cors_origins)
|
||||
@ -190,6 +209,7 @@ class HomeAssistantHTTP(object):
|
||||
self.hass = hass
|
||||
self.api_password = api_password
|
||||
self.ssl_certificate = ssl_certificate
|
||||
self.ssl_peer_certificate = ssl_peer_certificate
|
||||
self.ssl_key = ssl_key
|
||||
self.server_host = server_host
|
||||
self.server_port = server_port
|
||||
@ -287,8 +307,12 @@ class HomeAssistantHTTP(object):
|
||||
except OSError as error:
|
||||
_LOGGER.error("Could not read SSL certificate from %s: %s",
|
||||
self.ssl_certificate, error)
|
||||
context = None
|
||||
return
|
||||
|
||||
if self.ssl_peer_certificate:
|
||||
context.verify_mode = ssl.CERT_REQUIRED
|
||||
context.load_verify_locations(cafile=self.ssl_peer_certificate)
|
||||
|
||||
else:
|
||||
context = None
|
||||
|
||||
|
@ -17,37 +17,44 @@ _LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@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."""
|
||||
@middleware
|
||||
async def auth_middleware(request, handler):
|
||||
"""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
|
||||
|
||||
if (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'))):
|
||||
if use_auth and (HTTP_HEADER_HA_AUTH in request.headers or
|
||||
DATA_API_PASSWORD in request.query):
|
||||
_LOGGER.warning('Please use access_token instead api_password.')
|
||||
|
||||
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
|
||||
authenticated = True
|
||||
|
||||
elif (DATA_API_PASSWORD in request.query and
|
||||
elif (legacy_auth and DATA_API_PASSWORD in request.query and
|
||||
hmac.compare_digest(
|
||||
api_password.encode('utf-8'),
|
||||
request.query[DATA_API_PASSWORD].encode('utf-8'))):
|
||||
authenticated = True
|
||||
|
||||
elif (hdrs.AUTHORIZATION in request.headers and
|
||||
await async_validate_auth_header(api_password, request)):
|
||||
elif _is_trusted_ip(request, trusted_networks):
|
||||
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
|
||||
|
||||
request[KEY_AUTHENTICATED] = authenticated
|
||||
@ -76,8 +83,12 @@ def validate_password(request, api_password):
|
||||
request.app['hass'].http.api_password.encode('utf-8'))
|
||||
|
||||
|
||||
async def async_validate_auth_header(api_password, request):
|
||||
"""Test an authorization header if valid password."""
|
||||
async def async_validate_auth_header(request, api_password=None):
|
||||
"""
|
||||
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:
|
||||
return False
|
||||
|
||||
@ -88,7 +99,16 @@ async def async_validate_auth_header(api_password, request):
|
||||
# If no space in authorization header
|
||||
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')
|
||||
try:
|
||||
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'),
|
||||
password.encode('utf-8'))
|
||||
|
||||
if auth_type != 'Bearer':
|
||||
else:
|
||||
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
|
||||
|
@ -11,18 +11,25 @@ from .const import KEY_REAL_IP
|
||||
|
||||
|
||||
@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."""
|
||||
@middleware
|
||||
async def real_ip_middleware(request, handler):
|
||||
"""Real IP middleware."""
|
||||
if (use_x_forwarded_for and
|
||||
X_FORWARDED_FOR in request.headers):
|
||||
request[KEY_REAL_IP] = ip_address(
|
||||
request.headers.get(X_FORWARDED_FOR).split(',')[0])
|
||||
else:
|
||||
request[KEY_REAL_IP] = \
|
||||
ip_address(request.transport.get_extra_info('peername')[0])
|
||||
connected_ip = ip_address(
|
||||
request.transport.get_extra_info('peername')[0])
|
||||
request[KEY_REAL_IP] = connected_ip
|
||||
|
||||
# Only use the XFF header if enabled, present, and from a trusted proxy
|
||||
try:
|
||||
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)
|
||||
|
||||
|
@ -18,7 +18,6 @@ class CachingStaticResource(StaticResource):
|
||||
filename = URL(request.match_info['filename']).path
|
||||
try:
|
||||
# PyLint is wrong about resolve not being a member.
|
||||
# pylint: disable=no-member
|
||||
filepath = self._directory.joinpath(filename).resolve()
|
||||
if not self._follow_symlinks:
|
||||
filepath.relative_to(self._directory)
|
||||
|
@ -24,6 +24,6 @@
|
||||
"title": "Hub verbinden"
|
||||
}
|
||||
},
|
||||
"title": "Philips Hue Bridge"
|
||||
"title": ""
|
||||
}
|
||||
}
|
@ -24,6 +24,6 @@
|
||||
"title": "\u0421\u0432\u044f\u0437\u044c \u0441 \u0445\u0430\u0431\u043e\u043c"
|
||||
}
|
||||
},
|
||||
"title": "\u0428\u043b\u044e\u0437 Philips Hue"
|
||||
"title": "Philips Hue"
|
||||
}
|
||||
}
|
@ -124,24 +124,16 @@ class HueBridge(object):
|
||||
(group for group in self.api.groups.values()
|
||||
if group.name == group_name), None)
|
||||
|
||||
# The same scene name can exist in multiple groups.
|
||||
# In this case, activate first scene that contains the
|
||||
# the exact same light IDs as the group
|
||||
scenes = []
|
||||
for scene in self.api.scenes.values():
|
||||
if scene.name == scene_name:
|
||||
scenes.append(scene)
|
||||
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
|
||||
# Additional scene logic to handle duplicate scene names across groups
|
||||
scene = next(
|
||||
(scene for scene in self.api.scenes.values()
|
||||
if scene.name == scene_name
|
||||
and group is not None
|
||||
and sorted(scene.lights) == sorted(group.lights)),
|
||||
None)
|
||||
|
||||
# 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.scenes.update()
|
||||
await self.hue_activate_scene(call, updated=True)
|
||||
@ -151,11 +143,11 @@ class HueBridge(object):
|
||||
LOGGER.warning('Unable to find group %s', group_name)
|
||||
return
|
||||
|
||||
if scene_id is None:
|
||||
if scene is None:
|
||||
LOGGER.warning('Unable to find scene %s', scene_name)
|
||||
return
|
||||
|
||||
await group.set_action(scene=scene_id)
|
||||
await group.set_action(scene=scene.id)
|
||||
|
||||
|
||||
async def get_bridge(hass, host, username=None):
|
||||
|
@ -16,7 +16,7 @@ from homeassistant.components.image_processing import (
|
||||
from homeassistant.core import split_entity_id
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['numpy==1.14.3']
|
||||
REQUIREMENTS = ['numpy==1.14.5']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@ -152,7 +152,6 @@ class OpenCVImageProcessor(ImageProcessingEntity):
|
||||
import cv2 # pylint: disable=import-error
|
||||
import numpy
|
||||
|
||||
# pylint: disable=no-member
|
||||
cv_image = cv2.imdecode(
|
||||
numpy.asarray(bytearray(image)), cv2.IMREAD_UNCHANGED)
|
||||
|
||||
@ -168,7 +167,6 @@ class OpenCVImageProcessor(ImageProcessingEntity):
|
||||
else:
|
||||
path = classifier
|
||||
|
||||
# pylint: disable=no-member
|
||||
cascade = cv2.CascadeClassifier(path)
|
||||
|
||||
detections = cascade.detectMultiScale(
|
||||
|
@ -17,7 +17,7 @@ import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers import discovery
|
||||
from homeassistant.helpers.entity import Entity
|
||||
|
||||
REQUIREMENTS = ['insteonplm==0.10.0']
|
||||
REQUIREMENTS = ['insteonplm==0.11.3']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@ -300,7 +300,8 @@ class IPDB(object):
|
||||
OpenClosedRelay)
|
||||
|
||||
from insteonplm.states.dimmable import (DimmableSwitch,
|
||||
DimmableSwitch_Fan)
|
||||
DimmableSwitch_Fan,
|
||||
DimmableRemote)
|
||||
|
||||
from insteonplm.states.sensor import (VariableSensor,
|
||||
OnOffSensor,
|
||||
@ -328,6 +329,7 @@ class IPDB(object):
|
||||
|
||||
State(DimmableSwitch_Fan, 'fan'),
|
||||
State(DimmableSwitch, 'light'),
|
||||
State(DimmableRemote, 'binary_sensor'),
|
||||
|
||||
State(X10DimmableSwitch, 'light'),
|
||||
State(X10OnOffSwitch, 'switch'),
|
||||
|
@ -181,7 +181,6 @@ def devices_with_push():
|
||||
def enabled_push_ids():
|
||||
"""Return a list of push enabled target push IDs."""
|
||||
push_ids = list()
|
||||
# pylint: disable=unused-variable
|
||||
for device in CONFIG_FILE[ATTR_DEVICES].values():
|
||||
if device.get(ATTR_PUSH_ID) is not None:
|
||||
push_ids.append(device.get(ATTR_PUSH_ID))
|
||||
@ -203,7 +202,6 @@ def device_name_for_push_id(push_id):
|
||||
|
||||
def setup(hass, config):
|
||||
"""Set up the iOS component."""
|
||||
# pylint: disable=import-error
|
||||
global CONFIG_FILE
|
||||
global CONFIG_FILE_PATH
|
||||
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user