Merge pull request #14657 from home-assistant/rc

0.70.0
This commit is contained in:
Paulus Schoutsen 2018-05-27 20:22:19 -04:00 committed by GitHub
commit 0700886d1a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
257 changed files with 7293 additions and 3290 deletions

View File

@ -4,6 +4,8 @@ source = homeassistant
omit = omit =
homeassistant/__main__.py homeassistant/__main__.py
homeassistant/scripts/*.py homeassistant/scripts/*.py
homeassistant/util/async.py
homeassistant/monkey_patch.py
homeassistant/helpers/typing.py homeassistant/helpers/typing.py
homeassistant/helpers/signal.py homeassistant/helpers/signal.py
@ -127,7 +129,7 @@ omit =
homeassistant/components/insteon_local.py homeassistant/components/insteon_local.py
homeassistant/components/*/insteon_local.py homeassistant/components/*/insteon_local.py
homeassistant/components/insteon_plm.py homeassistant/components/insteon_plm/*
homeassistant/components/*/insteon_plm.py homeassistant/components/*/insteon_plm.py
homeassistant/components/ios.py homeassistant/components/ios.py
@ -151,6 +153,9 @@ omit =
homeassistant/components/knx.py homeassistant/components/knx.py
homeassistant/components/*/knx.py homeassistant/components/*/knx.py
homeassistant/components/konnected.py
homeassistant/components/*/konnected.py
homeassistant/components/lametric.py homeassistant/components/lametric.py
homeassistant/components/*/lametric.py homeassistant/components/*/lametric.py
@ -226,6 +231,9 @@ omit =
homeassistant/components/rpi_pfio.py homeassistant/components/rpi_pfio.py
homeassistant/components/*/rpi_pfio.py homeassistant/components/*/rpi_pfio.py
homeassistant/components/sabnzbd.py
homeassistant/components/*/sabnzbd.py
homeassistant/components/satel_integra.py homeassistant/components/satel_integra.py
homeassistant/components/*/satel_integra.py homeassistant/components/*/satel_integra.py
@ -342,6 +350,7 @@ omit =
homeassistant/components/calendar/todoist.py homeassistant/components/calendar/todoist.py
homeassistant/components/camera/bloomsky.py homeassistant/components/camera/bloomsky.py
homeassistant/components/camera/canary.py homeassistant/components/camera/canary.py
homeassistant/components/camera/familyhub.py
homeassistant/components/camera/ffmpeg.py homeassistant/components/camera/ffmpeg.py
homeassistant/components/camera/foscam.py homeassistant/components/camera/foscam.py
homeassistant/components/camera/mjpeg.py homeassistant/components/camera/mjpeg.py
@ -412,7 +421,6 @@ omit =
homeassistant/components/emoncms_history.py homeassistant/components/emoncms_history.py
homeassistant/components/emulated_hue/upnp.py homeassistant/components/emulated_hue/upnp.py
homeassistant/components/fan/mqtt.py homeassistant/components/fan/mqtt.py
homeassistant/components/feedreader.py
homeassistant/components/folder_watcher.py homeassistant/components/folder_watcher.py
homeassistant/components/foursquare.py homeassistant/components/foursquare.py
homeassistant/components/goalfeed.py homeassistant/components/goalfeed.py
@ -534,7 +542,6 @@ omit =
homeassistant/components/notify/rest.py homeassistant/components/notify/rest.py
homeassistant/components/notify/rocketchat.py homeassistant/components/notify/rocketchat.py
homeassistant/components/notify/sendgrid.py homeassistant/components/notify/sendgrid.py
homeassistant/components/notify/simplepush.py
homeassistant/components/notify/slack.py homeassistant/components/notify/slack.py
homeassistant/components/notify/smtp.py homeassistant/components/notify/smtp.py
homeassistant/components/notify/stride.py homeassistant/components/notify/stride.py
@ -592,6 +599,7 @@ omit =
homeassistant/components/sensor/fastdotcom.py homeassistant/components/sensor/fastdotcom.py
homeassistant/components/sensor/fedex.py homeassistant/components/sensor/fedex.py
homeassistant/components/sensor/filesize.py homeassistant/components/sensor/filesize.py
homeassistant/components/sensor/fints.py
homeassistant/components/sensor/fitbit.py homeassistant/components/sensor/fitbit.py
homeassistant/components/sensor/fixer.py homeassistant/components/sensor/fixer.py
homeassistant/components/sensor/folder.py homeassistant/components/sensor/folder.py
@ -650,7 +658,6 @@ omit =
homeassistant/components/sensor/radarr.py homeassistant/components/sensor/radarr.py
homeassistant/components/sensor/rainbird.py homeassistant/components/sensor/rainbird.py
homeassistant/components/sensor/ripple.py homeassistant/components/sensor/ripple.py
homeassistant/components/sensor/sabnzbd.py
homeassistant/components/sensor/scrape.py homeassistant/components/sensor/scrape.py
homeassistant/components/sensor/sense.py homeassistant/components/sensor/sense.py
homeassistant/components/sensor/sensehat.py homeassistant/components/sensor/sensehat.py

View File

@ -10,8 +10,8 @@ matrix:
env: TOXENV=lint env: TOXENV=lint
- python: "3.5.3" - python: "3.5.3"
env: TOXENV=pylint env: TOXENV=pylint
# - python: "3.5" - python: "3.5.3"
# env: TOXENV=typing env: TOXENV=typing
- python: "3.5.3" - python: "3.5.3"
env: TOXENV=py35 env: TOXENV=py35
- python: "3.6" - python: "3.6"

View File

@ -94,6 +94,8 @@ homeassistant/components/*/hive.py @Rendili @KJonline
homeassistant/components/homekit/* @cdce8p homeassistant/components/homekit/* @cdce8p
homeassistant/components/knx.py @Julius2342 homeassistant/components/knx.py @Julius2342
homeassistant/components/*/knx.py @Julius2342 homeassistant/components/*/knx.py @Julius2342
homeassistant/components/konnected.py @heythisisnate
homeassistant/components/*/konnected.py @heythisisnate
homeassistant/components/matrix.py @tinloaf homeassistant/components/matrix.py @tinloaf
homeassistant/components/*/matrix.py @tinloaf homeassistant/components/*/matrix.py @tinloaf
homeassistant/components/qwikswitch.py @kellerza homeassistant/components/qwikswitch.py @kellerza

View File

@ -8,7 +8,8 @@ import subprocess
import sys import sys
import threading import threading
from typing import Optional, List from typing import Optional, List, Dict, Any # noqa #pylint: disable=unused-import
from homeassistant import monkey_patch from homeassistant import monkey_patch
from homeassistant.const import ( from homeassistant.const import (
@ -259,7 +260,7 @@ def setup_and_run_hass(config_dir: str,
config = { config = {
'frontend': {}, 'frontend': {},
'demo': {} 'demo': {}
} } # type: Dict[str, Any]
hass = bootstrap.from_config_dict( hass = bootstrap.from_config_dict(
config, config_dir=config_dir, verbose=args.verbose, config, config_dir=config_dir, verbose=args.verbose,
skip_pip=args.skip_pip, log_rotate_days=args.log_rotate_days, skip_pip=args.skip_pip, log_rotate_days=args.log_rotate_days,

View File

@ -15,7 +15,6 @@ from voluptuous.humanize import humanize_error
from homeassistant import data_entry_flow, requirements from homeassistant import data_entry_flow, requirements
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.const import CONF_TYPE, CONF_NAME, CONF_ID from homeassistant.const import CONF_TYPE, CONF_NAME, CONF_ID
from homeassistant.exceptions import HomeAssistantError
from homeassistant.util.decorator import Registry from homeassistant.util.decorator import Registry
from homeassistant.util import dt as dt_util from homeassistant.util import dt as dt_util
@ -36,23 +35,7 @@ ACCESS_TOKEN_EXPIRATION = timedelta(minutes=30)
DATA_REQS = 'auth_reqs_processed' DATA_REQS = 'auth_reqs_processed'
class AuthError(HomeAssistantError): def generate_secret(entropy: int = 32) -> str:
"""Generic authentication error."""
class InvalidUser(AuthError):
"""Raised when an invalid user has been specified."""
class InvalidPassword(AuthError):
"""Raised when an invalid password has been supplied."""
class UnknownError(AuthError):
"""When an unknown error occurs."""
def generate_secret(entropy=32):
"""Generate a secret. """Generate a secret.
Backport of secrets.token_hex from Python 3.6 Backport of secrets.token_hex from Python 3.6
@ -69,8 +52,9 @@ class AuthProvider:
initialized = False initialized = False
def __init__(self, store, config): def __init__(self, hass, store, config):
"""Initialize an auth provider.""" """Initialize an auth provider."""
self.hass = hass
self.store = store self.store = store
self.config = config self.config = config
@ -210,6 +194,7 @@ class Client:
name = attr.ib(type=str) name = attr.ib(type=str)
id = attr.ib(type=str, default=attr.Factory(lambda: uuid.uuid4().hex)) id = attr.ib(type=str, default=attr.Factory(lambda: uuid.uuid4().hex))
secret = attr.ib(type=str, default=attr.Factory(generate_secret)) secret = attr.ib(type=str, default=attr.Factory(generate_secret))
redirect_uris = attr.ib(type=list, default=attr.Factory(list))
async def load_auth_provider_module(hass, provider): async def load_auth_provider_module(hass, provider):
@ -283,7 +268,7 @@ async def _auth_provider_from_config(hass, store, config):
provider_name, humanize_error(config, err)) provider_name, humanize_error(config, err))
return None return None
return AUTH_PROVIDERS[provider_name](store, config) return AUTH_PROVIDERS[provider_name](hass, store, config)
class AuthManager: class AuthManager:
@ -340,9 +325,11 @@ class AuthManager:
"""Get an access token.""" """Get an access token."""
return self.access_tokens.get(token) return self.access_tokens.get(token)
async def async_create_client(self, name): async def async_create_client(self, name, *, redirect_uris=None,
no_secret=False):
"""Create a new client.""" """Create a new client."""
return await self._store.async_create_client(name) return await self._store.async_create_client(
name, redirect_uris, no_secret)
async def async_get_client(self, client_id): async def async_get_client(self, client_id):
"""Get a client.""" """Get a client."""
@ -360,6 +347,9 @@ class AuthManager:
async def _async_finish_login_flow(self, result): async def _async_finish_login_flow(self, result):
"""Result of a credential login flow.""" """Result of a credential login flow."""
if result['type'] != data_entry_flow.RESULT_TYPE_CREATE_ENTRY:
return None
auth_provider = self._providers[result['handler']] auth_provider = self._providers[result['handler']]
return await auth_provider.async_get_or_create_credentials( return await auth_provider.async_get_or_create_credentials(
result['data']) result['data'])
@ -477,12 +467,20 @@ class AuthStore:
return None return None
async def async_create_client(self, name): async def async_create_client(self, name, redirect_uris, no_secret):
"""Create a new client.""" """Create a new client."""
if self.clients is None: if self.clients is None:
await self.async_load() await self.async_load()
client = Client(name) kwargs = {
'name': name,
'redirect_uris': redirect_uris
}
if no_secret:
kwargs['secret'] = None
client = Client(**kwargs)
self.clients[client.id] = client self.clients[client.id] = client
await self.async_save() await self.async_save()
return client return client

View File

@ -0,0 +1,181 @@
"""Home Assistant auth provider."""
import base64
from collections import OrderedDict
import hashlib
import hmac
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'
CONFIG_SCHEMA = auth.AUTH_PROVIDER_SCHEMA.extend({
}, extra=vol.PREVENT_EXTRA)
class InvalidAuth(HomeAssistantError):
"""Raised when we encounter invalid authentication."""
class InvalidUser(HomeAssistantError):
"""Raised when invalid user is specified.
Will not be raised when validating authentication.
"""
class Data:
"""Hold the user data."""
def __init__(self, path, data):
"""Initialize the user data store."""
self.path = path
if data is None:
data = {
'salt': auth.generate_secret(),
'users': []
}
self._data = data
@property
def users(self):
"""Return users."""
return self._data['users']
def validate_login(self, username, password):
"""Validate a username and password.
Raises InvalidAuth if auth invalid.
"""
password = self.hash_password(password)
found = None
# Compare all users to avoid timing attacks.
for user in self._data['users']:
if username == user['username']:
found = user
if found is None:
# Do one more compare to make timing the same as if user was found.
hmac.compare_digest(password, password)
raise InvalidAuth
if not hmac.compare_digest(password,
base64.b64decode(found['password'])):
raise InvalidAuth
def hash_password(self, password, for_storage=False):
"""Encode a password."""
hashed = hashlib.pbkdf2_hmac(
'sha512', password.encode(), self._data['salt'].encode(), 100000)
if for_storage:
hashed = base64.b64encode(hashed).decode()
return hashed
def add_user(self, username, password):
"""Add a user."""
if any(user['username'] == username for user in self.users):
raise InvalidUser
self.users.append({
'username': username,
'password': self.hash_password(password, True),
})
def change_password(self, username, new_password):
"""Update the password of a user.
Raises InvalidUser if user cannot be found.
"""
for user in self.users:
if user['username'] == username:
user['password'] = self.hash_password(new_password, True)
break
else:
raise InvalidUser
def 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))
@auth.AUTH_PROVIDERS.register('homeassistant')
class HassAuthProvider(auth.AuthProvider):
"""Auth provider based on a local storage of users in HASS config dir."""
DEFAULT_TITLE = 'Home Assistant Local'
async def async_credential_flow(self):
"""Return a flow to login."""
return LoginFlow(self)
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)
async def async_get_or_create_credentials(self, flow_result):
"""Get credentials based on the flow result."""
username = flow_result['username']
for credential in await self.async_credentials():
if credential.data['username'] == username:
return credential
# Create new credentials.
return self.async_create_credentials({
'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."""
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:
await self._auth_provider.async_validate_login(
user_input['username'], user_input['password'])
except InvalidAuth:
errors['base'] = 'invalid_auth'
if not errors:
return self.async_create_entry(
title=self._auth_provider.name,
data=user_input
)
schema = OrderedDict()
schema['username'] = str
schema['password'] = str
return self.async_show_form(
step_id='init',
data_schema=vol.Schema(schema),
errors=errors,
)

View File

@ -4,6 +4,7 @@ import hmac
import voluptuous as vol import voluptuous as vol
from homeassistant.exceptions import HomeAssistantError
from homeassistant import auth, data_entry_flow from homeassistant import auth, data_entry_flow
from homeassistant.core import callback from homeassistant.core import callback
@ -20,6 +21,10 @@ CONFIG_SCHEMA = auth.AUTH_PROVIDER_SCHEMA.extend({
}, extra=vol.PREVENT_EXTRA) }, extra=vol.PREVENT_EXTRA)
class InvalidAuthError(HomeAssistantError):
"""Raised when submitting invalid authentication."""
@auth.AUTH_PROVIDERS.register('insecure_example') @auth.AUTH_PROVIDERS.register('insecure_example')
class ExampleAuthProvider(auth.AuthProvider): class ExampleAuthProvider(auth.AuthProvider):
"""Example auth provider based on hardcoded usernames and passwords.""" """Example auth provider based on hardcoded usernames and passwords."""
@ -43,18 +48,15 @@ class ExampleAuthProvider(auth.AuthProvider):
# Do one more compare to make timing the same as if user was found. # Do one more compare to make timing the same as if user was found.
hmac.compare_digest(password.encode('utf-8'), hmac.compare_digest(password.encode('utf-8'),
password.encode('utf-8')) password.encode('utf-8'))
raise auth.InvalidUser raise InvalidAuthError
if not hmac.compare_digest(user['password'].encode('utf-8'), if not hmac.compare_digest(user['password'].encode('utf-8'),
password.encode('utf-8')): password.encode('utf-8')):
raise auth.InvalidPassword raise InvalidAuthError
async def async_get_or_create_credentials(self, flow_result): async def async_get_or_create_credentials(self, flow_result):
"""Get credentials based on the flow result.""" """Get credentials based on the flow result."""
username = flow_result['username'] username = flow_result['username']
password = flow_result['password']
self.async_validate_login(username, password)
for credential in await self.async_credentials(): for credential in await self.async_credentials():
if credential.data['username'] == username: if credential.data['username'] == username:
@ -96,7 +98,7 @@ class LoginFlow(data_entry_flow.FlowHandler):
try: try:
self._auth_provider.async_validate_login( self._auth_provider.async_validate_login(
user_input['username'], user_input['password']) user_input['username'], user_input['password'])
except (auth.InvalidUser, auth.InvalidPassword): except InvalidAuthError:
errors['base'] = 'invalid_auth' errors['base'] = 'invalid_auth'
if not errors: if not errors:

View File

@ -278,7 +278,8 @@ def async_enable_logging(hass: core.HomeAssistant,
if log_rotate_days: if log_rotate_days:
err_handler = logging.handlers.TimedRotatingFileHandler( err_handler = logging.handlers.TimedRotatingFileHandler(
err_log_path, when='midnight', backupCount=log_rotate_days) err_log_path, when='midnight',
backupCount=log_rotate_days) # type: logging.FileHandler
else: else:
err_handler = logging.FileHandler( err_handler = logging.FileHandler(
err_log_path, mode='w', delay=True) err_log_path, mode='w', delay=True)
@ -297,7 +298,7 @@ def async_enable_logging(hass: core.HomeAssistant,
EVENT_HOMEASSISTANT_CLOSE, async_stop_async_handler) EVENT_HOMEASSISTANT_CLOSE, async_stop_async_handler)
logger = logging.getLogger('') logger = logging.getLogger('')
logger.addHandler(async_handler) logger.addHandler(async_handler) # type: ignore
logger.setLevel(logging.INFO) logger.setLevel(logging.INFO)
# Save the log file location for access by other components. # Save the log file location for access by other components.

View File

@ -356,7 +356,8 @@ class APIErrorLog(HomeAssistantView):
async def get(self, request): async def get(self, request):
"""Retrieve API error log.""" """Retrieve API error log."""
return await self.file(request, request.app['hass'].data[DATA_LOGGING]) return web.FileResponse(
request.app['hass'].data[DATA_LOGGING])
async def async_services_json(hass): async def async_services_json(hass):

View File

@ -144,7 +144,7 @@ class AuthProvidersView(HomeAssistantView):
requires_auth = False requires_auth = False
@verify_client @verify_client
async def get(self, request, client_id): async def get(self, request, client):
"""Get available auth providers.""" """Get available auth providers."""
return self.json([{ return self.json([{
'name': provider.name, 'name': provider.name,
@ -166,8 +166,15 @@ class LoginFlowIndexView(FlowManagerIndexView):
# pylint: disable=arguments-differ # pylint: disable=arguments-differ
@verify_client @verify_client
async def post(self, request, client_id): @RequestDataValidator(vol.Schema({
vol.Required('handler'): vol.Any(str, list),
vol.Required('redirect_uri'): str,
}))
async def post(self, request, client, data):
"""Create a new login flow.""" """Create a new login flow."""
if data['redirect_uri'] not in client.redirect_uris:
return self.json_message('invalid redirect uri', )
# pylint: disable=no-value-for-parameter # pylint: disable=no-value-for-parameter
return await super().post(request) return await super().post(request)
@ -192,7 +199,7 @@ class LoginFlowResourceView(FlowManagerResourceView):
# pylint: disable=arguments-differ # pylint: disable=arguments-differ
@verify_client @verify_client
@RequestDataValidator(vol.Schema(dict), allow_empty=True) @RequestDataValidator(vol.Schema(dict), allow_empty=True)
async def post(self, request, client_id, flow_id, data): async def post(self, request, client, flow_id, data):
"""Handle progressing a login flow request.""" """Handle progressing a login flow request."""
try: try:
result = await self._flow_mgr.async_configure(flow_id, data) result = await self._flow_mgr.async_configure(flow_id, data)
@ -205,7 +212,7 @@ class LoginFlowResourceView(FlowManagerResourceView):
return self.json(self._prepare_result_json(result)) return self.json(self._prepare_result_json(result))
result.pop('data') result.pop('data')
result['result'] = self._store_credentials(client_id, result['result']) result['result'] = self._store_credentials(client.id, result['result'])
return self.json(result) return self.json(result)
@ -222,7 +229,7 @@ class GrantTokenView(HomeAssistantView):
self._retrieve_credentials = retrieve_credentials self._retrieve_credentials = retrieve_credentials
@verify_client @verify_client
async def post(self, request, client_id): async def post(self, request, client):
"""Grant a token.""" """Grant a token."""
hass = request.app['hass'] hass = request.app['hass']
data = await request.post() data = await request.post()
@ -230,11 +237,11 @@ class GrantTokenView(HomeAssistantView):
if grant_type == 'authorization_code': if grant_type == 'authorization_code':
return await self._async_handle_auth_code( return await self._async_handle_auth_code(
hass, client_id, data) hass, client.id, data)
elif grant_type == 'refresh_token': elif grant_type == 'refresh_token':
return await self._async_handle_refresh_token( return await self._async_handle_refresh_token(
hass, client_id, data) hass, client.id, data)
return self.json({ return self.json({
'error': 'unsupported_grant_type', 'error': 'unsupported_grant_type',

View File

@ -11,15 +11,15 @@ def verify_client(method):
@wraps(method) @wraps(method)
async def wrapper(view, request, *args, **kwargs): async def wrapper(view, request, *args, **kwargs):
"""Verify client id/secret before doing request.""" """Verify client id/secret before doing request."""
client_id = await _verify_client(request) client = await _verify_client(request)
if client_id is None: if client is None:
return view.json({ return view.json({
'error': 'invalid_client', 'error': 'invalid_client',
}, status_code=401) }, status_code=401)
return await method( return await method(
view, request, *args, client_id=client_id, **kwargs) view, request, *args, **kwargs, client=client)
return wrapper return wrapper
@ -46,18 +46,34 @@ async def _verify_client(request):
client_id, client_secret = decoded.split(':', 1) client_id, client_secret = decoded.split(':', 1)
except ValueError: except ValueError:
# If no ':' in decoded # If no ':' in decoded
return None client_id, client_secret = decoded, None
client = await request.app['hass'].auth.async_get_client(client_id) return await async_secure_get_client(
request.app['hass'], client_id, client_secret)
async def async_secure_get_client(hass, client_id, client_secret):
"""Get a client id/secret in consistent time."""
client = await hass.auth.async_get_client(client_id)
if client is None: if client is None:
# Still do a compare so we run same time as if a client was found. if client_secret is not None:
hmac.compare_digest(client_secret.encode('utf-8'), # Still do a compare so we run same time as if a client was found.
client_secret.encode('utf-8')) hmac.compare_digest(client_secret.encode('utf-8'),
client_secret.encode('utf-8'))
return None return None
if hmac.compare_digest(client_secret.encode('utf-8'), if client.secret is None:
client.secret.encode('utf-8')): return client
return client_id
elif client_secret is None:
# Still do a compare so we run same time as if a secret was passed.
hmac.compare_digest(client.secret.encode('utf-8'),
client.secret.encode('utf-8'))
return None
elif hmac.compare_digest(client_secret.encode('utf-8'),
client.secret.encode('utf-8')):
return client
return None return None

View File

@ -217,4 +217,4 @@ class BayesianBinarySensor(BinarySensorDevice):
@asyncio.coroutine @asyncio.coroutine
def async_update(self): def async_update(self):
"""Get the latest data and update the states.""" """Get the latest data and update the states."""
self._deviation = bool(self.probability > self._probability_threshold) self._deviation = bool(self.probability >= self._probability_threshold)

View File

@ -17,9 +17,19 @@ _LOGGER = logging.getLogger(__name__)
SENSOR_TYPES = { SENSOR_TYPES = {
'lids': ['Doors', 'opening'], 'lids': ['Doors', 'opening'],
'windows': ['Windows', 'opening'], 'windows': ['Windows', 'opening'],
'door_lock_state': ['Door lock state', 'safety'] 'door_lock_state': ['Door lock state', 'safety'],
'lights_parking': ['Parking lights', 'light'],
'condition_based_services': ['Condition based services', 'problem'],
'check_control_messages': ['Control messages', 'problem']
} }
SENSOR_TYPES_ELEC = {
'charging_status': ['Charging status', 'power'],
'connection_status': ['Connection status', 'plug']
}
SENSOR_TYPES_ELEC.update(SENSOR_TYPES)
def setup_platform(hass, config, add_devices, discovery_info=None): def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the BMW sensors.""" """Set up the BMW sensors."""
@ -29,10 +39,18 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
devices = [] devices = []
for account in accounts: for account in accounts:
for vehicle in account.account.vehicles: for vehicle in account.account.vehicles:
for key, value in sorted(SENSOR_TYPES.items()): if vehicle.has_hv_battery:
device = BMWConnectedDriveSensor(account, vehicle, key, _LOGGER.debug('BMW with a high voltage battery')
value[0], value[1]) for key, value in sorted(SENSOR_TYPES_ELEC.items()):
devices.append(device) device = BMWConnectedDriveSensor(account, vehicle, key,
value[0], value[1])
devices.append(device)
elif vehicle.has_internal_combustion_engine:
_LOGGER.debug('BMW with an internal combustion engine')
for key, value in sorted(SENSOR_TYPES.items()):
device = BMWConnectedDriveSensor(account, vehicle, key,
value[0], value[1])
devices.append(device)
add_devices(devices, True) add_devices(devices, True)
@ -92,12 +110,34 @@ class BMWConnectedDriveSensor(BinarySensorDevice):
result[window.name] = window.state.value result[window.name] = window.state.value
elif self._attribute == 'door_lock_state': elif self._attribute == 'door_lock_state':
result['door_lock_state'] = vehicle_state.door_lock_state.value result['door_lock_state'] = vehicle_state.door_lock_state.value
result['last_update_reason'] = vehicle_state.last_update_reason
elif self._attribute == 'lights_parking':
result['lights_parking'] = vehicle_state.parking_lights.value
elif self._attribute == 'condition_based_services':
for report in vehicle_state.condition_based_services:
result.update(self._format_cbs_report(report))
elif self._attribute == 'check_control_messages':
check_control_messages = vehicle_state.check_control_messages
if not check_control_messages:
result['check_control_messages'] = 'OK'
else:
result['check_control_messages'] = check_control_messages
elif self._attribute == 'charging_status':
result['charging_status'] = vehicle_state.charging_status.value
# pylint: disable=W0212
result['last_charging_end_result'] = \
vehicle_state._attributes['lastChargingEndResult']
if self._attribute == 'connection_status':
# pylint: disable=W0212
result['connection_status'] = \
vehicle_state._attributes['connectionStatus']
return result return sorted(result.items())
def update(self): def update(self):
"""Read new state data from the library.""" """Read new state data from the library."""
from bimmer_connected.state import LockState from bimmer_connected.state import LockState
from bimmer_connected.state import ChargingState
vehicle_state = self._vehicle.state vehicle_state = self._vehicle.state
# device class opening: On means open, Off means closed # device class opening: On means open, Off means closed
@ -111,6 +151,37 @@ class BMWConnectedDriveSensor(BinarySensorDevice):
# Possible values: LOCKED, SECURED, SELECTIVE_LOCKED, UNLOCKED # Possible values: LOCKED, SECURED, SELECTIVE_LOCKED, UNLOCKED
self._state = vehicle_state.door_lock_state not in \ self._state = vehicle_state.door_lock_state not in \
[LockState.LOCKED, LockState.SECURED] [LockState.LOCKED, LockState.SECURED]
# device class light: On means light detected, Off means no light
if self._attribute == 'lights_parking':
self._state = vehicle_state.are_parking_lights_on
# device class problem: On means problem detected, Off means no problem
if self._attribute == 'condition_based_services':
self._state = not vehicle_state.are_all_cbs_ok
if self._attribute == 'check_control_messages':
self._state = vehicle_state.has_check_control_messages
# device class power: On means power detected, Off means no power
if self._attribute == 'charging_status':
self._state = vehicle_state.charging_status in \
[ChargingState.CHARGING]
# device class plug: On means device is plugged in,
# Off means device is unplugged
if self._attribute == 'connection_status':
# pylint: disable=W0212
self._state = (vehicle_state._attributes['connectionStatus'] ==
'CONNECTED')
@staticmethod
def _format_cbs_report(report):
result = {}
service_type = report.service_type.lower().replace('_', ' ')
result['{} status'.format(service_type)] = report.state.value
if report.due_date is not None:
result['{} date'.format(service_type)] = \
report.due_date.strftime('%Y-%m-%d')
if report.due_distance is not None:
result['{} distance'.format(service_type)] = \
'{} km'.format(report.due_distance)
return result
def update_callback(self): def update_callback(self):
"""Schedule a state update.""" """Schedule a state update."""

View File

@ -0,0 +1,85 @@
"""
Support for HomematicIP binary sensor.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/binary_sensor.homematicip_cloud/
"""
import logging
from homeassistant.components.binary_sensor import BinarySensorDevice
from homeassistant.components.homematicip_cloud import (
HomematicipGenericDevice, DOMAIN as HOMEMATICIP_CLOUD_DOMAIN,
ATTR_HOME_ID)
DEPENDENCIES = ['homematicip_cloud']
_LOGGER = logging.getLogger(__name__)
ATTR_WINDOW_STATE = 'window_state'
ATTR_EVENT_DELAY = 'event_delay'
ATTR_MOTION_DETECTED = 'motion_detected'
ATTR_ILLUMINATION = 'illumination'
HMIP_OPEN = 'open'
async def async_setup_platform(hass, config, async_add_devices,
discovery_info=None):
"""Set up the HomematicIP binary sensor devices."""
from homematicip.device import (ShutterContact, MotionDetectorIndoor)
if discovery_info is None:
return
home = hass.data[HOMEMATICIP_CLOUD_DOMAIN][discovery_info[ATTR_HOME_ID]]
devices = []
for device in home.devices:
if isinstance(device, ShutterContact):
devices.append(HomematicipShutterContact(home, device))
elif isinstance(device, MotionDetectorIndoor):
devices.append(HomematicipMotionDetector(home, device))
if devices:
async_add_devices(devices)
class HomematicipShutterContact(HomematicipGenericDevice, BinarySensorDevice):
"""HomematicIP shutter contact."""
def __init__(self, home, device):
"""Initialize the shutter contact."""
super().__init__(home, device)
@property
def device_class(self):
"""Return the class of this sensor."""
return 'door'
@property
def is_on(self):
"""Return true if the shutter contact is on/open."""
if self._device.sabotage:
return True
if self._device.windowState is None:
return None
return self._device.windowState.lower() == HMIP_OPEN
class HomematicipMotionDetector(HomematicipGenericDevice, BinarySensorDevice):
"""MomematicIP motion detector."""
def __init__(self, home, device):
"""Initialize the shutter contact."""
super().__init__(home, device)
@property
def device_class(self):
"""Return the class of this sensor."""
return 'motion'
@property
def is_on(self):
"""Return true if motion is detected."""
if self._device.sabotage:
return True
return self._device.motionDetected

View File

@ -117,8 +117,10 @@ class ISYBinarySensorDevice(ISYDevice, BinarySensorDevice):
# pylint: disable=protected-access # pylint: disable=protected-access
if _is_val_unknown(self._node.status._val): if _is_val_unknown(self._node.status._val):
self._computed_state = None self._computed_state = None
self._status_was_unknown = True
else: else:
self._computed_state = bool(self._node.status._val) self._computed_state = bool(self._node.status._val)
self._status_was_unknown = False
@asyncio.coroutine @asyncio.coroutine
def async_added_to_hass(self) -> None: def async_added_to_hass(self) -> None:
@ -156,9 +158,13 @@ class ISYBinarySensorDevice(ISYDevice, BinarySensorDevice):
# pylint: disable=protected-access # pylint: disable=protected-access
if not _is_val_unknown(self._negative_node.status._val): if not _is_val_unknown(self._negative_node.status._val):
# If the negative node has a value, it means the negative node is # If the negative node has a value, it means the negative node is
# in use for this device. Therefore, we cannot determine the state # in use for this device. Next we need to check to see if the
# of the sensor until we receive our first ON event. # negative and positive nodes disagree on the state (both ON or
self._computed_state = None # both OFF).
if self._negative_node.status._val == self._node.status._val:
# The states disagree, therefore we cannot determine the state
# of the sensor until we receive our first ON event.
self._computed_state = None
def _negative_node_control_handler(self, event: object) -> None: def _negative_node_control_handler(self, event: object) -> None:
"""Handle an "On" control event from the "negative" node.""" """Handle an "On" control event from the "negative" node."""
@ -189,14 +195,21 @@ class ISYBinarySensorDevice(ISYDevice, BinarySensorDevice):
self.schedule_update_ha_state() self.schedule_update_ha_state()
self._heartbeat() self._heartbeat()
# pylint: disable=unused-argument
def on_update(self, event: object) -> None: def on_update(self, event: object) -> None:
"""Ignore primary node status updates. """Primary node status updates.
We listen directly to the Control events on all nodes for this We MOSTLY ignore these updates, as we listen directly to the Control
device. events on all nodes for this device. However, there is one edge case:
If a leak sensor is unknown, due to a recent reboot of the ISY, the
status will get updated to dry upon the first heartbeat. This status
update is the only way that a leak sensor's status changes without
an accompanying Control event, so we need to watch for it.
""" """
pass if self._status_was_unknown and self._computed_state is None:
self._computed_state = bool(int(self._node.status))
self._status_was_unknown = False
self.schedule_update_ha_state()
self._heartbeat()
@property @property
def value(self) -> object: def value(self) -> object:

View File

@ -0,0 +1,82 @@
"""
Support for wired binary sensors attached to a Konnected device.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/binary_sensor.konnected/
"""
import logging
from homeassistant.components.binary_sensor import BinarySensorDevice
from homeassistant.components.konnected import (
DOMAIN as KONNECTED_DOMAIN, PIN_TO_ZONE, SIGNAL_SENSOR_UPDATE)
from homeassistant.const import (
CONF_DEVICES, CONF_TYPE, CONF_NAME, CONF_BINARY_SENSORS, ATTR_ENTITY_ID,
ATTR_STATE)
from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
_LOGGER = logging.getLogger(__name__)
DEPENDENCIES = ['konnected']
async def async_setup_platform(hass, config, async_add_devices,
discovery_info=None):
"""Set up binary sensors attached to a Konnected device."""
if discovery_info is None:
return
data = hass.data[KONNECTED_DOMAIN]
device_id = discovery_info['device_id']
sensors = [KonnectedBinarySensor(device_id, pin_num, pin_data)
for pin_num, pin_data in
data[CONF_DEVICES][device_id][CONF_BINARY_SENSORS].items()]
async_add_devices(sensors)
class KonnectedBinarySensor(BinarySensorDevice):
"""Representation of a Konnected binary sensor."""
def __init__(self, device_id, pin_num, data):
"""Initialize the binary sensor."""
self._data = data
self._device_id = device_id
self._pin_num = pin_num
self._state = self._data.get(ATTR_STATE)
self._device_class = self._data.get(CONF_TYPE)
self._name = self._data.get(CONF_NAME, 'Konnected {} Zone {}'.format(
device_id, PIN_TO_ZONE[pin_num]))
_LOGGER.debug('Created new Konnected sensor: %s', self._name)
@property
def name(self):
"""Return the name of the sensor."""
return self._name
@property
def is_on(self):
"""Return the state of the sensor."""
return self._state
@property
def should_poll(self):
"""No polling needed."""
return False
@property
def device_class(self):
"""Return the device class."""
return self._device_class
async def async_added_to_hass(self):
"""Store entity_id and register state change callback."""
self._data[ATTR_ENTITY_ID] = self.entity_id
async_dispatcher_connect(
self.hass, SIGNAL_SENSOR_UPDATE.format(self.entity_id),
self.async_set_state)
@callback
def async_set_state(self, state):
"""Update the sensor's state."""
self._state = state
self.async_schedule_update_ha_state()

View File

@ -31,7 +31,8 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
sensors = [] sensors = []
hub = hass.data[MYCHEVY_DOMAIN] hub = hass.data[MYCHEVY_DOMAIN]
for sconfig in SENSORS: for sconfig in SENSORS:
sensors.append(EVBinarySensor(hub, sconfig)) for car in hub.cars:
sensors.append(EVBinarySensor(hub, sconfig, car.vid))
async_add_devices(sensors) async_add_devices(sensors)
@ -45,16 +46,18 @@ class EVBinarySensor(BinarySensorDevice):
""" """
def __init__(self, connection, config): def __init__(self, connection, config, car_vid):
"""Initialize sensor with car connection.""" """Initialize sensor with car connection."""
self._conn = connection self._conn = connection
self._name = config.name self._name = config.name
self._attr = config.attr self._attr = config.attr
self._type = config.device_class self._type = config.device_class
self._is_on = None self._is_on = None
self._car_vid = car_vid
self.entity_id = ENTITY_ID_FORMAT.format( self.entity_id = ENTITY_ID_FORMAT.format(
'{}_{}'.format(MYCHEVY_DOMAIN, slugify(self._name))) '{}_{}_{}'.format(MYCHEVY_DOMAIN,
slugify(self._car.name),
slugify(self._name)))
@property @property
def name(self): def name(self):
@ -66,6 +69,11 @@ class EVBinarySensor(BinarySensorDevice):
"""Return if on.""" """Return if on."""
return self._is_on return self._is_on
@property
def _car(self):
"""Return the car."""
return self._conn.get_car(self._car_vid)
@asyncio.coroutine @asyncio.coroutine
def async_added_to_hass(self): def async_added_to_hass(self):
"""Register callbacks.""" """Register callbacks."""
@ -75,8 +83,8 @@ class EVBinarySensor(BinarySensorDevice):
@callback @callback
def async_update_callback(self): def async_update_callback(self):
"""Update state.""" """Update state."""
if self._conn.car is not None: if self._car is not None:
self._is_on = getattr(self._conn.car, self._attr, None) self._is_on = getattr(self._car, self._attr, None)
self.async_schedule_update_ha_state() self.async_schedule_update_ha_state()
@property @property

View File

@ -10,7 +10,7 @@ import voluptuous as vol
from homeassistant.components import rfxtrx from homeassistant.components import rfxtrx
from homeassistant.components.binary_sensor import ( from homeassistant.components.binary_sensor import (
PLATFORM_SCHEMA, DEVICE_CLASSES_SCHEMA, BinarySensorDevice) DEVICE_CLASSES_SCHEMA, PLATFORM_SCHEMA, BinarySensorDevice)
from homeassistant.components.rfxtrx import ( from homeassistant.components.rfxtrx import (
ATTR_NAME, CONF_AUTOMATIC_ADD, CONF_DATA_BITS, CONF_DEVICES, ATTR_NAME, CONF_AUTOMATIC_ADD, CONF_DATA_BITS, CONF_DEVICES,
CONF_FIRE_EVENT, CONF_OFF_DELAY) CONF_FIRE_EVENT, CONF_OFF_DELAY)
@ -29,8 +29,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_DEVICES, default={}): { vol.Optional(CONF_DEVICES, default={}): {
cv.string: vol.Schema({ cv.string: vol.Schema({
vol.Optional(CONF_NAME): cv.string, vol.Optional(CONF_NAME): cv.string,
vol.Optional(CONF_DEVICE_CLASS): vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA,
DEVICE_CLASSES_SCHEMA,
vol.Optional(CONF_FIRE_EVENT, default=False): cv.boolean, vol.Optional(CONF_FIRE_EVENT, default=False): cv.boolean,
vol.Optional(CONF_OFF_DELAY): vol.Optional(CONF_OFF_DELAY):
vol.Any(cv.time_period, cv.positive_timedelta), vol.Any(cv.time_period, cv.positive_timedelta),

View File

@ -53,7 +53,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
'channel_1', hass, gateway)) 'channel_1', hass, gateway))
devices.append(XiaomiButton(device, 'Wall Switch (Both)', devices.append(XiaomiButton(device, 'Wall Switch (Both)',
'dual_channel', hass, gateway)) 'dual_channel', hass, gateway))
elif model in ['cube', 'sensor_cube']: elif model in ['cube', 'sensor_cube', 'sensor_cube.aqgl01']:
devices.append(XiaomiCube(device, hass, gateway)) devices.append(XiaomiCube(device, hass, gateway))
add_devices(devices) add_devices(devices)

View File

@ -108,7 +108,7 @@ class BinarySensor(zha.Entity, BinarySensorDevice):
@property @property
def is_on(self) -> bool: def is_on(self) -> bool:
"""Return True if entity is on.""" """Return True if entity is on."""
if self._state == 'unknown': if self._state is None:
return False return False
return bool(self._state) return bool(self._state)
@ -133,7 +133,8 @@ class BinarySensor(zha.Entity, BinarySensorDevice):
from bellows.types.basic import uint16_t from bellows.types.basic import uint16_t
result = await zha.safe_read(self._endpoint.ias_zone, result = await zha.safe_read(self._endpoint.ias_zone,
['zone_status']) ['zone_status'],
allow_cache=False)
state = result.get('zone_status', self._state) state = result.get('zone_status', self._state)
if isinstance(state, (int, uint16_t)): if isinstance(state, (int, uint16_t)):
self._state = result.get('zone_status', self._state) & 3 self._state = result.get('zone_status', self._state) & 3
@ -218,7 +219,10 @@ class Switch(zha.Entity, BinarySensorDevice):
@property @property
def device_state_attributes(self): def device_state_attributes(self):
"""Return the device state attributes.""" """Return the device state attributes."""
return {'level': self._state and self._level or 0} self._device_state_attributes.update({
'level': self._state and self._level or 0
})
return self._device_state_attributes
def move_level(self, change): def move_level(self, change):
"""Increment the level, setting state if appropriate.""" """Increment the level, setting state if appropriate."""

View File

@ -14,7 +14,7 @@ from homeassistant.helpers import discovery
from homeassistant.helpers.event import track_utc_time_change from homeassistant.helpers.event import track_utc_time_change
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['bimmer_connected==0.5.0'] REQUIREMENTS = ['bimmer_connected==0.5.1']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)

View File

@ -27,7 +27,7 @@ activate_air_conditioning:
description: > description: >
Start the air conditioning of the vehicle. What exactly is started here Start the air conditioning of the vehicle. What exactly is started here
depends on the type of vehicle. It might range from just ventilation over depends on the type of vehicle. It might range from just ventilation over
auxilary heating to real air conditioning. The vehicle is identified via auxiliary heating to real air conditioning. The vehicle is identified via
the vin (see below). the vin (see below).
fields: fields:
vin: vin:

View File

@ -256,6 +256,11 @@ class Camera(Entity):
"""Return the camera model.""" """Return the camera model."""
return None return None
@property
def frame_interval(self):
"""Return the interval between frames of the mjpeg stream."""
return 0.5
def camera_image(self): def camera_image(self):
"""Return bytes of camera image.""" """Return bytes of camera image."""
raise NotImplementedError() raise NotImplementedError()
@ -272,10 +277,6 @@ class Camera(Entity):
This method must be run in the event loop. This method must be run in the event loop.
""" """
if interval < MIN_STREAM_INTERVAL:
raise ValueError("Stream interval must be be > {}"
.format(MIN_STREAM_INTERVAL))
response = web.StreamResponse() response = web.StreamResponse()
response.content_type = ('multipart/x-mixed-replace; ' response.content_type = ('multipart/x-mixed-replace; '
'boundary=--frameboundary') 'boundary=--frameboundary')
@ -325,8 +326,7 @@ class Camera(Entity):
a direct stream from the camera. a direct stream from the camera.
This method must be run in the event loop. This method must be run in the event loop.
""" """
await self.handle_async_still_stream(request, await self.handle_async_still_stream(request, self.frame_interval)
FALLBACK_STREAM_INTERVAL)
@property @property
def state(self): def state(self):
@ -448,6 +448,9 @@ class CameraMjpegStream(CameraView):
try: try:
# Compose camera stream from stills # Compose camera stream from stills
interval = float(request.query.get('interval')) interval = float(request.query.get('interval'))
if interval < MIN_STREAM_INTERVAL:
raise ValueError("Stream interval must be be > {}"
.format(MIN_STREAM_INTERVAL))
await camera.handle_async_still_stream(request, interval) await camera.handle_async_still_stream(request, interval)
return return
except ValueError: except ValueError:

View File

@ -0,0 +1,58 @@
"""
Family Hub camera for Samsung Refrigerators.
For more details about this platform, please refer to the documentation
https://home-assistant.io/components/camera.familyhub/
"""
import logging
import voluptuous as vol
from homeassistant.components.camera import Camera
from homeassistant.components.sensor import PLATFORM_SCHEMA
from homeassistant.const import CONF_IP_ADDRESS, CONF_NAME
from homeassistant.helpers.aiohttp_client import async_get_clientsession
import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__)
REQUIREMENTS = ['python-family-hub-local==0.0.2']
DEFAULT_NAME = 'FamilyHub Camera'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_IP_ADDRESS): cv.string,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
})
async def async_setup_platform(
hass, config, async_add_devices, discovery_info=None):
"""Set up the Family Hub Camera."""
from pyfamilyhublocal import FamilyHubCam
address = config.get(CONF_IP_ADDRESS)
name = config.get(CONF_NAME)
session = async_get_clientsession(hass)
family_hub_cam = FamilyHubCam(address, hass.loop, session)
async_add_devices([FamilyHubCamera(name, family_hub_cam)], True)
class FamilyHubCamera(Camera):
"""The representation of a Family Hub camera."""
def __init__(self, name, family_hub_cam):
"""Initialize camera component."""
super().__init__()
self._name = name
self.family_hub_cam = family_hub_cam
async def async_camera_image(self):
"""Return a still image response."""
return await self.family_hub_cam.async_get_cam_image()
@property
def name(self):
"""Return the name of this camera."""
return self._name

View File

@ -28,6 +28,7 @@ _LOGGER = logging.getLogger(__name__)
CONF_CONTENT_TYPE = 'content_type' CONF_CONTENT_TYPE = 'content_type'
CONF_LIMIT_REFETCH_TO_URL_CHANGE = 'limit_refetch_to_url_change' CONF_LIMIT_REFETCH_TO_URL_CHANGE = 'limit_refetch_to_url_change'
CONF_STILL_IMAGE_URL = 'still_image_url' CONF_STILL_IMAGE_URL = 'still_image_url'
CONF_FRAMERATE = 'framerate'
DEFAULT_NAME = 'Generic Camera' DEFAULT_NAME = 'Generic Camera'
@ -40,6 +41,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_PASSWORD): cv.string, vol.Optional(CONF_PASSWORD): cv.string,
vol.Optional(CONF_USERNAME): cv.string, vol.Optional(CONF_USERNAME): cv.string,
vol.Optional(CONF_CONTENT_TYPE, default=DEFAULT_CONTENT_TYPE): cv.string, vol.Optional(CONF_CONTENT_TYPE, default=DEFAULT_CONTENT_TYPE): cv.string,
vol.Optional(CONF_FRAMERATE, default=2): cv.positive_int,
}) })
@ -62,6 +64,7 @@ class GenericCamera(Camera):
self._still_image_url = device_info[CONF_STILL_IMAGE_URL] self._still_image_url = device_info[CONF_STILL_IMAGE_URL]
self._still_image_url.hass = hass self._still_image_url.hass = hass
self._limit_refetch = device_info[CONF_LIMIT_REFETCH_TO_URL_CHANGE] self._limit_refetch = device_info[CONF_LIMIT_REFETCH_TO_URL_CHANGE]
self._frame_interval = 1 / device_info[CONF_FRAMERATE]
self.content_type = device_info[CONF_CONTENT_TYPE] self.content_type = device_info[CONF_CONTENT_TYPE]
username = device_info.get(CONF_USERNAME) username = device_info.get(CONF_USERNAME)
@ -78,6 +81,11 @@ class GenericCamera(Camera):
self._last_url = None self._last_url = None
self._last_image = None self._last_image = None
@property
def frame_interval(self):
"""Return the interval between frames of the mjpeg stream."""
return self._frame_interval
def camera_image(self): def camera_image(self):
"""Return bytes of camera image.""" """Return bytes of camera image."""
return run_coroutine_threadsafe( return run_coroutine_threadsafe(

View File

@ -115,7 +115,7 @@ class MySensorsHVAC(mysensors.MySensorsEntity, ClimateDevice):
"""List of available fan modes.""" """List of available fan modes."""
return ['Auto', 'Min', 'Normal', 'Max'] return ['Auto', 'Min', 'Normal', 'Max']
def set_temperature(self, **kwargs): async def async_set_temperature(self, **kwargs):
"""Set new target temperature.""" """Set new target temperature."""
set_req = self.gateway.const.SetReq set_req = self.gateway.const.SetReq
temp = kwargs.get(ATTR_TEMPERATURE) temp = kwargs.get(ATTR_TEMPERATURE)
@ -143,9 +143,9 @@ class MySensorsHVAC(mysensors.MySensorsEntity, ClimateDevice):
if self.gateway.optimistic: if self.gateway.optimistic:
# Optimistically assume that device has changed state # Optimistically assume that device has changed state
self._values[value_type] = value self._values[value_type] = value
self.schedule_update_ha_state() self.async_schedule_update_ha_state()
def set_fan_mode(self, fan_mode): async def async_set_fan_mode(self, fan_mode):
"""Set new target temperature.""" """Set new target temperature."""
set_req = self.gateway.const.SetReq set_req = self.gateway.const.SetReq
self.gateway.set_child_value( self.gateway.set_child_value(
@ -153,9 +153,9 @@ class MySensorsHVAC(mysensors.MySensorsEntity, ClimateDevice):
if self.gateway.optimistic: if self.gateway.optimistic:
# Optimistically assume that device has changed state # Optimistically assume that device has changed state
self._values[set_req.V_HVAC_SPEED] = fan_mode self._values[set_req.V_HVAC_SPEED] = fan_mode
self.schedule_update_ha_state() self.async_schedule_update_ha_state()
def set_operation_mode(self, operation_mode): async def async_set_operation_mode(self, operation_mode):
"""Set new target temperature.""" """Set new target temperature."""
self.gateway.set_child_value( self.gateway.set_child_value(
self.node_id, self.child_id, self.value_type, self.node_id, self.child_id, self.value_type,
@ -163,7 +163,7 @@ class MySensorsHVAC(mysensors.MySensorsEntity, ClimateDevice):
if self.gateway.optimistic: if self.gateway.optimistic:
# Optimistically assume that device has changed state # Optimistically assume that device has changed state
self._values[self.value_type] = operation_mode self._values[self.value_type] = operation_mode
self.schedule_update_ha_state() self.async_schedule_update_ha_state()
async def async_update(self): async def async_update(self):
"""Update the controller with the latest value from a sensor.""" """Update the controller with the latest value from a sensor."""

View File

@ -25,7 +25,7 @@ from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.util.temperature import convert as convert_temperature from homeassistant.util.temperature import convert as convert_temperature
REQUIREMENTS = ['pysensibo==1.0.2'] REQUIREMENTS = ['pysensibo==1.0.3']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)

View File

@ -11,9 +11,11 @@ import voluptuous as vol
from homeassistant.components.climate import ( from homeassistant.components.climate import (
ATTR_OPERATION_MODE, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, ATTR_OPERATION_MODE, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW,
PLATFORM_SCHEMA, STATE_AUTO, STATE_COOL, STATE_HEAT, SUPPORT_FAN_MODE, PLATFORM_SCHEMA, STATE_AUTO, STATE_COOL, STATE_HEAT, SUPPORT_FAN_MODE,
SUPPORT_OPERATION_MODE, SUPPORT_TARGET_HUMIDITY, SUPPORT_OPERATION_MODE, SUPPORT_TARGET_HUMIDITY, SUPPORT_AWAY_MODE,
SUPPORT_TARGET_TEMPERATURE, SUPPORT_TARGET_TEMPERATURE_HIGH, SUPPORT_TARGET_HUMIDITY_HIGH, SUPPORT_TARGET_HUMIDITY_LOW,
SUPPORT_TARGET_TEMPERATURE_LOW, ClimateDevice) SUPPORT_HOLD_MODE, SUPPORT_TARGET_TEMPERATURE,
SUPPORT_TARGET_TEMPERATURE_HIGH, SUPPORT_TARGET_TEMPERATURE_LOW,
ClimateDevice)
from homeassistant.const import ( from homeassistant.const import (
ATTR_TEMPERATURE, CONF_HOST, CONF_PASSWORD, CONF_SSL, CONF_TIMEOUT, ATTR_TEMPERATURE, CONF_HOST, CONF_PASSWORD, CONF_SSL, CONF_TIMEOUT,
CONF_USERNAME, PRECISION_WHOLE, STATE_OFF, STATE_ON, TEMP_CELSIUS, CONF_USERNAME, PRECISION_WHOLE, STATE_OFF, STATE_ON, TEMP_CELSIUS,
@ -27,14 +29,20 @@ _LOGGER = logging.getLogger(__name__)
ATTR_FAN_STATE = 'fan_state' ATTR_FAN_STATE = 'fan_state'
ATTR_HVAC_STATE = 'hvac_state' ATTR_HVAC_STATE = 'hvac_state'
CONF_HUMIDIFIER = 'humidifier'
DEFAULT_SSL = False DEFAULT_SSL = False
VALID_FAN_STATES = [STATE_ON, STATE_AUTO] VALID_FAN_STATES = [STATE_ON, STATE_AUTO]
VALID_THERMOSTAT_MODES = [STATE_HEAT, STATE_COOL, STATE_OFF, STATE_AUTO] VALID_THERMOSTAT_MODES = [STATE_HEAT, STATE_COOL, STATE_OFF, STATE_AUTO]
HOLD_MODE_OFF = 'off'
HOLD_MODE_TEMPERATURE = 'temperature'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_HOST): cv.string, vol.Required(CONF_HOST): cv.string,
vol.Optional(CONF_PASSWORD): cv.string, vol.Optional(CONF_PASSWORD): cv.string,
vol.Optional(CONF_HUMIDIFIER, default=True): cv.boolean,
vol.Optional(CONF_SSL, default=DEFAULT_SSL): cv.boolean, vol.Optional(CONF_SSL, default=DEFAULT_SSL): cv.boolean,
vol.Optional(CONF_TIMEOUT, default=5): vol.Optional(CONF_TIMEOUT, default=5):
vol.All(vol.Coerce(int), vol.Range(min=1)), vol.All(vol.Coerce(int), vol.Range(min=1)),
@ -50,6 +58,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
password = config.get(CONF_PASSWORD) password = config.get(CONF_PASSWORD)
host = config.get(CONF_HOST) host = config.get(CONF_HOST)
timeout = config.get(CONF_TIMEOUT) timeout = config.get(CONF_TIMEOUT)
humidifier = config.get(CONF_HUMIDIFIER)
if config.get(CONF_SSL): if config.get(CONF_SSL):
proto = 'https' proto = 'https'
@ -60,15 +69,16 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
addr=host, timeout=timeout, user=username, password=password, addr=host, timeout=timeout, user=username, password=password,
proto=proto) proto=proto)
add_devices([VenstarThermostat(client)], True) add_devices([VenstarThermostat(client, humidifier)], True)
class VenstarThermostat(ClimateDevice): class VenstarThermostat(ClimateDevice):
"""Representation of a Venstar thermostat.""" """Representation of a Venstar thermostat."""
def __init__(self, client): def __init__(self, client, humidifier):
"""Initialize the thermostat.""" """Initialize the thermostat."""
self._client = client self._client = client
self._humidifier = humidifier
def update(self): def update(self):
"""Update the data from the thermostat.""" """Update the data from the thermostat."""
@ -81,14 +91,18 @@ class VenstarThermostat(ClimateDevice):
def supported_features(self): def supported_features(self):
"""Return the list of supported features.""" """Return the list of supported features."""
features = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_FAN_MODE | features = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_FAN_MODE |
SUPPORT_OPERATION_MODE) SUPPORT_OPERATION_MODE | SUPPORT_AWAY_MODE |
SUPPORT_HOLD_MODE)
if self._client.mode == self._client.MODE_AUTO: if self._client.mode == self._client.MODE_AUTO:
features |= (SUPPORT_TARGET_TEMPERATURE_HIGH | features |= (SUPPORT_TARGET_TEMPERATURE_HIGH |
SUPPORT_TARGET_TEMPERATURE_LOW) SUPPORT_TARGET_TEMPERATURE_LOW)
if self._client.hum_active == 1: if (self._humidifier and
features |= SUPPORT_TARGET_HUMIDITY hasattr(self._client, 'hum_active')):
features |= (SUPPORT_TARGET_HUMIDITY |
SUPPORT_TARGET_HUMIDITY_HIGH |
SUPPORT_TARGET_HUMIDITY_LOW)
return features return features
@ -197,6 +211,18 @@ class VenstarThermostat(ClimateDevice):
"""Return the maximum humidity. Hardcoded to 60 in API.""" """Return the maximum humidity. Hardcoded to 60 in API."""
return 60 return 60
@property
def is_away_mode_on(self):
"""Return the status of away mode."""
return self._client.away == self._client.AWAY_AWAY
@property
def current_hold_mode(self):
"""Return the status of hold mode."""
if self._client.schedule == 0:
return HOLD_MODE_TEMPERATURE
return HOLD_MODE_OFF
def _set_operation_mode(self, operation_mode): def _set_operation_mode(self, operation_mode):
"""Change the operation mode (internal).""" """Change the operation mode (internal)."""
if operation_mode == STATE_HEAT: if operation_mode == STATE_HEAT:
@ -259,3 +285,30 @@ class VenstarThermostat(ClimateDevice):
if not success: if not success:
_LOGGER.error("Failed to change the target humidity level") _LOGGER.error("Failed to change the target humidity level")
def set_hold_mode(self, hold_mode):
"""Set the hold mode."""
if hold_mode == HOLD_MODE_TEMPERATURE:
success = self._client.set_schedule(0)
elif hold_mode == HOLD_MODE_OFF:
success = self._client.set_schedule(1)
else:
_LOGGER.error("Unknown hold mode: %s", hold_mode)
success = False
if not success:
_LOGGER.error("Failed to change the schedule/hold state")
def turn_away_mode_on(self):
"""Activate away mode."""
success = self._client.set_away(self._client.AWAY_AWAY)
if not success:
_LOGGER.error("Failed to activate away mode")
def turn_away_mode_off(self):
"""Deactivate away mode."""
success = self._client.set_away(self._client.AWAY_HOME)
if not success:
_LOGGER.error("Failed to deactivate away mode")

View File

@ -190,7 +190,7 @@ class WinkThermostat(WinkDevice, ClimateDevice):
@property @property
def cool_on(self): def cool_on(self):
"""Return whether or not the heat is actually heating.""" """Return whether or not the heat is actually heating."""
return self.wink.heat_on() return self.wink.cool_on()
@property @property
def current_operation(self): def current_operation(self):

View File

@ -96,6 +96,7 @@ async def async_setup(hass, config):
async def process(service): async def process(service):
"""Parse text into commands.""" """Parse text into commands."""
text = service.data[ATTR_TEXT] text = service.data[ATTR_TEXT]
_LOGGER.debug('Processing: <%s>', text)
try: try:
await _process(hass, text) await _process(hass, text)
except intent.IntentHandleError as err: except intent.IntentHandleError as err:

View File

@ -0,0 +1,10 @@
# Describes the format for available component services
process:
description: Launch a conversation from a transcribed text.
fields:
text:
description: Transcribed text
example: Turn all lights on

View File

@ -1,5 +1,5 @@
""" """
Support for Gogogate2 Garage Doors. Support for Gogogate2 garage Doors.
For more details about this platform, please refer to the documentation For more details about this platform, please refer to the documentation
https://home-assistant.io/components/cover.gogogate2/ https://home-assistant.io/components/cover.gogogate2/
@ -15,7 +15,7 @@ from homeassistant.const import (
CONF_IP_ADDRESS, CONF_NAME) CONF_IP_ADDRESS, CONF_NAME)
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['pygogogate2==0.0.7'] REQUIREMENTS = ['pygogogate2==0.1.1']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -25,9 +25,9 @@ NOTIFICATION_ID = 'gogogate2_notification'
NOTIFICATION_TITLE = 'Gogogate2 Cover Setup' NOTIFICATION_TITLE = 'Gogogate2 Cover Setup'
COVER_SCHEMA = vol.Schema({ COVER_SCHEMA = vol.Schema({
vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_PASSWORD): cv.string,
vol.Required(CONF_IP_ADDRESS): cv.string, vol.Required(CONF_IP_ADDRESS): cv.string,
vol.Required(CONF_PASSWORD): cv.string,
vol.Required(CONF_USERNAME): cv.string,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
}) })
@ -36,10 +36,11 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Gogogate2 component.""" """Set up the Gogogate2 component."""
from pygogogate2 import Gogogate2API as pygogogate2 from pygogogate2 import Gogogate2API as pygogogate2
username = config.get(CONF_USERNAME)
password = config.get(CONF_PASSWORD)
ip_address = config.get(CONF_IP_ADDRESS) ip_address = config.get(CONF_IP_ADDRESS)
name = config.get(CONF_NAME) name = config.get(CONF_NAME)
password = config.get(CONF_PASSWORD)
username = config.get(CONF_USERNAME)
mygogogate2 = pygogogate2(username, password, ip_address) mygogogate2 = pygogogate2(username, password, ip_address)
try: try:

View File

@ -42,7 +42,7 @@ class MySensorsCover(mysensors.MySensorsEntity, CoverDevice):
set_req = self.gateway.const.SetReq set_req = self.gateway.const.SetReq
return self._values.get(set_req.V_DIMMER) return self._values.get(set_req.V_DIMMER)
def open_cover(self, **kwargs): async def async_open_cover(self, **kwargs):
"""Move the cover up.""" """Move the cover up."""
set_req = self.gateway.const.SetReq set_req = self.gateway.const.SetReq
self.gateway.set_child_value( self.gateway.set_child_value(
@ -53,9 +53,9 @@ class MySensorsCover(mysensors.MySensorsEntity, CoverDevice):
self._values[set_req.V_DIMMER] = 100 self._values[set_req.V_DIMMER] = 100
else: else:
self._values[set_req.V_LIGHT] = STATE_ON self._values[set_req.V_LIGHT] = STATE_ON
self.schedule_update_ha_state() self.async_schedule_update_ha_state()
def close_cover(self, **kwargs): async def async_close_cover(self, **kwargs):
"""Move the cover down.""" """Move the cover down."""
set_req = self.gateway.const.SetReq set_req = self.gateway.const.SetReq
self.gateway.set_child_value( self.gateway.set_child_value(
@ -66,9 +66,9 @@ class MySensorsCover(mysensors.MySensorsEntity, CoverDevice):
self._values[set_req.V_DIMMER] = 0 self._values[set_req.V_DIMMER] = 0
else: else:
self._values[set_req.V_LIGHT] = STATE_OFF self._values[set_req.V_LIGHT] = STATE_OFF
self.schedule_update_ha_state() self.async_schedule_update_ha_state()
def set_cover_position(self, **kwargs): async def async_set_cover_position(self, **kwargs):
"""Move the cover to a specific position.""" """Move the cover to a specific position."""
position = kwargs.get(ATTR_POSITION) position = kwargs.get(ATTR_POSITION)
set_req = self.gateway.const.SetReq set_req = self.gateway.const.SetReq
@ -77,9 +77,9 @@ class MySensorsCover(mysensors.MySensorsEntity, CoverDevice):
if self.gateway.optimistic: if self.gateway.optimistic:
# Optimistically assume that cover has changed state. # Optimistically assume that cover has changed state.
self._values[set_req.V_DIMMER] = position self._values[set_req.V_DIMMER] = position
self.schedule_update_ha_state() self.async_schedule_update_ha_state()
def stop_cover(self, **kwargs): async def async_stop_cover(self, **kwargs):
"""Stop the device.""" """Stop the device."""
set_req = self.gateway.const.SetReq set_req = self.gateway.const.SetReq
self.gateway.set_child_value( self.gateway.set_child_value(

View File

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

View File

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

View File

@ -33,7 +33,7 @@ from homeassistant.helpers.event import async_track_utc_time_change
from homeassistant.const import ( from homeassistant.const import (
ATTR_GPS_ACCURACY, ATTR_LATITUDE, ATTR_LONGITUDE, CONF_NAME, CONF_MAC, ATTR_GPS_ACCURACY, ATTR_LATITUDE, ATTR_LONGITUDE, CONF_NAME, CONF_MAC,
DEVICE_DEFAULT_NAME, STATE_HOME, STATE_NOT_HOME, ATTR_ENTITY_ID, DEVICE_DEFAULT_NAME, STATE_HOME, STATE_NOT_HOME, ATTR_ENTITY_ID,
CONF_ICON, ATTR_ICON) CONF_ICON, ATTR_ICON, ATTR_NAME)
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -71,7 +71,6 @@ ATTR_GPS = 'gps'
ATTR_HOST_NAME = 'host_name' ATTR_HOST_NAME = 'host_name'
ATTR_LOCATION_NAME = 'location_name' ATTR_LOCATION_NAME = 'location_name'
ATTR_MAC = 'mac' ATTR_MAC = 'mac'
ATTR_NAME = 'name'
ATTR_SOURCE_TYPE = 'source_type' ATTR_SOURCE_TYPE = 'source_type'
ATTR_CONSIDER_HOME = 'consider_home' ATTR_CONSIDER_HOME = 'consider_home'

View File

@ -12,14 +12,18 @@ import voluptuous as vol
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.components.device_tracker import ( from homeassistant.components.device_tracker import (
PLATFORM_SCHEMA, SOURCE_TYPE_GPS) PLATFORM_SCHEMA, SOURCE_TYPE_GPS)
from homeassistant.const import CONF_USERNAME, CONF_PASSWORD from homeassistant.const import CONF_USERNAME, CONF_PASSWORD, ATTR_ID
from homeassistant.helpers.event import track_time_interval from homeassistant.helpers.event import track_time_interval
from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.typing import ConfigType
from homeassistant.util import slugify
REQUIREMENTS = ['locationsharinglib==2.0.2']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
REQUIREMENTS = ['locationsharinglib==1.2.2'] ATTR_ADDRESS = 'address'
ATTR_FULL_NAME = 'full_name'
ATTR_LAST_SEEN = 'last_seen'
ATTR_NICKNAME = 'nickname'
CREDENTIALS_FILE = '.google_maps_location_sharing.cookies' CREDENTIALS_FILE = '.google_maps_location_sharing.cookies'
@ -60,19 +64,23 @@ class GoogleMapsScanner(object):
self.success_init = True self.success_init = True
except InvalidUser: except InvalidUser:
_LOGGER.error('You have specified invalid login credentials') _LOGGER.error("You have specified invalid login credentials")
self.success_init = False self.success_init = False
def _update_info(self, now=None): def _update_info(self, now=None):
for person in self.service.get_all_people(): for person in self.service.get_all_people():
dev_id = 'google_maps_{0}'.format(slugify(person.id)) try:
dev_id = 'google_maps_{0}'.format(person.id)
except TypeError:
_LOGGER.warning("No location(s) shared with this account")
return
attrs = { attrs = {
'id': person.id, ATTR_ADDRESS: person.address,
'nickname': person.nickname, ATTR_FULL_NAME: person.full_name,
'full_name': person.full_name, ATTR_ID: person.id,
'last_seen': person.datetime, ATTR_LAST_SEEN: person.datetime,
'address': person.address ATTR_NICKNAME: person.nickname,
} }
self.see( self.see(
dev_id=dev_id, dev_id=dev_id,
@ -80,5 +88,5 @@ class GoogleMapsScanner(object):
picture=person.picture_url, picture=person.picture_url,
source_type=SOURCE_TYPE_GPS, source_type=SOURCE_TYPE_GPS,
gps_accuracy=person.accuracy, gps_accuracy=person.accuracy,
attributes=attrs attributes=attrs,
) )

View File

@ -14,15 +14,18 @@ import homeassistant.helpers.config_validation as cv
from homeassistant.components.device_tracker import ( from homeassistant.components.device_tracker import (
DOMAIN, PLATFORM_SCHEMA, DeviceScanner) DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
from homeassistant.const import ( from homeassistant.const import (
CONF_HOST, CONF_PASSWORD, CONF_USERNAME CONF_HOST, CONF_PASSWORD, CONF_USERNAME, CONF_TYPE
) )
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
DEFAULT_TYPE = "rogers"
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_HOST): cv.string, vol.Required(CONF_HOST): cv.string,
vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_PASSWORD): cv.string vol.Required(CONF_PASSWORD): cv.string,
vol.Optional(CONF_TYPE, default=DEFAULT_TYPE): cv.string,
}) })
@ -49,6 +52,11 @@ class HitronCODADeviceScanner(DeviceScanner):
self._username = config.get(CONF_USERNAME) self._username = config.get(CONF_USERNAME)
self._password = config.get(CONF_PASSWORD) self._password = config.get(CONF_PASSWORD)
if config.get(CONF_TYPE) == "shaw":
self._type = 'pwd'
else:
self._type = 'pws'
self._userid = None self._userid = None
self.success_init = self._update_info() self.success_init = self._update_info()
@ -74,7 +82,7 @@ class HitronCODADeviceScanner(DeviceScanner):
try: try:
data = [ data = [
('user', self._username), ('user', self._username),
('pws', self._password), (self._type, self._password),
] ]
res = requests.post(self._loginurl, data=data, timeout=10) res = requests.post(self._loginurl, data=data, timeout=10)
except requests.exceptions.Timeout: except requests.exceptions.Timeout:

View File

@ -24,8 +24,9 @@ _LOGGER = logging.getLogger(__name__)
REQUIREMENTS = ['pyicloud==0.9.1'] REQUIREMENTS = ['pyicloud==0.9.1']
CONF_IGNORED_DEVICES = 'ignored_devices'
CONF_ACCOUNTNAME = 'account_name' CONF_ACCOUNTNAME = 'account_name'
CONF_MAX_INTERVAL = 'max_interval'
CONF_GPS_ACCURACY_THRESHOLD = 'gps_accuracy_threshold'
# entity attributes # entity attributes
ATTR_ACCOUNTNAME = 'account_name' ATTR_ACCOUNTNAME = 'account_name'
@ -64,13 +65,15 @@ DEVICESTATUSCODES = {
SERVICE_SCHEMA = vol.Schema({ SERVICE_SCHEMA = vol.Schema({
vol.Optional(ATTR_ACCOUNTNAME): vol.All(cv.ensure_list, [cv.slugify]), vol.Optional(ATTR_ACCOUNTNAME): vol.All(cv.ensure_list, [cv.slugify]),
vol.Optional(ATTR_DEVICENAME): cv.slugify, vol.Optional(ATTR_DEVICENAME): cv.slugify,
vol.Optional(ATTR_INTERVAL): cv.positive_int, vol.Optional(ATTR_INTERVAL): cv.positive_int
}) })
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_PASSWORD): cv.string, vol.Required(CONF_PASSWORD): cv.string,
vol.Optional(ATTR_ACCOUNTNAME): cv.slugify, vol.Optional(ATTR_ACCOUNTNAME): cv.slugify,
vol.Optional(CONF_MAX_INTERVAL, default=30): cv.positive_int,
vol.Optional(CONF_GPS_ACCURACY_THRESHOLD, default=1000): cv.positive_int
}) })
@ -79,8 +82,11 @@ def setup_scanner(hass, config: dict, see, discovery_info=None):
username = config.get(CONF_USERNAME) username = config.get(CONF_USERNAME)
password = config.get(CONF_PASSWORD) password = config.get(CONF_PASSWORD)
account = config.get(CONF_ACCOUNTNAME, slugify(username.partition('@')[0])) account = config.get(CONF_ACCOUNTNAME, slugify(username.partition('@')[0]))
max_interval = config.get(CONF_MAX_INTERVAL)
gps_accuracy_threshold = config.get(CONF_GPS_ACCURACY_THRESHOLD)
icloudaccount = Icloud(hass, username, password, account, see) icloudaccount = Icloud(hass, username, password, account, max_interval,
gps_accuracy_threshold, see)
if icloudaccount.api is not None: if icloudaccount.api is not None:
ICLOUDTRACKERS[account] = icloudaccount ICLOUDTRACKERS[account] = icloudaccount
@ -96,6 +102,7 @@ def setup_scanner(hass, config: dict, see, discovery_info=None):
for account in accounts: for account in accounts:
if account in ICLOUDTRACKERS: if account in ICLOUDTRACKERS:
ICLOUDTRACKERS[account].lost_iphone(devicename) ICLOUDTRACKERS[account].lost_iphone(devicename)
hass.services.register(DOMAIN, 'icloud_lost_iphone', lost_iphone, hass.services.register(DOMAIN, 'icloud_lost_iphone', lost_iphone,
schema=SERVICE_SCHEMA) schema=SERVICE_SCHEMA)
@ -106,6 +113,7 @@ def setup_scanner(hass, config: dict, see, discovery_info=None):
for account in accounts: for account in accounts:
if account in ICLOUDTRACKERS: if account in ICLOUDTRACKERS:
ICLOUDTRACKERS[account].update_icloud(devicename) ICLOUDTRACKERS[account].update_icloud(devicename)
hass.services.register(DOMAIN, 'icloud_update', update_icloud, hass.services.register(DOMAIN, 'icloud_update', update_icloud,
schema=SERVICE_SCHEMA) schema=SERVICE_SCHEMA)
@ -115,6 +123,7 @@ def setup_scanner(hass, config: dict, see, discovery_info=None):
for account in accounts: for account in accounts:
if account in ICLOUDTRACKERS: if account in ICLOUDTRACKERS:
ICLOUDTRACKERS[account].reset_account_icloud() ICLOUDTRACKERS[account].reset_account_icloud()
hass.services.register(DOMAIN, 'icloud_reset_account', hass.services.register(DOMAIN, 'icloud_reset_account',
reset_account_icloud, schema=SERVICE_SCHEMA) reset_account_icloud, schema=SERVICE_SCHEMA)
@ -137,7 +146,8 @@ def setup_scanner(hass, config: dict, see, discovery_info=None):
class Icloud(DeviceScanner): class Icloud(DeviceScanner):
"""Representation of an iCloud account.""" """Representation of an iCloud account."""
def __init__(self, hass, username, password, name, see): def __init__(self, hass, username, password, name, max_interval,
gps_accuracy_threshold, see):
"""Initialize an iCloud account.""" """Initialize an iCloud account."""
self.hass = hass self.hass = hass
self.username = username self.username = username
@ -148,6 +158,8 @@ class Icloud(DeviceScanner):
self.seen_devices = {} self.seen_devices = {}
self._overridestates = {} self._overridestates = {}
self._intervals = {} self._intervals = {}
self._max_interval = max_interval
self._gps_accuracy_threshold = gps_accuracy_threshold
self.see = see self.see = see
self._trusted_device = None self._trusted_device = None
@ -348,7 +360,7 @@ class Icloud(DeviceScanner):
self._overridestates[devicename] = None self._overridestates[devicename] = None
if currentzone is not None: if currentzone is not None:
self._intervals[devicename] = 30 self._intervals[devicename] = self._max_interval
return return
if mindistance is None: if mindistance is None:
@ -363,7 +375,6 @@ class Icloud(DeviceScanner):
if interval > 180: if interval > 180:
# Three hour drive? This is far enough that they might be flying # Three hour drive? This is far enough that they might be flying
# home - check every half hour
interval = 30 interval = 30
if battery is not None and battery <= 33 and mindistance > 3: if battery is not None and battery <= 33 and mindistance > 3:
@ -403,22 +414,24 @@ class Icloud(DeviceScanner):
status = device.status(DEVICESTATUSSET) status = device.status(DEVICESTATUSSET)
battery = status.get('batteryLevel', 0) * 100 battery = status.get('batteryLevel', 0) * 100
location = status['location'] location = status['location']
if location: if location and location['horizontalAccuracy']:
self.determine_interval( horizontal_accuracy = int(location['horizontalAccuracy'])
devicename, location['latitude'], if horizontal_accuracy < self._gps_accuracy_threshold:
location['longitude'], battery) self.determine_interval(
interval = self._intervals.get(devicename, 1) devicename, location['latitude'],
attrs[ATTR_INTERVAL] = interval location['longitude'], battery)
accuracy = location['horizontalAccuracy'] interval = self._intervals.get(devicename, 1)
kwargs['dev_id'] = dev_id attrs[ATTR_INTERVAL] = interval
kwargs['host_name'] = status['name'] accuracy = location['horizontalAccuracy']
kwargs['gps'] = (location['latitude'], kwargs['dev_id'] = dev_id
location['longitude']) kwargs['host_name'] = status['name']
kwargs['battery'] = battery kwargs['gps'] = (location['latitude'],
kwargs['gps_accuracy'] = accuracy location['longitude'])
kwargs[ATTR_ATTRIBUTES] = attrs kwargs['battery'] = battery
self.see(**kwargs) kwargs['gps_accuracy'] = accuracy
self.seen_devices[devicename] = True kwargs[ATTR_ATTRIBUTES] = attrs
self.see(**kwargs)
self.seen_devices[devicename] = True
except PyiCloudNoDevicesException: except PyiCloudNoDevicesException:
_LOGGER.error("No iCloud Devices found") _LOGGER.error("No iCloud Devices found")
@ -434,7 +447,7 @@ class Icloud(DeviceScanner):
device.play_sound() device.play_sound()
def update_icloud(self, devicename=None): def update_icloud(self, devicename=None):
"""Authenticate against iCloud and scan for devices.""" """Request device information from iCloud and update device_tracker."""
from pyicloud.exceptions import PyiCloudNoDevicesException from pyicloud.exceptions import PyiCloudNoDevicesException
if self.api is None: if self.api is None:
@ -443,13 +456,13 @@ class Icloud(DeviceScanner):
try: try:
if devicename is not None: if devicename is not None:
if devicename in self.devices: if devicename in self.devices:
self.devices[devicename].location() self.update_device(devicename)
else: else:
_LOGGER.error("devicename %s unknown for account %s", _LOGGER.error("devicename %s unknown for account %s",
devicename, self._attrs[ATTR_ACCOUNTNAME]) devicename, self._attrs[ATTR_ACCOUNTNAME])
else: else:
for device in self.devices: for device in self.devices:
self.devices[device].location() self.update_device(device)
except PyiCloudNoDevicesException: except PyiCloudNoDevicesException:
_LOGGER.error("No iCloud Devices found") _LOGGER.error("No iCloud Devices found")

View File

@ -37,8 +37,10 @@ SERVICE_WINK = 'wink'
SERVICE_XIAOMI_GW = 'xiaomi_gw' SERVICE_XIAOMI_GW = 'xiaomi_gw'
SERVICE_TELLDUSLIVE = 'tellstick' SERVICE_TELLDUSLIVE = 'tellstick'
SERVICE_HUE = 'philips_hue' SERVICE_HUE = 'philips_hue'
SERVICE_KONNECTED = 'konnected'
SERVICE_DECONZ = 'deconz' SERVICE_DECONZ = 'deconz'
SERVICE_DAIKIN = 'daikin' SERVICE_DAIKIN = 'daikin'
SERVICE_SABNZBD = 'sabnzbd'
SERVICE_SAMSUNG_PRINTER = 'samsung_printer' SERVICE_SAMSUNG_PRINTER = 'samsung_printer'
SERVICE_HOMEKIT = 'homekit' SERVICE_HOMEKIT = 'homekit'
@ -59,7 +61,9 @@ SERVICE_HANDLERS = {
SERVICE_XIAOMI_GW: ('xiaomi_aqara', None), SERVICE_XIAOMI_GW: ('xiaomi_aqara', None),
SERVICE_TELLDUSLIVE: ('tellduslive', None), SERVICE_TELLDUSLIVE: ('tellduslive', None),
SERVICE_DAIKIN: ('daikin', None), SERVICE_DAIKIN: ('daikin', None),
SERVICE_SABNZBD: ('sabnzbd', None),
SERVICE_SAMSUNG_PRINTER: ('sensor', 'syncthru'), SERVICE_SAMSUNG_PRINTER: ('sensor', 'syncthru'),
SERVICE_KONNECTED: ('konnected', None),
'google_cast': ('media_player', 'cast'), 'google_cast': ('media_player', 'cast'),
'panasonic_viera': ('media_player', 'panasonic_viera'), 'panasonic_viera': ('media_player', 'panasonic_viera'),
'plex_mediaserver': ('media_player', 'plex'), 'plex_mediaserver': ('media_player', 'plex'),
@ -74,7 +78,6 @@ SERVICE_HANDLERS = {
'frontier_silicon': ('media_player', 'frontier_silicon'), 'frontier_silicon': ('media_player', 'frontier_silicon'),
'openhome': ('media_player', 'openhome'), 'openhome': ('media_player', 'openhome'),
'harmony': ('remote', 'harmony'), 'harmony': ('remote', 'harmony'),
'sabnzbd': ('sensor', 'sabnzbd'),
'bose_soundtouch': ('media_player', 'soundtouch'), 'bose_soundtouch': ('media_player', 'soundtouch'),
'bluesound': ('media_player', 'bluesound'), 'bluesound': ('media_player', 'bluesound'),
'songpal': ('media_player', 'songpal'), 'songpal': ('media_player', 'songpal'),
@ -190,6 +193,7 @@ def _discover(netdisco):
for disc in netdisco.discover(): for disc in netdisco.discover():
for service in netdisco.get_info(disc): for service in netdisco.get_info(disc):
results.append((disc, service)) results.append((disc, service))
finally: finally:
netdisco.stop() netdisco.stop()

View File

@ -15,7 +15,7 @@ from homeassistant.helpers import discovery
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['lakeside==0.5'] REQUIREMENTS = ['lakeside==0.6']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)

View File

@ -51,8 +51,8 @@ set_direction:
description: Name(s) of the entities to toggle description: Name(s) of the entities to toggle
example: 'fan.living_room' example: 'fan.living_room'
direction: direction:
description: The direction to rotate description: The direction to rotate. Either 'forward' or 'reverse'
example: 'left' example: 'forward'
dyson_set_night_mode: dyson_set_night_mode:
description: Set the fan in night mode. description: Set the fan in night mode.

View File

@ -18,11 +18,10 @@ from homeassistant.exceptions import TemplateError
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.config_validation import PLATFORM_SCHEMA from homeassistant.helpers.config_validation import PLATFORM_SCHEMA
from homeassistant.helpers.entity import ToggleEntity from homeassistant.helpers.entity import ToggleEntity
from homeassistant.components.fan import (SPEED_LOW, SPEED_MEDIUM, from homeassistant.components.fan import (
SPEED_HIGH, SUPPORT_SET_SPEED, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH, SUPPORT_SET_SPEED, SUPPORT_OSCILLATE,
SUPPORT_OSCILLATE, FanEntity, FanEntity, ATTR_SPEED, ATTR_OSCILLATING, ENTITY_ID_FORMAT,
ATTR_SPEED, ATTR_OSCILLATING, SUPPORT_DIRECTION, DIRECTION_FORWARD, DIRECTION_REVERSE, ATTR_DIRECTION)
ENTITY_ID_FORMAT)
from homeassistant.helpers.entity import async_generate_entity_id from homeassistant.helpers.entity import async_generate_entity_id
from homeassistant.helpers.script import Script from homeassistant.helpers.script import Script
@ -33,25 +32,30 @@ CONF_FANS = 'fans'
CONF_SPEED_LIST = 'speeds' CONF_SPEED_LIST = 'speeds'
CONF_SPEED_TEMPLATE = 'speed_template' CONF_SPEED_TEMPLATE = 'speed_template'
CONF_OSCILLATING_TEMPLATE = 'oscillating_template' CONF_OSCILLATING_TEMPLATE = 'oscillating_template'
CONF_DIRECTION_TEMPLATE = 'direction_template'
CONF_ON_ACTION = 'turn_on' CONF_ON_ACTION = 'turn_on'
CONF_OFF_ACTION = 'turn_off' CONF_OFF_ACTION = 'turn_off'
CONF_SET_SPEED_ACTION = 'set_speed' CONF_SET_SPEED_ACTION = 'set_speed'
CONF_SET_OSCILLATING_ACTION = 'set_oscillating' CONF_SET_OSCILLATING_ACTION = 'set_oscillating'
CONF_SET_DIRECTION_ACTION = 'set_direction'
_VALID_STATES = [STATE_ON, STATE_OFF] _VALID_STATES = [STATE_ON, STATE_OFF]
_VALID_OSC = [True, False] _VALID_OSC = [True, False]
_VALID_DIRECTIONS = [DIRECTION_FORWARD, DIRECTION_REVERSE]
FAN_SCHEMA = vol.Schema({ FAN_SCHEMA = vol.Schema({
vol.Optional(CONF_FRIENDLY_NAME): cv.string, vol.Optional(CONF_FRIENDLY_NAME): cv.string,
vol.Required(CONF_VALUE_TEMPLATE): cv.template, vol.Required(CONF_VALUE_TEMPLATE): cv.template,
vol.Optional(CONF_SPEED_TEMPLATE): cv.template, vol.Optional(CONF_SPEED_TEMPLATE): cv.template,
vol.Optional(CONF_OSCILLATING_TEMPLATE): cv.template, vol.Optional(CONF_OSCILLATING_TEMPLATE): cv.template,
vol.Optional(CONF_DIRECTION_TEMPLATE): cv.template,
vol.Required(CONF_ON_ACTION): cv.SCRIPT_SCHEMA, vol.Required(CONF_ON_ACTION): cv.SCRIPT_SCHEMA,
vol.Required(CONF_OFF_ACTION): cv.SCRIPT_SCHEMA, vol.Required(CONF_OFF_ACTION): cv.SCRIPT_SCHEMA,
vol.Optional(CONF_SET_SPEED_ACTION): cv.SCRIPT_SCHEMA, vol.Optional(CONF_SET_SPEED_ACTION): cv.SCRIPT_SCHEMA,
vol.Optional(CONF_SET_OSCILLATING_ACTION): cv.SCRIPT_SCHEMA, vol.Optional(CONF_SET_OSCILLATING_ACTION): cv.SCRIPT_SCHEMA,
vol.Optional(CONF_SET_DIRECTION_ACTION): cv.SCRIPT_SCHEMA,
vol.Optional( vol.Optional(
CONF_SPEED_LIST, CONF_SPEED_LIST,
@ -80,18 +84,21 @@ async def async_setup_platform(
oscillating_template = device_config.get( oscillating_template = device_config.get(
CONF_OSCILLATING_TEMPLATE CONF_OSCILLATING_TEMPLATE
) )
direction_template = device_config.get(CONF_DIRECTION_TEMPLATE)
on_action = device_config[CONF_ON_ACTION] on_action = device_config[CONF_ON_ACTION]
off_action = device_config[CONF_OFF_ACTION] off_action = device_config[CONF_OFF_ACTION]
set_speed_action = device_config.get(CONF_SET_SPEED_ACTION) set_speed_action = device_config.get(CONF_SET_SPEED_ACTION)
set_oscillating_action = device_config.get(CONF_SET_OSCILLATING_ACTION) set_oscillating_action = device_config.get(CONF_SET_OSCILLATING_ACTION)
set_direction_action = device_config.get(CONF_SET_DIRECTION_ACTION)
speed_list = device_config[CONF_SPEED_LIST] speed_list = device_config[CONF_SPEED_LIST]
entity_ids = set() entity_ids = set()
manual_entity_ids = device_config.get(CONF_ENTITY_ID) manual_entity_ids = device_config.get(CONF_ENTITY_ID)
for template in (state_template, speed_template, oscillating_template): for template in (state_template, speed_template, oscillating_template,
direction_template):
if template is None: if template is None:
continue continue
template.hass = hass template.hass = hass
@ -114,8 +121,9 @@ async def async_setup_platform(
TemplateFan( TemplateFan(
hass, device, friendly_name, hass, device, friendly_name,
state_template, speed_template, oscillating_template, state_template, speed_template, oscillating_template,
on_action, off_action, set_speed_action, direction_template, on_action, off_action, set_speed_action,
set_oscillating_action, speed_list, entity_ids set_oscillating_action, set_direction_action, speed_list,
entity_ids
) )
) )
@ -127,8 +135,9 @@ class TemplateFan(FanEntity):
def __init__(self, hass, device_id, friendly_name, def __init__(self, hass, device_id, friendly_name,
state_template, speed_template, oscillating_template, state_template, speed_template, oscillating_template,
on_action, off_action, set_speed_action, direction_template, on_action, off_action, set_speed_action,
set_oscillating_action, speed_list, entity_ids): set_oscillating_action, set_direction_action, speed_list,
entity_ids):
"""Initialize the fan.""" """Initialize the fan."""
self.hass = hass self.hass = hass
self.entity_id = async_generate_entity_id( self.entity_id = async_generate_entity_id(
@ -138,6 +147,7 @@ class TemplateFan(FanEntity):
self._template = state_template self._template = state_template
self._speed_template = speed_template self._speed_template = speed_template
self._oscillating_template = oscillating_template self._oscillating_template = oscillating_template
self._direction_template = direction_template
self._supported_features = 0 self._supported_features = 0
self._on_script = Script(hass, on_action) self._on_script = Script(hass, on_action)
@ -151,9 +161,14 @@ class TemplateFan(FanEntity):
if set_oscillating_action: if set_oscillating_action:
self._set_oscillating_script = Script(hass, set_oscillating_action) self._set_oscillating_script = Script(hass, set_oscillating_action)
self._set_direction_script = None
if set_direction_action:
self._set_direction_script = Script(hass, set_direction_action)
self._state = STATE_OFF self._state = STATE_OFF
self._speed = None self._speed = None
self._oscillating = None self._oscillating = None
self._direction = None
self._template.hass = self.hass self._template.hass = self.hass
if self._speed_template: if self._speed_template:
@ -162,6 +177,9 @@ class TemplateFan(FanEntity):
if self._oscillating_template: if self._oscillating_template:
self._oscillating_template.hass = self.hass self._oscillating_template.hass = self.hass
self._supported_features |= SUPPORT_OSCILLATE self._supported_features |= SUPPORT_OSCILLATE
if self._direction_template:
self._direction_template.hass = self.hass
self._supported_features |= SUPPORT_DIRECTION
self._entities = entity_ids self._entities = entity_ids
# List of valid speeds # List of valid speeds
@ -197,6 +215,11 @@ class TemplateFan(FanEntity):
"""Return the oscillation state.""" """Return the oscillation state."""
return self._oscillating return self._oscillating
@property
def direction(self):
"""Return the oscillation state."""
return self._direction
@property @property
def should_poll(self): def should_poll(self):
"""Return the polling state.""" """Return the polling state."""
@ -236,10 +259,30 @@ class TemplateFan(FanEntity):
if self._set_oscillating_script is None: if self._set_oscillating_script is None:
return return
await self._set_oscillating_script.async_run( if oscillating in _VALID_OSC:
{ATTR_OSCILLATING: oscillating} self._oscillating = oscillating
) await self._set_oscillating_script.async_run(
self._oscillating = oscillating {ATTR_OSCILLATING: oscillating})
else:
_LOGGER.error(
'Received invalid oscillating value: %s. ' +
'Expected: %s.',
oscillating, ', '.join(_VALID_OSC))
async def async_set_direction(self, direction: str) -> None:
"""Set the direction of the fan."""
if self._set_direction_script is None:
return
if direction in _VALID_DIRECTIONS:
self._direction = direction
await self._set_direction_script.async_run(
{ATTR_DIRECTION: direction})
else:
_LOGGER.error(
'Received invalid direction: %s. ' +
'Expected: %s.',
direction, ', '.join(_VALID_DIRECTIONS))
async def async_added_to_hass(self): async def async_added_to_hass(self):
"""Register callbacks.""" """Register callbacks."""
@ -308,6 +351,7 @@ class TemplateFan(FanEntity):
oscillating = self._oscillating_template.async_render() oscillating = self._oscillating_template.async_render()
except TemplateError as ex: except TemplateError as ex:
_LOGGER.error(ex) _LOGGER.error(ex)
oscillating = None
self._state = None self._state = None
# Validate osc # Validate osc
@ -322,3 +366,24 @@ class TemplateFan(FanEntity):
'Received invalid oscillating: %s. ' + 'Received invalid oscillating: %s. ' +
'Expected: True/False.', oscillating) 'Expected: True/False.', oscillating)
self._oscillating = None self._oscillating = None
# Update direction if 'direction_template' is configured
if self._direction_template is not None:
try:
direction = self._direction_template.async_render()
except TemplateError as ex:
_LOGGER.error(ex)
direction = None
self._state = None
# Validate speed
if direction in _VALID_DIRECTIONS:
self._direction = direction
elif direction == STATE_UNKNOWN:
self._direction = None
else:
_LOGGER.error(
'Received invalid direction: %s. ' +
'Expected: %s.',
direction, ', '.join(_VALID_DIRECTIONS))
self._direction = None

View File

@ -10,7 +10,6 @@ from homeassistant.components import zha
from homeassistant.components.fan import ( from homeassistant.components.fan import (
DOMAIN, FanEntity, SPEED_OFF, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH, DOMAIN, FanEntity, SPEED_OFF, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH,
SUPPORT_SET_SPEED) SUPPORT_SET_SPEED)
from homeassistant.const import STATE_UNKNOWN
DEPENDENCIES = ['zha'] DEPENDENCIES = ['zha']
@ -72,7 +71,7 @@ class ZhaFan(zha.Entity, FanEntity):
@property @property
def is_on(self) -> bool: def is_on(self) -> bool:
"""Return true if entity is on.""" """Return true if entity is on."""
if self._state == STATE_UNKNOWN: if self._state is None:
return False return False
return self._state != SPEED_OFF return self._state != SPEED_OFF
@ -103,7 +102,7 @@ class ZhaFan(zha.Entity, FanEntity):
"""Retrieve latest state.""" """Retrieve latest state."""
result = yield from zha.safe_read(self._endpoint.fan, ['fan_mode']) result = yield from zha.safe_read(self._endpoint.fan, ['fan_mode'])
new_value = result.get('fan_mode', None) new_value = result.get('fan_mode', None)
self._state = VALUE_TO_SPEED.get(new_value, STATE_UNKNOWN) self._state = VALUE_TO_SPEED.get(new_value, None)
@property @property
def should_poll(self) -> bool: def should_poll(self) -> bool:

View File

@ -4,7 +4,7 @@ Support for RSS/Atom feeds.
For more details about this component, please refer to the documentation at For more details about this component, please refer to the documentation at
https://home-assistant.io/components/feedreader/ https://home-assistant.io/components/feedreader/
""" """
from datetime import datetime from datetime import datetime, timedelta
from logging import getLogger from logging import getLogger
from os.path import exists from os.path import exists
from threading import Lock from threading import Lock
@ -12,8 +12,8 @@ import pickle
import voluptuous as vol import voluptuous as vol
from homeassistant.const import EVENT_HOMEASSISTANT_START from homeassistant.const import EVENT_HOMEASSISTANT_START, CONF_SCAN_INTERVAL
from homeassistant.helpers.event import track_utc_time_change from homeassistant.helpers.event import track_time_interval
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['feedparser==5.2.1'] REQUIREMENTS = ['feedparser==5.2.1']
@ -21,16 +21,22 @@ REQUIREMENTS = ['feedparser==5.2.1']
_LOGGER = getLogger(__name__) _LOGGER = getLogger(__name__)
CONF_URLS = 'urls' CONF_URLS = 'urls'
CONF_MAX_ENTRIES = 'max_entries'
DEFAULT_MAX_ENTRIES = 20
DEFAULT_SCAN_INTERVAL = timedelta(hours=1)
DOMAIN = 'feedreader' DOMAIN = 'feedreader'
EVENT_FEEDREADER = 'feedreader' EVENT_FEEDREADER = 'feedreader'
MAX_ENTRIES = 20
CONFIG_SCHEMA = vol.Schema({ CONFIG_SCHEMA = vol.Schema({
DOMAIN: { DOMAIN: {
vol.Required(CONF_URLS): vol.All(cv.ensure_list, [cv.url]), vol.Required(CONF_URLS): vol.All(cv.ensure_list, [cv.url]),
vol.Optional(CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL):
cv.time_period,
vol.Optional(CONF_MAX_ENTRIES, default=DEFAULT_MAX_ENTRIES):
cv.positive_int
} }
}, extra=vol.ALLOW_EXTRA) }, extra=vol.ALLOW_EXTRA)
@ -38,33 +44,50 @@ CONFIG_SCHEMA = vol.Schema({
def setup(hass, config): def setup(hass, config):
"""Set up the Feedreader component.""" """Set up the Feedreader component."""
urls = config.get(DOMAIN)[CONF_URLS] urls = config.get(DOMAIN)[CONF_URLS]
scan_interval = config.get(DOMAIN).get(CONF_SCAN_INTERVAL)
max_entries = config.get(DOMAIN).get(CONF_MAX_ENTRIES)
data_file = hass.config.path("{}.pickle".format(DOMAIN)) data_file = hass.config.path("{}.pickle".format(DOMAIN))
storage = StoredData(data_file) storage = StoredData(data_file)
feeds = [FeedManager(url, hass, storage) for url in urls] feeds = [FeedManager(url, scan_interval, max_entries, hass, storage) for
url in urls]
return len(feeds) > 0 return len(feeds) > 0
class FeedManager(object): class FeedManager(object):
"""Abstraction over Feedparser module.""" """Abstraction over Feedparser module."""
def __init__(self, url, hass, storage): def __init__(self, url, scan_interval, max_entries, hass, storage):
"""Initialize the FeedManager object, poll every hour.""" """Initialize the FeedManager object, poll as per scan interval."""
self._url = url self._url = url
self._scan_interval = scan_interval
self._max_entries = max_entries
self._feed = None self._feed = None
self._hass = hass self._hass = hass
self._firstrun = True self._firstrun = True
self._storage = storage self._storage = storage
self._last_entry_timestamp = None self._last_entry_timestamp = None
self._last_update_successful = False
self._has_published_parsed = False self._has_published_parsed = False
self._event_type = EVENT_FEEDREADER
self._feed_id = url
hass.bus.listen_once( hass.bus.listen_once(
EVENT_HOMEASSISTANT_START, lambda _: self._update()) EVENT_HOMEASSISTANT_START, lambda _: self._update())
track_utc_time_change( self._init_regular_updates(hass)
hass, lambda now: self._update(), minute=0, second=0)
def _log_no_entries(self): def _log_no_entries(self):
"""Send no entries log at debug level.""" """Send no entries log at debug level."""
_LOGGER.debug("No new entries to be published in feed %s", self._url) _LOGGER.debug("No new entries to be published in feed %s", self._url)
def _init_regular_updates(self, hass):
"""Schedule regular updates at the top of the clock."""
track_time_interval(hass, lambda now: self._update(),
self._scan_interval)
@property
def last_update_successful(self):
"""Return True if the last feed update was successful."""
return self._last_update_successful
def _update(self): def _update(self):
"""Update the feed and publish new entries to the event bus.""" """Update the feed and publish new entries to the event bus."""
import feedparser import feedparser
@ -76,26 +99,39 @@ class FeedManager(object):
else self._feed.get('modified')) else self._feed.get('modified'))
if not self._feed: if not self._feed:
_LOGGER.error("Error fetching feed data from %s", self._url) _LOGGER.error("Error fetching feed data from %s", self._url)
self._last_update_successful = False
else: else:
# The 'bozo' flag really only indicates that there was an issue
# during the initial parsing of the XML, but it doesn't indicate
# whether this is an unrecoverable error. In this case the
# feedparser lib is trying a less strict parsing approach.
# If an error is detected here, log error message but continue
# processing the feed entries if present.
if self._feed.bozo != 0: if self._feed.bozo != 0:
_LOGGER.error("Error parsing feed %s", self._url) _LOGGER.error("Error parsing feed %s: %s", self._url,
self._feed.bozo_exception)
# Using etag and modified, if there's no new data available, # Using etag and modified, if there's no new data available,
# the entries list will be empty # the entries list will be empty
elif self._feed.entries: if self._feed.entries:
_LOGGER.debug("%s entri(es) available in feed %s", _LOGGER.debug("%s entri(es) available in feed %s",
len(self._feed.entries), self._url) len(self._feed.entries), self._url)
if len(self._feed.entries) > MAX_ENTRIES: self._filter_entries()
_LOGGER.debug("Processing only the first %s entries "
"in feed %s", MAX_ENTRIES, self._url)
self._feed.entries = self._feed.entries[0:MAX_ENTRIES]
self._publish_new_entries() self._publish_new_entries()
if self._has_published_parsed: if self._has_published_parsed:
self._storage.put_timestamp( self._storage.put_timestamp(
self._url, self._last_entry_timestamp) self._feed_id, self._last_entry_timestamp)
else: else:
self._log_no_entries() self._log_no_entries()
self._last_update_successful = True
_LOGGER.info("Fetch from feed %s completed", self._url) _LOGGER.info("Fetch from feed %s completed", self._url)
def _filter_entries(self):
"""Filter the entries provided and return the ones to keep."""
if len(self._feed.entries) > self._max_entries:
_LOGGER.debug("Processing only the first %s entries "
"in feed %s", self._max_entries, self._url)
self._feed.entries = self._feed.entries[0:self._max_entries]
def _update_and_fire_entry(self, entry): def _update_and_fire_entry(self, entry):
"""Update last_entry_timestamp and fire entry.""" """Update last_entry_timestamp and fire entry."""
# We are lucky, `published_parsed` data available, let's make use of # We are lucky, `published_parsed` data available, let's make use of
@ -109,12 +145,12 @@ class FeedManager(object):
_LOGGER.debug("No published_parsed info available for entry %s", _LOGGER.debug("No published_parsed info available for entry %s",
entry.title) entry.title)
entry.update({'feed_url': self._url}) entry.update({'feed_url': self._url})
self._hass.bus.fire(EVENT_FEEDREADER, entry) self._hass.bus.fire(self._event_type, entry)
def _publish_new_entries(self): def _publish_new_entries(self):
"""Publish new entries to the event bus.""" """Publish new entries to the event bus."""
new_entries = False new_entries = False
self._last_entry_timestamp = self._storage.get_timestamp(self._url) self._last_entry_timestamp = self._storage.get_timestamp(self._feed_id)
if self._last_entry_timestamp: if self._last_entry_timestamp:
self._firstrun = False self._firstrun = False
else: else:
@ -157,18 +193,18 @@ class StoredData(object):
_LOGGER.error("Error loading data from pickled file %s", _LOGGER.error("Error loading data from pickled file %s",
self._data_file) self._data_file)
def get_timestamp(self, url): def get_timestamp(self, feed_id):
"""Return stored timestamp for given url.""" """Return stored timestamp for given feed id (usually the url)."""
self._fetch_data() self._fetch_data()
return self._data.get(url) return self._data.get(feed_id)
def put_timestamp(self, url, timestamp): def put_timestamp(self, feed_id, timestamp):
"""Update timestamp for given URL.""" """Update timestamp for given feed id (usually the url)."""
self._fetch_data() self._fetch_data()
with self._lock, open(self._data_file, 'wb') as myfile: with self._lock, open(self._data_file, 'wb') as myfile:
self._data.update({url: timestamp}) self._data.update({feed_id: timestamp})
_LOGGER.debug("Overwriting feed %s timestamp in storage file %s", _LOGGER.debug("Overwriting feed %s timestamp in storage file %s",
url, self._data_file) feed_id, self._data_file)
try: try:
pickle.dump(self._data, myfile) pickle.dump(self._data, myfile)
except: # noqa: E722 # pylint: disable=bare-except except: # noqa: E722 # pylint: disable=bare-except

View File

@ -43,7 +43,7 @@ def setup(hass, config):
def create_event_handler(patterns, hass): def create_event_handler(patterns, hass):
""""Return the Watchdog EventHandler object.""" """Return the Watchdog EventHandler object."""
from watchdog.events import PatternMatchingEventHandler from watchdog.events import PatternMatchingEventHandler
class EventHandler(PatternMatchingEventHandler): class EventHandler(PatternMatchingEventHandler):

View File

@ -25,7 +25,7 @@ from homeassistant.core import callback
from homeassistant.helpers.translation import async_get_translations from homeassistant.helpers.translation import async_get_translations
from homeassistant.loader import bind_hass from homeassistant.loader import bind_hass
REQUIREMENTS = ['home-assistant-frontend==20180509.0'] REQUIREMENTS = ['home-assistant-frontend==20180526.4']
DOMAIN = 'frontend' DOMAIN = 'frontend'
DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log'] DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log']
@ -147,21 +147,6 @@ class AbstractPanel:
'get', '/{}/{{extra:.+}}'.format(self.frontend_url_path), 'get', '/{}/{{extra:.+}}'.format(self.frontend_url_path),
index_view.get) index_view.get)
def to_response(self, hass, request):
"""Panel as dictionary."""
result = {
'component_name': self.component_name,
'icon': self.sidebar_icon,
'title': self.sidebar_title,
'url_path': self.frontend_url_path,
'config': self.config,
}
if _is_latest(hass.data[DATA_JS_VERSION], request):
result['url'] = self.webcomponent_url_latest
else:
result['url'] = self.webcomponent_url_es5
return result
class BuiltInPanel(AbstractPanel): class BuiltInPanel(AbstractPanel):
"""Panel that is part of hass_frontend.""" """Panel that is part of hass_frontend."""
@ -175,30 +160,15 @@ class BuiltInPanel(AbstractPanel):
self.frontend_url_path = frontend_url_path or component_name self.frontend_url_path = frontend_url_path or component_name
self.config = config self.config = config
@asyncio.coroutine def to_response(self, hass, request):
def async_finalize(self, hass, frontend_repository_path): """Panel as dictionary."""
"""Finalize this panel for usage. return {
'component_name': self.component_name,
If frontend_repository_path is set, will be prepended to path of 'icon': self.sidebar_icon,
built-in components. 'title': self.sidebar_title,
""" 'config': self.config,
if frontend_repository_path is None: 'url_path': self.frontend_url_path,
import hass_frontend }
import hass_frontend_es5
self.webcomponent_url_latest = \
'/frontend_latest/panels/ha-panel-{}-{}.html'.format(
self.component_name,
hass_frontend.FINGERPRINTS[self.component_name])
self.webcomponent_url_es5 = \
'/frontend_es5/panels/ha-panel-{}-{}.html'.format(
self.component_name,
hass_frontend_es5.FINGERPRINTS[self.component_name])
else:
# Dev mode
self.webcomponent_url_es5 = self.webcomponent_url_latest = \
'/home-assistant-polymer/panels/{}/ha-panel-{}.html'.format(
self.component_name, self.component_name)
class ExternalPanel(AbstractPanel): class ExternalPanel(AbstractPanel):
@ -244,6 +214,21 @@ class ExternalPanel(AbstractPanel):
frontend_repository_path is None) frontend_repository_path is None)
self.REGISTERED_COMPONENTS.add(self.component_name) self.REGISTERED_COMPONENTS.add(self.component_name)
def to_response(self, hass, request):
"""Panel as dictionary."""
result = {
'component_name': self.component_name,
'icon': self.sidebar_icon,
'title': self.sidebar_title,
'url_path': self.frontend_url_path,
'config': self.config,
}
if _is_latest(hass.data[DATA_JS_VERSION], request):
result['url'] = self.webcomponent_url_latest
else:
result['url'] = self.webcomponent_url_es5
return result
@bind_hass @bind_hass
@asyncio.coroutine @asyncio.coroutine
@ -296,6 +281,15 @@ def add_manifest_json_key(key, val):
@asyncio.coroutine @asyncio.coroutine
def async_setup(hass, config): def async_setup(hass, config):
"""Set up the serving of the frontend.""" """Set up the serving of the frontend."""
if list(hass.auth.async_auth_providers):
client = yield from hass.auth.async_create_client(
'Home Assistant Frontend',
redirect_uris=['/'],
no_secret=True,
)
else:
client = None
hass.components.websocket_api.async_register_command( hass.components.websocket_api.async_register_command(
WS_TYPE_GET_PANELS, websocket_handle_get_panels, SCHEMA_GET_PANELS) WS_TYPE_GET_PANELS, websocket_handle_get_panels, SCHEMA_GET_PANELS)
hass.http.register_view(ManifestJSONView) hass.http.register_view(ManifestJSONView)
@ -307,59 +301,40 @@ def async_setup(hass, config):
hass.data[DATA_JS_VERSION] = js_version = conf.get(CONF_JS_VERSION) hass.data[DATA_JS_VERSION] = js_version = conf.get(CONF_JS_VERSION)
if is_dev: if is_dev:
for subpath in ["src", "build-translations", "build-temp", "build", hass_frontend_path = os.path.join(repo_path, 'hass_frontend')
"hass_frontend", "bower_components", "panels", hass_frontend_es5_path = os.path.join(repo_path, 'hass_frontend_es5')
"hassio"]:
hass.http.register_static_path(
"/home-assistant-polymer/{}".format(subpath),
os.path.join(repo_path, subpath),
False)
hass.http.register_static_path(
"/static/translations",
os.path.join(repo_path, "build-translations/output"), False)
sw_path_es5 = os.path.join(repo_path, "build-es5/service_worker.js")
sw_path_latest = os.path.join(repo_path, "build/service_worker.js")
static_path = os.path.join(repo_path, 'hass_frontend')
frontend_es5_path = os.path.join(repo_path, 'build-es5')
frontend_latest_path = os.path.join(repo_path, 'build')
else: else:
import hass_frontend import hass_frontend
import hass_frontend_es5 import hass_frontend_es5
sw_path_es5 = os.path.join(hass_frontend_es5.where(), hass_frontend_path = hass_frontend.where()
"service_worker.js") hass_frontend_es5_path = hass_frontend_es5.where()
sw_path_latest = os.path.join(hass_frontend.where(),
"service_worker.js")
# /static points to dir with files that are JS-type agnostic.
# ES5 files are served from /frontend_es5.
# ES6 files are served from /frontend_latest.
static_path = hass_frontend.where()
frontend_es5_path = hass_frontend_es5.where()
frontend_latest_path = static_path
hass.http.register_static_path( hass.http.register_static_path(
"/service_worker_es5.js", sw_path_es5, False) "/service_worker_es5.js",
os.path.join(hass_frontend_es5_path, "service_worker.js"), False)
hass.http.register_static_path( hass.http.register_static_path(
"/service_worker.js", sw_path_latest, False) "/service_worker.js",
os.path.join(hass_frontend_path, "service_worker.js"), False)
hass.http.register_static_path( hass.http.register_static_path(
"/robots.txt", os.path.join(static_path, "robots.txt"), not is_dev) "/robots.txt",
hass.http.register_static_path("/static", static_path, not is_dev) os.path.join(hass_frontend_path, "robots.txt"), False)
hass.http.register_static_path("/static", hass_frontend_path, not is_dev)
hass.http.register_static_path( hass.http.register_static_path(
"/frontend_latest", frontend_latest_path, not is_dev) "/frontend_latest", hass_frontend_path, not is_dev)
hass.http.register_static_path( hass.http.register_static_path(
"/frontend_es5", frontend_es5_path, not is_dev) "/frontend_es5", hass_frontend_es5_path, not is_dev)
local = hass.config.path('www') local = hass.config.path('www')
if os.path.isdir(local): if os.path.isdir(local):
hass.http.register_static_path("/local", local, not is_dev) hass.http.register_static_path("/local", local, not is_dev)
index_view = IndexView(repo_path, js_version) index_view = IndexView(repo_path, js_version, client)
hass.http.register_view(index_view) hass.http.register_view(index_view)
@asyncio.coroutine async def finalize_panel(panel):
def finalize_panel(panel):
"""Finalize setup of a panel.""" """Finalize setup of a panel."""
yield from panel.async_finalize(hass, repo_path) if hasattr(panel, 'async_finalize'):
await panel.async_finalize(hass, repo_path)
panel.async_register_index_routes(hass.http.app.router, index_view) panel.async_register_index_routes(hass.http.app.router, index_view)
yield from asyncio.wait([ yield from asyncio.wait([
@ -451,10 +426,11 @@ class IndexView(HomeAssistantView):
requires_auth = False requires_auth = False
extra_urls = ['/states', '/states/{extra}'] extra_urls = ['/states', '/states/{extra}']
def __init__(self, repo_path, js_option): def __init__(self, repo_path, js_option, client):
"""Initialize the frontend view.""" """Initialize the frontend view."""
self.repo_path = repo_path self.repo_path = repo_path
self.js_option = js_option self.js_option = js_option
self.client = client
self._template_cache = {} self._template_cache = {}
def get_template(self, latest): def get_template(self, latest):
@ -508,7 +484,7 @@ class IndexView(HomeAssistantView):
extra_key = DATA_EXTRA_HTML_URL if latest else DATA_EXTRA_HTML_URL_ES5 extra_key = DATA_EXTRA_HTML_URL if latest else DATA_EXTRA_HTML_URL_ES5
resp = template.render( template_params = dict(
no_auth=no_auth, no_auth=no_auth,
panel_url=panel_url, panel_url=panel_url,
panels=hass.data[DATA_PANELS], panels=hass.data[DATA_PANELS],
@ -516,7 +492,11 @@ class IndexView(HomeAssistantView):
extra_urls=hass.data[extra_key], extra_urls=hass.data[extra_key],
) )
return web.Response(text=resp, content_type='text/html') if self.client is not None:
template_params['client_id'] = self.client.id
return web.Response(text=template.render(**template_params),
content_type='text/html')
class ManifestJSONView(HomeAssistantView): class ManifestJSONView(HomeAssistantView):

View File

@ -14,7 +14,7 @@ from homeassistant.const import (
ATTR_ENTITY_ID, CONF_ICON, CONF_NAME, STATE_CLOSED, STATE_HOME, ATTR_ENTITY_ID, CONF_ICON, CONF_NAME, STATE_CLOSED, STATE_HOME,
STATE_NOT_HOME, STATE_OFF, STATE_ON, STATE_OPEN, STATE_LOCKED, STATE_NOT_HOME, STATE_OFF, STATE_ON, STATE_OPEN, STATE_LOCKED,
STATE_UNLOCKED, STATE_OK, STATE_PROBLEM, STATE_UNKNOWN, STATE_UNLOCKED, STATE_OK, STATE_PROBLEM, STATE_UNKNOWN,
ATTR_ASSUMED_STATE, SERVICE_RELOAD) ATTR_ASSUMED_STATE, SERVICE_RELOAD, ATTR_NAME, ATTR_ICON)
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.loader import bind_hass from homeassistant.loader import bind_hass
from homeassistant.helpers.entity import Entity, async_generate_entity_id from homeassistant.helpers.entity import Entity, async_generate_entity_id
@ -35,8 +35,6 @@ ATTR_ADD_ENTITIES = 'add_entities'
ATTR_AUTO = 'auto' ATTR_AUTO = 'auto'
ATTR_CONTROL = 'control' ATTR_CONTROL = 'control'
ATTR_ENTITIES = 'entities' ATTR_ENTITIES = 'entities'
ATTR_ICON = 'icon'
ATTR_NAME = 'name'
ATTR_OBJECT_ID = 'object_id' ATTR_OBJECT_ID = 'object_id'
ATTR_ORDER = 'order' ATTR_ORDER = 'order'
ATTR_VIEW = 'view' ATTR_VIEW = 'view'

View File

@ -13,12 +13,13 @@ import voluptuous as vol
from homeassistant.components import SERVICE_CHECK_CONFIG from homeassistant.components import SERVICE_CHECK_CONFIG
from homeassistant.const import ( from homeassistant.const import (
SERVICE_HOMEASSISTANT_RESTART, SERVICE_HOMEASSISTANT_STOP) ATTR_NAME, SERVICE_HOMEASSISTANT_RESTART, SERVICE_HOMEASSISTANT_STOP)
from homeassistant.core import DOMAIN as HASS_DOMAIN from homeassistant.core import DOMAIN as HASS_DOMAIN
from homeassistant.core import callback from homeassistant.core import callback
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.loader import bind_hass from homeassistant.loader import bind_hass
from homeassistant.util.dt import utcnow from homeassistant.util.dt import utcnow
from .handler import HassIO from .handler import HassIO
from .http import HassIOView from .http import HassIOView
@ -47,7 +48,6 @@ ATTR_SNAPSHOT = 'snapshot'
ATTR_ADDONS = 'addons' ATTR_ADDONS = 'addons'
ATTR_FOLDERS = 'folders' ATTR_FOLDERS = 'folders'
ATTR_HOMEASSISTANT = 'homeassistant' ATTR_HOMEASSISTANT = 'homeassistant'
ATTR_NAME = 'name'
ATTR_PASSWORD = 'password' ATTR_PASSWORD = 'password'
SCHEMA_NO_DATA = vol.Schema({}) SCHEMA_NO_DATA = vol.Schema({})

View File

@ -33,7 +33,7 @@ def _api_bool(funct):
def _api_data(funct): def _api_data(funct):
"""Return a api data.""" """Return data of an api."""
@asyncio.coroutine @asyncio.coroutine
def _wrapper(*argv, **kwargs): def _wrapper(*argv, **kwargs):
"""Wrap function.""" """Wrap function."""

View File

@ -36,7 +36,7 @@ NO_TIMEOUT = {
} }
NO_AUTH = { NO_AUTH = {
re.compile(r'^app-(es5|latest)/(index|hassio-app).html$'), re.compile(r'^app-(es5|latest)/.+$'),
re.compile(r'^addons/[^/]*/logo$') re.compile(r'^addons/[^/]*/logo$')
} }

View File

@ -12,25 +12,25 @@ import voluptuous as vol
from homeassistant.components.cover import ( from homeassistant.components.cover import (
SUPPORT_CLOSE, SUPPORT_OPEN, SUPPORT_SET_POSITION) SUPPORT_CLOSE, SUPPORT_OPEN, SUPPORT_SET_POSITION)
from homeassistant.const import ( from homeassistant.const import (
ATTR_SUPPORTED_FEATURES, ATTR_UNIT_OF_MEASUREMENT, ATTR_DEVICE_CLASS, ATTR_SUPPORTED_FEATURES, ATTR_UNIT_OF_MEASUREMENT,
ATTR_DEVICE_CLASS, CONF_IP_ADDRESS, CONF_PORT, TEMP_CELSIUS, CONF_IP_ADDRESS, CONF_NAME, CONF_PORT,
TEMP_FAHRENHEIT, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_ILLUMINANCE, DEVICE_CLASS_TEMPERATURE,
DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_ILLUMINANCE, DEVICE_CLASS_TEMPERATURE) EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP,
TEMP_CELSIUS, TEMP_FAHRENHEIT)
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entityfilter import FILTER_SCHEMA from homeassistant.helpers.entityfilter import FILTER_SCHEMA
from homeassistant.util import get_local_ip from homeassistant.util import get_local_ip
from homeassistant.util.decorator import Registry from homeassistant.util.decorator import Registry
from .const import ( from .const import (
DOMAIN, HOMEKIT_FILE, CONF_AUTO_START, CONF_ENTITY_CONFIG, CONF_FILTER, CONF_AUTO_START, CONF_ENTITY_CONFIG, CONF_FILTER, DEFAULT_AUTO_START,
DEFAULT_PORT, DEFAULT_AUTO_START, SERVICE_HOMEKIT_START, DEFAULT_PORT, DEVICE_CLASS_CO2, DEVICE_CLASS_PM25, DOMAIN, HOMEKIT_FILE,
DEVICE_CLASS_CO2, DEVICE_CLASS_PM25) SERVICE_HOMEKIT_START)
from .util import ( from .util import show_setup_message, validate_entity_config
validate_entity_config, show_setup_message)
TYPES = Registry() TYPES = Registry()
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
REQUIREMENTS = ['HAP-python==2.0.0'] REQUIREMENTS = ['HAP-python==2.1.0']
# #### Driver Status #### # #### Driver Status ####
STATUS_READY = 0 STATUS_READY = 0
@ -93,7 +93,7 @@ def get_accessory(hass, state, aid, config):
return None return None
a_type = None a_type = None
config = config or {} name = config.get(CONF_NAME, state.name)
if state.domain == 'alarm_control_panel': if state.domain == 'alarm_control_panel':
a_type = 'SecuritySystem' a_type = 'SecuritySystem'
@ -116,6 +116,9 @@ def get_accessory(hass, state, aid, config):
elif features & (SUPPORT_OPEN | SUPPORT_CLOSE): elif features & (SUPPORT_OPEN | SUPPORT_CLOSE):
a_type = 'WindowCoveringBasic' a_type = 'WindowCoveringBasic'
elif state.domain == 'fan':
a_type = 'Fan'
elif state.domain == 'light': elif state.domain == 'light':
a_type = 'Light' a_type = 'Light'
@ -147,7 +150,7 @@ def get_accessory(hass, state, aid, config):
return None return None
_LOGGER.debug('Add "%s" as "%s"', state.entity_id, a_type) _LOGGER.debug('Add "%s" as "%s"', state.entity_id, a_type)
return TYPES[a_type](hass, state.name, state.entity_id, aid, config=config) return TYPES[a_type](hass, name, state.entity_id, aid, config)
def generate_aid(entity_id): def generate_aid(entity_id):
@ -183,7 +186,8 @@ class HomeKit():
ip_addr = self._ip_address or get_local_ip() ip_addr = self._ip_address or get_local_ip()
path = self.hass.config.path(HOMEKIT_FILE) path = self.hass.config.path(HOMEKIT_FILE)
self.bridge = HomeBridge(self.hass) self.bridge = HomeBridge(self.hass)
self.driver = HomeDriver(self.bridge, self._port, ip_addr, path) self.driver = HomeDriver(self.hass, self.bridge, port=self._port,
address=ip_addr, persist_file=path)
def add_bridge_accessory(self, state): def add_bridge_accessory(self, state):
"""Try adding accessory to bridge if configured beforehand.""" """Try adding accessory to bridge if configured beforehand."""
@ -203,15 +207,16 @@ class HomeKit():
# pylint: disable=unused-variable # pylint: disable=unused-variable
from . import ( # noqa F401 from . import ( # noqa F401
type_covers, type_lights, type_locks, type_security_systems, type_covers, type_fans, type_lights, type_locks,
type_sensors, type_switches, type_thermostats) type_security_systems, type_sensors, type_switches,
type_thermostats)
for state in self.hass.states.all(): for state in self.hass.states.all():
self.add_bridge_accessory(state) self.add_bridge_accessory(state)
self.bridge.set_driver(self.driver) self.bridge.set_driver(self.driver)
if not self.bridge.paired: if not self.driver.state.paired:
show_setup_message(self.hass, self.bridge) show_setup_message(self.hass, self.driver.state.pincode)
_LOGGER.debug('Driver start') _LOGGER.debug('Driver start')
self.hass.add_job(self.driver.start) self.hass.add_job(self.driver.start)

View File

@ -16,8 +16,8 @@ from homeassistant.helpers.event import (
from homeassistant.util import dt as dt_util from homeassistant.util import dt as dt_util
from .const import ( from .const import (
DEBOUNCE_TIMEOUT, BRIDGE_MODEL, BRIDGE_NAME, BRIDGE_MODEL, BRIDGE_NAME, BRIDGE_SERIAL_NUMBER,
BRIDGE_SERIAL_NUMBER, MANUFACTURER) DEBOUNCE_TIMEOUT, MANUFACTURER)
from .util import ( from .util import (
show_setup_message, dismiss_setup_message) show_setup_message, dismiss_setup_message)
@ -64,14 +64,16 @@ def debounce(func):
class HomeAccessory(Accessory): class HomeAccessory(Accessory):
"""Adapter class for Accessory.""" """Adapter class for Accessory."""
def __init__(self, hass, name, entity_id, aid, category=CATEGORY_OTHER): def __init__(self, hass, name, entity_id, aid, config,
category=CATEGORY_OTHER):
"""Initialize a Accessory object.""" """Initialize a Accessory object."""
super().__init__(name, aid=aid) super().__init__(name, aid=aid)
domain = split_entity_id(entity_id)[0].replace("_", " ").title() model = split_entity_id(entity_id)[0].replace("_", " ").title()
self.set_info_service( self.set_info_service(
firmware_revision=__version__, manufacturer=MANUFACTURER, firmware_revision=__version__, manufacturer=MANUFACTURER,
model=domain, serial_number=entity_id) model=model, serial_number=entity_id)
self.category = category self.category = category
self.config = config
self.entity_id = entity_id self.entity_id = entity_id
self.hass = hass self.hass = hass
@ -82,20 +84,21 @@ class HomeAccessory(Accessory):
async_track_state_change( async_track_state_change(
self.hass, self.entity_id, self.update_state_callback) self.hass, self.entity_id, self.update_state_callback)
@ha_callback
def update_state_callback(self, entity_id=None, old_state=None, def update_state_callback(self, entity_id=None, old_state=None,
new_state=None): new_state=None):
"""Callback from state change listener.""" """Callback from state change listener."""
_LOGGER.debug('New_state: %s', new_state) _LOGGER.debug('New_state: %s', new_state)
if new_state is None: if new_state is None:
return return
self.update_state(new_state) self.hass.async_add_job(self.update_state, new_state)
def update_state(self, new_state): def update_state(self, new_state):
"""Method called on state change to update HomeKit value. """Method called on state change to update HomeKit value.
Overridden by accessory types. Overridden by accessory types.
""" """
pass raise NotImplementedError()
class HomeBridge(Bridge): class HomeBridge(Bridge):
@ -113,20 +116,23 @@ class HomeBridge(Bridge):
"""Prevent print of pyhap setup message to terminal.""" """Prevent print of pyhap setup message to terminal."""
pass pass
def add_paired_client(self, client_uuid, client_public):
"""Override super function to dismiss setup message if paired."""
super().add_paired_client(client_uuid, client_public)
dismiss_setup_message(self.hass)
def remove_paired_client(self, client_uuid):
"""Override super function to show setup message if unpaired."""
super().remove_paired_client(client_uuid)
show_setup_message(self.hass, self)
class HomeDriver(AccessoryDriver): class HomeDriver(AccessoryDriver):
"""Adapter class for AccessoryDriver.""" """Adapter class for AccessoryDriver."""
def __init__(self, *args, **kwargs): def __init__(self, hass, *args, **kwargs):
"""Initialize a AccessoryDriver object.""" """Initialize a AccessoryDriver object."""
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.hass = hass
def pair(self, client_uuid, client_public):
"""Override super function to dismiss setup message if paired."""
value = super().pair(client_uuid, client_public)
if value:
dismiss_setup_message(self.hass)
return value
def unpair(self, client_uuid):
"""Override super function to show setup message if unpaired."""
super().unpair(client_uuid)
show_setup_message(self.hass, self.state.pincode)

View File

@ -1,23 +1,23 @@
"""Constants used be the HomeKit component.""" """Constants used be the HomeKit component."""
# #### MISC #### # #### Misc ####
DEBOUNCE_TIMEOUT = 0.5 DEBOUNCE_TIMEOUT = 0.5
DOMAIN = 'homekit' DOMAIN = 'homekit'
HOMEKIT_FILE = '.homekit.state' HOMEKIT_FILE = '.homekit.state'
HOMEKIT_NOTIFY_ID = 4663548 HOMEKIT_NOTIFY_ID = 4663548
# #### CONFIG #### # #### Config ####
CONF_AUTO_START = 'auto_start' CONF_AUTO_START = 'auto_start'
CONF_ENTITY_CONFIG = 'entity_config' CONF_ENTITY_CONFIG = 'entity_config'
CONF_FILTER = 'filter' CONF_FILTER = 'filter'
# #### CONFIG DEFAULTS #### # #### Config Defaults ####
DEFAULT_AUTO_START = True DEFAULT_AUTO_START = True
DEFAULT_PORT = 51827 DEFAULT_PORT = 51827
# #### HOMEKIT COMPONENT SERVICES #### # #### HomeKit Component Services ####
SERVICE_HOMEKIT_START = 'start' SERVICE_HOMEKIT_START = 'start'
# #### STRING CONSTANTS #### # #### String Constants ####
BRIDGE_MODEL = 'Bridge' BRIDGE_MODEL = 'Bridge'
BRIDGE_NAME = 'Home Assistant Bridge' BRIDGE_NAME = 'Home Assistant Bridge'
BRIDGE_SERIAL_NUMBER = 'homekit.bridge' BRIDGE_SERIAL_NUMBER = 'homekit.bridge'
@ -29,11 +29,12 @@ SERV_AIR_QUALITY_SENSOR = 'AirQualitySensor'
SERV_CARBON_DIOXIDE_SENSOR = 'CarbonDioxideSensor' SERV_CARBON_DIOXIDE_SENSOR = 'CarbonDioxideSensor'
SERV_CARBON_MONOXIDE_SENSOR = 'CarbonMonoxideSensor' SERV_CARBON_MONOXIDE_SENSOR = 'CarbonMonoxideSensor'
SERV_CONTACT_SENSOR = 'ContactSensor' SERV_CONTACT_SENSOR = 'ContactSensor'
SERV_FANV2 = 'Fanv2'
SERV_GARAGE_DOOR_OPENER = 'GarageDoorOpener' SERV_GARAGE_DOOR_OPENER = 'GarageDoorOpener'
SERV_HUMIDITY_SENSOR = 'HumiditySensor' # CurrentRelativeHumidity SERV_HUMIDITY_SENSOR = 'HumiditySensor'
SERV_LEAK_SENSOR = 'LeakSensor' SERV_LEAK_SENSOR = 'LeakSensor'
SERV_LIGHT_SENSOR = 'LightSensor' SERV_LIGHT_SENSOR = 'LightSensor'
SERV_LIGHTBULB = 'Lightbulb' # On | Brightness, Hue, Saturation, Name SERV_LIGHTBULB = 'Lightbulb'
SERV_LOCK = 'LockMechanism' SERV_LOCK = 'LockMechanism'
SERV_MOTION_SENSOR = 'MotionSensor' SERV_MOTION_SENSOR = 'MotionSensor'
SERV_OCCUPANCY_SENSOR = 'OccupancySensor' SERV_OCCUPANCY_SENSOR = 'OccupancySensor'
@ -43,12 +44,12 @@ SERV_SWITCH = 'Switch'
SERV_TEMPERATURE_SENSOR = 'TemperatureSensor' SERV_TEMPERATURE_SENSOR = 'TemperatureSensor'
SERV_THERMOSTAT = 'Thermostat' SERV_THERMOSTAT = 'Thermostat'
SERV_WINDOW_COVERING = 'WindowCovering' SERV_WINDOW_COVERING = 'WindowCovering'
# CurrentPosition, TargetPosition, PositionState
# #### Characteristics #### # #### Characteristics ####
CHAR_ACTIVE = 'Active'
CHAR_AIR_PARTICULATE_DENSITY = 'AirParticulateDensity' CHAR_AIR_PARTICULATE_DENSITY = 'AirParticulateDensity'
CHAR_AIR_QUALITY = 'AirQuality' CHAR_AIR_QUALITY = 'AirQuality'
CHAR_BRIGHTNESS = 'Brightness' # Int | [0, 100] CHAR_BRIGHTNESS = 'Brightness'
CHAR_CARBON_DIOXIDE_DETECTED = 'CarbonDioxideDetected' CHAR_CARBON_DIOXIDE_DETECTED = 'CarbonDioxideDetected'
CHAR_CARBON_DIOXIDE_LEVEL = 'CarbonDioxideLevel' CHAR_CARBON_DIOXIDE_LEVEL = 'CarbonDioxideLevel'
CHAR_CARBON_DIOXIDE_PEAK_LEVEL = 'CarbonDioxidePeakLevel' CHAR_CARBON_DIOXIDE_PEAK_LEVEL = 'CarbonDioxidePeakLevel'
@ -59,13 +60,13 @@ CHAR_COOLING_THRESHOLD_TEMPERATURE = 'CoolingThresholdTemperature'
CHAR_CURRENT_AMBIENT_LIGHT_LEVEL = 'CurrentAmbientLightLevel' CHAR_CURRENT_AMBIENT_LIGHT_LEVEL = 'CurrentAmbientLightLevel'
CHAR_CURRENT_DOOR_STATE = 'CurrentDoorState' CHAR_CURRENT_DOOR_STATE = 'CurrentDoorState'
CHAR_CURRENT_HEATING_COOLING = 'CurrentHeatingCoolingState' CHAR_CURRENT_HEATING_COOLING = 'CurrentHeatingCoolingState'
CHAR_CURRENT_POSITION = 'CurrentPosition' # Int | [0, 100] CHAR_CURRENT_POSITION = 'CurrentPosition'
CHAR_CURRENT_HUMIDITY = 'CurrentRelativeHumidity' # percent CHAR_CURRENT_HUMIDITY = 'CurrentRelativeHumidity'
CHAR_CURRENT_SECURITY_STATE = 'SecuritySystemCurrentState' CHAR_CURRENT_SECURITY_STATE = 'SecuritySystemCurrentState'
CHAR_CURRENT_TEMPERATURE = 'CurrentTemperature' CHAR_CURRENT_TEMPERATURE = 'CurrentTemperature'
CHAR_FIRMWARE_REVISION = 'FirmwareRevision' CHAR_FIRMWARE_REVISION = 'FirmwareRevision'
CHAR_HEATING_THRESHOLD_TEMPERATURE = 'HeatingThresholdTemperature' CHAR_HEATING_THRESHOLD_TEMPERATURE = 'HeatingThresholdTemperature'
CHAR_HUE = 'Hue' # arcdegress | [0, 360] CHAR_HUE = 'Hue'
CHAR_LEAK_DETECTED = 'LeakDetected' CHAR_LEAK_DETECTED = 'LeakDetected'
CHAR_LOCK_CURRENT_STATE = 'LockCurrentState' CHAR_LOCK_CURRENT_STATE = 'LockCurrentState'
CHAR_LOCK_TARGET_STATE = 'LockTargetState' CHAR_LOCK_TARGET_STATE = 'LockTargetState'
@ -75,33 +76,34 @@ CHAR_MODEL = 'Model'
CHAR_MOTION_DETECTED = 'MotionDetected' CHAR_MOTION_DETECTED = 'MotionDetected'
CHAR_NAME = 'Name' CHAR_NAME = 'Name'
CHAR_OCCUPANCY_DETECTED = 'OccupancyDetected' CHAR_OCCUPANCY_DETECTED = 'OccupancyDetected'
CHAR_ON = 'On' # boolean CHAR_ON = 'On'
CHAR_POSITION_STATE = 'PositionState' CHAR_POSITION_STATE = 'PositionState'
CHAR_SATURATION = 'Saturation' # percent CHAR_ROTATION_DIRECTION = 'RotationDirection'
CHAR_SATURATION = 'Saturation'
CHAR_SERIAL_NUMBER = 'SerialNumber' CHAR_SERIAL_NUMBER = 'SerialNumber'
CHAR_SMOKE_DETECTED = 'SmokeDetected' CHAR_SMOKE_DETECTED = 'SmokeDetected'
CHAR_SWING_MODE = 'SwingMode'
CHAR_TARGET_DOOR_STATE = 'TargetDoorState' CHAR_TARGET_DOOR_STATE = 'TargetDoorState'
CHAR_TARGET_HEATING_COOLING = 'TargetHeatingCoolingState' CHAR_TARGET_HEATING_COOLING = 'TargetHeatingCoolingState'
CHAR_TARGET_POSITION = 'TargetPosition' # Int | [0, 100] CHAR_TARGET_POSITION = 'TargetPosition'
CHAR_TARGET_SECURITY_STATE = 'SecuritySystemTargetState' CHAR_TARGET_SECURITY_STATE = 'SecuritySystemTargetState'
CHAR_TARGET_TEMPERATURE = 'TargetTemperature' CHAR_TARGET_TEMPERATURE = 'TargetTemperature'
CHAR_TEMP_DISPLAY_UNITS = 'TemperatureDisplayUnits' CHAR_TEMP_DISPLAY_UNITS = 'TemperatureDisplayUnits'
# #### Properties #### # #### Properties ####
PROP_MAX_VALUE = 'maxValue'
PROP_MIN_VALUE = 'minValue'
PROP_CELSIUS = {'minValue': -273, 'maxValue': 999} PROP_CELSIUS = {'minValue': -273, 'maxValue': 999}
# #### Device Class #### # #### Device Classes ####
DEVICE_CLASS_CO2 = 'co2' DEVICE_CLASS_CO2 = 'co2'
DEVICE_CLASS_DOOR = 'door' DEVICE_CLASS_DOOR = 'door'
DEVICE_CLASS_GARAGE_DOOR = 'garage_door' DEVICE_CLASS_GARAGE_DOOR = 'garage_door'
DEVICE_CLASS_GAS = 'gas' DEVICE_CLASS_GAS = 'gas'
DEVICE_CLASS_HUMIDITY = 'humidity'
DEVICE_CLASS_LIGHT = 'light'
DEVICE_CLASS_MOISTURE = 'moisture' DEVICE_CLASS_MOISTURE = 'moisture'
DEVICE_CLASS_MOTION = 'motion' DEVICE_CLASS_MOTION = 'motion'
DEVICE_CLASS_OCCUPANCY = 'occupancy' DEVICE_CLASS_OCCUPANCY = 'occupancy'
DEVICE_CLASS_OPENING = 'opening' DEVICE_CLASS_OPENING = 'opening'
DEVICE_CLASS_PM25 = 'pm25' DEVICE_CLASS_PM25 = 'pm25'
DEVICE_CLASS_SMOKE = 'smoke' DEVICE_CLASS_SMOKE = 'smoke'
DEVICE_CLASS_TEMPERATURE = 'temperature'
DEVICE_CLASS_WINDOW = 'window' DEVICE_CLASS_WINDOW = 'window'

View File

@ -1,21 +1,21 @@
"""Class to hold all cover accessories.""" """Class to hold all cover accessories."""
import logging import logging
from pyhap.const import CATEGORY_WINDOW_COVERING, CATEGORY_GARAGE_DOOR_OPENER from pyhap.const import CATEGORY_GARAGE_DOOR_OPENER, CATEGORY_WINDOW_COVERING
from homeassistant.components.cover import ( from homeassistant.components.cover import (
ATTR_CURRENT_POSITION, ATTR_POSITION, DOMAIN, SUPPORT_STOP) ATTR_CURRENT_POSITION, ATTR_POSITION, DOMAIN, SUPPORT_STOP)
from homeassistant.const import ( from homeassistant.const import (
ATTR_ENTITY_ID, SERVICE_SET_COVER_POSITION, STATE_OPEN, STATE_CLOSED, ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, SERVICE_CLOSE_COVER,
SERVICE_OPEN_COVER, SERVICE_CLOSE_COVER, SERVICE_STOP_COVER, SERVICE_OPEN_COVER, SERVICE_SET_COVER_POSITION, SERVICE_STOP_COVER,
ATTR_SUPPORTED_FEATURES) STATE_CLOSED, STATE_OPEN)
from . import TYPES from . import TYPES
from .accessories import HomeAccessory, debounce from .accessories import debounce, HomeAccessory
from .const import ( from .const import (
SERV_WINDOW_COVERING, CHAR_CURRENT_POSITION, CHAR_CURRENT_DOOR_STATE, CHAR_CURRENT_POSITION, CHAR_POSITION_STATE,
CHAR_TARGET_POSITION, CHAR_POSITION_STATE, CHAR_TARGET_DOOR_STATE, CHAR_TARGET_POSITION,
SERV_GARAGE_DOOR_OPENER, CHAR_CURRENT_DOOR_STATE, CHAR_TARGET_DOOR_STATE) SERV_GARAGE_DOOR_OPENER, SERV_WINDOW_COVERING)
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -28,7 +28,7 @@ class GarageDoorOpener(HomeAccessory):
and support no more than open, close, and stop. and support no more than open, close, and stop.
""" """
def __init__(self, *args, config): def __init__(self, *args):
"""Initialize a GarageDoorOpener accessory object.""" """Initialize a GarageDoorOpener accessory object."""
super().__init__(*args, category=CATEGORY_GARAGE_DOOR_OPENER) super().__init__(*args, category=CATEGORY_GARAGE_DOOR_OPENER)
self.flag_target_state = False self.flag_target_state = False
@ -44,12 +44,13 @@ class GarageDoorOpener(HomeAccessory):
_LOGGER.debug('%s: Set state to %d', self.entity_id, value) _LOGGER.debug('%s: Set state to %d', self.entity_id, value)
self.flag_target_state = True self.flag_target_state = True
params = {ATTR_ENTITY_ID: self.entity_id}
if value == 0: if value == 0:
self.char_current_state.set_value(3) self.char_current_state.set_value(3)
self.hass.components.cover.open_cover(self.entity_id) self.hass.services.call(DOMAIN, SERVICE_OPEN_COVER, params)
elif value == 1: elif value == 1:
self.char_current_state.set_value(2) self.char_current_state.set_value(2)
self.hass.components.cover.close_cover(self.entity_id) self.hass.services.call(DOMAIN, SERVICE_CLOSE_COVER, params)
def update_state(self, new_state): def update_state(self, new_state):
"""Update cover state after state changed.""" """Update cover state after state changed."""
@ -69,7 +70,7 @@ class WindowCovering(HomeAccessory):
The cover entity must support: set_cover_position. The cover entity must support: set_cover_position.
""" """
def __init__(self, *args, config): def __init__(self, *args):
"""Initialize a WindowCovering accessory object.""" """Initialize a WindowCovering accessory object."""
super().__init__(*args, category=CATEGORY_WINDOW_COVERING) super().__init__(*args, category=CATEGORY_WINDOW_COVERING)
self.homekit_target = None self.homekit_target = None
@ -108,7 +109,7 @@ class WindowCoveringBasic(HomeAccessory):
stop_cover (optional). stop_cover (optional).
""" """
def __init__(self, *args, config): def __init__(self, *args):
"""Initialize a WindowCovering accessory object.""" """Initialize a WindowCovering accessory object."""
super().__init__(*args, category=CATEGORY_WINDOW_COVERING) super().__init__(*args, category=CATEGORY_WINDOW_COVERING)
features = self.hass.states.get(self.entity_id) \ features = self.hass.states.get(self.entity_id) \
@ -141,8 +142,8 @@ class WindowCoveringBasic(HomeAccessory):
else: else:
service, position = (SERVICE_CLOSE_COVER, 0) service, position = (SERVICE_CLOSE_COVER, 0)
self.hass.services.call(DOMAIN, service, params = {ATTR_ENTITY_ID: self.entity_id}
{ATTR_ENTITY_ID: self.entity_id}) self.hass.services.call(DOMAIN, service, params)
# Snap the current/target position to the expected final position. # Snap the current/target position to the expected final position.
self.char_current_position.set_value(position) self.char_current_position.set_value(position)

View File

@ -0,0 +1,115 @@
"""Class to hold all light accessories."""
import logging
from pyhap.const import CATEGORY_FAN
from homeassistant.components.fan import (
ATTR_DIRECTION, ATTR_OSCILLATING, DIRECTION_FORWARD, DIRECTION_REVERSE,
DOMAIN, SERVICE_OSCILLATE, SERVICE_SET_DIRECTION, SUPPORT_DIRECTION,
SUPPORT_OSCILLATE)
from homeassistant.const import (
ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, SERVICE_TURN_OFF,
SERVICE_TURN_ON, STATE_OFF, STATE_ON)
from . import TYPES
from .accessories import HomeAccessory
from .const import (
CHAR_ACTIVE, CHAR_ROTATION_DIRECTION, CHAR_SWING_MODE, SERV_FANV2)
_LOGGER = logging.getLogger(__name__)
@TYPES.register('Fan')
class Fan(HomeAccessory):
"""Generate a Fan accessory for a fan entity.
Currently supports: state, speed, oscillate, direction.
"""
def __init__(self, *args):
"""Initialize a new Light accessory object."""
super().__init__(*args, category=CATEGORY_FAN)
self._flag = {CHAR_ACTIVE: False,
CHAR_ROTATION_DIRECTION: False,
CHAR_SWING_MODE: False}
self._state = 0
self.chars = []
features = self.hass.states.get(self.entity_id) \
.attributes.get(ATTR_SUPPORTED_FEATURES)
if features & SUPPORT_DIRECTION:
self.chars.append(CHAR_ROTATION_DIRECTION)
if features & SUPPORT_OSCILLATE:
self.chars.append(CHAR_SWING_MODE)
serv_fan = self.add_preload_service(SERV_FANV2, self.chars)
self.char_active = serv_fan.configure_char(
CHAR_ACTIVE, value=0, setter_callback=self.set_state)
if CHAR_ROTATION_DIRECTION in self.chars:
self.char_direction = serv_fan.configure_char(
CHAR_ROTATION_DIRECTION, value=0,
setter_callback=self.set_direction)
if CHAR_SWING_MODE in self.chars:
self.char_swing = serv_fan.configure_char(
CHAR_SWING_MODE, value=0, setter_callback=self.set_oscillating)
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
params = {ATTR_ENTITY_ID: self.entity_id}
self.hass.services.call(DOMAIN, service, params)
def set_direction(self, value):
"""Set state if call came from HomeKit."""
_LOGGER.debug('%s: Set direction to %d', self.entity_id, value)
self._flag[CHAR_ROTATION_DIRECTION] = True
direction = DIRECTION_REVERSE if value == 1 else DIRECTION_FORWARD
params = {ATTR_ENTITY_ID: self.entity_id, ATTR_DIRECTION: direction}
self.hass.services.call(DOMAIN, SERVICE_SET_DIRECTION, params)
def set_oscillating(self, value):
"""Set state if call came from HomeKit."""
_LOGGER.debug('%s: Set oscillating to %d', self.entity_id, value)
self._flag[CHAR_SWING_MODE] = True
oscillating = True if value == 1 else False
params = {ATTR_ENTITY_ID: self.entity_id,
ATTR_OSCILLATING: oscillating}
self.hass.services.call(DOMAIN, SERVICE_OSCILLATE, params)
def update_state(self, new_state):
"""Update fan after state change."""
# Handle State
state = new_state.state
if state in (STATE_ON, STATE_OFF):
self._state = 1 if state == STATE_ON else 0
if not self._flag[CHAR_ACTIVE] and \
self.char_active.value != self._state:
self.char_active.set_value(self._state)
self._flag[CHAR_ACTIVE] = False
# Handle Direction
if CHAR_ROTATION_DIRECTION in self.chars:
direction = new_state.attributes.get(ATTR_DIRECTION)
if not self._flag[CHAR_ROTATION_DIRECTION] and \
direction in (DIRECTION_FORWARD, DIRECTION_REVERSE):
hk_direction = 1 if direction == DIRECTION_REVERSE else 0
if self.char_direction.value != hk_direction:
self.char_direction.set_value(hk_direction)
self._flag[CHAR_ROTATION_DIRECTION] = False
# Handle Oscillating
if CHAR_SWING_MODE in self.chars:
oscillating = new_state.attributes.get(ATTR_OSCILLATING)
if not self._flag[CHAR_SWING_MODE] and \
oscillating in (True, False):
hk_oscillating = 1 if oscillating else 0
if self.char_swing.value != hk_oscillating:
self.char_swing.set_value(hk_oscillating)
self._flag[CHAR_SWING_MODE] = False

View File

@ -4,15 +4,18 @@ import logging
from pyhap.const import CATEGORY_LIGHTBULB from pyhap.const import CATEGORY_LIGHTBULB
from homeassistant.components.light import ( from homeassistant.components.light import (
ATTR_HS_COLOR, ATTR_COLOR_TEMP, ATTR_BRIGHTNESS, ATTR_MIN_MIREDS, ATTR_BRIGHTNESS, ATTR_BRIGHTNESS_PCT, ATTR_COLOR_TEMP, ATTR_HS_COLOR,
ATTR_MAX_MIREDS, SUPPORT_COLOR, SUPPORT_COLOR_TEMP, SUPPORT_BRIGHTNESS) ATTR_MAX_MIREDS, ATTR_MIN_MIREDS, DOMAIN,
from homeassistant.const import ATTR_SUPPORTED_FEATURES, STATE_ON, STATE_OFF SUPPORT_BRIGHTNESS, SUPPORT_COLOR, SUPPORT_COLOR_TEMP)
from homeassistant.const import (
ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, SERVICE_TURN_ON,
SERVICE_TURN_OFF, STATE_OFF, STATE_ON)
from . import TYPES from . import TYPES
from .accessories import HomeAccessory, debounce from .accessories import debounce, HomeAccessory
from .const import ( from .const import (
SERV_LIGHTBULB, CHAR_COLOR_TEMPERATURE, CHAR_BRIGHTNESS, CHAR_COLOR_TEMPERATURE, CHAR_HUE, CHAR_ON,
CHAR_BRIGHTNESS, CHAR_HUE, CHAR_ON, CHAR_SATURATION) CHAR_SATURATION, SERV_LIGHTBULB, PROP_MAX_VALUE, PROP_MIN_VALUE)
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -26,7 +29,7 @@ class Light(HomeAccessory):
Currently supports: state, brightness, color temperature, rgb_color. Currently supports: state, brightness, color temperature, rgb_color.
""" """
def __init__(self, *args, config): def __init__(self, *args):
"""Initialize a new Light accessory object.""" """Initialize a new Light accessory object."""
super().__init__(*args, category=CATEGORY_LIGHTBULB) super().__init__(*args, category=CATEGORY_LIGHTBULB)
self._flag = {CHAR_ON: False, CHAR_BRIGHTNESS: False, self._flag = {CHAR_ON: False, CHAR_BRIGHTNESS: False,
@ -61,7 +64,8 @@ class Light(HomeAccessory):
.attributes.get(ATTR_MAX_MIREDS, 500) .attributes.get(ATTR_MAX_MIREDS, 500)
self.char_color_temperature = serv_light.configure_char( self.char_color_temperature = serv_light.configure_char(
CHAR_COLOR_TEMPERATURE, value=min_mireds, CHAR_COLOR_TEMPERATURE, value=min_mireds,
properties={'minValue': min_mireds, 'maxValue': max_mireds}, properties={PROP_MIN_VALUE: min_mireds,
PROP_MAX_VALUE: max_mireds},
setter_callback=self.set_color_temperature) setter_callback=self.set_color_temperature)
if CHAR_HUE in self.chars: if CHAR_HUE in self.chars:
self.char_hue = serv_light.configure_char( self.char_hue = serv_light.configure_char(
@ -77,28 +81,27 @@ class Light(HomeAccessory):
_LOGGER.debug('%s: Set state to %d', self.entity_id, value) _LOGGER.debug('%s: Set state to %d', self.entity_id, value)
self._flag[CHAR_ON] = True self._flag[CHAR_ON] = True
params = {ATTR_ENTITY_ID: self.entity_id}
if value == 1: service = SERVICE_TURN_ON if value == 1 else SERVICE_TURN_OFF
self.hass.components.light.turn_on(self.entity_id) self.hass.services.call(DOMAIN, service, params)
elif value == 0:
self.hass.components.light.turn_off(self.entity_id)
@debounce @debounce
def set_brightness(self, value): def set_brightness(self, value):
"""Set brightness if call came from HomeKit.""" """Set brightness if call came from HomeKit."""
_LOGGER.debug('%s: Set brightness to %d', self.entity_id, value) _LOGGER.debug('%s: Set brightness to %d', self.entity_id, value)
self._flag[CHAR_BRIGHTNESS] = True self._flag[CHAR_BRIGHTNESS] = True
if value != 0: if value == 0:
self.hass.components.light.turn_on( self.set_state(0) # Turn off light
self.entity_id, brightness_pct=value) return
else: params = {ATTR_ENTITY_ID: self.entity_id, ATTR_BRIGHTNESS_PCT: value}
self.hass.components.light.turn_off(self.entity_id) self.hass.services.call(DOMAIN, SERVICE_TURN_ON, params)
def set_color_temperature(self, value): def set_color_temperature(self, value):
"""Set color temperature if call came from HomeKit.""" """Set color temperature if call came from HomeKit."""
_LOGGER.debug('%s: Set color temp to %s', self.entity_id, value) _LOGGER.debug('%s: Set color temp to %s', self.entity_id, value)
self._flag[CHAR_COLOR_TEMPERATURE] = True self._flag[CHAR_COLOR_TEMPERATURE] = True
self.hass.components.light.turn_on(self.entity_id, color_temp=value) params = {ATTR_ENTITY_ID: self.entity_id, ATTR_COLOR_TEMP: value}
self.hass.services.call(DOMAIN, SERVICE_TURN_ON, params)
def set_saturation(self, value): def set_saturation(self, value):
"""Set saturation if call came from HomeKit.""" """Set saturation if call came from HomeKit."""
@ -116,15 +119,14 @@ class Light(HomeAccessory):
def set_color(self): def set_color(self):
"""Set color if call came from HomeKit.""" """Set color if call came from HomeKit."""
# Handle Color
if self._features & SUPPORT_COLOR and self._flag[CHAR_HUE] and \ if self._features & SUPPORT_COLOR and self._flag[CHAR_HUE] and \
self._flag[CHAR_SATURATION]: self._flag[CHAR_SATURATION]:
color = (self._hue, self._saturation) color = (self._hue, self._saturation)
_LOGGER.debug('%s: Set hs_color to %s', self.entity_id, color) _LOGGER.debug('%s: Set hs_color to %s', self.entity_id, color)
self._flag.update({ self._flag.update({
CHAR_HUE: False, CHAR_SATURATION: False, RGB_COLOR: True}) CHAR_HUE: False, CHAR_SATURATION: False, RGB_COLOR: True})
self.hass.components.light.turn_on( params = {ATTR_ENTITY_ID: self.entity_id, ATTR_HS_COLOR: color}
self.entity_id, hs_color=color) self.hass.services.call(DOMAIN, SERVICE_TURN_ON, params)
def update_state(self, new_state): def update_state(self, new_state):
"""Update light after state change.""" """Update light after state change."""

View File

@ -4,12 +4,12 @@ import logging
from pyhap.const import CATEGORY_DOOR_LOCK from pyhap.const import CATEGORY_DOOR_LOCK
from homeassistant.components.lock import ( from homeassistant.components.lock import (
ATTR_ENTITY_ID, STATE_LOCKED, STATE_UNLOCKED, STATE_UNKNOWN) ATTR_ENTITY_ID, DOMAIN, STATE_LOCKED, STATE_UNLOCKED, STATE_UNKNOWN)
from homeassistant.const import ATTR_CODE
from . import TYPES from . import TYPES
from .accessories import HomeAccessory from .accessories import HomeAccessory
from .const import ( from .const import CHAR_LOCK_CURRENT_STATE, CHAR_LOCK_TARGET_STATE, SERV_LOCK
SERV_LOCK, CHAR_LOCK_CURRENT_STATE, CHAR_LOCK_TARGET_STATE)
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -29,9 +29,10 @@ class Lock(HomeAccessory):
The lock entity must support: unlock and lock. The lock entity must support: unlock and lock.
""" """
def __init__(self, *args, config): def __init__(self, *args):
"""Initialize a Lock accessory object.""" """Initialize a Lock accessory object."""
super().__init__(*args, category=CATEGORY_DOOR_LOCK) super().__init__(*args, category=CATEGORY_DOOR_LOCK)
self._code = self.config.get(ATTR_CODE)
self.flag_target_state = False self.flag_target_state = False
serv_lock_mechanism = self.add_preload_service(SERV_LOCK) serv_lock_mechanism = self.add_preload_service(SERV_LOCK)
@ -51,7 +52,9 @@ class Lock(HomeAccessory):
service = STATE_TO_SERVICE[hass_value] service = STATE_TO_SERVICE[hass_value]
params = {ATTR_ENTITY_ID: self.entity_id} params = {ATTR_ENTITY_ID: self.entity_id}
self.hass.services.call('lock', service, params) if self._code:
params[ATTR_CODE] = self._code
self.hass.services.call(DOMAIN, service, params)
def update_state(self, new_state): def update_state(self, new_state):
"""Update lock after state changed.""" """Update lock after state changed."""

View File

@ -3,16 +3,16 @@ import logging
from pyhap.const import CATEGORY_ALARM_SYSTEM from pyhap.const import CATEGORY_ALARM_SYSTEM
from homeassistant.components.alarm_control_panel import DOMAIN
from homeassistant.const import ( from homeassistant.const import (
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, ATTR_ENTITY_ID, ATTR_CODE, STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME,
STATE_ALARM_ARMED_NIGHT, STATE_ALARM_DISARMED, STATE_ALARM_ARMED_NIGHT, STATE_ALARM_TRIGGERED, STATE_ALARM_DISARMED)
STATE_ALARM_TRIGGERED, ATTR_ENTITY_ID, ATTR_CODE)
from . import TYPES from . import TYPES
from .accessories import HomeAccessory from .accessories import HomeAccessory
from .const import ( from .const import (
SERV_SECURITY_SYSTEM, CHAR_CURRENT_SECURITY_STATE, CHAR_CURRENT_SECURITY_STATE, CHAR_TARGET_SECURITY_STATE,
CHAR_TARGET_SECURITY_STATE) SERV_SECURITY_SYSTEM)
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -32,10 +32,10 @@ STATE_TO_SERVICE = {STATE_ALARM_ARMED_HOME: 'alarm_arm_home',
class SecuritySystem(HomeAccessory): class SecuritySystem(HomeAccessory):
"""Generate an SecuritySystem accessory for an alarm control panel.""" """Generate an SecuritySystem accessory for an alarm control panel."""
def __init__(self, *args, config): def __init__(self, *args):
"""Initialize a SecuritySystem accessory object.""" """Initialize a SecuritySystem accessory object."""
super().__init__(*args, category=CATEGORY_ALARM_SYSTEM) super().__init__(*args, category=CATEGORY_ALARM_SYSTEM)
self._alarm_code = config.get(ATTR_CODE) self._alarm_code = self.config.get(ATTR_CODE)
self.flag_target_state = False self.flag_target_state = False
serv_alarm = self.add_preload_service(SERV_SECURITY_SYSTEM) serv_alarm = self.add_preload_service(SERV_SECURITY_SYSTEM)
@ -56,7 +56,7 @@ class SecuritySystem(HomeAccessory):
params = {ATTR_ENTITY_ID: self.entity_id} params = {ATTR_ENTITY_ID: self.entity_id}
if self._alarm_code: if self._alarm_code:
params[ATTR_CODE] = self._alarm_code params[ATTR_CODE] = self._alarm_code
self.hass.services.call('alarm_control_panel', service, params) self.hass.services.call(DOMAIN, service, params)
def update_state(self, new_state): def update_state(self, new_state):
"""Update security state after state changed.""" """Update security state after state changed."""

View File

@ -4,26 +4,26 @@ import logging
from pyhap.const import CATEGORY_SENSOR from pyhap.const import CATEGORY_SENSOR
from homeassistant.const import ( from homeassistant.const import (
ATTR_UNIT_OF_MEASUREMENT, TEMP_CELSIUS, ATTR_DEVICE_CLASS, ATTR_UNIT_OF_MEASUREMENT, STATE_ON, STATE_HOME,
ATTR_DEVICE_CLASS, STATE_ON, STATE_HOME) TEMP_CELSIUS)
from . import TYPES from . import TYPES
from .accessories import HomeAccessory from .accessories import HomeAccessory
from .const import ( from .const import (
SERV_HUMIDITY_SENSOR, SERV_TEMPERATURE_SENSOR, CHAR_AIR_PARTICULATE_DENSITY, CHAR_AIR_QUALITY,
CHAR_CURRENT_HUMIDITY, CHAR_CURRENT_TEMPERATURE, PROP_CELSIUS, CHAR_CARBON_DIOXIDE_DETECTED, CHAR_CARBON_DIOXIDE_LEVEL,
SERV_AIR_QUALITY_SENSOR, CHAR_AIR_QUALITY, CHAR_AIR_PARTICULATE_DENSITY, CHAR_CARBON_DIOXIDE_PEAK_LEVEL, CHAR_CARBON_MONOXIDE_DETECTED,
CHAR_CARBON_DIOXIDE_LEVEL, CHAR_CARBON_DIOXIDE_PEAK_LEVEL, CHAR_CONTACT_SENSOR_STATE, CHAR_CURRENT_AMBIENT_LIGHT_LEVEL,
SERV_LIGHT_SENSOR, CHAR_CURRENT_AMBIENT_LIGHT_LEVEL, CHAR_CURRENT_HUMIDITY, CHAR_CURRENT_TEMPERATURE, CHAR_LEAK_DETECTED,
DEVICE_CLASS_CO2, SERV_CARBON_DIOXIDE_SENSOR, CHAR_CARBON_DIOXIDE_DETECTED, CHAR_MOTION_DETECTED, CHAR_OCCUPANCY_DETECTED, CHAR_SMOKE_DETECTED,
DEVICE_CLASS_GAS, SERV_CARBON_MONOXIDE_SENSOR, DEVICE_CLASS_CO2, DEVICE_CLASS_DOOR, DEVICE_CLASS_GARAGE_DOOR,
CHAR_CARBON_MONOXIDE_DETECTED, DEVICE_CLASS_GAS, DEVICE_CLASS_MOISTURE, DEVICE_CLASS_MOTION,
DEVICE_CLASS_MOISTURE, SERV_LEAK_SENSOR, CHAR_LEAK_DETECTED, DEVICE_CLASS_OCCUPANCY, DEVICE_CLASS_OPENING, DEVICE_CLASS_SMOKE,
DEVICE_CLASS_MOTION, SERV_MOTION_SENSOR, CHAR_MOTION_DETECTED, DEVICE_CLASS_WINDOW, PROP_CELSIUS, SERV_AIR_QUALITY_SENSOR,
DEVICE_CLASS_OCCUPANCY, SERV_OCCUPANCY_SENSOR, CHAR_OCCUPANCY_DETECTED, SERV_CARBON_DIOXIDE_SENSOR, SERV_CARBON_MONOXIDE_SENSOR,
DEVICE_CLASS_OPENING, SERV_CONTACT_SENSOR, CHAR_CONTACT_SENSOR_STATE, SERV_CONTACT_SENSOR, SERV_HUMIDITY_SENSOR, SERV_LEAK_SENSOR,
DEVICE_CLASS_DOOR, DEVICE_CLASS_GARAGE_DOOR, DEVICE_CLASS_WINDOW, SERV_LIGHT_SENSOR, SERV_MOTION_SENSOR, SERV_OCCUPANCY_SENSOR,
DEVICE_CLASS_SMOKE, SERV_SMOKE_SENSOR, CHAR_SMOKE_DETECTED) SERV_SMOKE_SENSOR, SERV_TEMPERATURE_SENSOR)
from .util import ( from .util import (
convert_to_float, temperature_to_homekit, density_to_air_quality) convert_to_float, temperature_to_homekit, density_to_air_quality)
@ -51,7 +51,7 @@ class TemperatureSensor(HomeAccessory):
Sensor entity must return temperature in °C, °F. Sensor entity must return temperature in °C, °F.
""" """
def __init__(self, *args, config): def __init__(self, *args):
"""Initialize a TemperatureSensor accessory object.""" """Initialize a TemperatureSensor accessory object."""
super().__init__(*args, category=CATEGORY_SENSOR) super().__init__(*args, category=CATEGORY_SENSOR)
serv_temp = self.add_preload_service(SERV_TEMPERATURE_SENSOR) serv_temp = self.add_preload_service(SERV_TEMPERATURE_SENSOR)
@ -74,7 +74,7 @@ class TemperatureSensor(HomeAccessory):
class HumiditySensor(HomeAccessory): class HumiditySensor(HomeAccessory):
"""Generate a HumiditySensor accessory as humidity sensor.""" """Generate a HumiditySensor accessory as humidity sensor."""
def __init__(self, *args, config): def __init__(self, *args):
"""Initialize a HumiditySensor accessory object.""" """Initialize a HumiditySensor accessory object."""
super().__init__(*args, category=CATEGORY_SENSOR) super().__init__(*args, category=CATEGORY_SENSOR)
serv_humidity = self.add_preload_service(SERV_HUMIDITY_SENSOR) serv_humidity = self.add_preload_service(SERV_HUMIDITY_SENSOR)
@ -94,7 +94,7 @@ class HumiditySensor(HomeAccessory):
class AirQualitySensor(HomeAccessory): class AirQualitySensor(HomeAccessory):
"""Generate a AirQualitySensor accessory as air quality sensor.""" """Generate a AirQualitySensor accessory as air quality sensor."""
def __init__(self, *args, config): def __init__(self, *args):
"""Initialize a AirQualitySensor accessory object.""" """Initialize a AirQualitySensor accessory object."""
super().__init__(*args, category=CATEGORY_SENSOR) super().__init__(*args, category=CATEGORY_SENSOR)
@ -108,7 +108,7 @@ class AirQualitySensor(HomeAccessory):
def update_state(self, new_state): def update_state(self, new_state):
"""Update accessory after state change.""" """Update accessory after state change."""
density = convert_to_float(new_state.state) density = convert_to_float(new_state.state)
if density is not None: if density:
self.char_density.set_value(density) self.char_density.set_value(density)
self.char_quality.set_value(density_to_air_quality(density)) self.char_quality.set_value(density_to_air_quality(density))
_LOGGER.debug('%s: Set to %d', self.entity_id, density) _LOGGER.debug('%s: Set to %d', self.entity_id, density)
@ -118,7 +118,7 @@ class AirQualitySensor(HomeAccessory):
class CarbonDioxideSensor(HomeAccessory): class CarbonDioxideSensor(HomeAccessory):
"""Generate a CarbonDioxideSensor accessory as CO2 sensor.""" """Generate a CarbonDioxideSensor accessory as CO2 sensor."""
def __init__(self, *args, config): def __init__(self, *args):
"""Initialize a CarbonDioxideSensor accessory object.""" """Initialize a CarbonDioxideSensor accessory object."""
super().__init__(*args, category=CATEGORY_SENSOR) super().__init__(*args, category=CATEGORY_SENSOR)
@ -134,7 +134,7 @@ class CarbonDioxideSensor(HomeAccessory):
def update_state(self, new_state): def update_state(self, new_state):
"""Update accessory after state change.""" """Update accessory after state change."""
co2 = convert_to_float(new_state.state) co2 = convert_to_float(new_state.state)
if co2 is not None: if co2:
self.char_co2.set_value(co2) self.char_co2.set_value(co2)
if co2 > self.char_peak.value: if co2 > self.char_peak.value:
self.char_peak.set_value(co2) self.char_peak.set_value(co2)
@ -146,7 +146,7 @@ class CarbonDioxideSensor(HomeAccessory):
class LightSensor(HomeAccessory): class LightSensor(HomeAccessory):
"""Generate a LightSensor accessory as light sensor.""" """Generate a LightSensor accessory as light sensor."""
def __init__(self, *args, config): def __init__(self, *args):
"""Initialize a LightSensor accessory object.""" """Initialize a LightSensor accessory object."""
super().__init__(*args, category=CATEGORY_SENSOR) super().__init__(*args, category=CATEGORY_SENSOR)
@ -157,7 +157,7 @@ class LightSensor(HomeAccessory):
def update_state(self, new_state): def update_state(self, new_state):
"""Update accessory after state change.""" """Update accessory after state change."""
luminance = convert_to_float(new_state.state) luminance = convert_to_float(new_state.state)
if luminance is not None: if luminance:
self.char_light.set_value(luminance) self.char_light.set_value(luminance)
_LOGGER.debug('%s: Set to %d', self.entity_id, luminance) _LOGGER.debug('%s: Set to %d', self.entity_id, luminance)
@ -166,7 +166,7 @@ class LightSensor(HomeAccessory):
class BinarySensor(HomeAccessory): class BinarySensor(HomeAccessory):
"""Generate a BinarySensor accessory as binary sensor.""" """Generate a BinarySensor accessory as binary sensor."""
def __init__(self, *args, config): def __init__(self, *args):
"""Initialize a BinarySensor accessory object.""" """Initialize a BinarySensor accessory object."""
super().__init__(*args, category=CATEGORY_SENSOR) super().__init__(*args, category=CATEGORY_SENSOR)
device_class = self.hass.states.get(self.entity_id).attributes \ device_class = self.hass.states.get(self.entity_id).attributes \

View File

@ -18,7 +18,7 @@ _LOGGER = logging.getLogger(__name__)
class Switch(HomeAccessory): class Switch(HomeAccessory):
"""Generate a Switch accessory.""" """Generate a Switch accessory."""
def __init__(self, *args, config): def __init__(self, *args):
"""Initialize a Switch accessory object to represent a remote.""" """Initialize a Switch accessory object to represent a remote."""
super().__init__(*args, category=CATEGORY_SWITCH) super().__init__(*args, category=CATEGORY_SWITCH)
self._domain = split_entity_id(self.entity_id)[0] self._domain = split_entity_id(self.entity_id)[0]
@ -33,9 +33,9 @@ class Switch(HomeAccessory):
_LOGGER.debug('%s: Set switch state to %s', _LOGGER.debug('%s: Set switch state to %s',
self.entity_id, value) self.entity_id, value)
self.flag_target_state = True self.flag_target_state = True
params = {ATTR_ENTITY_ID: self.entity_id}
service = SERVICE_TURN_ON if value else SERVICE_TURN_OFF service = SERVICE_TURN_ON if value else SERVICE_TURN_OFF
self.hass.services.call(self._domain, service, self.hass.services.call(self._domain, service, params)
{ATTR_ENTITY_ID: self.entity_id})
def update_state(self, new_state): def update_state(self, new_state):
"""Update switch state after state changed.""" """Update switch state after state changed."""

View File

@ -4,22 +4,23 @@ import logging
from pyhap.const import CATEGORY_THERMOSTAT from pyhap.const import CATEGORY_THERMOSTAT
from homeassistant.components.climate import ( from homeassistant.components.climate import (
ATTR_CURRENT_TEMPERATURE, ATTR_TEMPERATURE, ATTR_CURRENT_TEMPERATURE, ATTR_OPERATION_LIST, ATTR_OPERATION_MODE,
ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, ATTR_TEMPERATURE, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW,
ATTR_OPERATION_MODE, ATTR_OPERATION_LIST, DOMAIN, SERVICE_SET_TEMPERATURE, SERVICE_SET_OPERATION_MODE, STATE_AUTO,
STATE_HEAT, STATE_COOL, STATE_AUTO, SUPPORT_ON_OFF, STATE_COOL, STATE_HEAT, SUPPORT_ON_OFF, SUPPORT_TARGET_TEMPERATURE_HIGH,
SUPPORT_TARGET_TEMPERATURE_HIGH, SUPPORT_TARGET_TEMPERATURE_LOW) SUPPORT_TARGET_TEMPERATURE_LOW)
from homeassistant.const import ( from homeassistant.const import (
ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, ATTR_UNIT_OF_MEASUREMENT, ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, ATTR_UNIT_OF_MEASUREMENT,
STATE_OFF, TEMP_CELSIUS, TEMP_FAHRENHEIT) SERVICE_TURN_OFF, SERVICE_TURN_ON, STATE_OFF,
TEMP_CELSIUS, TEMP_FAHRENHEIT)
from . import TYPES from . import TYPES
from .accessories import HomeAccessory, debounce from .accessories import debounce, HomeAccessory
from .const import ( from .const import (
SERV_THERMOSTAT, CHAR_CURRENT_HEATING_COOLING, CHAR_COOLING_THRESHOLD_TEMPERATURE, CHAR_CURRENT_HEATING_COOLING,
CHAR_TARGET_HEATING_COOLING, CHAR_CURRENT_TEMPERATURE, CHAR_CURRENT_TEMPERATURE, CHAR_TARGET_HEATING_COOLING,
CHAR_TARGET_TEMPERATURE, CHAR_TEMP_DISPLAY_UNITS, CHAR_HEATING_THRESHOLD_TEMPERATURE, CHAR_TARGET_TEMPERATURE,
CHAR_COOLING_THRESHOLD_TEMPERATURE, CHAR_HEATING_THRESHOLD_TEMPERATURE) CHAR_TEMP_DISPLAY_UNITS, SERV_THERMOSTAT)
from .util import temperature_to_homekit, temperature_to_states from .util import temperature_to_homekit, temperature_to_states
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -38,7 +39,7 @@ SUPPORT_TEMP_RANGE = SUPPORT_TARGET_TEMPERATURE_LOW | \
class Thermostat(HomeAccessory): class Thermostat(HomeAccessory):
"""Generate a Thermostat accessory for a climate.""" """Generate a Thermostat accessory for a climate."""
def __init__(self, *args, config): def __init__(self, *args):
"""Initialize a Thermostat accessory object.""" """Initialize a Thermostat accessory object."""
super().__init__(*args, category=CATEGORY_THERMOSTAT) super().__init__(*args, category=CATEGORY_THERMOSTAT)
self._unit = TEMP_CELSIUS self._unit = TEMP_CELSIUS
@ -99,12 +100,13 @@ class Thermostat(HomeAccessory):
if self.support_power_state is True: if self.support_power_state is True:
params = {ATTR_ENTITY_ID: self.entity_id} params = {ATTR_ENTITY_ID: self.entity_id}
if hass_value == STATE_OFF: if hass_value == STATE_OFF:
self.hass.services.call('climate', 'turn_off', params) self.hass.services.call(DOMAIN, SERVICE_TURN_OFF, params)
return return
else: else:
self.hass.services.call('climate', 'turn_on', params) self.hass.services.call(DOMAIN, SERVICE_TURN_ON, params)
self.hass.components.climate.set_operation_mode( params = {ATTR_ENTITY_ID: self.entity_id,
operation_mode=hass_value, entity_id=self.entity_id) ATTR_OPERATION_MODE: hass_value}
self.hass.services.call(DOMAIN, SERVICE_SET_OPERATION_MODE, params)
@debounce @debounce
def set_cooling_threshold(self, value): def set_cooling_threshold(self, value):
@ -113,11 +115,11 @@ class Thermostat(HomeAccessory):
self.entity_id, value) self.entity_id, value)
self.coolingthresh_flag_target_state = True self.coolingthresh_flag_target_state = True
low = self.char_heating_thresh_temp.value low = self.char_heating_thresh_temp.value
low = temperature_to_states(low, self._unit) params = {
value = temperature_to_states(value, self._unit) ATTR_ENTITY_ID: self.entity_id,
self.hass.components.climate.set_temperature( ATTR_TARGET_TEMP_HIGH: temperature_to_states(value, self._unit),
entity_id=self.entity_id, target_temp_high=value, ATTR_TARGET_TEMP_LOW: temperature_to_states(low, self._unit)}
target_temp_low=low) self.hass.services.call(DOMAIN, SERVICE_SET_TEMPERATURE, params)
@debounce @debounce
def set_heating_threshold(self, value): def set_heating_threshold(self, value):
@ -125,13 +127,12 @@ class Thermostat(HomeAccessory):
_LOGGER.debug('%s: Set heating threshold temperature to %.2f°C', _LOGGER.debug('%s: Set heating threshold temperature to %.2f°C',
self.entity_id, value) self.entity_id, value)
self.heatingthresh_flag_target_state = True self.heatingthresh_flag_target_state = True
# Home assistant always wants to set low and high at the same time
high = self.char_cooling_thresh_temp.value high = self.char_cooling_thresh_temp.value
high = temperature_to_states(high, self._unit) params = {
value = temperature_to_states(value, self._unit) ATTR_ENTITY_ID: self.entity_id,
self.hass.components.climate.set_temperature( ATTR_TARGET_TEMP_HIGH: temperature_to_states(high, self._unit),
entity_id=self.entity_id, target_temp_high=high, ATTR_TARGET_TEMP_LOW: temperature_to_states(value, self._unit)}
target_temp_low=value) self.hass.services.call(DOMAIN, SERVICE_SET_TEMPERATURE, params)
@debounce @debounce
def set_target_temperature(self, value): def set_target_temperature(self, value):
@ -139,9 +140,10 @@ class Thermostat(HomeAccessory):
_LOGGER.debug('%s: Set target temperature to %.2f°C', _LOGGER.debug('%s: Set target temperature to %.2f°C',
self.entity_id, value) self.entity_id, value)
self.temperature_flag_target_state = True self.temperature_flag_target_state = True
value = temperature_to_states(value, self._unit) params = {
self.hass.components.climate.set_temperature( ATTR_ENTITY_ID: self.entity_id,
temperature=value, entity_id=self.entity_id) ATTR_TEMPERATURE: temperature_to_states(value, self._unit)}
self.hass.services.call(DOMAIN, SERVICE_SET_TEMPERATURE, params)
def update_state(self, new_state): def update_state(self, new_state):
"""Update security state after state changed.""" """Update security state after state changed."""

View File

@ -5,7 +5,7 @@ import voluptuous as vol
from homeassistant.core import split_entity_id from homeassistant.core import split_entity_id
from homeassistant.const import ( from homeassistant.const import (
ATTR_CODE, TEMP_CELSIUS) ATTR_CODE, CONF_NAME, TEMP_CELSIUS)
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
import homeassistant.util.temperature as temp_util import homeassistant.util.temperature as temp_util
from .const import HOMEKIT_NOTIFY_ID from .const import HOMEKIT_NOTIFY_ID
@ -16,16 +16,21 @@ _LOGGER = logging.getLogger(__name__)
def validate_entity_config(values): def validate_entity_config(values):
"""Validate config entry for CONF_ENTITY.""" """Validate config entry for CONF_ENTITY."""
entities = {} entities = {}
for key, config in values.items(): for entity_id, config in values.items():
entity = cv.entity_id(key) entity = cv.entity_id(entity_id)
params = {} params = {}
if not isinstance(config, dict): if not isinstance(config, dict):
raise vol.Invalid('The configuration for "{}" must be ' raise vol.Invalid('The configuration for "{}" must be '
' an dictionary.'.format(entity)) ' a dictionary.'.format(entity))
for key in (CONF_NAME, ):
value = config.get(key, -1)
if value != -1:
params[key] = cv.string(value)
domain, _ = split_entity_id(entity) domain, _ = split_entity_id(entity)
if domain == 'alarm_control_panel': if domain in ('alarm_control_panel', 'lock'):
code = config.get(ATTR_CODE) code = config.get(ATTR_CODE)
params[ATTR_CODE] = cv.string(code) if code else None params[ATTR_CODE] = cv.string(code) if code else None
@ -33,9 +38,9 @@ def validate_entity_config(values):
return entities return entities
def show_setup_message(hass, bridge): def show_setup_message(hass, pincode):
"""Display persistent notification with setup information.""" """Display persistent notification with setup information."""
pin = bridge.pincode.decode() pin = pincode.decode()
_LOGGER.info('Pincode: %s', pin) _LOGGER.info('Pincode: %s', pin)
message = 'To setup Home Assistant in the Home App, enter the ' \ message = 'To setup Home Assistant in the Home App, enter the ' \
'following code:\n### {}'.format(pin) 'following code:\n### {}'.format(pin)

View File

@ -13,17 +13,19 @@ import socket
import voluptuous as vol import voluptuous as vol
from homeassistant.const import ( from homeassistant.const import (
EVENT_HOMEASSISTANT_STOP, CONF_USERNAME, CONF_PASSWORD, CONF_PLATFORM, ATTR_ENTITY_ID, ATTR_NAME, CONF_HOST, CONF_HOSTS, CONF_PASSWORD,
CONF_HOSTS, CONF_HOST, ATTR_ENTITY_ID, STATE_UNKNOWN) CONF_PLATFORM, CONF_USERNAME, EVENT_HOMEASSISTANT_STOP, STATE_UNKNOWN)
from homeassistant.helpers import discovery from homeassistant.helpers import discovery
from homeassistant.helpers.entity import Entity
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity
from homeassistant.loader import bind_hass from homeassistant.loader import bind_hass
REQUIREMENTS = ['pyhomematic==0.1.42'] REQUIREMENTS = ['pyhomematic==0.1.42']
DOMAIN = 'homematic'
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
DOMAIN = 'homematic'
SCAN_INTERVAL_HUB = timedelta(seconds=300) SCAN_INTERVAL_HUB = timedelta(seconds=300)
SCAN_INTERVAL_VARIABLES = timedelta(seconds=30) SCAN_INTERVAL_VARIABLES = timedelta(seconds=30)
@ -38,7 +40,6 @@ DISCOVER_LOCKS = 'homematic.locks'
ATTR_DISCOVER_DEVICES = 'devices' ATTR_DISCOVER_DEVICES = 'devices'
ATTR_PARAM = 'param' ATTR_PARAM = 'param'
ATTR_CHANNEL = 'channel' ATTR_CHANNEL = 'channel'
ATTR_NAME = 'name'
ATTR_ADDRESS = 'address' ATTR_ADDRESS = 'address'
ATTR_VALUE = 'value' ATTR_VALUE = 'value'
ATTR_INTERFACE = 'interface' ATTR_INTERFACE = 'interface'
@ -70,7 +71,7 @@ HM_DEVICE_TYPES = {
'TemperatureSensor', 'CO2Sensor', 'IPSwitchPowermeter', 'HMWIOSwitch', 'TemperatureSensor', 'CO2Sensor', 'IPSwitchPowermeter', 'HMWIOSwitch',
'FillingLevel', 'ValveDrive', 'EcoLogic', 'IPThermostatWall', 'FillingLevel', 'ValveDrive', 'EcoLogic', 'IPThermostatWall',
'IPSmoke', 'RFSiren', 'PresenceIP', 'IPAreaThermostat', 'IPSmoke', 'RFSiren', 'PresenceIP', 'IPAreaThermostat',
'IPWeatherSensor'], 'IPWeatherSensor', 'RotaryHandleSensorIP'],
DISCOVER_CLIMATE: [ DISCOVER_CLIMATE: [
'Thermostat', 'ThermostatWall', 'MAXThermostat', 'ThermostatWall2', 'Thermostat', 'ThermostatWall', 'MAXThermostat', 'ThermostatWall2',
'MAXWallThermostat', 'IPThermostat', 'IPThermostatWall', 'MAXWallThermostat', 'IPThermostat', 'IPThermostatWall',
@ -97,6 +98,7 @@ HM_ATTRIBUTE_SUPPORT = {
'LOWBAT': ['battery', {0: 'High', 1: 'Low'}], 'LOWBAT': ['battery', {0: 'High', 1: 'Low'}],
'LOW_BAT': ['battery', {0: 'High', 1: 'Low'}], 'LOW_BAT': ['battery', {0: 'High', 1: 'Low'}],
'ERROR': ['sabotage', {0: 'No', 1: 'Yes'}], 'ERROR': ['sabotage', {0: 'No', 1: 'Yes'}],
'SABOTAGE': ['sabotage', {0: 'No', 1: 'Yes'}],
'RSSI_DEVICE': ['rssi', {}], 'RSSI_DEVICE': ['rssi', {}],
'VALVE_STATE': ['valve', {}], 'VALVE_STATE': ['valve', {}],
'BATTERY_STATE': ['battery', {}], 'BATTERY_STATE': ['battery', {}],

View File

@ -24,7 +24,10 @@ _LOGGER = logging.getLogger(__name__)
DOMAIN = 'homematicip_cloud' DOMAIN = 'homematicip_cloud'
COMPONENTS = [ COMPONENTS = [
'sensor' 'sensor',
'binary_sensor',
'switch',
'light'
] ]
CONF_NAME = 'name' CONF_NAME = 'name'

View File

@ -81,7 +81,12 @@ async def async_validate_auth_header(api_password, request):
if hdrs.AUTHORIZATION not in request.headers: if hdrs.AUTHORIZATION not in request.headers:
return False return False
auth_type, auth_val = request.headers.get(hdrs.AUTHORIZATION).split(' ', 1) try:
auth_type, auth_val = \
request.headers.get(hdrs.AUTHORIZATION).split(' ', 1)
except ValueError:
# If no space in authorization header
return False
if auth_type == 'Basic': if auth_type == 'Basic':
decoded = base64.b64decode(auth_val).decode('utf-8') decoded = base64.b64decode(auth_val).decode('utf-8')

View File

@ -51,12 +51,6 @@ class HomeAssistantView(object):
data['code'] = message_code data['code'] = message_code
return self.json(data, status_code, headers=headers) return self.json(data, status_code, headers=headers)
# pylint: disable=no-self-use
async def file(self, request, fil):
"""Return a file."""
assert isinstance(fil, str), 'only string paths allowed'
return web.FileResponse(fil)
def register(self, router): def register(self, router):
"""Register the view with a router.""" """Register the view with a router."""
assert self.url is not None, 'No url set for view' assert self.url is not None, 'No url set for view'

View File

@ -10,14 +10,14 @@ import logging
import voluptuous as vol import voluptuous as vol
from homeassistant.core import callback
import homeassistant.helpers.config_validation as cv
from homeassistant.const import ( from homeassistant.const import (
ATTR_ENTITY_ID, CONF_NAME, CONF_ENTITY_ID) ATTR_ENTITY_ID, ATTR_NAME, CONF_ENTITY_ID, CONF_NAME)
from homeassistant.core import callback
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
from homeassistant.loader import bind_hass import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.loader import bind_hass
from homeassistant.util.async_ import run_callback_threadsafe from homeassistant.util.async_ import run_callback_threadsafe
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -42,7 +42,6 @@ ATTR_CONFIDENCE = 'confidence'
ATTR_FACES = 'faces' ATTR_FACES = 'faces'
ATTR_GENDER = 'gender' ATTR_GENDER = 'gender'
ATTR_GLASSES = 'glasses' ATTR_GLASSES = 'glasses'
ATTR_NAME = 'name'
ATTR_MOTION = 'motion' ATTR_MOTION = 'motion'
ATTR_TOTAL_FACES = 'total_faces' ATTR_TOTAL_FACES = 'total_faces'
@ -60,7 +59,7 @@ SOURCE_SCHEMA = vol.Schema({
PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA.extend({ PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA.extend({
vol.Optional(CONF_SOURCE): vol.All(cv.ensure_list, [SOURCE_SCHEMA]), vol.Optional(CONF_SOURCE): vol.All(cv.ensure_list, [SOURCE_SCHEMA]),
vol.Optional(CONF_CONFIDENCE, default=DEFAULT_CONFIDENCE): vol.Optional(CONF_CONFIDENCE, default=DEFAULT_CONFIDENCE):
vol.All(vol.Coerce(float), vol.Range(min=0, max=100)) vol.All(vol.Coerce(float), vol.Range(min=0, max=100)),
}) })
SERVICE_SCAN_SCHEMA = vol.Schema({ SERVICE_SCAN_SCHEMA = vol.Schema({
@ -77,7 +76,7 @@ def scan(hass, entity_id=None):
@asyncio.coroutine @asyncio.coroutine
def async_setup(hass, config): def async_setup(hass, config):
"""Set up image processing.""" """Set up the image processing."""
component = EntityComponent(_LOGGER, DOMAIN, hass, SCAN_INTERVAL) component = EntityComponent(_LOGGER, DOMAIN, hass, SCAN_INTERVAL)
yield from component.async_setup(config) yield from component.async_setup(config)

View File

@ -0,0 +1,110 @@
"""
Component that will perform facial detection and identification via facebox.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/image_processing.facebox
"""
import base64
import logging
import requests
import voluptuous as vol
from homeassistant.core import split_entity_id
import homeassistant.helpers.config_validation as cv
from homeassistant.components.image_processing import (
PLATFORM_SCHEMA, ImageProcessingFaceEntity, CONF_SOURCE, CONF_ENTITY_ID,
CONF_NAME)
from homeassistant.const import (CONF_IP_ADDRESS, CONF_PORT)
_LOGGER = logging.getLogger(__name__)
CLASSIFIER = 'facebox'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_IP_ADDRESS): cv.string,
vol.Required(CONF_PORT): cv.port,
})
def encode_image(image):
"""base64 encode an image stream."""
base64_img = base64.b64encode(image).decode('ascii')
return {"base64": base64_img}
def get_matched_faces(faces):
"""Return the name and rounded confidence of matched faces."""
return {face['name']: round(face['confidence'], 2)
for face in faces if face['matched']}
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the classifier."""
entities = []
for camera in config[CONF_SOURCE]:
entities.append(FaceClassifyEntity(
config[CONF_IP_ADDRESS],
config[CONF_PORT],
camera[CONF_ENTITY_ID],
camera.get(CONF_NAME)
))
add_devices(entities)
class FaceClassifyEntity(ImageProcessingFaceEntity):
"""Perform a face classification."""
def __init__(self, ip, port, camera_entity, name=None):
"""Init with the API key and model id."""
super().__init__()
self._url = "http://{}:{}/{}/check".format(ip, port, CLASSIFIER)
self._camera = camera_entity
if name:
self._name = name
else:
camera_name = split_entity_id(camera_entity)[1]
self._name = "{} {}".format(
CLASSIFIER, camera_name)
self._matched = {}
def process_image(self, image):
"""Process an image."""
response = {}
try:
response = requests.post(
self._url,
json=encode_image(image),
timeout=9
).json()
except requests.exceptions.ConnectionError:
_LOGGER.error("ConnectionError: Is %s running?", CLASSIFIER)
response['success'] = False
if response['success']:
faces = response['faces']
total = response['facesCount']
self.process_faces(faces, total)
self._matched = get_matched_faces(faces)
else:
self.total_faces = None
self.faces = []
self._matched = {}
@property
def camera_entity(self):
"""Return camera entity id from process pictures."""
return self._camera
@property
def name(self):
"""Return the name of the sensor."""
return self._name
@property
def device_state_attributes(self):
"""Return the classifier attributes."""
return {
'matched_faces': self._matched,
}

View File

@ -9,12 +9,12 @@ import logging
import voluptuous as vol import voluptuous as vol
from homeassistant.components.image_processing import (
ATTR_AGE, ATTR_GENDER, ATTR_GLASSES, CONF_ENTITY_ID, CONF_NAME,
CONF_SOURCE, PLATFORM_SCHEMA, ImageProcessingFaceEntity)
from homeassistant.components.microsoft_face import DATA_MICROSOFT_FACE
from homeassistant.core import split_entity_id from homeassistant.core import split_entity_id
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
from homeassistant.components.microsoft_face import DATA_MICROSOFT_FACE
from homeassistant.components.image_processing import (
PLATFORM_SCHEMA, ImageProcessingFaceEntity, ATTR_AGE, ATTR_GENDER,
ATTR_GLASSES, CONF_SOURCE, CONF_ENTITY_ID, CONF_NAME)
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
DEPENDENCIES = ['microsoft_face'] DEPENDENCIES = ['microsoft_face']

View File

@ -9,12 +9,13 @@ import logging
import voluptuous as vol import voluptuous as vol
from homeassistant.components.image_processing import (
ATTR_CONFIDENCE, CONF_CONFIDENCE, CONF_ENTITY_ID, CONF_NAME, CONF_SOURCE,
PLATFORM_SCHEMA, ImageProcessingFaceEntity)
from homeassistant.components.microsoft_face import DATA_MICROSOFT_FACE
from homeassistant.const import ATTR_NAME
from homeassistant.core import split_entity_id from homeassistant.core import split_entity_id
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
from homeassistant.components.microsoft_face import DATA_MICROSOFT_FACE
from homeassistant.components.image_processing import (
PLATFORM_SCHEMA, ImageProcessingFaceEntity, ATTR_NAME,
CONF_CONFIDENCE, CONF_SOURCE, CONF_ENTITY_ID, CONF_NAME, ATTR_CONFIDENCE)
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
DEPENDENCIES = ['microsoft_face'] DEPENDENCIES = ['microsoft_face']

View File

@ -17,7 +17,7 @@ import homeassistant.helpers.config_validation as cv
from homeassistant.helpers import discovery from homeassistant.helpers import discovery
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
REQUIREMENTS = ['insteonplm==0.9.1'] REQUIREMENTS = ['insteonplm==0.9.2']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)

View File

@ -14,7 +14,7 @@ delete_all_link:
description: All-Link group number. description: All-Link group number.
example: 1 example: 1
load_all_link_database: load_all_link_database:
description: Load the All-Link Database for a device. WARNING - Loading a device All-LInk database is very time consuming and inconsistant. This may take a LONG time and may need to be repeated to obtain all records. description: Load the All-Link Database for a device. WARNING - Loading a device All-LInk database is very time consuming and inconsistent. This may take a LONG time and may need to be repeated to obtain all records.
fields: fields:
entity_id: entity_id:
description: Name of the device to print description: Name of the device to print

View File

@ -13,7 +13,7 @@ import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.discovery import load_platform from homeassistant.helpers.discovery import load_platform
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
REQUIREMENTS = ['pyota==2.0.4'] REQUIREMENTS = ['pyota==2.0.5']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)

View File

@ -202,7 +202,7 @@ def _check_for_uom_id(hass: HomeAssistant, node,
node_uom = set(map(str.lower, node.uom)) node_uom = set(map(str.lower, node.uom))
if uom_list: if uom_list:
if node_uom.intersection(NODE_FILTERS[single_domain]['uom']): if node_uom.intersection(uom_list):
hass.data[ISY994_NODES][single_domain].append(node) hass.data[ISY994_NODES][single_domain].append(node)
return True return True
else: else:

View File

@ -0,0 +1,319 @@
"""
Support for Konnected devices.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/konnected/
"""
import logging
import hmac
import json
import voluptuous as vol
from aiohttp.hdrs import AUTHORIZATION
from aiohttp.web import Request, Response # NOQA
from homeassistant.components.binary_sensor import DEVICE_CLASSES_SCHEMA
from homeassistant.components.discovery import SERVICE_KONNECTED
from homeassistant.components.http import HomeAssistantView
from homeassistant.const import (
HTTP_BAD_REQUEST, HTTP_INTERNAL_SERVER_ERROR, HTTP_UNAUTHORIZED,
CONF_DEVICES, CONF_BINARY_SENSORS, CONF_SWITCHES, CONF_HOST, CONF_PORT,
CONF_ID, CONF_NAME, CONF_TYPE, CONF_PIN, CONF_ZONE, CONF_ACCESS_TOKEN,
ATTR_ENTITY_ID, ATTR_STATE)
from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.helpers import discovery
from homeassistant.helpers import config_validation as cv
_LOGGER = logging.getLogger(__name__)
REQUIREMENTS = ['konnected==0.1.2']
DOMAIN = 'konnected'
CONF_ACTIVATION = 'activation'
STATE_LOW = 'low'
STATE_HIGH = 'high'
PIN_TO_ZONE = {1: 1, 2: 2, 5: 3, 6: 4, 7: 5, 8: 'out', 9: 6}
ZONE_TO_PIN = {zone: pin for pin, zone in PIN_TO_ZONE.items()}
_BINARY_SENSOR_SCHEMA = vol.All(
vol.Schema({
vol.Exclusive(CONF_PIN, 's_pin'): vol.Any(*PIN_TO_ZONE),
vol.Exclusive(CONF_ZONE, 's_pin'): vol.Any(*ZONE_TO_PIN),
vol.Required(CONF_TYPE): DEVICE_CLASSES_SCHEMA,
vol.Optional(CONF_NAME): cv.string,
}), cv.has_at_least_one_key(CONF_PIN, CONF_ZONE)
)
_SWITCH_SCHEMA = vol.All(
vol.Schema({
vol.Exclusive(CONF_PIN, 'a_pin'): vol.Any(*PIN_TO_ZONE),
vol.Exclusive(CONF_ZONE, 'a_pin'): vol.Any(*ZONE_TO_PIN),
vol.Optional(CONF_NAME): cv.string,
vol.Optional(CONF_ACTIVATION, default=STATE_HIGH):
vol.All(vol.Lower, vol.Any(STATE_HIGH, STATE_LOW))
}), cv.has_at_least_one_key(CONF_PIN, CONF_ZONE)
)
CONFIG_SCHEMA = vol.Schema(
{
DOMAIN: vol.Schema({
vol.Required(CONF_ACCESS_TOKEN): cv.string,
vol.Required(CONF_DEVICES): [{
vol.Required(CONF_ID): cv.string,
vol.Optional(CONF_BINARY_SENSORS): vol.All(
cv.ensure_list, [_BINARY_SENSOR_SCHEMA]),
vol.Optional(CONF_SWITCHES): vol.All(
cv.ensure_list, [_SWITCH_SCHEMA]),
}],
}),
},
extra=vol.ALLOW_EXTRA,
)
DEPENDENCIES = ['http', 'discovery']
ENDPOINT_ROOT = '/api/konnected'
UPDATE_ENDPOINT = (ENDPOINT_ROOT + r'/device/{device_id:[a-zA-Z0-9]+}')
SIGNAL_SENSOR_UPDATE = 'konnected.{}.update'
async def async_setup(hass, config):
"""Set up the Konnected platform."""
cfg = config.get(DOMAIN)
if cfg is None:
cfg = {}
access_token = cfg.get(CONF_ACCESS_TOKEN)
if DOMAIN not in hass.data:
hass.data[DOMAIN] = {CONF_ACCESS_TOKEN: access_token}
def device_discovered(service, info):
"""Call when a Konnected device has been discovered."""
_LOGGER.debug("Discovered a new Konnected device: %s", info)
host = info.get(CONF_HOST)
port = info.get(CONF_PORT)
device = KonnectedDevice(hass, host, port, cfg)
device.setup()
discovery.async_listen(
hass,
SERVICE_KONNECTED,
device_discovered)
hass.http.register_view(KonnectedView(access_token))
return True
class KonnectedDevice(object):
"""A representation of a single Konnected device."""
def __init__(self, hass, host, port, config):
"""Initialize the Konnected device."""
self.hass = hass
self.host = host
self.port = port
self.user_config = config
import konnected
self.client = konnected.Client(host, str(port))
self.status = self.client.get_status()
_LOGGER.info('Initialized Konnected device %s', self.device_id)
def setup(self):
"""Set up a newly discovered Konnected device."""
user_config = self.config()
if user_config:
_LOGGER.debug('Configuring Konnected device %s', self.device_id)
self.save_data()
self.sync_device_config()
discovery.load_platform(
self.hass, 'binary_sensor',
DOMAIN, {'device_id': self.device_id})
discovery.load_platform(
self.hass, 'switch', DOMAIN,
{'device_id': self.device_id})
@property
def device_id(self):
"""Device id is the MAC address as string with punctuation removed."""
return self.status['mac'].replace(':', '')
def config(self):
"""Return an object representing the user defined configuration."""
device_id = self.device_id
valid_keys = [device_id, device_id.upper(),
device_id[6:], device_id.upper()[6:]]
configured_devices = self.user_config[CONF_DEVICES]
return next((device for device in
configured_devices if device[CONF_ID] in valid_keys),
None)
def save_data(self):
"""Save the device configuration to `hass.data`."""
sensors = {}
for entity in self.config().get(CONF_BINARY_SENSORS) or []:
if CONF_ZONE in entity:
pin = ZONE_TO_PIN[entity[CONF_ZONE]]
else:
pin = entity[CONF_PIN]
sensor_status = next((sensor for sensor in
self.status.get('sensors') if
sensor.get(CONF_PIN) == pin), {})
if sensor_status.get(ATTR_STATE):
initial_state = bool(int(sensor_status.get(ATTR_STATE)))
else:
initial_state = None
sensors[pin] = {
CONF_TYPE: entity[CONF_TYPE],
CONF_NAME: entity.get(CONF_NAME, 'Konnected {} Zone {}'.format(
self.device_id[6:], PIN_TO_ZONE[pin])),
ATTR_STATE: initial_state
}
_LOGGER.debug('Set up sensor %s (initial state: %s)',
sensors[pin].get('name'),
sensors[pin].get(ATTR_STATE))
actuators = {}
for entity in self.config().get(CONF_SWITCHES) or []:
if 'zone' in entity:
pin = ZONE_TO_PIN[entity['zone']]
else:
pin = entity['pin']
actuator_status = next((actuator for actuator in
self.status.get('actuators') if
actuator.get('pin') == pin), {})
if actuator_status.get(ATTR_STATE):
initial_state = bool(int(actuator_status.get(ATTR_STATE)))
else:
initial_state = None
actuators[pin] = {
CONF_NAME: entity.get(
CONF_NAME, 'Konnected {} Actuator {}'.format(
self.device_id[6:], PIN_TO_ZONE[pin])),
ATTR_STATE: initial_state,
CONF_ACTIVATION: entity[CONF_ACTIVATION],
}
_LOGGER.debug('Set up actuator %s (initial state: %s)',
actuators[pin].get(CONF_NAME),
actuators[pin].get(ATTR_STATE))
device_data = {
'client': self.client,
CONF_BINARY_SENSORS: sensors,
CONF_SWITCHES: actuators,
CONF_HOST: self.host,
CONF_PORT: self.port,
}
if CONF_DEVICES not in self.hass.data[DOMAIN]:
self.hass.data[DOMAIN][CONF_DEVICES] = {}
_LOGGER.debug('Storing data in hass.data[konnected]: %s', device_data)
self.hass.data[DOMAIN][CONF_DEVICES][self.device_id] = device_data
@property
def stored_configuration(self):
"""Return the configuration stored in `hass.data` for this device."""
return self.hass.data[DOMAIN][CONF_DEVICES][self.device_id]
def sensor_configuration(self):
"""Return the configuration map for syncing sensors."""
return [{'pin': p} for p in
self.stored_configuration[CONF_BINARY_SENSORS]]
def actuator_configuration(self):
"""Return the configuration map for syncing actuators."""
return [{'pin': p,
'trigger': (0 if data.get(CONF_ACTIVATION) in [0, STATE_LOW]
else 1)}
for p, data in
self.stored_configuration[CONF_SWITCHES].items()]
def sync_device_config(self):
"""Sync the new pin configuration to the Konnected device."""
desired_sensor_configuration = self.sensor_configuration()
current_sensor_configuration = [
{'pin': s[CONF_PIN]} for s in self.status.get('sensors')]
_LOGGER.debug('%s: desired sensor config: %s', self.device_id,
desired_sensor_configuration)
_LOGGER.debug('%s: current sensor config: %s', self.device_id,
current_sensor_configuration)
desired_actuator_config = self.actuator_configuration()
current_actuator_config = self.status.get('actuators')
_LOGGER.debug('%s: desired actuator config: %s', self.device_id,
desired_actuator_config)
_LOGGER.debug('%s: current actuator config: %s', self.device_id,
current_actuator_config)
if (desired_sensor_configuration != current_sensor_configuration) or \
(current_actuator_config != desired_actuator_config):
_LOGGER.debug('pushing settings to device %s', self.device_id)
self.client.put_settings(
desired_sensor_configuration,
desired_actuator_config,
self.hass.data[DOMAIN].get(CONF_ACCESS_TOKEN),
self.hass.config.api.base_url + ENDPOINT_ROOT
)
class KonnectedView(HomeAssistantView):
"""View creates an endpoint to receive push updates from the device."""
url = UPDATE_ENDPOINT
extra_urls = [UPDATE_ENDPOINT + '/{pin_num}/{state}']
name = 'api:konnected'
requires_auth = False # Uses access token from configuration
def __init__(self, auth_token):
"""Initialize the view."""
self.auth_token = auth_token
async def put(self, request: Request, device_id,
pin_num=None, state=None) -> Response:
"""Receive a sensor update via PUT request and async set state."""
hass = request.app['hass']
data = hass.data[DOMAIN]
try: # Konnected 2.2.0 and above supports JSON payloads
payload = await request.json()
pin_num = payload['pin']
state = payload['state']
except json.decoder.JSONDecodeError:
_LOGGER.warning(("Your Konnected device software may be out of "
"date. Visit https://help.konnected.io for "
"updating instructions."))
auth = request.headers.get(AUTHORIZATION, None)
if not hmac.compare_digest('Bearer {}'.format(self.auth_token), auth):
return self.json_message(
"unauthorized", status_code=HTTP_UNAUTHORIZED)
pin_num = int(pin_num)
state = bool(int(state))
device = data[CONF_DEVICES].get(device_id)
if device is None:
return self.json_message('unregistered device',
status_code=HTTP_BAD_REQUEST)
pin_data = device[CONF_BINARY_SENSORS].get(pin_num) or \
device[CONF_SWITCHES].get(pin_num)
if pin_data is None:
return self.json_message('unregistered sensor/actuator',
status_code=HTTP_BAD_REQUEST)
entity_id = pin_data.get(ATTR_ENTITY_ID)
if entity_id is None:
return self.json_message('uninitialized sensor/actuator',
status_code=HTTP_INTERNAL_SERVER_ERROR)
async_dispatcher_send(
hass, SIGNAL_SENSOR_UPDATE.format(entity_id), state)
return self.json_message('ok')

View File

@ -222,27 +222,34 @@ class FluxLight(Light):
effect = kwargs.get(ATTR_EFFECT) effect = kwargs.get(ATTR_EFFECT)
white = kwargs.get(ATTR_WHITE_VALUE) white = kwargs.get(ATTR_WHITE_VALUE)
# color change only # Show warning if effect set with rgb, brightness, or white level
if rgb is not None: if effect and (brightness or white or rgb):
self._bulb.setRgb(*tuple(rgb), brightness=self.brightness) _LOGGER.warning("RGB, brightness and white level are ignored when"
" an effect is specified for a flux bulb")
# brightness change only # Random color effect
elif brightness is not None: if effect == EFFECT_RANDOM:
(red, green, blue) = self._bulb.getRgb()
self._bulb.setRgb(red, green, blue, brightness=brightness)
# random color effect
elif effect == EFFECT_RANDOM:
self._bulb.setRgb(random.randint(0, 255), self._bulb.setRgb(random.randint(0, 255),
random.randint(0, 255), random.randint(0, 255),
random.randint(0, 255)) random.randint(0, 255))
return
# effect selection # Effect selection
elif effect in EFFECT_MAP: elif effect in EFFECT_MAP:
self._bulb.setPresetPattern(EFFECT_MAP[effect], 50) self._bulb.setPresetPattern(EFFECT_MAP[effect], 50)
return
# white change only # Preserve current brightness on color/white level change
elif white is not None: if brightness is None:
brightness = self.brightness
# Preserve color on brightness/white level change
if rgb is None:
rgb = self._bulb.getRgb()
self._bulb.setRgb(*tuple(rgb), brightness=brightness)
if white is not None:
self._bulb.setWarmWhite255(white) self._bulb.setWarmWhite255(white)
def turn_off(self, **kwargs): def turn_off(self, **kwargs):

View File

@ -0,0 +1,76 @@
"""
Support for HomematicIP light.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/light.homematicip_cloud/
"""
import logging
from homeassistant.components.light import Light
from homeassistant.components.homematicip_cloud import (
HomematicipGenericDevice, DOMAIN as HOMEMATICIP_CLOUD_DOMAIN,
ATTR_HOME_ID)
DEPENDENCIES = ['homematicip_cloud']
_LOGGER = logging.getLogger(__name__)
ATTR_POWER_CONSUMPTION = 'power_consumption'
ATTR_ENERGIE_COUNTER = 'energie_counter'
ATTR_PROFILE_MODE = 'profile_mode'
async def async_setup_platform(hass, config, async_add_devices,
discovery_info=None):
"""Set up the HomematicIP light devices."""
from homematicip.device import (
BrandSwitchMeasuring)
if discovery_info is None:
return
home = hass.data[HOMEMATICIP_CLOUD_DOMAIN][discovery_info[ATTR_HOME_ID]]
devices = []
for device in home.devices:
if isinstance(device, BrandSwitchMeasuring):
devices.append(HomematicipLightMeasuring(home, device))
if devices:
async_add_devices(devices)
class HomematicipLight(HomematicipGenericDevice, Light):
"""MomematicIP light device."""
def __init__(self, home, device):
"""Initialize the light device."""
super().__init__(home, device)
@property
def is_on(self):
"""Return true if device is on."""
return self._device.on
async def async_turn_on(self, **kwargs):
"""Turn the device on."""
await self._device.turn_on()
async def async_turn_off(self, **kwargs):
"""Turn the device off."""
await self._device.turn_off()
class HomematicipLightMeasuring(HomematicipLight):
"""MomematicIP measuring light device."""
@property
def current_power_w(self):
"""Return the current power usage in W."""
return self._device.currentPowerConsumption
@property
def today_energy_kwh(self):
"""Return the today total energy usage in kWh."""
if self._device.energyCounter is None:
return 0
return round(self._device.energyCounter)

View File

@ -142,10 +142,9 @@ def state(new_state):
from limitlessled.pipeline import Pipeline from limitlessled.pipeline import Pipeline
pipeline = Pipeline() pipeline = Pipeline()
transition_time = DEFAULT_TRANSITION transition_time = DEFAULT_TRANSITION
# Stop any repeating pipeline. if self._effect == EFFECT_COLORLOOP:
if self.repeating:
self.repeating = False
self.group.stop() self.group.stop()
self._effect = None
# Set transition time. # Set transition time.
if ATTR_TRANSITION in kwargs: if ATTR_TRANSITION in kwargs:
transition_time = int(kwargs[ATTR_TRANSITION]) transition_time = int(kwargs[ATTR_TRANSITION])
@ -183,11 +182,11 @@ class LimitlessLEDGroup(Light):
self.group = group self.group = group
self.config = config self.config = config
self.repeating = False
self._is_on = False self._is_on = False
self._brightness = None self._brightness = None
self._temperature = None self._temperature = None
self._color = None self._color = None
self._effect = None
@asyncio.coroutine @asyncio.coroutine
def async_added_to_hass(self): def async_added_to_hass(self):
@ -222,6 +221,9 @@ class LimitlessLEDGroup(Light):
@property @property
def brightness(self): def brightness(self):
"""Return the brightness property.""" """Return the brightness property."""
if self._effect == EFFECT_NIGHT:
return 1
return self._brightness return self._brightness
@property @property
@ -242,6 +244,9 @@ class LimitlessLEDGroup(Light):
@property @property
def hs_color(self): def hs_color(self):
"""Return the color property.""" """Return the color property."""
if self._effect == EFFECT_NIGHT:
return None
return self._color return self._color
@property @property
@ -249,6 +254,11 @@ class LimitlessLEDGroup(Light):
"""Flag supported features.""" """Flag supported features."""
return self._supported return self._supported
@property
def effect(self):
"""Return the current effect for this light."""
return self._effect
@property @property
def effect_list(self): def effect_list(self):
"""Return the list of supported effects for this light.""" """Return the list of supported effects for this light."""
@ -270,6 +280,7 @@ class LimitlessLEDGroup(Light):
if kwargs.get(ATTR_EFFECT) == EFFECT_NIGHT: if kwargs.get(ATTR_EFFECT) == EFFECT_NIGHT:
if EFFECT_NIGHT in self._effect_list: if EFFECT_NIGHT in self._effect_list:
pipeline.night_light() pipeline.night_light()
self._effect = EFFECT_NIGHT
return return
pipeline.on() pipeline.on()
@ -314,7 +325,7 @@ class LimitlessLEDGroup(Light):
if ATTR_EFFECT in kwargs and self._effect_list: if ATTR_EFFECT in kwargs and self._effect_list:
if kwargs[ATTR_EFFECT] == EFFECT_COLORLOOP: if kwargs[ATTR_EFFECT] == EFFECT_COLORLOOP:
from limitlessled.presets import COLORLOOP from limitlessled.presets import COLORLOOP
self.repeating = True self._effect = EFFECT_COLORLOOP
pipeline.append(COLORLOOP) pipeline.append(COLORLOOP)
if kwargs[ATTR_EFFECT] == EFFECT_WHITE: if kwargs[ATTR_EFFECT] == EFFECT_WHITE:
pipeline.white() pipeline.white()

View File

@ -4,7 +4,6 @@ Support for MQTT lights.
For more details about this platform, please refer to the documentation at For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/light.mqtt/ https://home-assistant.io/components/light.mqtt/
""" """
import asyncio
import logging import logging
import voluptuous as vol import voluptuous as vol
@ -17,12 +16,13 @@ from homeassistant.components.light import (
SUPPORT_EFFECT, SUPPORT_COLOR, SUPPORT_WHITE_VALUE) SUPPORT_EFFECT, SUPPORT_COLOR, SUPPORT_WHITE_VALUE)
from homeassistant.const import ( from homeassistant.const import (
CONF_BRIGHTNESS, CONF_COLOR_TEMP, CONF_EFFECT, CONF_NAME, CONF_BRIGHTNESS, CONF_COLOR_TEMP, CONF_EFFECT, CONF_NAME,
CONF_OPTIMISTIC, CONF_PAYLOAD_OFF, CONF_PAYLOAD_ON, CONF_OPTIMISTIC, CONF_PAYLOAD_OFF, CONF_PAYLOAD_ON, STATE_ON,
CONF_RGB, CONF_STATE, CONF_VALUE_TEMPLATE, CONF_WHITE_VALUE, CONF_XY) CONF_RGB, CONF_STATE, CONF_VALUE_TEMPLATE, CONF_WHITE_VALUE, CONF_XY)
from homeassistant.components.mqtt import ( from homeassistant.components.mqtt import (
CONF_AVAILABILITY_TOPIC, CONF_COMMAND_TOPIC, CONF_PAYLOAD_AVAILABLE, CONF_AVAILABILITY_TOPIC, CONF_COMMAND_TOPIC, CONF_PAYLOAD_AVAILABLE,
CONF_PAYLOAD_NOT_AVAILABLE, CONF_QOS, CONF_RETAIN, CONF_STATE_TOPIC, CONF_PAYLOAD_NOT_AVAILABLE, CONF_QOS, CONF_RETAIN, CONF_STATE_TOPIC,
MqttAvailability) MqttAvailability)
from homeassistant.helpers.restore_state import async_get_last_state
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
import homeassistant.util.color as color_util import homeassistant.util.color as color_util
@ -100,8 +100,8 @@ PLATFORM_SCHEMA = mqtt.MQTT_RW_PLATFORM_SCHEMA.extend({
}).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema) }).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema)
@asyncio.coroutine async def async_setup_platform(hass, config, async_add_devices,
def async_setup_platform(hass, config, async_add_devices, discovery_info=None): discovery_info=None):
"""Set up a MQTT Light.""" """Set up a MQTT Light."""
if discovery_info is not None: if discovery_info is not None:
config = PLATFORM_SCHEMA(discovery_info) config = PLATFORM_SCHEMA(discovery_info)
@ -213,10 +213,9 @@ class MqttLight(MqttAvailability, Light):
self._supported_features |= ( self._supported_features |= (
topic[CONF_XY_COMMAND_TOPIC] is not None and SUPPORT_COLOR) topic[CONF_XY_COMMAND_TOPIC] is not None and SUPPORT_COLOR)
@asyncio.coroutine async def async_added_to_hass(self):
def async_added_to_hass(self):
"""Subscribe to MQTT events.""" """Subscribe to MQTT events."""
yield from super().async_added_to_hass() await super().async_added_to_hass()
templates = {} templates = {}
for key, tpl in list(self._templates.items()): for key, tpl in list(self._templates.items()):
@ -226,6 +225,8 @@ class MqttLight(MqttAvailability, Light):
tpl.hass = self.hass tpl.hass = self.hass
templates[key] = tpl.async_render_with_possible_json_value templates[key] = tpl.async_render_with_possible_json_value
last_state = await async_get_last_state(self.hass, self.entity_id)
@callback @callback
def state_received(topic, payload, qos): def state_received(topic, payload, qos):
"""Handle new MQTT messages.""" """Handle new MQTT messages."""
@ -237,9 +238,11 @@ class MqttLight(MqttAvailability, Light):
self.async_schedule_update_ha_state() self.async_schedule_update_ha_state()
if self._topic[CONF_STATE_TOPIC] is not None: if self._topic[CONF_STATE_TOPIC] is not None:
yield from mqtt.async_subscribe( await mqtt.async_subscribe(
self.hass, self._topic[CONF_STATE_TOPIC], state_received, self.hass, self._topic[CONF_STATE_TOPIC], state_received,
self._qos) self._qos)
elif self._optimistic and last_state:
self._state = last_state.state == STATE_ON
@callback @callback
def brightness_received(topic, payload, qos): def brightness_received(topic, payload, qos):
@ -250,10 +253,13 @@ class MqttLight(MqttAvailability, Light):
self.async_schedule_update_ha_state() self.async_schedule_update_ha_state()
if self._topic[CONF_BRIGHTNESS_STATE_TOPIC] is not None: if self._topic[CONF_BRIGHTNESS_STATE_TOPIC] is not None:
yield from mqtt.async_subscribe( await mqtt.async_subscribe(
self.hass, self._topic[CONF_BRIGHTNESS_STATE_TOPIC], self.hass, self._topic[CONF_BRIGHTNESS_STATE_TOPIC],
brightness_received, self._qos) brightness_received, self._qos)
self._brightness = 255 self._brightness = 255
elif self._optimistic_brightness and last_state\
and last_state.attributes.get(ATTR_BRIGHTNESS):
self._brightness = last_state.attributes.get(ATTR_BRIGHTNESS)
elif self._topic[CONF_BRIGHTNESS_COMMAND_TOPIC] is not None: elif self._topic[CONF_BRIGHTNESS_COMMAND_TOPIC] is not None:
self._brightness = 255 self._brightness = 255
else: else:
@ -268,11 +274,14 @@ class MqttLight(MqttAvailability, Light):
self.async_schedule_update_ha_state() self.async_schedule_update_ha_state()
if self._topic[CONF_RGB_STATE_TOPIC] is not None: if self._topic[CONF_RGB_STATE_TOPIC] is not None:
yield from mqtt.async_subscribe( await mqtt.async_subscribe(
self.hass, self._topic[CONF_RGB_STATE_TOPIC], rgb_received, self.hass, self._topic[CONF_RGB_STATE_TOPIC], rgb_received,
self._qos) self._qos)
self._hs = (0, 0) self._hs = (0, 0)
if self._topic[CONF_RGB_COMMAND_TOPIC] is not None: if self._optimistic_rgb and last_state\
and last_state.attributes.get(ATTR_HS_COLOR):
self._hs = last_state.attributes.get(ATTR_HS_COLOR)
elif self._topic[CONF_RGB_COMMAND_TOPIC] is not None:
self._hs = (0, 0) self._hs = (0, 0)
@callback @callback
@ -282,11 +291,14 @@ class MqttLight(MqttAvailability, Light):
self.async_schedule_update_ha_state() self.async_schedule_update_ha_state()
if self._topic[CONF_COLOR_TEMP_STATE_TOPIC] is not None: if self._topic[CONF_COLOR_TEMP_STATE_TOPIC] is not None:
yield from mqtt.async_subscribe( await mqtt.async_subscribe(
self.hass, self._topic[CONF_COLOR_TEMP_STATE_TOPIC], self.hass, self._topic[CONF_COLOR_TEMP_STATE_TOPIC],
color_temp_received, self._qos) color_temp_received, self._qos)
self._color_temp = 150 self._color_temp = 150
if self._topic[CONF_COLOR_TEMP_COMMAND_TOPIC] is not None: if self._optimistic_color_temp and last_state\
and last_state.attributes.get(ATTR_COLOR_TEMP):
self._color_temp = last_state.attributes.get(ATTR_COLOR_TEMP)
elif self._topic[CONF_COLOR_TEMP_COMMAND_TOPIC] is not None:
self._color_temp = 150 self._color_temp = 150
else: else:
self._color_temp = None self._color_temp = None
@ -298,11 +310,14 @@ class MqttLight(MqttAvailability, Light):
self.async_schedule_update_ha_state() self.async_schedule_update_ha_state()
if self._topic[CONF_EFFECT_STATE_TOPIC] is not None: if self._topic[CONF_EFFECT_STATE_TOPIC] is not None:
yield from mqtt.async_subscribe( await mqtt.async_subscribe(
self.hass, self._topic[CONF_EFFECT_STATE_TOPIC], self.hass, self._topic[CONF_EFFECT_STATE_TOPIC],
effect_received, self._qos) effect_received, self._qos)
self._effect = 'none' self._effect = 'none'
if self._topic[CONF_EFFECT_COMMAND_TOPIC] is not None: if self._optimistic_effect and last_state\
and last_state.attributes.get(ATTR_EFFECT):
self._effect = last_state.attributes.get(ATTR_EFFECT)
elif self._topic[CONF_EFFECT_COMMAND_TOPIC] is not None:
self._effect = 'none' self._effect = 'none'
else: else:
self._effect = None self._effect = None
@ -316,10 +331,13 @@ class MqttLight(MqttAvailability, Light):
self.async_schedule_update_ha_state() self.async_schedule_update_ha_state()
if self._topic[CONF_WHITE_VALUE_STATE_TOPIC] is not None: if self._topic[CONF_WHITE_VALUE_STATE_TOPIC] is not None:
yield from mqtt.async_subscribe( await mqtt.async_subscribe(
self.hass, self._topic[CONF_WHITE_VALUE_STATE_TOPIC], self.hass, self._topic[CONF_WHITE_VALUE_STATE_TOPIC],
white_value_received, self._qos) white_value_received, self._qos)
self._white_value = 255 self._white_value = 255
elif self._optimistic_white_value and last_state\
and last_state.attributes.get(ATTR_WHITE_VALUE):
self._white_value = last_state.attributes.get(ATTR_WHITE_VALUE)
elif self._topic[CONF_WHITE_VALUE_COMMAND_TOPIC] is not None: elif self._topic[CONF_WHITE_VALUE_COMMAND_TOPIC] is not None:
self._white_value = 255 self._white_value = 255
else: else:
@ -334,11 +352,14 @@ class MqttLight(MqttAvailability, Light):
self.async_schedule_update_ha_state() self.async_schedule_update_ha_state()
if self._topic[CONF_XY_STATE_TOPIC] is not None: if self._topic[CONF_XY_STATE_TOPIC] is not None:
yield from mqtt.async_subscribe( await mqtt.async_subscribe(
self.hass, self._topic[CONF_XY_STATE_TOPIC], xy_received, self.hass, self._topic[CONF_XY_STATE_TOPIC], xy_received,
self._qos) self._qos)
self._hs = (0, 0) self._hs = (0, 0)
if self._topic[CONF_XY_COMMAND_TOPIC] is not None: if self._optimistic_xy and last_state\
and last_state.attributes.get(ATTR_HS_COLOR):
self._hs = last_state.attributes.get(ATTR_HS_COLOR)
elif self._topic[CONF_XY_COMMAND_TOPIC] is not None:
self._hs = (0, 0) self._hs = (0, 0)
@property @property
@ -396,8 +417,7 @@ class MqttLight(MqttAvailability, Light):
"""Flag supported features.""" """Flag supported features."""
return self._supported_features return self._supported_features
@asyncio.coroutine async def async_turn_on(self, **kwargs):
def async_turn_on(self, **kwargs):
"""Turn the device on. """Turn the device on.
This method is a coroutine. This method is a coroutine.
@ -517,8 +537,7 @@ class MqttLight(MqttAvailability, Light):
if should_update: if should_update:
self.async_schedule_update_ha_state() self.async_schedule_update_ha_state()
@asyncio.coroutine async def async_turn_off(self, **kwargs):
def async_turn_off(self, **kwargs):
"""Turn the device off. """Turn the device off.
This method is a coroutine. This method is a coroutine.

View File

@ -18,7 +18,7 @@ from homeassistant.components.light import (
SUPPORT_TRANSITION, SUPPORT_WHITE_VALUE) SUPPORT_TRANSITION, SUPPORT_WHITE_VALUE)
from homeassistant.components.light.mqtt import CONF_BRIGHTNESS_SCALE from homeassistant.components.light.mqtt import CONF_BRIGHTNESS_SCALE
from homeassistant.const import ( from homeassistant.const import (
CONF_BRIGHTNESS, CONF_COLOR_TEMP, CONF_EFFECT, CONF_BRIGHTNESS, CONF_COLOR_TEMP, CONF_EFFECT, STATE_ON,
CONF_NAME, CONF_OPTIMISTIC, CONF_RGB, CONF_WHITE_VALUE, CONF_XY) CONF_NAME, CONF_OPTIMISTIC, CONF_RGB, CONF_WHITE_VALUE, CONF_XY)
from homeassistant.components.mqtt import ( from homeassistant.components.mqtt import (
CONF_AVAILABILITY_TOPIC, CONF_STATE_TOPIC, CONF_COMMAND_TOPIC, CONF_AVAILABILITY_TOPIC, CONF_STATE_TOPIC, CONF_COMMAND_TOPIC,
@ -26,6 +26,7 @@ from homeassistant.components.mqtt import (
MqttAvailability) MqttAvailability)
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.typing import HomeAssistantType, ConfigType from homeassistant.helpers.typing import HomeAssistantType, ConfigType
from homeassistant.helpers.restore_state import async_get_last_state
import homeassistant.util.color as color_util import homeassistant.util.color as color_util
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -177,6 +178,8 @@ class MqttJson(MqttAvailability, Light):
"""Subscribe to MQTT events.""" """Subscribe to MQTT events."""
await super().async_added_to_hass() await super().async_added_to_hass()
last_state = await async_get_last_state(self.hass, self.entity_id)
@callback @callback
def state_received(topic, payload, qos): def state_received(topic, payload, qos):
"""Handle new MQTT messages.""" """Handle new MQTT messages."""
@ -260,6 +263,19 @@ class MqttJson(MqttAvailability, Light):
self.hass, self._topic[CONF_STATE_TOPIC], state_received, self.hass, self._topic[CONF_STATE_TOPIC], state_received,
self._qos) self._qos)
if self._optimistic and last_state:
self._state = last_state.state == STATE_ON
if last_state.attributes.get(ATTR_BRIGHTNESS):
self._brightness = last_state.attributes.get(ATTR_BRIGHTNESS)
if last_state.attributes.get(ATTR_HS_COLOR):
self._hs = last_state.attributes.get(ATTR_HS_COLOR)
if last_state.attributes.get(ATTR_COLOR_TEMP):
self._color_temp = last_state.attributes.get(ATTR_COLOR_TEMP)
if last_state.attributes.get(ATTR_EFFECT):
self._effect = last_state.attributes.get(ATTR_EFFECT)
if last_state.attributes.get(ATTR_WHITE_VALUE):
self._white_value = last_state.attributes.get(ATTR_WHITE_VALUE)
@property @property
def brightness(self): def brightness(self):
"""Return the brightness of this light between 0..255.""" """Return the brightness of this light between 0..255."""

View File

@ -4,7 +4,6 @@ Support for MQTT Template lights.
For more details about this platform, please refer to the documentation at For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/light.mqtt_template/ https://home-assistant.io/components/light.mqtt_template/
""" """
import asyncio
import logging import logging
import voluptuous as vol import voluptuous as vol
@ -22,6 +21,7 @@ from homeassistant.components.mqtt import (
MqttAvailability) MqttAvailability)
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
import homeassistant.util.color as color_util import homeassistant.util.color as color_util
from homeassistant.helpers.restore_state import async_get_last_state
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -66,8 +66,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
}).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema) }).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema)
@asyncio.coroutine async def async_setup_platform(hass, config, async_add_devices,
def async_setup_platform(hass, config, async_add_devices, discovery_info=None): discovery_info=None):
"""Set up a MQTT Template light.""" """Set up a MQTT Template light."""
if discovery_info is not None: if discovery_info is not None:
config = PLATFORM_SCHEMA(discovery_info) config = PLATFORM_SCHEMA(discovery_info)
@ -152,10 +152,11 @@ class MqttTemplate(MqttAvailability, Light):
if tpl is not None: if tpl is not None:
tpl.hass = hass tpl.hass = hass
@asyncio.coroutine async def async_added_to_hass(self):
def async_added_to_hass(self):
"""Subscribe to MQTT events.""" """Subscribe to MQTT events."""
yield from super().async_added_to_hass() await super().async_added_to_hass()
last_state = await async_get_last_state(self.hass, self.entity_id)
@callback @callback
def state_received(topic, payload, qos): def state_received(topic, payload, qos):
@ -223,10 +224,23 @@ class MqttTemplate(MqttAvailability, Light):
self.async_schedule_update_ha_state() self.async_schedule_update_ha_state()
if self._topics[CONF_STATE_TOPIC] is not None: if self._topics[CONF_STATE_TOPIC] is not None:
yield from mqtt.async_subscribe( await mqtt.async_subscribe(
self.hass, self._topics[CONF_STATE_TOPIC], state_received, self.hass, self._topics[CONF_STATE_TOPIC], state_received,
self._qos) self._qos)
if self._optimistic and last_state:
self._state = last_state.state == STATE_ON
if last_state.attributes.get(ATTR_BRIGHTNESS):
self._brightness = last_state.attributes.get(ATTR_BRIGHTNESS)
if last_state.attributes.get(ATTR_HS_COLOR):
self._hs = last_state.attributes.get(ATTR_HS_COLOR)
if last_state.attributes.get(ATTR_COLOR_TEMP):
self._color_temp = last_state.attributes.get(ATTR_COLOR_TEMP)
if last_state.attributes.get(ATTR_EFFECT):
self._effect = last_state.attributes.get(ATTR_EFFECT)
if last_state.attributes.get(ATTR_WHITE_VALUE):
self._white_value = last_state.attributes.get(ATTR_WHITE_VALUE)
@property @property
def brightness(self): def brightness(self):
"""Return the brightness of this light between 0..255.""" """Return the brightness of this light between 0..255."""
@ -280,8 +294,7 @@ class MqttTemplate(MqttAvailability, Light):
"""Return the current effect.""" """Return the current effect."""
return self._effect return self._effect
@asyncio.coroutine async def async_turn_on(self, **kwargs):
def async_turn_on(self, **kwargs):
"""Turn the entity on. """Turn the entity on.
This method is a coroutine. This method is a coroutine.
@ -339,8 +352,7 @@ class MqttTemplate(MqttAvailability, Light):
if self._optimistic: if self._optimistic:
self.async_schedule_update_ha_state() self.async_schedule_update_ha_state()
@asyncio.coroutine async def async_turn_off(self, **kwargs):
def async_turn_off(self, **kwargs):
"""Turn the entity off. """Turn the entity off.
This method is a coroutine. This method is a coroutine.

View File

@ -130,7 +130,7 @@ class MySensorsLight(mysensors.MySensorsEntity, Light):
self._white = white self._white = white
self._values[self.value_type] = hex_color self._values[self.value_type] = hex_color
def turn_off(self, **kwargs): async def async_turn_off(self, **kwargs):
"""Turn the device off.""" """Turn the device off."""
value_type = self.gateway.const.SetReq.V_LIGHT value_type = self.gateway.const.SetReq.V_LIGHT
self.gateway.set_child_value( self.gateway.set_child_value(
@ -139,7 +139,7 @@ class MySensorsLight(mysensors.MySensorsEntity, Light):
# optimistically assume that light has changed state # optimistically assume that light has changed state
self._state = False self._state = False
self._values[value_type] = STATE_OFF self._values[value_type] = STATE_OFF
self.schedule_update_ha_state() self.async_schedule_update_ha_state()
def _async_update_light(self): def _async_update_light(self):
"""Update the controller with values from light child.""" """Update the controller with values from light child."""
@ -171,12 +171,12 @@ class MySensorsLightDimmer(MySensorsLight):
"""Flag supported features.""" """Flag supported features."""
return SUPPORT_BRIGHTNESS return SUPPORT_BRIGHTNESS
def turn_on(self, **kwargs): async def async_turn_on(self, **kwargs):
"""Turn the device on.""" """Turn the device on."""
self._turn_on_light() self._turn_on_light()
self._turn_on_dimmer(**kwargs) self._turn_on_dimmer(**kwargs)
if self.gateway.optimistic: if self.gateway.optimistic:
self.schedule_update_ha_state() self.async_schedule_update_ha_state()
async def async_update(self): async def async_update(self):
"""Update the controller with the latest value from a sensor.""" """Update the controller with the latest value from a sensor."""
@ -196,13 +196,13 @@ class MySensorsLightRGB(MySensorsLight):
return SUPPORT_BRIGHTNESS | SUPPORT_COLOR return SUPPORT_BRIGHTNESS | SUPPORT_COLOR
return SUPPORT_COLOR return SUPPORT_COLOR
def turn_on(self, **kwargs): async def async_turn_on(self, **kwargs):
"""Turn the device on.""" """Turn the device on."""
self._turn_on_light() self._turn_on_light()
self._turn_on_dimmer(**kwargs) self._turn_on_dimmer(**kwargs)
self._turn_on_rgb_and_w('%02x%02x%02x', **kwargs) self._turn_on_rgb_and_w('%02x%02x%02x', **kwargs)
if self.gateway.optimistic: if self.gateway.optimistic:
self.schedule_update_ha_state() self.async_schedule_update_ha_state()
async def async_update(self): async def async_update(self):
"""Update the controller with the latest value from a sensor.""" """Update the controller with the latest value from a sensor."""
@ -225,10 +225,10 @@ class MySensorsLightRGBW(MySensorsLightRGB):
return SUPPORT_BRIGHTNESS | SUPPORT_MYSENSORS_RGBW return SUPPORT_BRIGHTNESS | SUPPORT_MYSENSORS_RGBW
return SUPPORT_MYSENSORS_RGBW return SUPPORT_MYSENSORS_RGBW
def turn_on(self, **kwargs): async def async_turn_on(self, **kwargs):
"""Turn the device on.""" """Turn the device on."""
self._turn_on_light() self._turn_on_light()
self._turn_on_dimmer(**kwargs) self._turn_on_dimmer(**kwargs)
self._turn_on_rgb_and_w('%02x%02x%02x%02x', **kwargs) self._turn_on_rgb_and_w('%02x%02x%02x%02x', **kwargs)
if self.gateway.optimistic: if self.gateway.optimistic:
self.schedule_update_ha_state() self.async_schedule_update_ha_state()

View File

@ -92,6 +92,16 @@ class AuroraLight(Light):
"""Return the list of supported effects.""" """Return the list of supported effects."""
return self._effects_list return self._effects_list
@property
def min_mireds(self):
"""Return the coldest color_temp that this light supports."""
return 154
@property
def max_mireds(self):
"""Return the warmest color_temp that this light supports."""
return 833
@property @property
def name(self): def name(self):
"""Return the display name of this light.""" """Return the display name of this light."""

View File

@ -16,8 +16,6 @@ from homeassistant.util.color import \
DEPENDENCIES = ['wink'] DEPENDENCIES = ['wink']
SUPPORT_WINK = SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP | SUPPORT_COLOR
def setup_platform(hass, config, add_devices, discovery_info=None): def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Wink lights.""" """Set up the Wink lights."""
@ -78,7 +76,14 @@ class WinkLight(WinkDevice, Light):
@property @property
def supported_features(self): def supported_features(self):
"""Flag supported features.""" """Flag supported features."""
return SUPPORT_WINK supports = SUPPORT_BRIGHTNESS
if self.wink.supports_temperature():
supports = supports | SUPPORT_COLOR_TEMP
if self.wink.supports_xy_color():
supports = supports | SUPPORT_COLOR
elif self.wink.supports_hue_saturation():
supports = supports | SUPPORT_COLOR
return supports
def turn_on(self, **kwargs): def turn_on(self, **kwargs):
"""Turn the switch on.""" """Turn the switch on."""

View File

@ -6,7 +6,6 @@ at https://home-assistant.io/components/light.zha/
""" """
import logging import logging
from homeassistant.components import light, zha from homeassistant.components import light, zha
from homeassistant.const import STATE_UNKNOWN
import homeassistant.util.color as color_util import homeassistant.util.color as color_util
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -76,7 +75,7 @@ class Light(zha.Entity, light.Light):
@property @property
def is_on(self) -> bool: def is_on(self) -> bool:
"""Return true if entity is on.""" """Return true if entity is on."""
if self._state == STATE_UNKNOWN: if self._state is None:
return False return False
return bool(self._state) return bool(self._state)

View File

@ -11,7 +11,8 @@ import voluptuous as vol
from homeassistant.components.lock import LockDevice from homeassistant.components.lock import LockDevice
from homeassistant.components.wink import DOMAIN, WinkDevice from homeassistant.components.wink import DOMAIN, WinkDevice
from homeassistant.const import ATTR_CODE, ATTR_ENTITY_ID, STATE_UNKNOWN from homeassistant.const import (
ATTR_CODE, ATTR_ENTITY_ID, ATTR_NAME, STATE_UNKNOWN)
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
DEPENDENCIES = ['wink'] DEPENDENCIES = ['wink']
@ -28,7 +29,6 @@ SERVICE_ADD_KEY = 'wink_add_new_lock_key_code'
ATTR_ENABLED = 'enabled' ATTR_ENABLED = 'enabled'
ATTR_SENSITIVITY = 'sensitivity' ATTR_SENSITIVITY = 'sensitivity'
ATTR_MODE = 'mode' ATTR_MODE = 'mode'
ATTR_NAME = 'name'
ALARM_SENSITIVITY_MAP = { ALARM_SENSITIVITY_MAP = {
'low': 0.2, 'low': 0.2,

View File

@ -4,44 +4,49 @@ Event parser and human readable log generator.
For more details about this component, please refer to the documentation at For more details about this component, please refer to the documentation at
https://home-assistant.io/components/logbook/ https://home-assistant.io/components/logbook/
""" """
import logging
from datetime import timedelta from datetime import timedelta
from itertools import groupby from itertools import groupby
import logging
import voluptuous as vol import voluptuous as vol
from homeassistant.core import callback
import homeassistant.helpers.config_validation as cv
import homeassistant.util.dt as dt_util
from homeassistant.components import sun from homeassistant.components import sun
from homeassistant.components.http import HomeAssistantView from homeassistant.components.http import HomeAssistantView
from homeassistant.const import ( from homeassistant.const import (
EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, EVENT_STATE_CHANGED, ATTR_DOMAIN, ATTR_ENTITY_ID, ATTR_HIDDEN, ATTR_NAME, CONF_EXCLUDE,
STATE_NOT_HOME, STATE_OFF, STATE_ON, ATTR_HIDDEN, HTTP_BAD_REQUEST, CONF_INCLUDE, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP,
EVENT_LOGBOOK_ENTRY) EVENT_LOGBOOK_ENTRY, EVENT_STATE_CHANGED, HTTP_BAD_REQUEST, STATE_NOT_HOME,
from homeassistant.core import State, split_entity_id, DOMAIN as HA_DOMAIN STATE_OFF, STATE_ON)
from homeassistant.core import DOMAIN as HA_DOMAIN
DOMAIN = 'logbook' from homeassistant.core import State, callback, split_entity_id
DEPENDENCIES = ['recorder', 'frontend'] import homeassistant.helpers.config_validation as cv
import homeassistant.util.dt as dt_util
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
CONF_EXCLUDE = 'exclude' ATTR_MESSAGE = 'message'
CONF_INCLUDE = 'include'
CONF_ENTITIES = 'entities'
CONF_DOMAINS = 'domains' CONF_DOMAINS = 'domains'
CONF_ENTITIES = 'entities'
CONTINUOUS_DOMAINS = ['proximity', 'sensor']
DEPENDENCIES = ['recorder', 'frontend']
DOMAIN = 'logbook'
GROUP_BY_MINUTES = 15
CONFIG_SCHEMA = vol.Schema({ CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({ DOMAIN: vol.Schema({
CONF_EXCLUDE: vol.Schema({ CONF_EXCLUDE: vol.Schema({
vol.Optional(CONF_ENTITIES, default=[]): cv.entity_ids, vol.Optional(CONF_ENTITIES, default=[]): cv.entity_ids,
vol.Optional(CONF_DOMAINS, default=[]): vol.All(cv.ensure_list, vol.Optional(CONF_DOMAINS, default=[]):
[cv.string]) vol.All(cv.ensure_list, [cv.string])
}), }),
CONF_INCLUDE: vol.Schema({ CONF_INCLUDE: vol.Schema({
vol.Optional(CONF_ENTITIES, default=[]): cv.entity_ids, vol.Optional(CONF_ENTITIES, default=[]): cv.entity_ids,
vol.Optional(CONF_DOMAINS, default=[]): vol.All(cv.ensure_list, vol.Optional(CONF_DOMAINS, default=[]):
[cv.string]) vol.All(cv.ensure_list, [cv.string])
}) })
}), }),
}, extra=vol.ALLOW_EXTRA) }, extra=vol.ALLOW_EXTRA)
@ -51,15 +56,6 @@ ALL_EVENT_TYPES = [
EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP
] ]
GROUP_BY_MINUTES = 15
CONTINUOUS_DOMAINS = ['proximity', 'sensor']
ATTR_NAME = 'name'
ATTR_MESSAGE = 'message'
ATTR_DOMAIN = 'domain'
ATTR_ENTITY_ID = 'entity_id'
LOG_MESSAGE_SCHEMA = vol.Schema({ LOG_MESSAGE_SCHEMA = vol.Schema({
vol.Required(ATTR_NAME): cv.string, vol.Required(ATTR_NAME): cv.string,
vol.Required(ATTR_MESSAGE): cv.template, vol.Required(ATTR_MESSAGE): cv.template,

View File

@ -14,7 +14,7 @@ from homeassistant.components.media_player import (
SERVICE_PLAY_MEDIA) SERVICE_PLAY_MEDIA)
from homeassistant.helpers import config_validation as cv from homeassistant.helpers import config_validation as cv
REQUIREMENTS = ['youtube_dl==2018.04.25'] REQUIREMENTS = ['youtube_dl==2018.05.09']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)

View File

@ -19,7 +19,7 @@ from homeassistant.const import (
CONF_PASSWORD) CONF_PASSWORD)
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['pycmus==0.1.0'] REQUIREMENTS = ['pycmus==0.1.1']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)

View File

@ -24,7 +24,6 @@ _LOGGER = logging.getLogger(__name__)
CONF_SOURCES = 'sources' CONF_SOURCES = 'sources'
CONF_MAX_VOLUME = 'max_volume' CONF_MAX_VOLUME = 'max_volume'
CONF_ZONE2 = 'zone2'
DEFAULT_NAME = 'Onkyo Receiver' DEFAULT_NAME = 'Onkyo Receiver'
SUPPORTED_MAX_VOLUME = 80 SUPPORTED_MAX_VOLUME = 80
@ -47,9 +46,36 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.All(vol.Coerce(int), vol.Range(min=1, max=SUPPORTED_MAX_VOLUME)), vol.All(vol.Coerce(int), vol.Range(min=1, max=SUPPORTED_MAX_VOLUME)),
vol.Optional(CONF_SOURCES, default=DEFAULT_SOURCES): vol.Optional(CONF_SOURCES, default=DEFAULT_SOURCES):
{cv.string: cv.string}, {cv.string: cv.string},
vol.Optional(CONF_ZONE2, default=False): cv.boolean,
}) })
TIMEOUT_MESSAGE = 'Timeout waiting for response.'
def determine_zones(receiver):
"""Determine what zones are available for the receiver."""
out = {
"zone2": False,
"zone3": False,
}
try:
_LOGGER.debug("Checking for zone 2 capability")
receiver.raw("ZPW")
out["zone2"] = True
except ValueError as error:
if str(error) != TIMEOUT_MESSAGE:
raise error
_LOGGER.debug("Zone 2 timed out, assuming no functionality")
try:
_LOGGER.debug("Checking for zone 3 capability")
receiver.raw("PW3")
out["zone3"] = True
except ValueError as error:
if str(error) != TIMEOUT_MESSAGE:
raise error
_LOGGER.debug("Zone 3 timed out, assuming no functionality")
return out
def setup_platform(hass, config, add_devices, discovery_info=None): def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Onkyo platform.""" """Set up the Onkyo platform."""
@ -61,20 +87,31 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
if CONF_HOST in config and host not in KNOWN_HOSTS: if CONF_HOST in config and host not in KNOWN_HOSTS:
try: try:
receiver = eiscp.eISCP(host)
hosts.append(OnkyoDevice( hosts.append(OnkyoDevice(
eiscp.eISCP(host), config.get(CONF_SOURCES), receiver,
config.get(CONF_SOURCES),
name=config.get(CONF_NAME), name=config.get(CONF_NAME),
max_volume=config.get(CONF_MAX_VOLUME), max_volume=config.get(CONF_MAX_VOLUME),
)) ))
KNOWN_HOSTS.append(host) KNOWN_HOSTS.append(host)
# Add Zone2 if configured zones = determine_zones(receiver)
if config.get(CONF_ZONE2):
# Add Zone2 if available
if zones["zone2"]:
_LOGGER.debug("Setting up zone 2") _LOGGER.debug("Setting up zone 2")
hosts.append(OnkyoDeviceZone2(eiscp.eISCP(host), hosts.append(OnkyoDeviceZone(
config.get(CONF_SOURCES), "2", receiver,
name=config.get(CONF_NAME) + config.get(CONF_SOURCES),
" Zone 2")) name="{} Zone 2".format(config[CONF_NAME])))
# Add Zone3 if available
if zones["zone3"]:
_LOGGER.debug("Setting up zone 3")
hosts.append(OnkyoDeviceZone(
"3", receiver,
config.get(CONF_SOURCES),
name="{} Zone 3".format(config[CONF_NAME])))
except OSError: except OSError:
_LOGGER.error("Unable to connect to receiver at %s", host) _LOGGER.error("Unable to connect to receiver at %s", host)
else: else:
@ -227,12 +264,17 @@ class OnkyoDevice(MediaPlayerDevice):
self.command('input-selector {}'.format(source)) self.command('input-selector {}'.format(source))
class OnkyoDeviceZone2(OnkyoDevice): class OnkyoDeviceZone(OnkyoDevice):
"""Representation of an Onkyo device's zone 2.""" """Representation of an Onkyo device's extra zone."""
def __init__(self, zone, receiver, sources, name=None):
"""Initialize the Zone with the zone identifier."""
self._zone = zone
super().__init__(receiver, sources, name)
def update(self): def update(self):
"""Get the latest state from the device.""" """Get the latest state from the device."""
status = self.command('zone2.power=query') status = self.command('zone{}.power=query'.format(self._zone))
if not status: if not status:
return return
@ -242,9 +284,10 @@ class OnkyoDeviceZone2(OnkyoDevice):
self._pwstate = STATE_OFF self._pwstate = STATE_OFF
return return
volume_raw = self.command('zone2.volume=query') volume_raw = self.command('zone{}.volume=query'.format(self._zone))
mute_raw = self.command('zone2.muting=query') mute_raw = self.command('zone{}.muting=query'.format(self._zone))
current_source_raw = self.command('zone2.selector=query') current_source_raw = self.command(
'zone{}.selector=query'.format(self._zone))
if not (volume_raw and mute_raw and current_source_raw): if not (volume_raw and mute_raw and current_source_raw):
return return
@ -268,33 +311,33 @@ class OnkyoDeviceZone2(OnkyoDevice):
def turn_off(self): def turn_off(self):
"""Turn the media player off.""" """Turn the media player off."""
self.command('zone2.power=standby') self.command('zone{}.power=standby'.format(self._zone))
def set_volume_level(self, volume): def set_volume_level(self, volume):
"""Set volume level, input is range 0..1. Onkyo ranges from 1-80.""" """Set volume level, input is range 0..1. Onkyo ranges from 1-80."""
self.command('zone2.volume={}'.format(int(volume*80))) self.command('zone{}.volume={}'.format(self._zone, int(volume*80)))
def volume_up(self): def volume_up(self):
"""Increase volume by 1 step.""" """Increase volume by 1 step."""
self.command('zone2.volume=level-up') self.command('zone{}.volume=level-up'.format(self._zone))
def volume_down(self): def volume_down(self):
"""Decrease volume by 1 step.""" """Decrease volume by 1 step."""
self.command('zone2.volume=level-down') self.command('zone{}.volume=level-down'.format(self._zone))
def mute_volume(self, mute): def mute_volume(self, mute):
"""Mute (true) or unmute (false) media player.""" """Mute (true) or unmute (false) media player."""
if mute: if mute:
self.command('zone2.muting=on') self.command('zone{}.muting=on'.format(self._zone))
else: else:
self.command('zone2.muting=off') self.command('zone{}.muting=off'.format(self._zone))
def turn_on(self): def turn_on(self):
"""Turn the media player on.""" """Turn the media player on."""
self.command('zone2.power=on') self.command('zone{}.power=on'.format(self._zone))
def select_source(self, source): def select_source(self, source):
"""Set the input source.""" """Set the input source."""
if source in self._source_list: if source in self._source_list:
source = self._reverse_mapping[source] source = self._reverse_mapping[source]
self.command('zone2.selector={}'.format(source)) self.command('zone{}.selector={}'.format(self._zone, source))

View File

@ -146,6 +146,11 @@ class RokuDevice(MediaPlayerDevice):
"""Flag media player features that are supported.""" """Flag media player features that are supported."""
return SUPPORT_ROKU return SUPPORT_ROKU
@property
def unique_id(self):
"""Return a unique, HASS-friendly identifier for this entity."""
return self.device_info.sernum
@property @property
def media_content_type(self): def media_content_type(self):
"""Content type of current playing media.""" """Content type of current playing media."""

View File

@ -151,8 +151,8 @@ class SongpalDevice(MediaPlayerDevice):
return return
if len(volumes) > 1: if len(volumes) > 1:
_LOGGER.warning("Got %s volume controls, using the first one", _LOGGER.debug("Got %s volume controls, using the first one",
volumes) volumes)
volume = volumes[0] volume = volumes[0]
_LOGGER.debug("Current volume: %s", volume) _LOGGER.debug("Current volume: %s", volume)

View File

@ -682,11 +682,15 @@ class SonosDevice(MediaPlayerDevice):
if group: if group:
# New group information is pushed # New group information is pushed
coordinator_uid, *slave_uids = group.split(',') coordinator_uid, *slave_uids = group.split(',')
else: elif self.soco.group:
# Use SoCo cache for existing topology # Use SoCo cache for existing topology
coordinator_uid = self.soco.group.coordinator.uid coordinator_uid = self.soco.group.coordinator.uid
slave_uids = [p.uid for p in self.soco.group.members slave_uids = [p.uid for p in self.soco.group.members
if p.uid != coordinator_uid] if p.uid != coordinator_uid]
else:
# Not yet in the cache, this can happen when a speaker boots
coordinator_uid = self.unique_id
slave_uids = []
if self.unique_id == coordinator_uid: if self.unique_id == coordinator_uid:
sonos_group = [] sonos_group = []

View File

@ -30,7 +30,8 @@ from homeassistant.const import (
SERVICE_MEDIA_PLAY, SERVICE_MEDIA_PLAY_PAUSE, SERVICE_MEDIA_PREVIOUS_TRACK, SERVICE_MEDIA_PLAY, SERVICE_MEDIA_PLAY_PAUSE, SERVICE_MEDIA_PREVIOUS_TRACK,
SERVICE_MEDIA_SEEK, SERVICE_TURN_OFF, SERVICE_TURN_ON, SERVICE_VOLUME_DOWN, SERVICE_MEDIA_SEEK, SERVICE_TURN_OFF, SERVICE_TURN_ON, SERVICE_VOLUME_DOWN,
SERVICE_VOLUME_MUTE, SERVICE_VOLUME_SET, SERVICE_VOLUME_UP, SERVICE_VOLUME_MUTE, SERVICE_VOLUME_SET, SERVICE_VOLUME_UP,
SERVICE_SHUFFLE_SET, STATE_IDLE, STATE_OFF, STATE_ON, SERVICE_MEDIA_STOP) SERVICE_SHUFFLE_SET, STATE_IDLE, STATE_OFF, STATE_ON, STATE_UNAVAILABLE,
SERVICE_MEDIA_STOP)
from homeassistant.helpers import config_validation as cv from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.service import async_call_from_config from homeassistant.helpers.service import async_call_from_config
@ -45,7 +46,7 @@ CONF_SERVICE_DATA = 'service_data'
ATTR_DATA = 'data' ATTR_DATA = 'data'
CONF_STATE = 'state' CONF_STATE = 'state'
OFF_STATES = [STATE_IDLE, STATE_OFF] OFF_STATES = [STATE_IDLE, STATE_OFF, STATE_UNAVAILABLE]
REQUIREMENTS = [] REQUIREMENTS = []
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)

View File

@ -222,7 +222,7 @@ class YamahaDevice(MediaPlayerDevice):
@property @property
def zone_id(self): def zone_id(self):
"""Return an zone_id to ensure 1 media player per zone.""" """Return a zone_id to ensure 1 media player per zone."""
return '{0}:{1}'.format(self.receiver.ctrl_url, self._zone) return '{0}:{1}'.format(self.receiver.ctrl_url, self._zone)
@property @property

View File

@ -1,5 +1,5 @@
""" """
Support for microsoft face recognition. Support for Microsoft face recognition.
For more details about this component, please refer to the documentation at For more details about this component, please refer to the documentation at
https://home-assistant.io/components/microsoft_face/ https://home-assistant.io/components/microsoft_face/
@ -13,7 +13,7 @@ from aiohttp.hdrs import CONTENT_TYPE
import async_timeout import async_timeout
import voluptuous as vol import voluptuous as vol
from homeassistant.const import CONF_API_KEY, CONF_TIMEOUT from homeassistant.const import CONF_API_KEY, CONF_TIMEOUT, ATTR_NAME
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.aiohttp_client import async_get_clientsession
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
@ -22,28 +22,25 @@ from homeassistant.util import slugify
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
DOMAIN = 'microsoft_face' ATTR_CAMERA_ENTITY = 'camera_entity'
DEPENDENCIES = ['camera'] ATTR_GROUP = 'group'
ATTR_PERSON = 'person'
FACE_API_URL = "api.cognitive.microsoft.com/face/v1.0/{0}"
DATA_MICROSOFT_FACE = 'microsoft_face'
CONF_AZURE_REGION = 'azure_region' CONF_AZURE_REGION = 'azure_region'
DATA_MICROSOFT_FACE = 'microsoft_face'
DEFAULT_TIMEOUT = 10
DEPENDENCIES = ['camera']
DOMAIN = 'microsoft_face'
FACE_API_URL = "api.cognitive.microsoft.com/face/v1.0/{0}"
SERVICE_CREATE_GROUP = 'create_group' SERVICE_CREATE_GROUP = 'create_group'
SERVICE_DELETE_GROUP = 'delete_group'
SERVICE_TRAIN_GROUP = 'train_group'
SERVICE_CREATE_PERSON = 'create_person' SERVICE_CREATE_PERSON = 'create_person'
SERVICE_DELETE_GROUP = 'delete_group'
SERVICE_DELETE_PERSON = 'delete_person' SERVICE_DELETE_PERSON = 'delete_person'
SERVICE_FACE_PERSON = 'face_person' SERVICE_FACE_PERSON = 'face_person'
SERVICE_TRAIN_GROUP = 'train_group'
ATTR_GROUP = 'group'
ATTR_PERSON = 'person'
ATTR_CAMERA_ENTITY = 'camera_entity'
ATTR_NAME = 'name'
DEFAULT_TIMEOUT = 10
CONFIG_SCHEMA = vol.Schema({ CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({ DOMAIN: vol.Schema({
@ -111,7 +108,7 @@ def face_person(hass, group, person, camera_entity):
@asyncio.coroutine @asyncio.coroutine
def async_setup(hass, config): def async_setup(hass, config):
"""Set up microsoft face.""" """Set up Microsoft Face."""
entities = {} entities = {}
face = MicrosoftFace( face = MicrosoftFace(
hass, hass,

View File

@ -16,7 +16,7 @@ from homeassistant.helpers import config_validation as cv
from homeassistant.helpers import discovery from homeassistant.helpers import discovery
from homeassistant.util import Throttle from homeassistant.util import Throttle
REQUIREMENTS = ["mychevy==0.1.1"] REQUIREMENTS = ["mychevy==0.4.0"]
DOMAIN = 'mychevy' DOMAIN = 'mychevy'
UPDATE_TOPIC = DOMAIN UPDATE_TOPIC = DOMAIN
@ -73,9 +73,6 @@ def setup(hass, base_config):
hass.data[DOMAIN] = MyChevyHub(mc.MyChevy(email, password), hass) hass.data[DOMAIN] = MyChevyHub(mc.MyChevy(email, password), hass)
hass.data[DOMAIN].start() hass.data[DOMAIN].start()
discovery.load_platform(hass, 'sensor', DOMAIN, {}, config)
discovery.load_platform(hass, 'binary_sensor', DOMAIN, {}, config)
return True return True
@ -98,8 +95,9 @@ class MyChevyHub(threading.Thread):
super().__init__() super().__init__()
self._client = client self._client = client
self.hass = hass self.hass = hass
self.car = None self.cars = []
self.status = None self.status = None
self.ready = False
@Throttle(MIN_TIME_BETWEEN_UPDATES) @Throttle(MIN_TIME_BETWEEN_UPDATES)
def update(self): def update(self):
@ -109,7 +107,22 @@ class MyChevyHub(threading.Thread):
(like 2 to 3 minutes long time) (like 2 to 3 minutes long time)
""" """
self.car = self._client.data() self._client.login()
self._client.get_cars()
self.cars = self._client.cars
if self.ready is not True:
discovery.load_platform(self.hass, 'sensor', DOMAIN, {}, {})
discovery.load_platform(self.hass, 'binary_sensor', DOMAIN, {}, {})
self.ready = True
self.cars = self._client.update_cars()
def get_car(self, vid):
"""Compatibility to work with one car."""
if self.cars:
for car in self.cars:
if car.vid == vid:
return car
return None
def run(self): def run(self):
"""Thread run loop.""" """Thread run loop."""

View File

@ -4,6 +4,7 @@ Connect to a MySensors gateway via pymysensors API.
For more details about this component, please refer to the documentation at For more details about this component, please refer to the documentation at
https://home-assistant.io/components/mysensors/ https://home-assistant.io/components/mysensors/
""" """
import asyncio
from collections import defaultdict from collections import defaultdict
import logging import logging
import os import os
@ -11,22 +12,23 @@ import socket
import sys import sys
from timeit import default_timer as timer from timeit import default_timer as timer
import async_timeout
import voluptuous as vol import voluptuous as vol
from homeassistant.components.mqtt import ( from homeassistant.components.mqtt import (
valid_publish_topic, valid_subscribe_topic) valid_publish_topic, valid_subscribe_topic)
from homeassistant.const import ( from homeassistant.const import (
ATTR_BATTERY_LEVEL, CONF_NAME, CONF_OPTIMISTIC, EVENT_HOMEASSISTANT_START, ATTR_BATTERY_LEVEL, CONF_NAME, CONF_OPTIMISTIC, EVENT_HOMEASSISTANT_STOP,
EVENT_HOMEASSISTANT_STOP, STATE_OFF, STATE_ON) STATE_OFF, STATE_ON)
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.helpers import discovery from homeassistant.helpers import discovery
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.dispatcher import ( from homeassistant.helpers.dispatcher import (
async_dispatcher_connect, dispatcher_send) async_dispatcher_connect, async_dispatcher_send)
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
from homeassistant.setup import setup_component from homeassistant.setup import async_setup_component
REQUIREMENTS = ['pymysensors==0.11.1'] REQUIREMENTS = ['pymysensors==0.14.0']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -56,9 +58,11 @@ DEFAULT_TCP_PORT = 5003
DEFAULT_VERSION = '1.4' DEFAULT_VERSION = '1.4'
DOMAIN = 'mysensors' DOMAIN = 'mysensors'
GATEWAY_READY_TIMEOUT = 15.0
MQTT_COMPONENT = 'mqtt' MQTT_COMPONENT = 'mqtt'
MYSENSORS_GATEWAYS = 'mysensors_gateways' MYSENSORS_GATEWAYS = 'mysensors_gateways'
MYSENSORS_PLATFORM_DEVICES = 'mysensors_devices_{}' MYSENSORS_PLATFORM_DEVICES = 'mysensors_devices_{}'
MYSENSORS_GATEWAY_READY = 'mysensors_gateway_ready_{}'
PLATFORM = 'platform' PLATFORM = 'platform'
SCHEMA = 'schema' SCHEMA = 'schema'
SIGNAL_CALLBACK = 'mysensors_callback_{}_{}_{}_{}' SIGNAL_CALLBACK = 'mysensors_callback_{}_{}_{}_{}'
@ -280,67 +284,62 @@ MYSENSORS_CONST_SCHEMA = {
} }
def setup(hass, config): async def async_setup(hass, config):
"""Set up the MySensors component.""" """Set up the MySensors component."""
import mysensors.mysensors as mysensors import mysensors.mysensors as mysensors
version = config[DOMAIN].get(CONF_VERSION) version = config[DOMAIN].get(CONF_VERSION)
persistence = config[DOMAIN].get(CONF_PERSISTENCE) persistence = config[DOMAIN].get(CONF_PERSISTENCE)
def setup_gateway(device, persistence_file, baud_rate, tcp_port, in_prefix, async def setup_gateway(
out_prefix): device, persistence_file, baud_rate, tcp_port, in_prefix,
out_prefix):
"""Return gateway after setup of the gateway.""" """Return gateway after setup of the gateway."""
if device == MQTT_COMPONENT: if device == MQTT_COMPONENT:
if not setup_component(hass, MQTT_COMPONENT, config): if not await async_setup_component(hass, MQTT_COMPONENT, config):
return return None
mqtt = hass.components.mqtt mqtt = hass.components.mqtt
retain = config[DOMAIN].get(CONF_RETAIN) retain = config[DOMAIN].get(CONF_RETAIN)
def pub_callback(topic, payload, qos, retain): def pub_callback(topic, payload, qos, retain):
"""Call MQTT publish function.""" """Call MQTT publish function."""
mqtt.publish(topic, payload, qos, retain) mqtt.async_publish(topic, payload, qos, retain)
def sub_callback(topic, sub_cb, qos): def sub_callback(topic, sub_cb, qos):
"""Call MQTT subscribe function.""" """Call MQTT subscribe function."""
mqtt.subscribe(topic, sub_cb, qos) @callback
gateway = mysensors.MQTTGateway( def internal_callback(*args):
pub_callback, sub_callback, """Call callback."""
sub_cb(*args)
hass.async_add_job(
mqtt.async_subscribe(topic, internal_callback, qos))
gateway = mysensors.AsyncMQTTGateway(
pub_callback, sub_callback, in_prefix=in_prefix,
out_prefix=out_prefix, retain=retain, loop=hass.loop,
event_callback=None, persistence=persistence, event_callback=None, persistence=persistence,
persistence_file=persistence_file, persistence_file=persistence_file,
protocol_version=version, in_prefix=in_prefix, protocol_version=version)
out_prefix=out_prefix, retain=retain)
else: else:
try: try:
is_serial_port(device) await hass.async_add_job(is_serial_port, device)
gateway = mysensors.SerialGateway( gateway = mysensors.AsyncSerialGateway(
device, event_callback=None, persistence=persistence, device, baud=baud_rate, loop=hass.loop,
event_callback=None, persistence=persistence,
persistence_file=persistence_file, persistence_file=persistence_file,
protocol_version=version, baud=baud_rate) protocol_version=version)
except vol.Invalid: except vol.Invalid:
try: gateway = mysensors.AsyncTCPGateway(
socket.getaddrinfo(device, None) device, port=tcp_port, loop=hass.loop, event_callback=None,
# valid ip address persistence=persistence, persistence_file=persistence_file,
gateway = mysensors.TCPGateway( protocol_version=version)
device, event_callback=None, persistence=persistence,
persistence_file=persistence_file,
protocol_version=version, port=tcp_port)
except OSError:
# invalid ip address
return
gateway.metric = hass.config.units.is_metric gateway.metric = hass.config.units.is_metric
gateway.optimistic = config[DOMAIN].get(CONF_OPTIMISTIC) gateway.optimistic = config[DOMAIN].get(CONF_OPTIMISTIC)
gateway.device = device gateway.device = device
gateway.event_callback = gw_callback_factory(hass) gateway.event_callback = gw_callback_factory(hass)
if persistence:
def gw_start(event): await gateway.start_persistence()
"""Trigger to start of the gateway and any persistence."""
if persistence:
discover_persistent_devices(hass, gateway)
gateway.start()
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP,
lambda event: gateway.stop())
hass.bus.listen_once(EVENT_HOMEASSISTANT_START, gw_start)
return gateway return gateway
@ -357,12 +356,12 @@ def setup(hass, config):
tcp_port = gway.get(CONF_TCP_PORT) tcp_port = gway.get(CONF_TCP_PORT)
in_prefix = gway.get(CONF_TOPIC_IN_PREFIX, '') in_prefix = gway.get(CONF_TOPIC_IN_PREFIX, '')
out_prefix = gway.get(CONF_TOPIC_OUT_PREFIX, '') out_prefix = gway.get(CONF_TOPIC_OUT_PREFIX, '')
ready_gateway = setup_gateway( gateway = await setup_gateway(
device, persistence_file, baud_rate, tcp_port, in_prefix, device, persistence_file, baud_rate, tcp_port, in_prefix,
out_prefix) out_prefix)
if ready_gateway is not None: if gateway is not None:
ready_gateway.nodes_config = gway.get(CONF_NODES) gateway.nodes_config = gway.get(CONF_NODES)
gateways[id(ready_gateway)] = ready_gateway gateways[id(gateway)] = gateway
if not gateways: if not gateways:
_LOGGER.error( _LOGGER.error(
@ -371,9 +370,65 @@ def setup(hass, config):
hass.data[MYSENSORS_GATEWAYS] = gateways hass.data[MYSENSORS_GATEWAYS] = gateways
hass.async_add_job(finish_setup(hass, gateways))
return True return True
async def finish_setup(hass, gateways):
"""Load any persistent devices and platforms and start gateway."""
discover_tasks = []
start_tasks = []
for gateway in gateways.values():
discover_tasks.append(discover_persistent_devices(hass, gateway))
start_tasks.append(gw_start(hass, gateway))
if discover_tasks:
# Make sure all devices and platforms are loaded before gateway start.
await asyncio.wait(discover_tasks, loop=hass.loop)
if start_tasks:
await asyncio.wait(start_tasks, loop=hass.loop)
async def gw_start(hass, gateway):
"""Start the gateway."""
@callback
def gw_stop(event):
"""Trigger to stop the gateway."""
hass.async_add_job(gateway.stop())
await gateway.start()
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, gw_stop)
if gateway.device == 'mqtt':
# Gatways connected via mqtt doesn't send gateway ready message.
return
gateway_ready = asyncio.Future()
gateway_ready_key = MYSENSORS_GATEWAY_READY.format(id(gateway))
hass.data[gateway_ready_key] = gateway_ready
try:
with async_timeout.timeout(GATEWAY_READY_TIMEOUT, loop=hass.loop):
await gateway_ready
except asyncio.TimeoutError:
_LOGGER.warning(
"Gateway %s not ready after %s secs so continuing with setup",
gateway.device, GATEWAY_READY_TIMEOUT)
finally:
hass.data.pop(gateway_ready_key, None)
@callback
def set_gateway_ready(hass, msg):
"""Set asyncio future result if gateway is ready."""
if (msg.type != msg.gateway.const.MessageType.internal or
msg.sub_type != msg.gateway.const.Internal.I_GATEWAY_READY):
return
gateway_ready = hass.data.get(MYSENSORS_GATEWAY_READY.format(
id(msg.gateway)))
if gateway_ready is None or gateway_ready.cancelled():
return
gateway_ready.set_result(True)
def validate_child(gateway, node_id, child): def validate_child(gateway, node_id, child):
"""Validate that a child has the correct values according to schema. """Validate that a child has the correct values according to schema.
@ -431,14 +486,18 @@ def validate_child(gateway, node_id, child):
return validated return validated
@callback
def discover_mysensors_platform(hass, platform, new_devices): def discover_mysensors_platform(hass, platform, new_devices):
"""Discover a MySensors platform.""" """Discover a MySensors platform."""
discovery.load_platform( task = hass.async_add_job(discovery.async_load_platform(
hass, platform, DOMAIN, {ATTR_DEVICES: new_devices, CONF_NAME: DOMAIN}) hass, platform, DOMAIN,
{ATTR_DEVICES: new_devices, CONF_NAME: DOMAIN}))
return task
def discover_persistent_devices(hass, gateway): async def discover_persistent_devices(hass, gateway):
"""Discover platforms for devices loaded via persistence file.""" """Discover platforms for devices loaded via persistence file."""
tasks = []
new_devices = defaultdict(list) new_devices = defaultdict(list)
for node_id in gateway.sensors: for node_id in gateway.sensors:
node = gateway.sensors[node_id] node = gateway.sensors[node_id]
@ -447,7 +506,9 @@ def discover_persistent_devices(hass, gateway):
for platform, dev_ids in validated.items(): for platform, dev_ids in validated.items():
new_devices[platform].extend(dev_ids) new_devices[platform].extend(dev_ids)
for platform, dev_ids in new_devices.items(): for platform, dev_ids in new_devices.items():
discover_mysensors_platform(hass, platform, dev_ids) tasks.append(discover_mysensors_platform(hass, platform, dev_ids))
if tasks:
await asyncio.wait(tasks, loop=hass.loop)
def get_mysensors_devices(hass, domain): def get_mysensors_devices(hass, domain):
@ -459,14 +520,18 @@ def get_mysensors_devices(hass, domain):
def gw_callback_factory(hass): def gw_callback_factory(hass):
"""Return a new callback for the gateway.""" """Return a new callback for the gateway."""
@callback
def mysensors_callback(msg): def mysensors_callback(msg):
"""Handle messages from a MySensors gateway.""" """Handle messages from a MySensors gateway."""
start = timer() start = timer()
_LOGGER.debug( _LOGGER.debug(
"Node update: node %s child %s", msg.node_id, msg.child_id) "Node update: node %s child %s", msg.node_id, msg.child_id)
child = msg.gateway.sensors[msg.node_id].children.get(msg.child_id) set_gateway_ready(hass, msg)
if child is None:
try:
child = msg.gateway.sensors[msg.node_id].children[msg.child_id]
except KeyError:
_LOGGER.debug("Not a child update for node %s", msg.node_id) _LOGGER.debug("Not a child update for node %s", msg.node_id)
return return
@ -489,7 +554,7 @@ def gw_callback_factory(hass):
# Only one signal per device is needed. # Only one signal per device is needed.
# A device can have multiple platforms, ie multiple schemas. # A device can have multiple platforms, ie multiple schemas.
# FOR LATER: Add timer to not signal if another update comes in. # FOR LATER: Add timer to not signal if another update comes in.
dispatcher_send(hass, signal) async_dispatcher_send(hass, signal)
end = timer() end = timer()
if end - start > 0.1: if end - start > 0.1:
_LOGGER.debug( _LOGGER.debug(

View File

@ -12,8 +12,8 @@ import voluptuous as vol
from homeassistant.helpers.event import track_state_change from homeassistant.helpers.event import track_state_change
from homeassistant.config import load_yaml_config_file from homeassistant.config import load_yaml_config_file
from homeassistant.components.notify import ( from homeassistant.components.notify import (
ATTR_TARGET, ATTR_DATA, BaseNotificationService, DOMAIN) ATTR_TARGET, ATTR_DATA, BaseNotificationService, DOMAIN, PLATFORM_SCHEMA)
from homeassistant.const import CONF_NAME, CONF_PLATFORM from homeassistant.const import CONF_NAME, CONF_PLATFORM, ATTR_NAME
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers import template as template_helper from homeassistant.helpers import template as template_helper
@ -27,9 +27,8 @@ DEVICE_TRACKER_DOMAIN = 'device_tracker'
SERVICE_REGISTER = 'apns_register' SERVICE_REGISTER = 'apns_register'
ATTR_PUSH_ID = 'push_id' ATTR_PUSH_ID = 'push_id'
ATTR_NAME = 'name'
PLATFORM_SCHEMA = vol.Schema({ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_PLATFORM): 'apns', vol.Required(CONF_PLATFORM): 'apns',
vol.Required(CONF_NAME): cv.string, vol.Required(CONF_NAME): cv.string,
vol.Required(CONF_CERTFILE): cv.isfile, vol.Required(CONF_CERTFILE): cv.isfile,
@ -66,7 +65,7 @@ class ApnsDevice(object):
""" """
def __init__(self, push_id, name, tracking_device_id=None, disabled=False): def __init__(self, push_id, name, tracking_device_id=None, disabled=False):
"""Initialize Apns Device.""" """Initialize APNS Device."""
self.device_push_id = push_id self.device_push_id = push_id
self.device_name = name self.device_name = name
self.tracking_id = tracking_device_id self.tracking_id = tracking_device_id
@ -104,7 +103,7 @@ class ApnsDevice(object):
@property @property
def disabled(self): def disabled(self):
"""Return the .""" """Return the state of the service."""
return self.device_disabled return self.device_disabled
def disable(self): def disable(self):

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