Merge pull request #15330 from home-assistant/rc

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

View File

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

3
.gitignore vendored
View File

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

View File

@ -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)

View File

@ -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."""

View File

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

View File

@ -123,7 +123,6 @@ async def async_from_config_dict(config: Dict[str, Any],
components.update(hass.config_entries.async_domains())
# 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. "

View File

@ -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."""

View File

@ -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."""

View File

@ -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

View File

@ -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)

View File

@ -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__)

View File

@ -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)

View File

@ -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."""

View File

@ -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')

View File

@ -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,

View File

@ -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__)

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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)

View File

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

View File

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

View File

@ -23,7 +23,7 @@ from homeassistant.helpers.entity import generate_entity_id
from homeassistant.helpers.event import async_track_state_change
from homeassistant.util import utcnow
REQUIREMENTS = ['numpy==1.14.3']
REQUIREMENTS = ['numpy==1.14.5']
_LOGGER = logging.getLogger(__name__)

View File

@ -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

View File

@ -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', [])

View File

@ -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:

View File

@ -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()

View File

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

View File

@ -67,8 +67,6 @@ async def async_setup_platform(hass, config, async_add_devices,
]
for cam in config.get(CONF_CAMERAS, []):
# 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)

View File

@ -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)

View File

@ -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)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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."""

View File

@ -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

View File

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

View File

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

View File

@ -129,6 +129,9 @@ PLATFORM_SCHEMA = SCHEMA_BASE.extend({
@asyncio.coroutine
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

View File

@ -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."""

View File

@ -5,15 +5,15 @@ For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/climate.zwave/
"""
# 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."""

View File

@ -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.

View File

@ -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."""

View File

@ -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'

View File

@ -5,7 +5,7 @@ For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/cover.isy994/
"""
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,

View File

@ -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

View File

@ -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(

View File

@ -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']:

View File

@ -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

View File

@ -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"
}

View File

@ -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"
}
}

View File

@ -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"
}

View File

@ -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"

View File

@ -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"
}

View File

@ -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"
}

View File

@ -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({

View File

@ -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)

View File

@ -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

View File

@ -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__)

View File

@ -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

View File

@ -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.

View File

@ -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'

View File

@ -23,13 +23,13 @@ async def async_setup_scanner(hass, config, async_see, discovery_info=None):
id(device.gateway), device.node_id, device.child_id,
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):

View File

@ -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 '?')

View File

@ -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

View File

@ -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'

View File

@ -21,7 +21,7 @@ from homeassistant.helpers.event import async_track_point_in_utc_time
from homeassistant.helpers.discovery import async_load_platform, async_discover
import homeassistant.util.dt as dt_util
REQUIREMENTS = ['netdisco==1.4.1']
REQUIREMENTS = ['netdisco==1.5.0']
DOMAIN = 'discovery'

View File

@ -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:

View File

@ -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

View File

@ -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,

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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,

View File

@ -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

View File

@ -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

View File

@ -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):

View File

@ -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.

View File

@ -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'

View File

@ -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

View File

@ -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')

View File

@ -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."""

View File

@ -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):

View File

@ -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),

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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)

View File

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

View File

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

View File

@ -124,24 +124,16 @@ class HueBridge(object):
(group for group in self.api.groups.values()
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):

View File

@ -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(

View File

@ -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'),

View File

@ -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