mirror of
https://github.com/home-assistant/core.git
synced 2025-07-23 21:27:38 +00:00
commit
0700886d1a
15
.coveragerc
15
.coveragerc
@ -4,6 +4,8 @@ source = homeassistant
|
||||
omit =
|
||||
homeassistant/__main__.py
|
||||
homeassistant/scripts/*.py
|
||||
homeassistant/util/async.py
|
||||
homeassistant/monkey_patch.py
|
||||
homeassistant/helpers/typing.py
|
||||
homeassistant/helpers/signal.py
|
||||
|
||||
@ -127,7 +129,7 @@ omit =
|
||||
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/ios.py
|
||||
@ -151,6 +153,9 @@ omit =
|
||||
homeassistant/components/knx.py
|
||||
homeassistant/components/*/knx.py
|
||||
|
||||
homeassistant/components/konnected.py
|
||||
homeassistant/components/*/konnected.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/sabnzbd.py
|
||||
homeassistant/components/*/sabnzbd.py
|
||||
|
||||
homeassistant/components/satel_integra.py
|
||||
homeassistant/components/*/satel_integra.py
|
||||
|
||||
@ -342,6 +350,7 @@ omit =
|
||||
homeassistant/components/calendar/todoist.py
|
||||
homeassistant/components/camera/bloomsky.py
|
||||
homeassistant/components/camera/canary.py
|
||||
homeassistant/components/camera/familyhub.py
|
||||
homeassistant/components/camera/ffmpeg.py
|
||||
homeassistant/components/camera/foscam.py
|
||||
homeassistant/components/camera/mjpeg.py
|
||||
@ -412,7 +421,6 @@ omit =
|
||||
homeassistant/components/emoncms_history.py
|
||||
homeassistant/components/emulated_hue/upnp.py
|
||||
homeassistant/components/fan/mqtt.py
|
||||
homeassistant/components/feedreader.py
|
||||
homeassistant/components/folder_watcher.py
|
||||
homeassistant/components/foursquare.py
|
||||
homeassistant/components/goalfeed.py
|
||||
@ -534,7 +542,6 @@ omit =
|
||||
homeassistant/components/notify/rest.py
|
||||
homeassistant/components/notify/rocketchat.py
|
||||
homeassistant/components/notify/sendgrid.py
|
||||
homeassistant/components/notify/simplepush.py
|
||||
homeassistant/components/notify/slack.py
|
||||
homeassistant/components/notify/smtp.py
|
||||
homeassistant/components/notify/stride.py
|
||||
@ -592,6 +599,7 @@ omit =
|
||||
homeassistant/components/sensor/fastdotcom.py
|
||||
homeassistant/components/sensor/fedex.py
|
||||
homeassistant/components/sensor/filesize.py
|
||||
homeassistant/components/sensor/fints.py
|
||||
homeassistant/components/sensor/fitbit.py
|
||||
homeassistant/components/sensor/fixer.py
|
||||
homeassistant/components/sensor/folder.py
|
||||
@ -650,7 +658,6 @@ omit =
|
||||
homeassistant/components/sensor/radarr.py
|
||||
homeassistant/components/sensor/rainbird.py
|
||||
homeassistant/components/sensor/ripple.py
|
||||
homeassistant/components/sensor/sabnzbd.py
|
||||
homeassistant/components/sensor/scrape.py
|
||||
homeassistant/components/sensor/sense.py
|
||||
homeassistant/components/sensor/sensehat.py
|
||||
|
@ -10,8 +10,8 @@ matrix:
|
||||
env: TOXENV=lint
|
||||
- python: "3.5.3"
|
||||
env: TOXENV=pylint
|
||||
# - python: "3.5"
|
||||
# env: TOXENV=typing
|
||||
- python: "3.5.3"
|
||||
env: TOXENV=typing
|
||||
- python: "3.5.3"
|
||||
env: TOXENV=py35
|
||||
- python: "3.6"
|
||||
|
@ -94,6 +94,8 @@ homeassistant/components/*/hive.py @Rendili @KJonline
|
||||
homeassistant/components/homekit/* @cdce8p
|
||||
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/qwikswitch.py @kellerza
|
||||
|
@ -8,7 +8,8 @@ import subprocess
|
||||
import sys
|
||||
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.const import (
|
||||
@ -259,7 +260,7 @@ def setup_and_run_hass(config_dir: str,
|
||||
config = {
|
||||
'frontend': {},
|
||||
'demo': {}
|
||||
}
|
||||
} # type: Dict[str, Any]
|
||||
hass = bootstrap.from_config_dict(
|
||||
config, config_dir=config_dir, verbose=args.verbose,
|
||||
skip_pip=args.skip_pip, log_rotate_days=args.log_rotate_days,
|
||||
|
@ -15,7 +15,6 @@ from voluptuous.humanize import humanize_error
|
||||
from homeassistant import data_entry_flow, requirements
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.const import CONF_TYPE, CONF_NAME, CONF_ID
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.util.decorator import Registry
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
@ -36,23 +35,7 @@ ACCESS_TOKEN_EXPIRATION = timedelta(minutes=30)
|
||||
DATA_REQS = 'auth_reqs_processed'
|
||||
|
||||
|
||||
class AuthError(HomeAssistantError):
|
||||
"""Generic authentication error."""
|
||||
|
||||
|
||||
class InvalidUser(AuthError):
|
||||
"""Raised when an invalid user has been specified."""
|
||||
|
||||
|
||||
class InvalidPassword(AuthError):
|
||||
"""Raised when an invalid password has been supplied."""
|
||||
|
||||
|
||||
class UnknownError(AuthError):
|
||||
"""When an unknown error occurs."""
|
||||
|
||||
|
||||
def generate_secret(entropy=32):
|
||||
def generate_secret(entropy: int = 32) -> str:
|
||||
"""Generate a secret.
|
||||
|
||||
Backport of secrets.token_hex from Python 3.6
|
||||
@ -69,8 +52,9 @@ class AuthProvider:
|
||||
|
||||
initialized = False
|
||||
|
||||
def __init__(self, store, config):
|
||||
def __init__(self, hass, store, config):
|
||||
"""Initialize an auth provider."""
|
||||
self.hass = hass
|
||||
self.store = store
|
||||
self.config = config
|
||||
|
||||
@ -210,6 +194,7 @@ class Client:
|
||||
name = attr.ib(type=str)
|
||||
id = attr.ib(type=str, default=attr.Factory(lambda: uuid.uuid4().hex))
|
||||
secret = attr.ib(type=str, default=attr.Factory(generate_secret))
|
||||
redirect_uris = attr.ib(type=list, default=attr.Factory(list))
|
||||
|
||||
|
||||
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))
|
||||
return None
|
||||
|
||||
return AUTH_PROVIDERS[provider_name](store, config)
|
||||
return AUTH_PROVIDERS[provider_name](hass, store, config)
|
||||
|
||||
|
||||
class AuthManager:
|
||||
@ -340,9 +325,11 @@ class AuthManager:
|
||||
"""Get an access 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."""
|
||||
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):
|
||||
"""Get a client."""
|
||||
@ -360,6 +347,9 @@ class AuthManager:
|
||||
|
||||
async def _async_finish_login_flow(self, result):
|
||||
"""Result of a credential login flow."""
|
||||
if result['type'] != data_entry_flow.RESULT_TYPE_CREATE_ENTRY:
|
||||
return None
|
||||
|
||||
auth_provider = self._providers[result['handler']]
|
||||
return await auth_provider.async_get_or_create_credentials(
|
||||
result['data'])
|
||||
@ -477,12 +467,20 @@ class AuthStore:
|
||||
|
||||
return None
|
||||
|
||||
async def async_create_client(self, name):
|
||||
async def async_create_client(self, name, redirect_uris, no_secret):
|
||||
"""Create a new client."""
|
||||
if self.clients is None:
|
||||
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
|
||||
await self.async_save()
|
||||
return client
|
||||
|
181
homeassistant/auth_providers/homeassistant.py
Normal file
181
homeassistant/auth_providers/homeassistant.py
Normal 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,
|
||||
)
|
@ -4,6 +4,7 @@ import hmac
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant import auth, data_entry_flow
|
||||
from homeassistant.core import callback
|
||||
|
||||
@ -20,6 +21,10 @@ CONFIG_SCHEMA = auth.AUTH_PROVIDER_SCHEMA.extend({
|
||||
}, extra=vol.PREVENT_EXTRA)
|
||||
|
||||
|
||||
class InvalidAuthError(HomeAssistantError):
|
||||
"""Raised when submitting invalid authentication."""
|
||||
|
||||
|
||||
@auth.AUTH_PROVIDERS.register('insecure_example')
|
||||
class ExampleAuthProvider(auth.AuthProvider):
|
||||
"""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.
|
||||
hmac.compare_digest(password.encode('utf-8'),
|
||||
password.encode('utf-8'))
|
||||
raise auth.InvalidUser
|
||||
raise InvalidAuthError
|
||||
|
||||
if not hmac.compare_digest(user['password'].encode('utf-8'),
|
||||
password.encode('utf-8')):
|
||||
raise auth.InvalidPassword
|
||||
raise InvalidAuthError
|
||||
|
||||
async def async_get_or_create_credentials(self, flow_result):
|
||||
"""Get credentials based on the flow result."""
|
||||
username = flow_result['username']
|
||||
password = flow_result['password']
|
||||
|
||||
self.async_validate_login(username, password)
|
||||
|
||||
for credential in await self.async_credentials():
|
||||
if credential.data['username'] == username:
|
||||
@ -96,7 +98,7 @@ class LoginFlow(data_entry_flow.FlowHandler):
|
||||
try:
|
||||
self._auth_provider.async_validate_login(
|
||||
user_input['username'], user_input['password'])
|
||||
except (auth.InvalidUser, auth.InvalidPassword):
|
||||
except InvalidAuthError:
|
||||
errors['base'] = 'invalid_auth'
|
||||
|
||||
if not errors:
|
||||
|
@ -278,7 +278,8 @@ def async_enable_logging(hass: core.HomeAssistant,
|
||||
|
||||
if log_rotate_days:
|
||||
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:
|
||||
err_handler = logging.FileHandler(
|
||||
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)
|
||||
|
||||
logger = logging.getLogger('')
|
||||
logger.addHandler(async_handler)
|
||||
logger.addHandler(async_handler) # type: ignore
|
||||
logger.setLevel(logging.INFO)
|
||||
|
||||
# Save the log file location for access by other components.
|
||||
|
@ -356,7 +356,8 @@ class APIErrorLog(HomeAssistantView):
|
||||
|
||||
async def get(self, request):
|
||||
"""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):
|
||||
|
@ -144,7 +144,7 @@ class AuthProvidersView(HomeAssistantView):
|
||||
requires_auth = False
|
||||
|
||||
@verify_client
|
||||
async def get(self, request, client_id):
|
||||
async def get(self, request, client):
|
||||
"""Get available auth providers."""
|
||||
return self.json([{
|
||||
'name': provider.name,
|
||||
@ -166,8 +166,15 @@ class LoginFlowIndexView(FlowManagerIndexView):
|
||||
|
||||
# pylint: disable=arguments-differ
|
||||
@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."""
|
||||
if data['redirect_uri'] not in client.redirect_uris:
|
||||
return self.json_message('invalid redirect uri', )
|
||||
|
||||
# pylint: disable=no-value-for-parameter
|
||||
return await super().post(request)
|
||||
|
||||
@ -192,7 +199,7 @@ class LoginFlowResourceView(FlowManagerResourceView):
|
||||
# pylint: disable=arguments-differ
|
||||
@verify_client
|
||||
@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."""
|
||||
try:
|
||||
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))
|
||||
|
||||
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)
|
||||
|
||||
@ -222,7 +229,7 @@ class GrantTokenView(HomeAssistantView):
|
||||
self._retrieve_credentials = retrieve_credentials
|
||||
|
||||
@verify_client
|
||||
async def post(self, request, client_id):
|
||||
async def post(self, request, client):
|
||||
"""Grant a token."""
|
||||
hass = request.app['hass']
|
||||
data = await request.post()
|
||||
@ -230,11 +237,11 @@ class GrantTokenView(HomeAssistantView):
|
||||
|
||||
if grant_type == 'authorization_code':
|
||||
return await self._async_handle_auth_code(
|
||||
hass, client_id, data)
|
||||
hass, client.id, data)
|
||||
|
||||
elif grant_type == 'refresh_token':
|
||||
return await self._async_handle_refresh_token(
|
||||
hass, client_id, data)
|
||||
hass, client.id, data)
|
||||
|
||||
return self.json({
|
||||
'error': 'unsupported_grant_type',
|
||||
|
@ -11,15 +11,15 @@ def verify_client(method):
|
||||
@wraps(method)
|
||||
async def wrapper(view, request, *args, **kwargs):
|
||||
"""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({
|
||||
'error': 'invalid_client',
|
||||
}, status_code=401)
|
||||
|
||||
return await method(
|
||||
view, request, *args, client_id=client_id, **kwargs)
|
||||
view, request, *args, **kwargs, client=client)
|
||||
|
||||
return wrapper
|
||||
|
||||
@ -46,18 +46,34 @@ async def _verify_client(request):
|
||||
client_id, client_secret = decoded.split(':', 1)
|
||||
except ValueError:
|
||||
# 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:
|
||||
# Still do a compare so we run same time as if a client was found.
|
||||
hmac.compare_digest(client_secret.encode('utf-8'),
|
||||
client_secret.encode('utf-8'))
|
||||
if client_secret is not None:
|
||||
# Still do a compare so we run same time as if a client was found.
|
||||
hmac.compare_digest(client_secret.encode('utf-8'),
|
||||
client_secret.encode('utf-8'))
|
||||
return None
|
||||
|
||||
if hmac.compare_digest(client_secret.encode('utf-8'),
|
||||
client.secret.encode('utf-8')):
|
||||
return client_id
|
||||
if client.secret is None:
|
||||
return client
|
||||
|
||||
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
|
||||
|
@ -217,4 +217,4 @@ class BayesianBinarySensor(BinarySensorDevice):
|
||||
@asyncio.coroutine
|
||||
def async_update(self):
|
||||
"""Get the latest data and update the states."""
|
||||
self._deviation = bool(self.probability > self._probability_threshold)
|
||||
self._deviation = bool(self.probability >= self._probability_threshold)
|
||||
|
@ -17,9 +17,19 @@ _LOGGER = logging.getLogger(__name__)
|
||||
SENSOR_TYPES = {
|
||||
'lids': ['Doors', '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):
|
||||
"""Set up the BMW sensors."""
|
||||
@ -29,10 +39,18 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
devices = []
|
||||
for account in accounts:
|
||||
for vehicle in account.account.vehicles:
|
||||
for key, value in sorted(SENSOR_TYPES.items()):
|
||||
device = BMWConnectedDriveSensor(account, vehicle, key,
|
||||
value[0], value[1])
|
||||
devices.append(device)
|
||||
if vehicle.has_hv_battery:
|
||||
_LOGGER.debug('BMW with a high voltage battery')
|
||||
for key, value in sorted(SENSOR_TYPES_ELEC.items()):
|
||||
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)
|
||||
|
||||
|
||||
@ -92,12 +110,34 @@ class BMWConnectedDriveSensor(BinarySensorDevice):
|
||||
result[window.name] = window.state.value
|
||||
elif self._attribute == 'door_lock_state':
|
||||
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):
|
||||
"""Read new state data from the library."""
|
||||
from bimmer_connected.state import LockState
|
||||
from bimmer_connected.state import ChargingState
|
||||
vehicle_state = self._vehicle.state
|
||||
|
||||
# device class opening: On means open, Off means closed
|
||||
@ -111,6 +151,37 @@ class BMWConnectedDriveSensor(BinarySensorDevice):
|
||||
# Possible values: LOCKED, SECURED, SELECTIVE_LOCKED, UNLOCKED
|
||||
self._state = vehicle_state.door_lock_state not in \
|
||||
[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):
|
||||
"""Schedule a state update."""
|
||||
|
85
homeassistant/components/binary_sensor/homematicip_cloud.py
Normal file
85
homeassistant/components/binary_sensor/homematicip_cloud.py
Normal 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
|
@ -117,8 +117,10 @@ class ISYBinarySensorDevice(ISYDevice, BinarySensorDevice):
|
||||
# pylint: disable=protected-access
|
||||
if _is_val_unknown(self._node.status._val):
|
||||
self._computed_state = None
|
||||
self._status_was_unknown = True
|
||||
else:
|
||||
self._computed_state = bool(self._node.status._val)
|
||||
self._status_was_unknown = False
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_added_to_hass(self) -> None:
|
||||
@ -156,9 +158,13 @@ class ISYBinarySensorDevice(ISYDevice, BinarySensorDevice):
|
||||
# pylint: disable=protected-access
|
||||
if not _is_val_unknown(self._negative_node.status._val):
|
||||
# If the negative node has a value, it means the negative node is
|
||||
# in use for this device. Therefore, we cannot determine the state
|
||||
# of the sensor until we receive our first ON event.
|
||||
self._computed_state = None
|
||||
# in use for this device. Next we need to check to see if the
|
||||
# negative and positive nodes disagree on the state (both ON or
|
||||
# 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:
|
||||
"""Handle an "On" control event from the "negative" node."""
|
||||
@ -189,14 +195,21 @@ class ISYBinarySensorDevice(ISYDevice, BinarySensorDevice):
|
||||
self.schedule_update_ha_state()
|
||||
self._heartbeat()
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
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
|
||||
device.
|
||||
We MOSTLY ignore these updates, as we listen directly to the Control
|
||||
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
|
||||
def value(self) -> object:
|
||||
|
82
homeassistant/components/binary_sensor/konnected.py
Normal file
82
homeassistant/components/binary_sensor/konnected.py
Normal 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()
|
@ -31,7 +31,8 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
sensors = []
|
||||
hub = hass.data[MYCHEVY_DOMAIN]
|
||||
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)
|
||||
|
||||
@ -45,16 +46,18 @@ class EVBinarySensor(BinarySensorDevice):
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, connection, config):
|
||||
def __init__(self, connection, config, car_vid):
|
||||
"""Initialize sensor with car connection."""
|
||||
self._conn = connection
|
||||
self._name = config.name
|
||||
self._attr = config.attr
|
||||
self._type = config.device_class
|
||||
self._is_on = None
|
||||
|
||||
self._car_vid = car_vid
|
||||
self.entity_id = ENTITY_ID_FORMAT.format(
|
||||
'{}_{}'.format(MYCHEVY_DOMAIN, slugify(self._name)))
|
||||
'{}_{}_{}'.format(MYCHEVY_DOMAIN,
|
||||
slugify(self._car.name),
|
||||
slugify(self._name)))
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
@ -66,6 +69,11 @@ class EVBinarySensor(BinarySensorDevice):
|
||||
"""Return if on."""
|
||||
return self._is_on
|
||||
|
||||
@property
|
||||
def _car(self):
|
||||
"""Return the car."""
|
||||
return self._conn.get_car(self._car_vid)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_added_to_hass(self):
|
||||
"""Register callbacks."""
|
||||
@ -75,8 +83,8 @@ class EVBinarySensor(BinarySensorDevice):
|
||||
@callback
|
||||
def async_update_callback(self):
|
||||
"""Update state."""
|
||||
if self._conn.car is not None:
|
||||
self._is_on = getattr(self._conn.car, self._attr, None)
|
||||
if self._car is not None:
|
||||
self._is_on = getattr(self._car, self._attr, None)
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
@property
|
||||
|
@ -10,7 +10,7 @@ import voluptuous as vol
|
||||
|
||||
from homeassistant.components import rfxtrx
|
||||
from homeassistant.components.binary_sensor import (
|
||||
PLATFORM_SCHEMA, DEVICE_CLASSES_SCHEMA, BinarySensorDevice)
|
||||
DEVICE_CLASSES_SCHEMA, PLATFORM_SCHEMA, BinarySensorDevice)
|
||||
from homeassistant.components.rfxtrx import (
|
||||
ATTR_NAME, CONF_AUTOMATIC_ADD, CONF_DATA_BITS, CONF_DEVICES,
|
||||
CONF_FIRE_EVENT, CONF_OFF_DELAY)
|
||||
@ -29,8 +29,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_DEVICES, default={}): {
|
||||
cv.string: vol.Schema({
|
||||
vol.Optional(CONF_NAME): cv.string,
|
||||
vol.Optional(CONF_DEVICE_CLASS):
|
||||
DEVICE_CLASSES_SCHEMA,
|
||||
vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA,
|
||||
vol.Optional(CONF_FIRE_EVENT, default=False): cv.boolean,
|
||||
vol.Optional(CONF_OFF_DELAY):
|
||||
vol.Any(cv.time_period, cv.positive_timedelta),
|
||||
|
@ -53,7 +53,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
'channel_1', hass, gateway))
|
||||
devices.append(XiaomiButton(device, 'Wall Switch (Both)',
|
||||
'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))
|
||||
add_devices(devices)
|
||||
|
||||
|
@ -108,7 +108,7 @@ class BinarySensor(zha.Entity, BinarySensorDevice):
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
"""Return True if entity is on."""
|
||||
if self._state == 'unknown':
|
||||
if self._state is None:
|
||||
return False
|
||||
return bool(self._state)
|
||||
|
||||
@ -133,7 +133,8 @@ class BinarySensor(zha.Entity, BinarySensorDevice):
|
||||
from bellows.types.basic import uint16_t
|
||||
|
||||
result = await zha.safe_read(self._endpoint.ias_zone,
|
||||
['zone_status'])
|
||||
['zone_status'],
|
||||
allow_cache=False)
|
||||
state = result.get('zone_status', self._state)
|
||||
if isinstance(state, (int, uint16_t)):
|
||||
self._state = result.get('zone_status', self._state) & 3
|
||||
@ -218,7 +219,10 @@ class Switch(zha.Entity, BinarySensorDevice):
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""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):
|
||||
"""Increment the level, setting state if appropriate."""
|
||||
|
@ -14,7 +14,7 @@ from homeassistant.helpers import discovery
|
||||
from homeassistant.helpers.event import track_utc_time_change
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['bimmer_connected==0.5.0']
|
||||
REQUIREMENTS = ['bimmer_connected==0.5.1']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
@ -27,7 +27,7 @@ activate_air_conditioning:
|
||||
description: >
|
||||
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
|
||||
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).
|
||||
fields:
|
||||
vin:
|
||||
@ -39,4 +39,4 @@ update_state:
|
||||
description: >
|
||||
Fetch the last state of the vehicles of all your accounts from the BMW
|
||||
server. This does *not* trigger an update from the vehicle, it just gets
|
||||
the data from the BMW servers. This service does not require any attributes.
|
||||
the data from the BMW servers. This service does not require any attributes.
|
||||
|
@ -256,6 +256,11 @@ class Camera(Entity):
|
||||
"""Return the camera model."""
|
||||
return None
|
||||
|
||||
@property
|
||||
def frame_interval(self):
|
||||
"""Return the interval between frames of the mjpeg stream."""
|
||||
return 0.5
|
||||
|
||||
def camera_image(self):
|
||||
"""Return bytes of camera image."""
|
||||
raise NotImplementedError()
|
||||
@ -272,10 +277,6 @@ class Camera(Entity):
|
||||
|
||||
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.content_type = ('multipart/x-mixed-replace; '
|
||||
'boundary=--frameboundary')
|
||||
@ -325,8 +326,7 @@ class Camera(Entity):
|
||||
a direct stream from the camera.
|
||||
This method must be run in the event loop.
|
||||
"""
|
||||
await self.handle_async_still_stream(request,
|
||||
FALLBACK_STREAM_INTERVAL)
|
||||
await self.handle_async_still_stream(request, self.frame_interval)
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
@ -448,6 +448,9 @@ class CameraMjpegStream(CameraView):
|
||||
try:
|
||||
# Compose camera stream from stills
|
||||
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)
|
||||
return
|
||||
except ValueError:
|
||||
|
58
homeassistant/components/camera/familyhub.py
Normal file
58
homeassistant/components/camera/familyhub.py
Normal 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
|
@ -28,6 +28,7 @@ _LOGGER = logging.getLogger(__name__)
|
||||
CONF_CONTENT_TYPE = 'content_type'
|
||||
CONF_LIMIT_REFETCH_TO_URL_CHANGE = 'limit_refetch_to_url_change'
|
||||
CONF_STILL_IMAGE_URL = 'still_image_url'
|
||||
CONF_FRAMERATE = 'framerate'
|
||||
|
||||
DEFAULT_NAME = 'Generic Camera'
|
||||
|
||||
@ -40,6 +41,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_PASSWORD): cv.string,
|
||||
vol.Optional(CONF_USERNAME): 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.hass = hass
|
||||
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]
|
||||
|
||||
username = device_info.get(CONF_USERNAME)
|
||||
@ -78,6 +81,11 @@ class GenericCamera(Camera):
|
||||
self._last_url = 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):
|
||||
"""Return bytes of camera image."""
|
||||
return run_coroutine_threadsafe(
|
||||
|
@ -115,7 +115,7 @@ class MySensorsHVAC(mysensors.MySensorsEntity, ClimateDevice):
|
||||
"""List of available fan modes."""
|
||||
return ['Auto', 'Min', 'Normal', 'Max']
|
||||
|
||||
def set_temperature(self, **kwargs):
|
||||
async def async_set_temperature(self, **kwargs):
|
||||
"""Set new target temperature."""
|
||||
set_req = self.gateway.const.SetReq
|
||||
temp = kwargs.get(ATTR_TEMPERATURE)
|
||||
@ -143,9 +143,9 @@ class MySensorsHVAC(mysensors.MySensorsEntity, ClimateDevice):
|
||||
if self.gateway.optimistic:
|
||||
# Optimistically assume that device has changed state
|
||||
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_req = self.gateway.const.SetReq
|
||||
self.gateway.set_child_value(
|
||||
@ -153,9 +153,9 @@ class MySensorsHVAC(mysensors.MySensorsEntity, ClimateDevice):
|
||||
if self.gateway.optimistic:
|
||||
# Optimistically assume that device has changed state
|
||||
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."""
|
||||
self.gateway.set_child_value(
|
||||
self.node_id, self.child_id, self.value_type,
|
||||
@ -163,7 +163,7 @@ class MySensorsHVAC(mysensors.MySensorsEntity, ClimateDevice):
|
||||
if self.gateway.optimistic:
|
||||
# Optimistically assume that device has changed state
|
||||
self._values[self.value_type] = operation_mode
|
||||
self.schedule_update_ha_state()
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
async def async_update(self):
|
||||
"""Update the controller with the latest value from a sensor."""
|
||||
|
@ -25,7 +25,7 @@ from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.util.temperature import convert as convert_temperature
|
||||
|
||||
REQUIREMENTS = ['pysensibo==1.0.2']
|
||||
REQUIREMENTS = ['pysensibo==1.0.3']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
@ -11,9 +11,11 @@ import voluptuous as vol
|
||||
from homeassistant.components.climate import (
|
||||
ATTR_OPERATION_MODE, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW,
|
||||
PLATFORM_SCHEMA, STATE_AUTO, STATE_COOL, STATE_HEAT, SUPPORT_FAN_MODE,
|
||||
SUPPORT_OPERATION_MODE, SUPPORT_TARGET_HUMIDITY,
|
||||
SUPPORT_TARGET_TEMPERATURE, SUPPORT_TARGET_TEMPERATURE_HIGH,
|
||||
SUPPORT_TARGET_TEMPERATURE_LOW, ClimateDevice)
|
||||
SUPPORT_OPERATION_MODE, SUPPORT_TARGET_HUMIDITY, SUPPORT_AWAY_MODE,
|
||||
SUPPORT_TARGET_HUMIDITY_HIGH, SUPPORT_TARGET_HUMIDITY_LOW,
|
||||
SUPPORT_HOLD_MODE, SUPPORT_TARGET_TEMPERATURE,
|
||||
SUPPORT_TARGET_TEMPERATURE_HIGH, SUPPORT_TARGET_TEMPERATURE_LOW,
|
||||
ClimateDevice)
|
||||
from homeassistant.const import (
|
||||
ATTR_TEMPERATURE, CONF_HOST, CONF_PASSWORD, CONF_SSL, CONF_TIMEOUT,
|
||||
CONF_USERNAME, PRECISION_WHOLE, STATE_OFF, STATE_ON, TEMP_CELSIUS,
|
||||
@ -27,14 +29,20 @@ _LOGGER = logging.getLogger(__name__)
|
||||
ATTR_FAN_STATE = 'fan_state'
|
||||
ATTR_HVAC_STATE = 'hvac_state'
|
||||
|
||||
CONF_HUMIDIFIER = 'humidifier'
|
||||
|
||||
DEFAULT_SSL = False
|
||||
|
||||
VALID_FAN_STATES = [STATE_ON, 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({
|
||||
vol.Required(CONF_HOST): 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_TIMEOUT, default=5):
|
||||
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)
|
||||
host = config.get(CONF_HOST)
|
||||
timeout = config.get(CONF_TIMEOUT)
|
||||
humidifier = config.get(CONF_HUMIDIFIER)
|
||||
|
||||
if config.get(CONF_SSL):
|
||||
proto = 'https'
|
||||
@ -60,15 +69,16 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
addr=host, timeout=timeout, user=username, password=password,
|
||||
proto=proto)
|
||||
|
||||
add_devices([VenstarThermostat(client)], True)
|
||||
add_devices([VenstarThermostat(client, humidifier)], True)
|
||||
|
||||
|
||||
class VenstarThermostat(ClimateDevice):
|
||||
"""Representation of a Venstar thermostat."""
|
||||
|
||||
def __init__(self, client):
|
||||
def __init__(self, client, humidifier):
|
||||
"""Initialize the thermostat."""
|
||||
self._client = client
|
||||
self._humidifier = humidifier
|
||||
|
||||
def update(self):
|
||||
"""Update the data from the thermostat."""
|
||||
@ -81,14 +91,18 @@ class VenstarThermostat(ClimateDevice):
|
||||
def supported_features(self):
|
||||
"""Return the list of supported features."""
|
||||
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:
|
||||
features |= (SUPPORT_TARGET_TEMPERATURE_HIGH |
|
||||
SUPPORT_TARGET_TEMPERATURE_LOW)
|
||||
|
||||
if self._client.hum_active == 1:
|
||||
features |= SUPPORT_TARGET_HUMIDITY
|
||||
if (self._humidifier and
|
||||
hasattr(self._client, 'hum_active')):
|
||||
features |= (SUPPORT_TARGET_HUMIDITY |
|
||||
SUPPORT_TARGET_HUMIDITY_HIGH |
|
||||
SUPPORT_TARGET_HUMIDITY_LOW)
|
||||
|
||||
return features
|
||||
|
||||
@ -197,6 +211,18 @@ class VenstarThermostat(ClimateDevice):
|
||||
"""Return the maximum humidity. Hardcoded to 60 in API."""
|
||||
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):
|
||||
"""Change the operation mode (internal)."""
|
||||
if operation_mode == STATE_HEAT:
|
||||
@ -259,3 +285,30 @@ class VenstarThermostat(ClimateDevice):
|
||||
|
||||
if not success:
|
||||
_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")
|
||||
|
@ -190,7 +190,7 @@ class WinkThermostat(WinkDevice, ClimateDevice):
|
||||
@property
|
||||
def cool_on(self):
|
||||
"""Return whether or not the heat is actually heating."""
|
||||
return self.wink.heat_on()
|
||||
return self.wink.cool_on()
|
||||
|
||||
@property
|
||||
def current_operation(self):
|
||||
|
@ -96,6 +96,7 @@ async def async_setup(hass, config):
|
||||
async def process(service):
|
||||
"""Parse text into commands."""
|
||||
text = service.data[ATTR_TEXT]
|
||||
_LOGGER.debug('Processing: <%s>', text)
|
||||
try:
|
||||
await _process(hass, text)
|
||||
except intent.IntentHandleError as err:
|
10
homeassistant/components/conversation/services.yaml
Normal file
10
homeassistant/components/conversation/services.yaml
Normal 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
|
||||
|
||||
|
@ -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
|
||||
https://home-assistant.io/components/cover.gogogate2/
|
||||
@ -15,7 +15,7 @@ from homeassistant.const import (
|
||||
CONF_IP_ADDRESS, CONF_NAME)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['pygogogate2==0.0.7']
|
||||
REQUIREMENTS = ['pygogogate2==0.1.1']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@ -25,9 +25,9 @@ NOTIFICATION_ID = 'gogogate2_notification'
|
||||
NOTIFICATION_TITLE = 'Gogogate2 Cover Setup'
|
||||
|
||||
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_PASSWORD): cv.string,
|
||||
vol.Required(CONF_USERNAME): 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."""
|
||||
from pygogogate2 import Gogogate2API as pygogogate2
|
||||
|
||||
username = config.get(CONF_USERNAME)
|
||||
password = config.get(CONF_PASSWORD)
|
||||
ip_address = config.get(CONF_IP_ADDRESS)
|
||||
name = config.get(CONF_NAME)
|
||||
password = config.get(CONF_PASSWORD)
|
||||
username = config.get(CONF_USERNAME)
|
||||
|
||||
mygogogate2 = pygogogate2(username, password, ip_address)
|
||||
|
||||
try:
|
||||
|
@ -42,7 +42,7 @@ class MySensorsCover(mysensors.MySensorsEntity, CoverDevice):
|
||||
set_req = self.gateway.const.SetReq
|
||||
return self._values.get(set_req.V_DIMMER)
|
||||
|
||||
def open_cover(self, **kwargs):
|
||||
async def async_open_cover(self, **kwargs):
|
||||
"""Move the cover up."""
|
||||
set_req = self.gateway.const.SetReq
|
||||
self.gateway.set_child_value(
|
||||
@ -53,9 +53,9 @@ class MySensorsCover(mysensors.MySensorsEntity, CoverDevice):
|
||||
self._values[set_req.V_DIMMER] = 100
|
||||
else:
|
||||
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."""
|
||||
set_req = self.gateway.const.SetReq
|
||||
self.gateway.set_child_value(
|
||||
@ -66,9 +66,9 @@ class MySensorsCover(mysensors.MySensorsEntity, CoverDevice):
|
||||
self._values[set_req.V_DIMMER] = 0
|
||||
else:
|
||||
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."""
|
||||
position = kwargs.get(ATTR_POSITION)
|
||||
set_req = self.gateway.const.SetReq
|
||||
@ -77,9 +77,9 @@ class MySensorsCover(mysensors.MySensorsEntity, CoverDevice):
|
||||
if self.gateway.optimistic:
|
||||
# Optimistically assume that cover has changed state.
|
||||
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."""
|
||||
set_req = self.gateway.const.SetReq
|
||||
self.gateway.set_child_value(
|
||||
|
@ -79,7 +79,9 @@ class TahomaCover(TahomaDevice, CoverDevice):
|
||||
if self.tahoma_device.type == \
|
||||
'io:RollerShutterWithLowSpeedManagementIOComponent':
|
||||
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')
|
||||
else:
|
||||
self.apply_action('stopIdentify')
|
||||
|
@ -22,7 +22,7 @@ from .const import (
|
||||
CONFIG_FILE, DATA_DECONZ_EVENT, DATA_DECONZ_ID,
|
||||
DATA_DECONZ_UNSUB, DOMAIN, _LOGGER)
|
||||
|
||||
REQUIREMENTS = ['pydeconz==37']
|
||||
REQUIREMENTS = ['pydeconz==38']
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.Schema({
|
||||
|
@ -33,7 +33,7 @@ from homeassistant.helpers.event import async_track_utc_time_change
|
||||
from homeassistant.const import (
|
||||
ATTR_GPS_ACCURACY, ATTR_LATITUDE, ATTR_LONGITUDE, CONF_NAME, CONF_MAC,
|
||||
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__)
|
||||
|
||||
@ -71,7 +71,6 @@ ATTR_GPS = 'gps'
|
||||
ATTR_HOST_NAME = 'host_name'
|
||||
ATTR_LOCATION_NAME = 'location_name'
|
||||
ATTR_MAC = 'mac'
|
||||
ATTR_NAME = 'name'
|
||||
ATTR_SOURCE_TYPE = 'source_type'
|
||||
ATTR_CONSIDER_HOME = 'consider_home'
|
||||
|
||||
|
@ -12,14 +12,18 @@ import voluptuous as vol
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.components.device_tracker import (
|
||||
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.typing import ConfigType
|
||||
from homeassistant.util import slugify
|
||||
|
||||
REQUIREMENTS = ['locationsharinglib==2.0.2']
|
||||
|
||||
_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'
|
||||
|
||||
@ -60,19 +64,23 @@ class GoogleMapsScanner(object):
|
||||
self.success_init = True
|
||||
|
||||
except InvalidUser:
|
||||
_LOGGER.error('You have specified invalid login credentials')
|
||||
_LOGGER.error("You have specified invalid login credentials")
|
||||
self.success_init = False
|
||||
|
||||
def _update_info(self, now=None):
|
||||
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 = {
|
||||
'id': person.id,
|
||||
'nickname': person.nickname,
|
||||
'full_name': person.full_name,
|
||||
'last_seen': person.datetime,
|
||||
'address': person.address
|
||||
ATTR_ADDRESS: person.address,
|
||||
ATTR_FULL_NAME: person.full_name,
|
||||
ATTR_ID: person.id,
|
||||
ATTR_LAST_SEEN: person.datetime,
|
||||
ATTR_NICKNAME: person.nickname,
|
||||
}
|
||||
self.see(
|
||||
dev_id=dev_id,
|
||||
@ -80,5 +88,5 @@ class GoogleMapsScanner(object):
|
||||
picture=person.picture_url,
|
||||
source_type=SOURCE_TYPE_GPS,
|
||||
gps_accuracy=person.accuracy,
|
||||
attributes=attrs
|
||||
attributes=attrs,
|
||||
)
|
||||
|
@ -14,15 +14,18 @@ import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.components.device_tracker import (
|
||||
DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
|
||||
from homeassistant.const import (
|
||||
CONF_HOST, CONF_PASSWORD, CONF_USERNAME
|
||||
CONF_HOST, CONF_PASSWORD, CONF_USERNAME, CONF_TYPE
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_TYPE = "rogers"
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_HOST): 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._password = config.get(CONF_PASSWORD)
|
||||
|
||||
if config.get(CONF_TYPE) == "shaw":
|
||||
self._type = 'pwd'
|
||||
else:
|
||||
self._type = 'pws'
|
||||
|
||||
self._userid = None
|
||||
|
||||
self.success_init = self._update_info()
|
||||
@ -74,7 +82,7 @@ class HitronCODADeviceScanner(DeviceScanner):
|
||||
try:
|
||||
data = [
|
||||
('user', self._username),
|
||||
('pws', self._password),
|
||||
(self._type, self._password),
|
||||
]
|
||||
res = requests.post(self._loginurl, data=data, timeout=10)
|
||||
except requests.exceptions.Timeout:
|
||||
|
@ -24,8 +24,9 @@ _LOGGER = logging.getLogger(__name__)
|
||||
|
||||
REQUIREMENTS = ['pyicloud==0.9.1']
|
||||
|
||||
CONF_IGNORED_DEVICES = 'ignored_devices'
|
||||
CONF_ACCOUNTNAME = 'account_name'
|
||||
CONF_MAX_INTERVAL = 'max_interval'
|
||||
CONF_GPS_ACCURACY_THRESHOLD = 'gps_accuracy_threshold'
|
||||
|
||||
# entity attributes
|
||||
ATTR_ACCOUNTNAME = 'account_name'
|
||||
@ -64,13 +65,15 @@ DEVICESTATUSCODES = {
|
||||
SERVICE_SCHEMA = vol.Schema({
|
||||
vol.Optional(ATTR_ACCOUNTNAME): vol.All(cv.ensure_list, [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({
|
||||
vol.Required(CONF_USERNAME): cv.string,
|
||||
vol.Required(CONF_PASSWORD): cv.string,
|
||||
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)
|
||||
password = config.get(CONF_PASSWORD)
|
||||
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:
|
||||
ICLOUDTRACKERS[account] = icloudaccount
|
||||
@ -96,6 +102,7 @@ def setup_scanner(hass, config: dict, see, discovery_info=None):
|
||||
for account in accounts:
|
||||
if account in ICLOUDTRACKERS:
|
||||
ICLOUDTRACKERS[account].lost_iphone(devicename)
|
||||
|
||||
hass.services.register(DOMAIN, 'icloud_lost_iphone', lost_iphone,
|
||||
schema=SERVICE_SCHEMA)
|
||||
|
||||
@ -106,6 +113,7 @@ def setup_scanner(hass, config: dict, see, discovery_info=None):
|
||||
for account in accounts:
|
||||
if account in ICLOUDTRACKERS:
|
||||
ICLOUDTRACKERS[account].update_icloud(devicename)
|
||||
|
||||
hass.services.register(DOMAIN, 'icloud_update', update_icloud,
|
||||
schema=SERVICE_SCHEMA)
|
||||
|
||||
@ -115,6 +123,7 @@ def setup_scanner(hass, config: dict, see, discovery_info=None):
|
||||
for account in accounts:
|
||||
if account in ICLOUDTRACKERS:
|
||||
ICLOUDTRACKERS[account].reset_account_icloud()
|
||||
|
||||
hass.services.register(DOMAIN, 'icloud_reset_account',
|
||||
reset_account_icloud, schema=SERVICE_SCHEMA)
|
||||
|
||||
@ -137,7 +146,8 @@ def setup_scanner(hass, config: dict, see, discovery_info=None):
|
||||
class Icloud(DeviceScanner):
|
||||
"""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."""
|
||||
self.hass = hass
|
||||
self.username = username
|
||||
@ -148,6 +158,8 @@ class Icloud(DeviceScanner):
|
||||
self.seen_devices = {}
|
||||
self._overridestates = {}
|
||||
self._intervals = {}
|
||||
self._max_interval = max_interval
|
||||
self._gps_accuracy_threshold = gps_accuracy_threshold
|
||||
self.see = see
|
||||
|
||||
self._trusted_device = None
|
||||
@ -348,7 +360,7 @@ class Icloud(DeviceScanner):
|
||||
self._overridestates[devicename] = None
|
||||
|
||||
if currentzone is not None:
|
||||
self._intervals[devicename] = 30
|
||||
self._intervals[devicename] = self._max_interval
|
||||
return
|
||||
|
||||
if mindistance is None:
|
||||
@ -363,7 +375,6 @@ class Icloud(DeviceScanner):
|
||||
|
||||
if interval > 180:
|
||||
# Three hour drive? This is far enough that they might be flying
|
||||
# home - check every half hour
|
||||
interval = 30
|
||||
|
||||
if battery is not None and battery <= 33 and mindistance > 3:
|
||||
@ -403,22 +414,24 @@ class Icloud(DeviceScanner):
|
||||
status = device.status(DEVICESTATUSSET)
|
||||
battery = status.get('batteryLevel', 0) * 100
|
||||
location = status['location']
|
||||
if location:
|
||||
self.determine_interval(
|
||||
devicename, location['latitude'],
|
||||
location['longitude'], battery)
|
||||
interval = self._intervals.get(devicename, 1)
|
||||
attrs[ATTR_INTERVAL] = interval
|
||||
accuracy = location['horizontalAccuracy']
|
||||
kwargs['dev_id'] = dev_id
|
||||
kwargs['host_name'] = status['name']
|
||||
kwargs['gps'] = (location['latitude'],
|
||||
location['longitude'])
|
||||
kwargs['battery'] = battery
|
||||
kwargs['gps_accuracy'] = accuracy
|
||||
kwargs[ATTR_ATTRIBUTES] = attrs
|
||||
self.see(**kwargs)
|
||||
self.seen_devices[devicename] = True
|
||||
if location and location['horizontalAccuracy']:
|
||||
horizontal_accuracy = int(location['horizontalAccuracy'])
|
||||
if horizontal_accuracy < self._gps_accuracy_threshold:
|
||||
self.determine_interval(
|
||||
devicename, location['latitude'],
|
||||
location['longitude'], battery)
|
||||
interval = self._intervals.get(devicename, 1)
|
||||
attrs[ATTR_INTERVAL] = interval
|
||||
accuracy = location['horizontalAccuracy']
|
||||
kwargs['dev_id'] = dev_id
|
||||
kwargs['host_name'] = status['name']
|
||||
kwargs['gps'] = (location['latitude'],
|
||||
location['longitude'])
|
||||
kwargs['battery'] = battery
|
||||
kwargs['gps_accuracy'] = accuracy
|
||||
kwargs[ATTR_ATTRIBUTES] = attrs
|
||||
self.see(**kwargs)
|
||||
self.seen_devices[devicename] = True
|
||||
except PyiCloudNoDevicesException:
|
||||
_LOGGER.error("No iCloud Devices found")
|
||||
|
||||
@ -434,7 +447,7 @@ class Icloud(DeviceScanner):
|
||||
device.play_sound()
|
||||
|
||||
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
|
||||
|
||||
if self.api is None:
|
||||
@ -443,13 +456,13 @@ class Icloud(DeviceScanner):
|
||||
try:
|
||||
if devicename is not None:
|
||||
if devicename in self.devices:
|
||||
self.devices[devicename].location()
|
||||
self.update_device(devicename)
|
||||
else:
|
||||
_LOGGER.error("devicename %s unknown for account %s",
|
||||
devicename, self._attrs[ATTR_ACCOUNTNAME])
|
||||
else:
|
||||
for device in self.devices:
|
||||
self.devices[device].location()
|
||||
self.update_device(device)
|
||||
except PyiCloudNoDevicesException:
|
||||
_LOGGER.error("No iCloud Devices found")
|
||||
|
||||
|
@ -37,8 +37,10 @@ SERVICE_WINK = 'wink'
|
||||
SERVICE_XIAOMI_GW = 'xiaomi_gw'
|
||||
SERVICE_TELLDUSLIVE = 'tellstick'
|
||||
SERVICE_HUE = 'philips_hue'
|
||||
SERVICE_KONNECTED = 'konnected'
|
||||
SERVICE_DECONZ = 'deconz'
|
||||
SERVICE_DAIKIN = 'daikin'
|
||||
SERVICE_SABNZBD = 'sabnzbd'
|
||||
SERVICE_SAMSUNG_PRINTER = 'samsung_printer'
|
||||
SERVICE_HOMEKIT = 'homekit'
|
||||
|
||||
@ -59,7 +61,9 @@ SERVICE_HANDLERS = {
|
||||
SERVICE_XIAOMI_GW: ('xiaomi_aqara', None),
|
||||
SERVICE_TELLDUSLIVE: ('tellduslive', None),
|
||||
SERVICE_DAIKIN: ('daikin', None),
|
||||
SERVICE_SABNZBD: ('sabnzbd', None),
|
||||
SERVICE_SAMSUNG_PRINTER: ('sensor', 'syncthru'),
|
||||
SERVICE_KONNECTED: ('konnected', None),
|
||||
'google_cast': ('media_player', 'cast'),
|
||||
'panasonic_viera': ('media_player', 'panasonic_viera'),
|
||||
'plex_mediaserver': ('media_player', 'plex'),
|
||||
@ -74,7 +78,6 @@ SERVICE_HANDLERS = {
|
||||
'frontier_silicon': ('media_player', 'frontier_silicon'),
|
||||
'openhome': ('media_player', 'openhome'),
|
||||
'harmony': ('remote', 'harmony'),
|
||||
'sabnzbd': ('sensor', 'sabnzbd'),
|
||||
'bose_soundtouch': ('media_player', 'soundtouch'),
|
||||
'bluesound': ('media_player', 'bluesound'),
|
||||
'songpal': ('media_player', 'songpal'),
|
||||
@ -190,6 +193,7 @@ def _discover(netdisco):
|
||||
for disc in netdisco.discover():
|
||||
for service in netdisco.get_info(disc):
|
||||
results.append((disc, service))
|
||||
|
||||
finally:
|
||||
netdisco.stop()
|
||||
|
||||
|
@ -15,7 +15,7 @@ from homeassistant.helpers import discovery
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
|
||||
REQUIREMENTS = ['lakeside==0.5']
|
||||
REQUIREMENTS = ['lakeside==0.6']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
@ -51,8 +51,8 @@ set_direction:
|
||||
description: Name(s) of the entities to toggle
|
||||
example: 'fan.living_room'
|
||||
direction:
|
||||
description: The direction to rotate
|
||||
example: 'left'
|
||||
description: The direction to rotate. Either 'forward' or 'reverse'
|
||||
example: 'forward'
|
||||
|
||||
dyson_set_night_mode:
|
||||
description: Set the fan in night mode.
|
||||
|
@ -18,11 +18,10 @@ from homeassistant.exceptions import TemplateError
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.config_validation import PLATFORM_SCHEMA
|
||||
from homeassistant.helpers.entity import ToggleEntity
|
||||
from homeassistant.components.fan import (SPEED_LOW, SPEED_MEDIUM,
|
||||
SPEED_HIGH, SUPPORT_SET_SPEED,
|
||||
SUPPORT_OSCILLATE, FanEntity,
|
||||
ATTR_SPEED, ATTR_OSCILLATING,
|
||||
ENTITY_ID_FORMAT)
|
||||
from homeassistant.components.fan import (
|
||||
SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH, SUPPORT_SET_SPEED, SUPPORT_OSCILLATE,
|
||||
FanEntity, ATTR_SPEED, ATTR_OSCILLATING, ENTITY_ID_FORMAT,
|
||||
SUPPORT_DIRECTION, DIRECTION_FORWARD, DIRECTION_REVERSE, ATTR_DIRECTION)
|
||||
|
||||
from homeassistant.helpers.entity import async_generate_entity_id
|
||||
from homeassistant.helpers.script import Script
|
||||
@ -33,25 +32,30 @@ CONF_FANS = 'fans'
|
||||
CONF_SPEED_LIST = 'speeds'
|
||||
CONF_SPEED_TEMPLATE = 'speed_template'
|
||||
CONF_OSCILLATING_TEMPLATE = 'oscillating_template'
|
||||
CONF_DIRECTION_TEMPLATE = 'direction_template'
|
||||
CONF_ON_ACTION = 'turn_on'
|
||||
CONF_OFF_ACTION = 'turn_off'
|
||||
CONF_SET_SPEED_ACTION = 'set_speed'
|
||||
CONF_SET_OSCILLATING_ACTION = 'set_oscillating'
|
||||
CONF_SET_DIRECTION_ACTION = 'set_direction'
|
||||
|
||||
_VALID_STATES = [STATE_ON, STATE_OFF]
|
||||
_VALID_OSC = [True, False]
|
||||
_VALID_DIRECTIONS = [DIRECTION_FORWARD, DIRECTION_REVERSE]
|
||||
|
||||
FAN_SCHEMA = vol.Schema({
|
||||
vol.Optional(CONF_FRIENDLY_NAME): cv.string,
|
||||
vol.Required(CONF_VALUE_TEMPLATE): cv.template,
|
||||
vol.Optional(CONF_SPEED_TEMPLATE): cv.template,
|
||||
vol.Optional(CONF_OSCILLATING_TEMPLATE): cv.template,
|
||||
vol.Optional(CONF_DIRECTION_TEMPLATE): cv.template,
|
||||
|
||||
vol.Required(CONF_ON_ACTION): cv.SCRIPT_SCHEMA,
|
||||
vol.Required(CONF_OFF_ACTION): cv.SCRIPT_SCHEMA,
|
||||
|
||||
vol.Optional(CONF_SET_SPEED_ACTION): cv.SCRIPT_SCHEMA,
|
||||
vol.Optional(CONF_SET_OSCILLATING_ACTION): cv.SCRIPT_SCHEMA,
|
||||
vol.Optional(CONF_SET_DIRECTION_ACTION): cv.SCRIPT_SCHEMA,
|
||||
|
||||
vol.Optional(
|
||||
CONF_SPEED_LIST,
|
||||
@ -80,18 +84,21 @@ async def async_setup_platform(
|
||||
oscillating_template = device_config.get(
|
||||
CONF_OSCILLATING_TEMPLATE
|
||||
)
|
||||
direction_template = device_config.get(CONF_DIRECTION_TEMPLATE)
|
||||
|
||||
on_action = device_config[CONF_ON_ACTION]
|
||||
off_action = device_config[CONF_OFF_ACTION]
|
||||
set_speed_action = device_config.get(CONF_SET_SPEED_ACTION)
|
||||
set_oscillating_action = device_config.get(CONF_SET_OSCILLATING_ACTION)
|
||||
set_direction_action = device_config.get(CONF_SET_DIRECTION_ACTION)
|
||||
|
||||
speed_list = device_config[CONF_SPEED_LIST]
|
||||
|
||||
entity_ids = set()
|
||||
manual_entity_ids = device_config.get(CONF_ENTITY_ID)
|
||||
|
||||
for template in (state_template, speed_template, oscillating_template):
|
||||
for template in (state_template, speed_template, oscillating_template,
|
||||
direction_template):
|
||||
if template is None:
|
||||
continue
|
||||
template.hass = hass
|
||||
@ -114,8 +121,9 @@ async def async_setup_platform(
|
||||
TemplateFan(
|
||||
hass, device, friendly_name,
|
||||
state_template, speed_template, oscillating_template,
|
||||
on_action, off_action, set_speed_action,
|
||||
set_oscillating_action, speed_list, entity_ids
|
||||
direction_template, on_action, off_action, set_speed_action,
|
||||
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,
|
||||
state_template, speed_template, oscillating_template,
|
||||
on_action, off_action, set_speed_action,
|
||||
set_oscillating_action, speed_list, entity_ids):
|
||||
direction_template, on_action, off_action, set_speed_action,
|
||||
set_oscillating_action, set_direction_action, speed_list,
|
||||
entity_ids):
|
||||
"""Initialize the fan."""
|
||||
self.hass = hass
|
||||
self.entity_id = async_generate_entity_id(
|
||||
@ -138,6 +147,7 @@ class TemplateFan(FanEntity):
|
||||
self._template = state_template
|
||||
self._speed_template = speed_template
|
||||
self._oscillating_template = oscillating_template
|
||||
self._direction_template = direction_template
|
||||
self._supported_features = 0
|
||||
|
||||
self._on_script = Script(hass, on_action)
|
||||
@ -151,9 +161,14 @@ class TemplateFan(FanEntity):
|
||||
if 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._speed = None
|
||||
self._oscillating = None
|
||||
self._direction = None
|
||||
|
||||
self._template.hass = self.hass
|
||||
if self._speed_template:
|
||||
@ -162,6 +177,9 @@ class TemplateFan(FanEntity):
|
||||
if self._oscillating_template:
|
||||
self._oscillating_template.hass = self.hass
|
||||
self._supported_features |= SUPPORT_OSCILLATE
|
||||
if self._direction_template:
|
||||
self._direction_template.hass = self.hass
|
||||
self._supported_features |= SUPPORT_DIRECTION
|
||||
|
||||
self._entities = entity_ids
|
||||
# List of valid speeds
|
||||
@ -197,6 +215,11 @@ class TemplateFan(FanEntity):
|
||||
"""Return the oscillation state."""
|
||||
return self._oscillating
|
||||
|
||||
@property
|
||||
def direction(self):
|
||||
"""Return the oscillation state."""
|
||||
return self._direction
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""Return the polling state."""
|
||||
@ -236,10 +259,30 @@ class TemplateFan(FanEntity):
|
||||
if self._set_oscillating_script is None:
|
||||
return
|
||||
|
||||
await self._set_oscillating_script.async_run(
|
||||
{ATTR_OSCILLATING: oscillating}
|
||||
)
|
||||
self._oscillating = oscillating
|
||||
if oscillating in _VALID_OSC:
|
||||
self._oscillating = oscillating
|
||||
await self._set_oscillating_script.async_run(
|
||||
{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):
|
||||
"""Register callbacks."""
|
||||
@ -308,6 +351,7 @@ class TemplateFan(FanEntity):
|
||||
oscillating = self._oscillating_template.async_render()
|
||||
except TemplateError as ex:
|
||||
_LOGGER.error(ex)
|
||||
oscillating = None
|
||||
self._state = None
|
||||
|
||||
# Validate osc
|
||||
@ -322,3 +366,24 @@ class TemplateFan(FanEntity):
|
||||
'Received invalid oscillating: %s. ' +
|
||||
'Expected: True/False.', oscillating)
|
||||
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
|
||||
|
@ -10,7 +10,6 @@ from homeassistant.components import zha
|
||||
from homeassistant.components.fan import (
|
||||
DOMAIN, FanEntity, SPEED_OFF, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH,
|
||||
SUPPORT_SET_SPEED)
|
||||
from homeassistant.const import STATE_UNKNOWN
|
||||
|
||||
DEPENDENCIES = ['zha']
|
||||
|
||||
@ -72,7 +71,7 @@ class ZhaFan(zha.Entity, FanEntity):
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
"""Return true if entity is on."""
|
||||
if self._state == STATE_UNKNOWN:
|
||||
if self._state is None:
|
||||
return False
|
||||
return self._state != SPEED_OFF
|
||||
|
||||
@ -103,7 +102,7 @@ class ZhaFan(zha.Entity, FanEntity):
|
||||
"""Retrieve latest state."""
|
||||
result = yield from zha.safe_read(self._endpoint.fan, ['fan_mode'])
|
||||
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
|
||||
def should_poll(self) -> bool:
|
||||
|
@ -4,7 +4,7 @@ Support for RSS/Atom feeds.
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/feedreader/
|
||||
"""
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timedelta
|
||||
from logging import getLogger
|
||||
from os.path import exists
|
||||
from threading import Lock
|
||||
@ -12,8 +12,8 @@ import pickle
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_START
|
||||
from homeassistant.helpers.event import track_utc_time_change
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_START, CONF_SCAN_INTERVAL
|
||||
from homeassistant.helpers.event import track_time_interval
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['feedparser==5.2.1']
|
||||
@ -21,16 +21,22 @@ REQUIREMENTS = ['feedparser==5.2.1']
|
||||
_LOGGER = getLogger(__name__)
|
||||
|
||||
CONF_URLS = 'urls'
|
||||
CONF_MAX_ENTRIES = 'max_entries'
|
||||
|
||||
DEFAULT_MAX_ENTRIES = 20
|
||||
DEFAULT_SCAN_INTERVAL = timedelta(hours=1)
|
||||
|
||||
DOMAIN = 'feedreader'
|
||||
|
||||
EVENT_FEEDREADER = 'feedreader'
|
||||
|
||||
MAX_ENTRIES = 20
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: {
|
||||
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)
|
||||
|
||||
@ -38,33 +44,50 @@ CONFIG_SCHEMA = vol.Schema({
|
||||
def setup(hass, config):
|
||||
"""Set up the Feedreader component."""
|
||||
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))
|
||||
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
|
||||
|
||||
|
||||
class FeedManager(object):
|
||||
"""Abstraction over Feedparser module."""
|
||||
|
||||
def __init__(self, url, hass, storage):
|
||||
"""Initialize the FeedManager object, poll every hour."""
|
||||
def __init__(self, url, scan_interval, max_entries, hass, storage):
|
||||
"""Initialize the FeedManager object, poll as per scan interval."""
|
||||
self._url = url
|
||||
self._scan_interval = scan_interval
|
||||
self._max_entries = max_entries
|
||||
self._feed = None
|
||||
self._hass = hass
|
||||
self._firstrun = True
|
||||
self._storage = storage
|
||||
self._last_entry_timestamp = None
|
||||
self._last_update_successful = False
|
||||
self._has_published_parsed = False
|
||||
self._event_type = EVENT_FEEDREADER
|
||||
self._feed_id = url
|
||||
hass.bus.listen_once(
|
||||
EVENT_HOMEASSISTANT_START, lambda _: self._update())
|
||||
track_utc_time_change(
|
||||
hass, lambda now: self._update(), minute=0, second=0)
|
||||
self._init_regular_updates(hass)
|
||||
|
||||
def _log_no_entries(self):
|
||||
"""Send no entries log at debug level."""
|
||||
_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):
|
||||
"""Update the feed and publish new entries to the event bus."""
|
||||
import feedparser
|
||||
@ -76,26 +99,39 @@ class FeedManager(object):
|
||||
else self._feed.get('modified'))
|
||||
if not self._feed:
|
||||
_LOGGER.error("Error fetching feed data from %s", self._url)
|
||||
self._last_update_successful = False
|
||||
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:
|
||||
_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,
|
||||
# the entries list will be empty
|
||||
elif self._feed.entries:
|
||||
if self._feed.entries:
|
||||
_LOGGER.debug("%s entri(es) available in feed %s",
|
||||
len(self._feed.entries), self._url)
|
||||
if len(self._feed.entries) > MAX_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._filter_entries()
|
||||
self._publish_new_entries()
|
||||
if self._has_published_parsed:
|
||||
self._storage.put_timestamp(
|
||||
self._url, self._last_entry_timestamp)
|
||||
self._feed_id, self._last_entry_timestamp)
|
||||
else:
|
||||
self._log_no_entries()
|
||||
self._last_update_successful = True
|
||||
_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):
|
||||
"""Update last_entry_timestamp and fire entry."""
|
||||
# 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",
|
||||
entry.title)
|
||||
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):
|
||||
"""Publish new entries to the event bus."""
|
||||
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:
|
||||
self._firstrun = False
|
||||
else:
|
||||
@ -157,18 +193,18 @@ class StoredData(object):
|
||||
_LOGGER.error("Error loading data from pickled file %s",
|
||||
self._data_file)
|
||||
|
||||
def get_timestamp(self, url):
|
||||
"""Return stored timestamp for given url."""
|
||||
def get_timestamp(self, feed_id):
|
||||
"""Return stored timestamp for given feed id (usually the url)."""
|
||||
self._fetch_data()
|
||||
return self._data.get(url)
|
||||
return self._data.get(feed_id)
|
||||
|
||||
def put_timestamp(self, url, timestamp):
|
||||
"""Update timestamp for given URL."""
|
||||
def put_timestamp(self, feed_id, timestamp):
|
||||
"""Update timestamp for given feed id (usually the url)."""
|
||||
self._fetch_data()
|
||||
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",
|
||||
url, self._data_file)
|
||||
feed_id, self._data_file)
|
||||
try:
|
||||
pickle.dump(self._data, myfile)
|
||||
except: # noqa: E722 # pylint: disable=bare-except
|
||||
|
@ -43,7 +43,7 @@ def setup(hass, config):
|
||||
|
||||
|
||||
def create_event_handler(patterns, hass):
|
||||
""""Return the Watchdog EventHandler object."""
|
||||
"""Return the Watchdog EventHandler object."""
|
||||
from watchdog.events import PatternMatchingEventHandler
|
||||
|
||||
class EventHandler(PatternMatchingEventHandler):
|
||||
|
@ -25,7 +25,7 @@ from homeassistant.core import callback
|
||||
from homeassistant.helpers.translation import async_get_translations
|
||||
from homeassistant.loader import bind_hass
|
||||
|
||||
REQUIREMENTS = ['home-assistant-frontend==20180509.0']
|
||||
REQUIREMENTS = ['home-assistant-frontend==20180526.4']
|
||||
|
||||
DOMAIN = 'frontend'
|
||||
DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log']
|
||||
@ -147,21 +147,6 @@ class AbstractPanel:
|
||||
'get', '/{}/{{extra:.+}}'.format(self.frontend_url_path),
|
||||
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):
|
||||
"""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.config = config
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_finalize(self, hass, frontend_repository_path):
|
||||
"""Finalize this panel for usage.
|
||||
|
||||
If frontend_repository_path is set, will be prepended to path of
|
||||
built-in components.
|
||||
"""
|
||||
if frontend_repository_path is None:
|
||||
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)
|
||||
def to_response(self, hass, request):
|
||||
"""Panel as dictionary."""
|
||||
return {
|
||||
'component_name': self.component_name,
|
||||
'icon': self.sidebar_icon,
|
||||
'title': self.sidebar_title,
|
||||
'config': self.config,
|
||||
'url_path': self.frontend_url_path,
|
||||
}
|
||||
|
||||
|
||||
class ExternalPanel(AbstractPanel):
|
||||
@ -244,6 +214,21 @@ class ExternalPanel(AbstractPanel):
|
||||
frontend_repository_path is None)
|
||||
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
|
||||
@asyncio.coroutine
|
||||
@ -296,6 +281,15 @@ def add_manifest_json_key(key, val):
|
||||
@asyncio.coroutine
|
||||
def async_setup(hass, config):
|
||||
"""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(
|
||||
WS_TYPE_GET_PANELS, websocket_handle_get_panels, SCHEMA_GET_PANELS)
|
||||
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)
|
||||
|
||||
if is_dev:
|
||||
for subpath in ["src", "build-translations", "build-temp", "build",
|
||||
"hass_frontend", "bower_components", "panels",
|
||||
"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')
|
||||
hass_frontend_path = os.path.join(repo_path, 'hass_frontend')
|
||||
hass_frontend_es5_path = os.path.join(repo_path, 'hass_frontend_es5')
|
||||
else:
|
||||
import hass_frontend
|
||||
import hass_frontend_es5
|
||||
sw_path_es5 = os.path.join(hass_frontend_es5.where(),
|
||||
"service_worker.js")
|
||||
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_frontend_path = hass_frontend.where()
|
||||
hass_frontend_es5_path = hass_frontend_es5.where()
|
||||
|
||||
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(
|
||||
"/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(
|
||||
"/robots.txt", os.path.join(static_path, "robots.txt"), not is_dev)
|
||||
hass.http.register_static_path("/static", static_path, not is_dev)
|
||||
"/robots.txt",
|
||||
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(
|
||||
"/frontend_latest", frontend_latest_path, not is_dev)
|
||||
"/frontend_latest", hass_frontend_path, not is_dev)
|
||||
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')
|
||||
if os.path.isdir(local):
|
||||
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)
|
||||
|
||||
@asyncio.coroutine
|
||||
def finalize_panel(panel):
|
||||
async def finalize_panel(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)
|
||||
|
||||
yield from asyncio.wait([
|
||||
@ -451,10 +426,11 @@ class IndexView(HomeAssistantView):
|
||||
requires_auth = False
|
||||
extra_urls = ['/states', '/states/{extra}']
|
||||
|
||||
def __init__(self, repo_path, js_option):
|
||||
def __init__(self, repo_path, js_option, client):
|
||||
"""Initialize the frontend view."""
|
||||
self.repo_path = repo_path
|
||||
self.js_option = js_option
|
||||
self.client = client
|
||||
self._template_cache = {}
|
||||
|
||||
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
|
||||
|
||||
resp = template.render(
|
||||
template_params = dict(
|
||||
no_auth=no_auth,
|
||||
panel_url=panel_url,
|
||||
panels=hass.data[DATA_PANELS],
|
||||
@ -516,7 +492,11 @@ class IndexView(HomeAssistantView):
|
||||
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):
|
||||
|
@ -14,7 +14,7 @@ from homeassistant.const import (
|
||||
ATTR_ENTITY_ID, CONF_ICON, CONF_NAME, STATE_CLOSED, STATE_HOME,
|
||||
STATE_NOT_HOME, STATE_OFF, STATE_ON, STATE_OPEN, STATE_LOCKED,
|
||||
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.loader import bind_hass
|
||||
from homeassistant.helpers.entity import Entity, async_generate_entity_id
|
||||
@ -35,8 +35,6 @@ ATTR_ADD_ENTITIES = 'add_entities'
|
||||
ATTR_AUTO = 'auto'
|
||||
ATTR_CONTROL = 'control'
|
||||
ATTR_ENTITIES = 'entities'
|
||||
ATTR_ICON = 'icon'
|
||||
ATTR_NAME = 'name'
|
||||
ATTR_OBJECT_ID = 'object_id'
|
||||
ATTR_ORDER = 'order'
|
||||
ATTR_VIEW = 'view'
|
||||
|
@ -13,12 +13,13 @@ import voluptuous as vol
|
||||
|
||||
from homeassistant.components import SERVICE_CHECK_CONFIG
|
||||
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 callback
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.loader import bind_hass
|
||||
from homeassistant.util.dt import utcnow
|
||||
|
||||
from .handler import HassIO
|
||||
from .http import HassIOView
|
||||
|
||||
@ -47,7 +48,6 @@ ATTR_SNAPSHOT = 'snapshot'
|
||||
ATTR_ADDONS = 'addons'
|
||||
ATTR_FOLDERS = 'folders'
|
||||
ATTR_HOMEASSISTANT = 'homeassistant'
|
||||
ATTR_NAME = 'name'
|
||||
ATTR_PASSWORD = 'password'
|
||||
|
||||
SCHEMA_NO_DATA = vol.Schema({})
|
||||
|
@ -33,7 +33,7 @@ def _api_bool(funct):
|
||||
|
||||
|
||||
def _api_data(funct):
|
||||
"""Return a api data."""
|
||||
"""Return data of an api."""
|
||||
@asyncio.coroutine
|
||||
def _wrapper(*argv, **kwargs):
|
||||
"""Wrap function."""
|
||||
|
@ -36,7 +36,7 @@ NO_TIMEOUT = {
|
||||
}
|
||||
|
||||
NO_AUTH = {
|
||||
re.compile(r'^app-(es5|latest)/(index|hassio-app).html$'),
|
||||
re.compile(r'^app-(es5|latest)/.+$'),
|
||||
re.compile(r'^addons/[^/]*/logo$')
|
||||
}
|
||||
|
||||
|
@ -12,25 +12,25 @@ import voluptuous as vol
|
||||
from homeassistant.components.cover import (
|
||||
SUPPORT_CLOSE, SUPPORT_OPEN, SUPPORT_SET_POSITION)
|
||||
from homeassistant.const import (
|
||||
ATTR_SUPPORTED_FEATURES, ATTR_UNIT_OF_MEASUREMENT,
|
||||
ATTR_DEVICE_CLASS, CONF_IP_ADDRESS, CONF_PORT, TEMP_CELSIUS,
|
||||
TEMP_FAHRENHEIT, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP,
|
||||
DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_ILLUMINANCE, DEVICE_CLASS_TEMPERATURE)
|
||||
ATTR_DEVICE_CLASS, ATTR_SUPPORTED_FEATURES, ATTR_UNIT_OF_MEASUREMENT,
|
||||
CONF_IP_ADDRESS, CONF_NAME, CONF_PORT,
|
||||
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
|
||||
from homeassistant.helpers.entityfilter import FILTER_SCHEMA
|
||||
from homeassistant.util import get_local_ip
|
||||
from homeassistant.util.decorator import Registry
|
||||
from .const import (
|
||||
DOMAIN, HOMEKIT_FILE, CONF_AUTO_START, CONF_ENTITY_CONFIG, CONF_FILTER,
|
||||
DEFAULT_PORT, DEFAULT_AUTO_START, SERVICE_HOMEKIT_START,
|
||||
DEVICE_CLASS_CO2, DEVICE_CLASS_PM25)
|
||||
from .util import (
|
||||
validate_entity_config, show_setup_message)
|
||||
CONF_AUTO_START, CONF_ENTITY_CONFIG, CONF_FILTER, DEFAULT_AUTO_START,
|
||||
DEFAULT_PORT, DEVICE_CLASS_CO2, DEVICE_CLASS_PM25, DOMAIN, HOMEKIT_FILE,
|
||||
SERVICE_HOMEKIT_START)
|
||||
from .util import show_setup_message, validate_entity_config
|
||||
|
||||
TYPES = Registry()
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
REQUIREMENTS = ['HAP-python==2.0.0']
|
||||
REQUIREMENTS = ['HAP-python==2.1.0']
|
||||
|
||||
# #### Driver Status ####
|
||||
STATUS_READY = 0
|
||||
@ -93,7 +93,7 @@ def get_accessory(hass, state, aid, config):
|
||||
return None
|
||||
|
||||
a_type = None
|
||||
config = config or {}
|
||||
name = config.get(CONF_NAME, state.name)
|
||||
|
||||
if state.domain == 'alarm_control_panel':
|
||||
a_type = 'SecuritySystem'
|
||||
@ -116,6 +116,9 @@ def get_accessory(hass, state, aid, config):
|
||||
elif features & (SUPPORT_OPEN | SUPPORT_CLOSE):
|
||||
a_type = 'WindowCoveringBasic'
|
||||
|
||||
elif state.domain == 'fan':
|
||||
a_type = 'Fan'
|
||||
|
||||
elif state.domain == 'light':
|
||||
a_type = 'Light'
|
||||
|
||||
@ -147,7 +150,7 @@ def get_accessory(hass, state, aid, config):
|
||||
return None
|
||||
|
||||
_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):
|
||||
@ -183,7 +186,8 @@ class HomeKit():
|
||||
ip_addr = self._ip_address or get_local_ip()
|
||||
path = self.hass.config.path(HOMEKIT_FILE)
|
||||
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):
|
||||
"""Try adding accessory to bridge if configured beforehand."""
|
||||
@ -203,15 +207,16 @@ class HomeKit():
|
||||
|
||||
# pylint: disable=unused-variable
|
||||
from . import ( # noqa F401
|
||||
type_covers, type_lights, type_locks, type_security_systems,
|
||||
type_sensors, type_switches, type_thermostats)
|
||||
type_covers, type_fans, type_lights, type_locks,
|
||||
type_security_systems, type_sensors, type_switches,
|
||||
type_thermostats)
|
||||
|
||||
for state in self.hass.states.all():
|
||||
self.add_bridge_accessory(state)
|
||||
self.bridge.set_driver(self.driver)
|
||||
|
||||
if not self.bridge.paired:
|
||||
show_setup_message(self.hass, self.bridge)
|
||||
if not self.driver.state.paired:
|
||||
show_setup_message(self.hass, self.driver.state.pincode)
|
||||
|
||||
_LOGGER.debug('Driver start')
|
||||
self.hass.add_job(self.driver.start)
|
||||
|
@ -16,8 +16,8 @@ from homeassistant.helpers.event import (
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from .const import (
|
||||
DEBOUNCE_TIMEOUT, BRIDGE_MODEL, BRIDGE_NAME,
|
||||
BRIDGE_SERIAL_NUMBER, MANUFACTURER)
|
||||
BRIDGE_MODEL, BRIDGE_NAME, BRIDGE_SERIAL_NUMBER,
|
||||
DEBOUNCE_TIMEOUT, MANUFACTURER)
|
||||
from .util import (
|
||||
show_setup_message, dismiss_setup_message)
|
||||
|
||||
@ -64,14 +64,16 @@ def debounce(func):
|
||||
class HomeAccessory(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."""
|
||||
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(
|
||||
firmware_revision=__version__, manufacturer=MANUFACTURER,
|
||||
model=domain, serial_number=entity_id)
|
||||
model=model, serial_number=entity_id)
|
||||
self.category = category
|
||||
self.config = config
|
||||
self.entity_id = entity_id
|
||||
self.hass = hass
|
||||
|
||||
@ -82,20 +84,21 @@ class HomeAccessory(Accessory):
|
||||
async_track_state_change(
|
||||
self.hass, self.entity_id, self.update_state_callback)
|
||||
|
||||
@ha_callback
|
||||
def update_state_callback(self, entity_id=None, old_state=None,
|
||||
new_state=None):
|
||||
"""Callback from state change listener."""
|
||||
_LOGGER.debug('New_state: %s', new_state)
|
||||
if new_state is None:
|
||||
return
|
||||
self.update_state(new_state)
|
||||
self.hass.async_add_job(self.update_state, new_state)
|
||||
|
||||
def update_state(self, new_state):
|
||||
"""Method called on state change to update HomeKit value.
|
||||
|
||||
Overridden by accessory types.
|
||||
"""
|
||||
pass
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
class HomeBridge(Bridge):
|
||||
@ -113,20 +116,23 @@ class HomeBridge(Bridge):
|
||||
"""Prevent print of pyhap setup message to terminal."""
|
||||
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):
|
||||
"""Adapter class for AccessoryDriver."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
def __init__(self, hass, *args, **kwargs):
|
||||
"""Initialize a AccessoryDriver object."""
|
||||
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)
|
||||
|
@ -1,23 +1,23 @@
|
||||
"""Constants used be the HomeKit component."""
|
||||
# #### MISC ####
|
||||
# #### Misc ####
|
||||
DEBOUNCE_TIMEOUT = 0.5
|
||||
DOMAIN = 'homekit'
|
||||
HOMEKIT_FILE = '.homekit.state'
|
||||
HOMEKIT_NOTIFY_ID = 4663548
|
||||
|
||||
# #### CONFIG ####
|
||||
# #### Config ####
|
||||
CONF_AUTO_START = 'auto_start'
|
||||
CONF_ENTITY_CONFIG = 'entity_config'
|
||||
CONF_FILTER = 'filter'
|
||||
|
||||
# #### CONFIG DEFAULTS ####
|
||||
# #### Config Defaults ####
|
||||
DEFAULT_AUTO_START = True
|
||||
DEFAULT_PORT = 51827
|
||||
|
||||
# #### HOMEKIT COMPONENT SERVICES ####
|
||||
# #### HomeKit Component Services ####
|
||||
SERVICE_HOMEKIT_START = 'start'
|
||||
|
||||
# #### STRING CONSTANTS ####
|
||||
# #### String Constants ####
|
||||
BRIDGE_MODEL = 'Bridge'
|
||||
BRIDGE_NAME = 'Home Assistant Bridge'
|
||||
BRIDGE_SERIAL_NUMBER = 'homekit.bridge'
|
||||
@ -29,11 +29,12 @@ SERV_AIR_QUALITY_SENSOR = 'AirQualitySensor'
|
||||
SERV_CARBON_DIOXIDE_SENSOR = 'CarbonDioxideSensor'
|
||||
SERV_CARBON_MONOXIDE_SENSOR = 'CarbonMonoxideSensor'
|
||||
SERV_CONTACT_SENSOR = 'ContactSensor'
|
||||
SERV_FANV2 = 'Fanv2'
|
||||
SERV_GARAGE_DOOR_OPENER = 'GarageDoorOpener'
|
||||
SERV_HUMIDITY_SENSOR = 'HumiditySensor' # CurrentRelativeHumidity
|
||||
SERV_HUMIDITY_SENSOR = 'HumiditySensor'
|
||||
SERV_LEAK_SENSOR = 'LeakSensor'
|
||||
SERV_LIGHT_SENSOR = 'LightSensor'
|
||||
SERV_LIGHTBULB = 'Lightbulb' # On | Brightness, Hue, Saturation, Name
|
||||
SERV_LIGHTBULB = 'Lightbulb'
|
||||
SERV_LOCK = 'LockMechanism'
|
||||
SERV_MOTION_SENSOR = 'MotionSensor'
|
||||
SERV_OCCUPANCY_SENSOR = 'OccupancySensor'
|
||||
@ -43,12 +44,12 @@ SERV_SWITCH = 'Switch'
|
||||
SERV_TEMPERATURE_SENSOR = 'TemperatureSensor'
|
||||
SERV_THERMOSTAT = 'Thermostat'
|
||||
SERV_WINDOW_COVERING = 'WindowCovering'
|
||||
# CurrentPosition, TargetPosition, PositionState
|
||||
|
||||
# #### Characteristics ####
|
||||
CHAR_ACTIVE = 'Active'
|
||||
CHAR_AIR_PARTICULATE_DENSITY = 'AirParticulateDensity'
|
||||
CHAR_AIR_QUALITY = 'AirQuality'
|
||||
CHAR_BRIGHTNESS = 'Brightness' # Int | [0, 100]
|
||||
CHAR_BRIGHTNESS = 'Brightness'
|
||||
CHAR_CARBON_DIOXIDE_DETECTED = 'CarbonDioxideDetected'
|
||||
CHAR_CARBON_DIOXIDE_LEVEL = 'CarbonDioxideLevel'
|
||||
CHAR_CARBON_DIOXIDE_PEAK_LEVEL = 'CarbonDioxidePeakLevel'
|
||||
@ -59,13 +60,13 @@ CHAR_COOLING_THRESHOLD_TEMPERATURE = 'CoolingThresholdTemperature'
|
||||
CHAR_CURRENT_AMBIENT_LIGHT_LEVEL = 'CurrentAmbientLightLevel'
|
||||
CHAR_CURRENT_DOOR_STATE = 'CurrentDoorState'
|
||||
CHAR_CURRENT_HEATING_COOLING = 'CurrentHeatingCoolingState'
|
||||
CHAR_CURRENT_POSITION = 'CurrentPosition' # Int | [0, 100]
|
||||
CHAR_CURRENT_HUMIDITY = 'CurrentRelativeHumidity' # percent
|
||||
CHAR_CURRENT_POSITION = 'CurrentPosition'
|
||||
CHAR_CURRENT_HUMIDITY = 'CurrentRelativeHumidity'
|
||||
CHAR_CURRENT_SECURITY_STATE = 'SecuritySystemCurrentState'
|
||||
CHAR_CURRENT_TEMPERATURE = 'CurrentTemperature'
|
||||
CHAR_FIRMWARE_REVISION = 'FirmwareRevision'
|
||||
CHAR_HEATING_THRESHOLD_TEMPERATURE = 'HeatingThresholdTemperature'
|
||||
CHAR_HUE = 'Hue' # arcdegress | [0, 360]
|
||||
CHAR_HUE = 'Hue'
|
||||
CHAR_LEAK_DETECTED = 'LeakDetected'
|
||||
CHAR_LOCK_CURRENT_STATE = 'LockCurrentState'
|
||||
CHAR_LOCK_TARGET_STATE = 'LockTargetState'
|
||||
@ -75,33 +76,34 @@ CHAR_MODEL = 'Model'
|
||||
CHAR_MOTION_DETECTED = 'MotionDetected'
|
||||
CHAR_NAME = 'Name'
|
||||
CHAR_OCCUPANCY_DETECTED = 'OccupancyDetected'
|
||||
CHAR_ON = 'On' # boolean
|
||||
CHAR_ON = 'On'
|
||||
CHAR_POSITION_STATE = 'PositionState'
|
||||
CHAR_SATURATION = 'Saturation' # percent
|
||||
CHAR_ROTATION_DIRECTION = 'RotationDirection'
|
||||
CHAR_SATURATION = 'Saturation'
|
||||
CHAR_SERIAL_NUMBER = 'SerialNumber'
|
||||
CHAR_SMOKE_DETECTED = 'SmokeDetected'
|
||||
CHAR_SWING_MODE = 'SwingMode'
|
||||
CHAR_TARGET_DOOR_STATE = 'TargetDoorState'
|
||||
CHAR_TARGET_HEATING_COOLING = 'TargetHeatingCoolingState'
|
||||
CHAR_TARGET_POSITION = 'TargetPosition' # Int | [0, 100]
|
||||
CHAR_TARGET_POSITION = 'TargetPosition'
|
||||
CHAR_TARGET_SECURITY_STATE = 'SecuritySystemTargetState'
|
||||
CHAR_TARGET_TEMPERATURE = 'TargetTemperature'
|
||||
CHAR_TEMP_DISPLAY_UNITS = 'TemperatureDisplayUnits'
|
||||
|
||||
# #### Properties ####
|
||||
PROP_MAX_VALUE = 'maxValue'
|
||||
PROP_MIN_VALUE = 'minValue'
|
||||
PROP_CELSIUS = {'minValue': -273, 'maxValue': 999}
|
||||
|
||||
# #### Device Class ####
|
||||
# #### Device Classes ####
|
||||
DEVICE_CLASS_CO2 = 'co2'
|
||||
DEVICE_CLASS_DOOR = 'door'
|
||||
DEVICE_CLASS_GARAGE_DOOR = 'garage_door'
|
||||
DEVICE_CLASS_GAS = 'gas'
|
||||
DEVICE_CLASS_HUMIDITY = 'humidity'
|
||||
DEVICE_CLASS_LIGHT = 'light'
|
||||
DEVICE_CLASS_MOISTURE = 'moisture'
|
||||
DEVICE_CLASS_MOTION = 'motion'
|
||||
DEVICE_CLASS_OCCUPANCY = 'occupancy'
|
||||
DEVICE_CLASS_OPENING = 'opening'
|
||||
DEVICE_CLASS_PM25 = 'pm25'
|
||||
DEVICE_CLASS_SMOKE = 'smoke'
|
||||
DEVICE_CLASS_TEMPERATURE = 'temperature'
|
||||
DEVICE_CLASS_WINDOW = 'window'
|
||||
|
@ -1,21 +1,21 @@
|
||||
"""Class to hold all cover accessories."""
|
||||
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 (
|
||||
ATTR_CURRENT_POSITION, ATTR_POSITION, DOMAIN, SUPPORT_STOP)
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID, SERVICE_SET_COVER_POSITION, STATE_OPEN, STATE_CLOSED,
|
||||
SERVICE_OPEN_COVER, SERVICE_CLOSE_COVER, SERVICE_STOP_COVER,
|
||||
ATTR_SUPPORTED_FEATURES)
|
||||
ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, SERVICE_CLOSE_COVER,
|
||||
SERVICE_OPEN_COVER, SERVICE_SET_COVER_POSITION, SERVICE_STOP_COVER,
|
||||
STATE_CLOSED, STATE_OPEN)
|
||||
|
||||
from . import TYPES
|
||||
from .accessories import HomeAccessory, debounce
|
||||
from .accessories import debounce, HomeAccessory
|
||||
from .const import (
|
||||
SERV_WINDOW_COVERING, CHAR_CURRENT_POSITION,
|
||||
CHAR_TARGET_POSITION, CHAR_POSITION_STATE,
|
||||
SERV_GARAGE_DOOR_OPENER, CHAR_CURRENT_DOOR_STATE, CHAR_TARGET_DOOR_STATE)
|
||||
CHAR_CURRENT_DOOR_STATE, CHAR_CURRENT_POSITION, CHAR_POSITION_STATE,
|
||||
CHAR_TARGET_DOOR_STATE, CHAR_TARGET_POSITION,
|
||||
SERV_GARAGE_DOOR_OPENER, SERV_WINDOW_COVERING)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@ -28,7 +28,7 @@ class GarageDoorOpener(HomeAccessory):
|
||||
and support no more than open, close, and stop.
|
||||
"""
|
||||
|
||||
def __init__(self, *args, config):
|
||||
def __init__(self, *args):
|
||||
"""Initialize a GarageDoorOpener accessory object."""
|
||||
super().__init__(*args, category=CATEGORY_GARAGE_DOOR_OPENER)
|
||||
self.flag_target_state = False
|
||||
@ -44,12 +44,13 @@ class GarageDoorOpener(HomeAccessory):
|
||||
_LOGGER.debug('%s: Set state to %d', self.entity_id, value)
|
||||
self.flag_target_state = True
|
||||
|
||||
params = {ATTR_ENTITY_ID: self.entity_id}
|
||||
if value == 0:
|
||||
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:
|
||||
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):
|
||||
"""Update cover state after state changed."""
|
||||
@ -69,7 +70,7 @@ class WindowCovering(HomeAccessory):
|
||||
The cover entity must support: set_cover_position.
|
||||
"""
|
||||
|
||||
def __init__(self, *args, config):
|
||||
def __init__(self, *args):
|
||||
"""Initialize a WindowCovering accessory object."""
|
||||
super().__init__(*args, category=CATEGORY_WINDOW_COVERING)
|
||||
self.homekit_target = None
|
||||
@ -108,7 +109,7 @@ class WindowCoveringBasic(HomeAccessory):
|
||||
stop_cover (optional).
|
||||
"""
|
||||
|
||||
def __init__(self, *args, config):
|
||||
def __init__(self, *args):
|
||||
"""Initialize a WindowCovering accessory object."""
|
||||
super().__init__(*args, category=CATEGORY_WINDOW_COVERING)
|
||||
features = self.hass.states.get(self.entity_id) \
|
||||
@ -141,8 +142,8 @@ class WindowCoveringBasic(HomeAccessory):
|
||||
else:
|
||||
service, position = (SERVICE_CLOSE_COVER, 0)
|
||||
|
||||
self.hass.services.call(DOMAIN, service,
|
||||
{ATTR_ENTITY_ID: self.entity_id})
|
||||
params = {ATTR_ENTITY_ID: self.entity_id}
|
||||
self.hass.services.call(DOMAIN, service, params)
|
||||
|
||||
# Snap the current/target position to the expected final position.
|
||||
self.char_current_position.set_value(position)
|
||||
|
115
homeassistant/components/homekit/type_fans.py
Normal file
115
homeassistant/components/homekit/type_fans.py
Normal 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
|
@ -4,15 +4,18 @@ import logging
|
||||
from pyhap.const import CATEGORY_LIGHTBULB
|
||||
|
||||
from homeassistant.components.light import (
|
||||
ATTR_HS_COLOR, ATTR_COLOR_TEMP, ATTR_BRIGHTNESS, ATTR_MIN_MIREDS,
|
||||
ATTR_MAX_MIREDS, SUPPORT_COLOR, SUPPORT_COLOR_TEMP, SUPPORT_BRIGHTNESS)
|
||||
from homeassistant.const import ATTR_SUPPORTED_FEATURES, STATE_ON, STATE_OFF
|
||||
ATTR_BRIGHTNESS, ATTR_BRIGHTNESS_PCT, ATTR_COLOR_TEMP, ATTR_HS_COLOR,
|
||||
ATTR_MAX_MIREDS, ATTR_MIN_MIREDS, DOMAIN,
|
||||
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 .accessories import HomeAccessory, debounce
|
||||
from .accessories import debounce, HomeAccessory
|
||||
from .const import (
|
||||
SERV_LIGHTBULB, CHAR_COLOR_TEMPERATURE,
|
||||
CHAR_BRIGHTNESS, CHAR_HUE, CHAR_ON, CHAR_SATURATION)
|
||||
CHAR_BRIGHTNESS, CHAR_COLOR_TEMPERATURE, CHAR_HUE, CHAR_ON,
|
||||
CHAR_SATURATION, SERV_LIGHTBULB, PROP_MAX_VALUE, PROP_MIN_VALUE)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@ -26,7 +29,7 @@ class Light(HomeAccessory):
|
||||
Currently supports: state, brightness, color temperature, rgb_color.
|
||||
"""
|
||||
|
||||
def __init__(self, *args, config):
|
||||
def __init__(self, *args):
|
||||
"""Initialize a new Light accessory object."""
|
||||
super().__init__(*args, category=CATEGORY_LIGHTBULB)
|
||||
self._flag = {CHAR_ON: False, CHAR_BRIGHTNESS: False,
|
||||
@ -61,7 +64,8 @@ class Light(HomeAccessory):
|
||||
.attributes.get(ATTR_MAX_MIREDS, 500)
|
||||
self.char_color_temperature = serv_light.configure_char(
|
||||
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)
|
||||
if CHAR_HUE in self.chars:
|
||||
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)
|
||||
self._flag[CHAR_ON] = True
|
||||
|
||||
if value == 1:
|
||||
self.hass.components.light.turn_on(self.entity_id)
|
||||
elif value == 0:
|
||||
self.hass.components.light.turn_off(self.entity_id)
|
||||
params = {ATTR_ENTITY_ID: self.entity_id}
|
||||
service = SERVICE_TURN_ON if value == 1 else SERVICE_TURN_OFF
|
||||
self.hass.services.call(DOMAIN, service, params)
|
||||
|
||||
@debounce
|
||||
def set_brightness(self, value):
|
||||
"""Set brightness if call came from HomeKit."""
|
||||
_LOGGER.debug('%s: Set brightness to %d', self.entity_id, value)
|
||||
self._flag[CHAR_BRIGHTNESS] = True
|
||||
if value != 0:
|
||||
self.hass.components.light.turn_on(
|
||||
self.entity_id, brightness_pct=value)
|
||||
else:
|
||||
self.hass.components.light.turn_off(self.entity_id)
|
||||
if value == 0:
|
||||
self.set_state(0) # Turn off light
|
||||
return
|
||||
params = {ATTR_ENTITY_ID: self.entity_id, ATTR_BRIGHTNESS_PCT: value}
|
||||
self.hass.services.call(DOMAIN, SERVICE_TURN_ON, params)
|
||||
|
||||
def set_color_temperature(self, value):
|
||||
"""Set color temperature if call came from HomeKit."""
|
||||
_LOGGER.debug('%s: Set color temp to %s', self.entity_id, value)
|
||||
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):
|
||||
"""Set saturation if call came from HomeKit."""
|
||||
@ -116,15 +119,14 @@ class Light(HomeAccessory):
|
||||
|
||||
def set_color(self):
|
||||
"""Set color if call came from HomeKit."""
|
||||
# Handle Color
|
||||
if self._features & SUPPORT_COLOR and self._flag[CHAR_HUE] and \
|
||||
self._flag[CHAR_SATURATION]:
|
||||
color = (self._hue, self._saturation)
|
||||
_LOGGER.debug('%s: Set hs_color to %s', self.entity_id, color)
|
||||
self._flag.update({
|
||||
CHAR_HUE: False, CHAR_SATURATION: False, RGB_COLOR: True})
|
||||
self.hass.components.light.turn_on(
|
||||
self.entity_id, hs_color=color)
|
||||
params = {ATTR_ENTITY_ID: self.entity_id, ATTR_HS_COLOR: color}
|
||||
self.hass.services.call(DOMAIN, SERVICE_TURN_ON, params)
|
||||
|
||||
def update_state(self, new_state):
|
||||
"""Update light after state change."""
|
||||
|
@ -4,12 +4,12 @@ import logging
|
||||
from pyhap.const import CATEGORY_DOOR_LOCK
|
||||
|
||||
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 .accessories import HomeAccessory
|
||||
from .const import (
|
||||
SERV_LOCK, CHAR_LOCK_CURRENT_STATE, CHAR_LOCK_TARGET_STATE)
|
||||
from .const import CHAR_LOCK_CURRENT_STATE, CHAR_LOCK_TARGET_STATE, SERV_LOCK
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@ -29,9 +29,10 @@ class Lock(HomeAccessory):
|
||||
The lock entity must support: unlock and lock.
|
||||
"""
|
||||
|
||||
def __init__(self, *args, config):
|
||||
def __init__(self, *args):
|
||||
"""Initialize a Lock accessory object."""
|
||||
super().__init__(*args, category=CATEGORY_DOOR_LOCK)
|
||||
self._code = self.config.get(ATTR_CODE)
|
||||
self.flag_target_state = False
|
||||
|
||||
serv_lock_mechanism = self.add_preload_service(SERV_LOCK)
|
||||
@ -51,7 +52,9 @@ class Lock(HomeAccessory):
|
||||
service = STATE_TO_SERVICE[hass_value]
|
||||
|
||||
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):
|
||||
"""Update lock after state changed."""
|
||||
|
@ -3,16 +3,16 @@ import logging
|
||||
|
||||
from pyhap.const import CATEGORY_ALARM_SYSTEM
|
||||
|
||||
from homeassistant.components.alarm_control_panel import DOMAIN
|
||||
from homeassistant.const import (
|
||||
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME,
|
||||
STATE_ALARM_ARMED_NIGHT, STATE_ALARM_DISARMED,
|
||||
STATE_ALARM_TRIGGERED, ATTR_ENTITY_ID, ATTR_CODE)
|
||||
ATTR_ENTITY_ID, ATTR_CODE, STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME,
|
||||
STATE_ALARM_ARMED_NIGHT, STATE_ALARM_TRIGGERED, STATE_ALARM_DISARMED)
|
||||
|
||||
from . import TYPES
|
||||
from .accessories import HomeAccessory
|
||||
from .const import (
|
||||
SERV_SECURITY_SYSTEM, CHAR_CURRENT_SECURITY_STATE,
|
||||
CHAR_TARGET_SECURITY_STATE)
|
||||
CHAR_CURRENT_SECURITY_STATE, CHAR_TARGET_SECURITY_STATE,
|
||||
SERV_SECURITY_SYSTEM)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@ -32,10 +32,10 @@ STATE_TO_SERVICE = {STATE_ALARM_ARMED_HOME: 'alarm_arm_home',
|
||||
class SecuritySystem(HomeAccessory):
|
||||
"""Generate an SecuritySystem accessory for an alarm control panel."""
|
||||
|
||||
def __init__(self, *args, config):
|
||||
def __init__(self, *args):
|
||||
"""Initialize a SecuritySystem accessory object."""
|
||||
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
|
||||
|
||||
serv_alarm = self.add_preload_service(SERV_SECURITY_SYSTEM)
|
||||
@ -56,7 +56,7 @@ class SecuritySystem(HomeAccessory):
|
||||
params = {ATTR_ENTITY_ID: self.entity_id}
|
||||
if 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):
|
||||
"""Update security state after state changed."""
|
||||
|
@ -4,26 +4,26 @@ import logging
|
||||
from pyhap.const import CATEGORY_SENSOR
|
||||
|
||||
from homeassistant.const import (
|
||||
ATTR_UNIT_OF_MEASUREMENT, TEMP_CELSIUS,
|
||||
ATTR_DEVICE_CLASS, STATE_ON, STATE_HOME)
|
||||
ATTR_DEVICE_CLASS, ATTR_UNIT_OF_MEASUREMENT, STATE_ON, STATE_HOME,
|
||||
TEMP_CELSIUS)
|
||||
|
||||
from . import TYPES
|
||||
from .accessories import HomeAccessory
|
||||
from .const import (
|
||||
SERV_HUMIDITY_SENSOR, SERV_TEMPERATURE_SENSOR,
|
||||
CHAR_CURRENT_HUMIDITY, CHAR_CURRENT_TEMPERATURE, PROP_CELSIUS,
|
||||
SERV_AIR_QUALITY_SENSOR, CHAR_AIR_QUALITY, CHAR_AIR_PARTICULATE_DENSITY,
|
||||
CHAR_CARBON_DIOXIDE_LEVEL, CHAR_CARBON_DIOXIDE_PEAK_LEVEL,
|
||||
SERV_LIGHT_SENSOR, CHAR_CURRENT_AMBIENT_LIGHT_LEVEL,
|
||||
DEVICE_CLASS_CO2, SERV_CARBON_DIOXIDE_SENSOR, CHAR_CARBON_DIOXIDE_DETECTED,
|
||||
DEVICE_CLASS_GAS, SERV_CARBON_MONOXIDE_SENSOR,
|
||||
CHAR_CARBON_MONOXIDE_DETECTED,
|
||||
DEVICE_CLASS_MOISTURE, SERV_LEAK_SENSOR, CHAR_LEAK_DETECTED,
|
||||
DEVICE_CLASS_MOTION, SERV_MOTION_SENSOR, CHAR_MOTION_DETECTED,
|
||||
DEVICE_CLASS_OCCUPANCY, SERV_OCCUPANCY_SENSOR, CHAR_OCCUPANCY_DETECTED,
|
||||
DEVICE_CLASS_OPENING, SERV_CONTACT_SENSOR, CHAR_CONTACT_SENSOR_STATE,
|
||||
DEVICE_CLASS_DOOR, DEVICE_CLASS_GARAGE_DOOR, DEVICE_CLASS_WINDOW,
|
||||
DEVICE_CLASS_SMOKE, SERV_SMOKE_SENSOR, CHAR_SMOKE_DETECTED)
|
||||
CHAR_AIR_PARTICULATE_DENSITY, CHAR_AIR_QUALITY,
|
||||
CHAR_CARBON_DIOXIDE_DETECTED, CHAR_CARBON_DIOXIDE_LEVEL,
|
||||
CHAR_CARBON_DIOXIDE_PEAK_LEVEL, CHAR_CARBON_MONOXIDE_DETECTED,
|
||||
CHAR_CONTACT_SENSOR_STATE, CHAR_CURRENT_AMBIENT_LIGHT_LEVEL,
|
||||
CHAR_CURRENT_HUMIDITY, CHAR_CURRENT_TEMPERATURE, CHAR_LEAK_DETECTED,
|
||||
CHAR_MOTION_DETECTED, CHAR_OCCUPANCY_DETECTED, CHAR_SMOKE_DETECTED,
|
||||
DEVICE_CLASS_CO2, DEVICE_CLASS_DOOR, DEVICE_CLASS_GARAGE_DOOR,
|
||||
DEVICE_CLASS_GAS, DEVICE_CLASS_MOISTURE, DEVICE_CLASS_MOTION,
|
||||
DEVICE_CLASS_OCCUPANCY, DEVICE_CLASS_OPENING, DEVICE_CLASS_SMOKE,
|
||||
DEVICE_CLASS_WINDOW, PROP_CELSIUS, SERV_AIR_QUALITY_SENSOR,
|
||||
SERV_CARBON_DIOXIDE_SENSOR, SERV_CARBON_MONOXIDE_SENSOR,
|
||||
SERV_CONTACT_SENSOR, SERV_HUMIDITY_SENSOR, SERV_LEAK_SENSOR,
|
||||
SERV_LIGHT_SENSOR, SERV_MOTION_SENSOR, SERV_OCCUPANCY_SENSOR,
|
||||
SERV_SMOKE_SENSOR, SERV_TEMPERATURE_SENSOR)
|
||||
from .util import (
|
||||
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.
|
||||
"""
|
||||
|
||||
def __init__(self, *args, config):
|
||||
def __init__(self, *args):
|
||||
"""Initialize a TemperatureSensor accessory object."""
|
||||
super().__init__(*args, category=CATEGORY_SENSOR)
|
||||
serv_temp = self.add_preload_service(SERV_TEMPERATURE_SENSOR)
|
||||
@ -74,7 +74,7 @@ class TemperatureSensor(HomeAccessory):
|
||||
class HumiditySensor(HomeAccessory):
|
||||
"""Generate a HumiditySensor accessory as humidity sensor."""
|
||||
|
||||
def __init__(self, *args, config):
|
||||
def __init__(self, *args):
|
||||
"""Initialize a HumiditySensor accessory object."""
|
||||
super().__init__(*args, category=CATEGORY_SENSOR)
|
||||
serv_humidity = self.add_preload_service(SERV_HUMIDITY_SENSOR)
|
||||
@ -94,7 +94,7 @@ class HumiditySensor(HomeAccessory):
|
||||
class AirQualitySensor(HomeAccessory):
|
||||
"""Generate a AirQualitySensor accessory as air quality sensor."""
|
||||
|
||||
def __init__(self, *args, config):
|
||||
def __init__(self, *args):
|
||||
"""Initialize a AirQualitySensor accessory object."""
|
||||
super().__init__(*args, category=CATEGORY_SENSOR)
|
||||
|
||||
@ -108,7 +108,7 @@ class AirQualitySensor(HomeAccessory):
|
||||
def update_state(self, new_state):
|
||||
"""Update accessory after state change."""
|
||||
density = convert_to_float(new_state.state)
|
||||
if density is not None:
|
||||
if density:
|
||||
self.char_density.set_value(density)
|
||||
self.char_quality.set_value(density_to_air_quality(density))
|
||||
_LOGGER.debug('%s: Set to %d', self.entity_id, density)
|
||||
@ -118,7 +118,7 @@ class AirQualitySensor(HomeAccessory):
|
||||
class CarbonDioxideSensor(HomeAccessory):
|
||||
"""Generate a CarbonDioxideSensor accessory as CO2 sensor."""
|
||||
|
||||
def __init__(self, *args, config):
|
||||
def __init__(self, *args):
|
||||
"""Initialize a CarbonDioxideSensor accessory object."""
|
||||
super().__init__(*args, category=CATEGORY_SENSOR)
|
||||
|
||||
@ -134,7 +134,7 @@ class CarbonDioxideSensor(HomeAccessory):
|
||||
def update_state(self, new_state):
|
||||
"""Update accessory after state change."""
|
||||
co2 = convert_to_float(new_state.state)
|
||||
if co2 is not None:
|
||||
if co2:
|
||||
self.char_co2.set_value(co2)
|
||||
if co2 > self.char_peak.value:
|
||||
self.char_peak.set_value(co2)
|
||||
@ -146,7 +146,7 @@ class CarbonDioxideSensor(HomeAccessory):
|
||||
class LightSensor(HomeAccessory):
|
||||
"""Generate a LightSensor accessory as light sensor."""
|
||||
|
||||
def __init__(self, *args, config):
|
||||
def __init__(self, *args):
|
||||
"""Initialize a LightSensor accessory object."""
|
||||
super().__init__(*args, category=CATEGORY_SENSOR)
|
||||
|
||||
@ -157,7 +157,7 @@ class LightSensor(HomeAccessory):
|
||||
def update_state(self, new_state):
|
||||
"""Update accessory after state change."""
|
||||
luminance = convert_to_float(new_state.state)
|
||||
if luminance is not None:
|
||||
if luminance:
|
||||
self.char_light.set_value(luminance)
|
||||
_LOGGER.debug('%s: Set to %d', self.entity_id, luminance)
|
||||
|
||||
@ -166,7 +166,7 @@ class LightSensor(HomeAccessory):
|
||||
class BinarySensor(HomeAccessory):
|
||||
"""Generate a BinarySensor accessory as binary sensor."""
|
||||
|
||||
def __init__(self, *args, config):
|
||||
def __init__(self, *args):
|
||||
"""Initialize a BinarySensor accessory object."""
|
||||
super().__init__(*args, category=CATEGORY_SENSOR)
|
||||
device_class = self.hass.states.get(self.entity_id).attributes \
|
||||
|
@ -18,7 +18,7 @@ _LOGGER = logging.getLogger(__name__)
|
||||
class Switch(HomeAccessory):
|
||||
"""Generate a Switch accessory."""
|
||||
|
||||
def __init__(self, *args, config):
|
||||
def __init__(self, *args):
|
||||
"""Initialize a Switch accessory object to represent a remote."""
|
||||
super().__init__(*args, category=CATEGORY_SWITCH)
|
||||
self._domain = split_entity_id(self.entity_id)[0]
|
||||
@ -33,9 +33,9 @@ class Switch(HomeAccessory):
|
||||
_LOGGER.debug('%s: Set switch state to %s',
|
||||
self.entity_id, value)
|
||||
self.flag_target_state = True
|
||||
params = {ATTR_ENTITY_ID: self.entity_id}
|
||||
service = SERVICE_TURN_ON if value else SERVICE_TURN_OFF
|
||||
self.hass.services.call(self._domain, service,
|
||||
{ATTR_ENTITY_ID: self.entity_id})
|
||||
self.hass.services.call(self._domain, service, params)
|
||||
|
||||
def update_state(self, new_state):
|
||||
"""Update switch state after state changed."""
|
||||
|
@ -4,22 +4,23 @@ import logging
|
||||
from pyhap.const import CATEGORY_THERMOSTAT
|
||||
|
||||
from homeassistant.components.climate import (
|
||||
ATTR_CURRENT_TEMPERATURE, ATTR_TEMPERATURE,
|
||||
ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW,
|
||||
ATTR_OPERATION_MODE, ATTR_OPERATION_LIST,
|
||||
STATE_HEAT, STATE_COOL, STATE_AUTO, SUPPORT_ON_OFF,
|
||||
SUPPORT_TARGET_TEMPERATURE_HIGH, SUPPORT_TARGET_TEMPERATURE_LOW)
|
||||
ATTR_CURRENT_TEMPERATURE, ATTR_OPERATION_LIST, ATTR_OPERATION_MODE,
|
||||
ATTR_TEMPERATURE, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW,
|
||||
DOMAIN, SERVICE_SET_TEMPERATURE, SERVICE_SET_OPERATION_MODE, STATE_AUTO,
|
||||
STATE_COOL, STATE_HEAT, SUPPORT_ON_OFF, SUPPORT_TARGET_TEMPERATURE_HIGH,
|
||||
SUPPORT_TARGET_TEMPERATURE_LOW)
|
||||
from homeassistant.const import (
|
||||
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 .accessories import HomeAccessory, debounce
|
||||
from .accessories import debounce, HomeAccessory
|
||||
from .const import (
|
||||
SERV_THERMOSTAT, CHAR_CURRENT_HEATING_COOLING,
|
||||
CHAR_TARGET_HEATING_COOLING, CHAR_CURRENT_TEMPERATURE,
|
||||
CHAR_TARGET_TEMPERATURE, CHAR_TEMP_DISPLAY_UNITS,
|
||||
CHAR_COOLING_THRESHOLD_TEMPERATURE, CHAR_HEATING_THRESHOLD_TEMPERATURE)
|
||||
CHAR_COOLING_THRESHOLD_TEMPERATURE, CHAR_CURRENT_HEATING_COOLING,
|
||||
CHAR_CURRENT_TEMPERATURE, CHAR_TARGET_HEATING_COOLING,
|
||||
CHAR_HEATING_THRESHOLD_TEMPERATURE, CHAR_TARGET_TEMPERATURE,
|
||||
CHAR_TEMP_DISPLAY_UNITS, SERV_THERMOSTAT)
|
||||
from .util import temperature_to_homekit, temperature_to_states
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@ -38,7 +39,7 @@ SUPPORT_TEMP_RANGE = SUPPORT_TARGET_TEMPERATURE_LOW | \
|
||||
class Thermostat(HomeAccessory):
|
||||
"""Generate a Thermostat accessory for a climate."""
|
||||
|
||||
def __init__(self, *args, config):
|
||||
def __init__(self, *args):
|
||||
"""Initialize a Thermostat accessory object."""
|
||||
super().__init__(*args, category=CATEGORY_THERMOSTAT)
|
||||
self._unit = TEMP_CELSIUS
|
||||
@ -99,12 +100,13 @@ class Thermostat(HomeAccessory):
|
||||
if self.support_power_state is True:
|
||||
params = {ATTR_ENTITY_ID: self.entity_id}
|
||||
if hass_value == STATE_OFF:
|
||||
self.hass.services.call('climate', 'turn_off', params)
|
||||
self.hass.services.call(DOMAIN, SERVICE_TURN_OFF, params)
|
||||
return
|
||||
else:
|
||||
self.hass.services.call('climate', 'turn_on', params)
|
||||
self.hass.components.climate.set_operation_mode(
|
||||
operation_mode=hass_value, entity_id=self.entity_id)
|
||||
self.hass.services.call(DOMAIN, SERVICE_TURN_ON, params)
|
||||
params = {ATTR_ENTITY_ID: self.entity_id,
|
||||
ATTR_OPERATION_MODE: hass_value}
|
||||
self.hass.services.call(DOMAIN, SERVICE_SET_OPERATION_MODE, params)
|
||||
|
||||
@debounce
|
||||
def set_cooling_threshold(self, value):
|
||||
@ -113,11 +115,11 @@ class Thermostat(HomeAccessory):
|
||||
self.entity_id, value)
|
||||
self.coolingthresh_flag_target_state = True
|
||||
low = self.char_heating_thresh_temp.value
|
||||
low = temperature_to_states(low, self._unit)
|
||||
value = temperature_to_states(value, self._unit)
|
||||
self.hass.components.climate.set_temperature(
|
||||
entity_id=self.entity_id, target_temp_high=value,
|
||||
target_temp_low=low)
|
||||
params = {
|
||||
ATTR_ENTITY_ID: self.entity_id,
|
||||
ATTR_TARGET_TEMP_HIGH: temperature_to_states(value, self._unit),
|
||||
ATTR_TARGET_TEMP_LOW: temperature_to_states(low, self._unit)}
|
||||
self.hass.services.call(DOMAIN, SERVICE_SET_TEMPERATURE, params)
|
||||
|
||||
@debounce
|
||||
def set_heating_threshold(self, value):
|
||||
@ -125,13 +127,12 @@ class Thermostat(HomeAccessory):
|
||||
_LOGGER.debug('%s: Set heating threshold temperature to %.2f°C',
|
||||
self.entity_id, value)
|
||||
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 = temperature_to_states(high, self._unit)
|
||||
value = temperature_to_states(value, self._unit)
|
||||
self.hass.components.climate.set_temperature(
|
||||
entity_id=self.entity_id, target_temp_high=high,
|
||||
target_temp_low=value)
|
||||
params = {
|
||||
ATTR_ENTITY_ID: self.entity_id,
|
||||
ATTR_TARGET_TEMP_HIGH: temperature_to_states(high, self._unit),
|
||||
ATTR_TARGET_TEMP_LOW: temperature_to_states(value, self._unit)}
|
||||
self.hass.services.call(DOMAIN, SERVICE_SET_TEMPERATURE, params)
|
||||
|
||||
@debounce
|
||||
def set_target_temperature(self, value):
|
||||
@ -139,9 +140,10 @@ class Thermostat(HomeAccessory):
|
||||
_LOGGER.debug('%s: Set target temperature to %.2f°C',
|
||||
self.entity_id, value)
|
||||
self.temperature_flag_target_state = True
|
||||
value = temperature_to_states(value, self._unit)
|
||||
self.hass.components.climate.set_temperature(
|
||||
temperature=value, entity_id=self.entity_id)
|
||||
params = {
|
||||
ATTR_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):
|
||||
"""Update security state after state changed."""
|
||||
|
@ -5,7 +5,7 @@ import voluptuous as vol
|
||||
|
||||
from homeassistant.core import split_entity_id
|
||||
from homeassistant.const import (
|
||||
ATTR_CODE, TEMP_CELSIUS)
|
||||
ATTR_CODE, CONF_NAME, TEMP_CELSIUS)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
import homeassistant.util.temperature as temp_util
|
||||
from .const import HOMEKIT_NOTIFY_ID
|
||||
@ -16,16 +16,21 @@ _LOGGER = logging.getLogger(__name__)
|
||||
def validate_entity_config(values):
|
||||
"""Validate config entry for CONF_ENTITY."""
|
||||
entities = {}
|
||||
for key, config in values.items():
|
||||
entity = cv.entity_id(key)
|
||||
for entity_id, config in values.items():
|
||||
entity = cv.entity_id(entity_id)
|
||||
params = {}
|
||||
if not isinstance(config, dict):
|
||||
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)
|
||||
|
||||
if domain == 'alarm_control_panel':
|
||||
if domain in ('alarm_control_panel', 'lock'):
|
||||
code = config.get(ATTR_CODE)
|
||||
params[ATTR_CODE] = cv.string(code) if code else None
|
||||
|
||||
@ -33,9 +38,9 @@ def validate_entity_config(values):
|
||||
return entities
|
||||
|
||||
|
||||
def show_setup_message(hass, bridge):
|
||||
def show_setup_message(hass, pincode):
|
||||
"""Display persistent notification with setup information."""
|
||||
pin = bridge.pincode.decode()
|
||||
pin = pincode.decode()
|
||||
_LOGGER.info('Pincode: %s', pin)
|
||||
message = 'To setup Home Assistant in the Home App, enter the ' \
|
||||
'following code:\n### {}'.format(pin)
|
||||
|
@ -13,17 +13,19 @@ import socket
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import (
|
||||
EVENT_HOMEASSISTANT_STOP, CONF_USERNAME, CONF_PASSWORD, CONF_PLATFORM,
|
||||
CONF_HOSTS, CONF_HOST, ATTR_ENTITY_ID, STATE_UNKNOWN)
|
||||
ATTR_ENTITY_ID, ATTR_NAME, CONF_HOST, CONF_HOSTS, CONF_PASSWORD,
|
||||
CONF_PLATFORM, CONF_USERNAME, EVENT_HOMEASSISTANT_STOP, STATE_UNKNOWN)
|
||||
from homeassistant.helpers import discovery
|
||||
from homeassistant.helpers.entity import Entity
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.loader import bind_hass
|
||||
|
||||
REQUIREMENTS = ['pyhomematic==0.1.42']
|
||||
DOMAIN = 'homematic'
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DOMAIN = 'homematic'
|
||||
|
||||
SCAN_INTERVAL_HUB = timedelta(seconds=300)
|
||||
SCAN_INTERVAL_VARIABLES = timedelta(seconds=30)
|
||||
|
||||
@ -38,7 +40,6 @@ DISCOVER_LOCKS = 'homematic.locks'
|
||||
ATTR_DISCOVER_DEVICES = 'devices'
|
||||
ATTR_PARAM = 'param'
|
||||
ATTR_CHANNEL = 'channel'
|
||||
ATTR_NAME = 'name'
|
||||
ATTR_ADDRESS = 'address'
|
||||
ATTR_VALUE = 'value'
|
||||
ATTR_INTERFACE = 'interface'
|
||||
@ -70,7 +71,7 @@ HM_DEVICE_TYPES = {
|
||||
'TemperatureSensor', 'CO2Sensor', 'IPSwitchPowermeter', 'HMWIOSwitch',
|
||||
'FillingLevel', 'ValveDrive', 'EcoLogic', 'IPThermostatWall',
|
||||
'IPSmoke', 'RFSiren', 'PresenceIP', 'IPAreaThermostat',
|
||||
'IPWeatherSensor'],
|
||||
'IPWeatherSensor', 'RotaryHandleSensorIP'],
|
||||
DISCOVER_CLIMATE: [
|
||||
'Thermostat', 'ThermostatWall', 'MAXThermostat', 'ThermostatWall2',
|
||||
'MAXWallThermostat', 'IPThermostat', 'IPThermostatWall',
|
||||
@ -97,6 +98,7 @@ HM_ATTRIBUTE_SUPPORT = {
|
||||
'LOWBAT': ['battery', {0: 'High', 1: 'Low'}],
|
||||
'LOW_BAT': ['battery', {0: 'High', 1: 'Low'}],
|
||||
'ERROR': ['sabotage', {0: 'No', 1: 'Yes'}],
|
||||
'SABOTAGE': ['sabotage', {0: 'No', 1: 'Yes'}],
|
||||
'RSSI_DEVICE': ['rssi', {}],
|
||||
'VALVE_STATE': ['valve', {}],
|
||||
'BATTERY_STATE': ['battery', {}],
|
||||
|
@ -24,7 +24,10 @@ _LOGGER = logging.getLogger(__name__)
|
||||
DOMAIN = 'homematicip_cloud'
|
||||
|
||||
COMPONENTS = [
|
||||
'sensor'
|
||||
'sensor',
|
||||
'binary_sensor',
|
||||
'switch',
|
||||
'light'
|
||||
]
|
||||
|
||||
CONF_NAME = 'name'
|
||||
|
@ -81,7 +81,12 @@ async def async_validate_auth_header(api_password, request):
|
||||
if hdrs.AUTHORIZATION not in request.headers:
|
||||
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':
|
||||
decoded = base64.b64decode(auth_val).decode('utf-8')
|
||||
|
@ -51,12 +51,6 @@ class HomeAssistantView(object):
|
||||
data['code'] = message_code
|
||||
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):
|
||||
"""Register the view with a router."""
|
||||
assert self.url is not None, 'No url set for view'
|
||||
|
@ -10,14 +10,14 @@ import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.core import callback
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
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.loader import bind_hass
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
from homeassistant.loader import bind_hass
|
||||
from homeassistant.util.async_ import run_callback_threadsafe
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@ -42,7 +42,6 @@ ATTR_CONFIDENCE = 'confidence'
|
||||
ATTR_FACES = 'faces'
|
||||
ATTR_GENDER = 'gender'
|
||||
ATTR_GLASSES = 'glasses'
|
||||
ATTR_NAME = 'name'
|
||||
ATTR_MOTION = 'motion'
|
||||
ATTR_TOTAL_FACES = 'total_faces'
|
||||
|
||||
@ -60,7 +59,7 @@ SOURCE_SCHEMA = vol.Schema({
|
||||
PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_SOURCE): vol.All(cv.ensure_list, [SOURCE_SCHEMA]),
|
||||
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({
|
||||
@ -77,7 +76,7 @@ def scan(hass, entity_id=None):
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup(hass, config):
|
||||
"""Set up image processing."""
|
||||
"""Set up the image processing."""
|
||||
component = EntityComponent(_LOGGER, DOMAIN, hass, SCAN_INTERVAL)
|
||||
|
||||
yield from component.async_setup(config)
|
||||
|
110
homeassistant/components/image_processing/facebox.py
Normal file
110
homeassistant/components/image_processing/facebox.py
Normal 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,
|
||||
}
|
@ -9,12 +9,12 @@ import logging
|
||||
|
||||
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.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
|
||||
|
||||
DEPENDENCIES = ['microsoft_face']
|
||||
|
@ -9,12 +9,13 @@ import logging
|
||||
|
||||
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.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
|
||||
|
||||
DEPENDENCIES = ['microsoft_face']
|
||||
|
@ -17,7 +17,7 @@ import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers import discovery
|
||||
from homeassistant.helpers.entity import Entity
|
||||
|
||||
REQUIREMENTS = ['insteonplm==0.9.1']
|
||||
REQUIREMENTS = ['insteonplm==0.9.2']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
@ -14,7 +14,7 @@ delete_all_link:
|
||||
description: All-Link group number.
|
||||
example: 1
|
||||
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:
|
||||
entity_id:
|
||||
description: Name of the device to print
|
||||
|
@ -13,7 +13,7 @@ import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.discovery import load_platform
|
||||
from homeassistant.helpers.entity import Entity
|
||||
|
||||
REQUIREMENTS = ['pyota==2.0.4']
|
||||
REQUIREMENTS = ['pyota==2.0.5']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
@ -202,7 +202,7 @@ def _check_for_uom_id(hass: HomeAssistant, node,
|
||||
node_uom = set(map(str.lower, node.uom))
|
||||
|
||||
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)
|
||||
return True
|
||||
else:
|
||||
|
319
homeassistant/components/konnected.py
Normal file
319
homeassistant/components/konnected.py
Normal 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')
|
@ -222,27 +222,34 @@ class FluxLight(Light):
|
||||
effect = kwargs.get(ATTR_EFFECT)
|
||||
white = kwargs.get(ATTR_WHITE_VALUE)
|
||||
|
||||
# color change only
|
||||
if rgb is not None:
|
||||
self._bulb.setRgb(*tuple(rgb), brightness=self.brightness)
|
||||
# Show warning if effect set with rgb, brightness, or white level
|
||||
if effect and (brightness or white or rgb):
|
||||
_LOGGER.warning("RGB, brightness and white level are ignored when"
|
||||
" an effect is specified for a flux bulb")
|
||||
|
||||
# brightness change only
|
||||
elif brightness is not None:
|
||||
(red, green, blue) = self._bulb.getRgb()
|
||||
self._bulb.setRgb(red, green, blue, brightness=brightness)
|
||||
|
||||
# random color effect
|
||||
elif effect == EFFECT_RANDOM:
|
||||
# Random color effect
|
||||
if effect == EFFECT_RANDOM:
|
||||
self._bulb.setRgb(random.randint(0, 255),
|
||||
random.randint(0, 255),
|
||||
random.randint(0, 255))
|
||||
return
|
||||
|
||||
# effect selection
|
||||
# Effect selection
|
||||
elif effect in EFFECT_MAP:
|
||||
self._bulb.setPresetPattern(EFFECT_MAP[effect], 50)
|
||||
return
|
||||
|
||||
# white change only
|
||||
elif white is not None:
|
||||
# Preserve current brightness on color/white level change
|
||||
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)
|
||||
|
||||
def turn_off(self, **kwargs):
|
||||
|
76
homeassistant/components/light/homematicip_cloud.py
Normal file
76
homeassistant/components/light/homematicip_cloud.py
Normal 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)
|
@ -142,10 +142,9 @@ def state(new_state):
|
||||
from limitlessled.pipeline import Pipeline
|
||||
pipeline = Pipeline()
|
||||
transition_time = DEFAULT_TRANSITION
|
||||
# Stop any repeating pipeline.
|
||||
if self.repeating:
|
||||
self.repeating = False
|
||||
if self._effect == EFFECT_COLORLOOP:
|
||||
self.group.stop()
|
||||
self._effect = None
|
||||
# Set transition time.
|
||||
if ATTR_TRANSITION in kwargs:
|
||||
transition_time = int(kwargs[ATTR_TRANSITION])
|
||||
@ -183,11 +182,11 @@ class LimitlessLEDGroup(Light):
|
||||
|
||||
self.group = group
|
||||
self.config = config
|
||||
self.repeating = False
|
||||
self._is_on = False
|
||||
self._brightness = None
|
||||
self._temperature = None
|
||||
self._color = None
|
||||
self._effect = None
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_added_to_hass(self):
|
||||
@ -222,6 +221,9 @@ class LimitlessLEDGroup(Light):
|
||||
@property
|
||||
def brightness(self):
|
||||
"""Return the brightness property."""
|
||||
if self._effect == EFFECT_NIGHT:
|
||||
return 1
|
||||
|
||||
return self._brightness
|
||||
|
||||
@property
|
||||
@ -242,6 +244,9 @@ class LimitlessLEDGroup(Light):
|
||||
@property
|
||||
def hs_color(self):
|
||||
"""Return the color property."""
|
||||
if self._effect == EFFECT_NIGHT:
|
||||
return None
|
||||
|
||||
return self._color
|
||||
|
||||
@property
|
||||
@ -249,6 +254,11 @@ class LimitlessLEDGroup(Light):
|
||||
"""Flag supported features."""
|
||||
return self._supported
|
||||
|
||||
@property
|
||||
def effect(self):
|
||||
"""Return the current effect for this light."""
|
||||
return self._effect
|
||||
|
||||
@property
|
||||
def effect_list(self):
|
||||
"""Return the list of supported effects for this light."""
|
||||
@ -270,6 +280,7 @@ class LimitlessLEDGroup(Light):
|
||||
if kwargs.get(ATTR_EFFECT) == EFFECT_NIGHT:
|
||||
if EFFECT_NIGHT in self._effect_list:
|
||||
pipeline.night_light()
|
||||
self._effect = EFFECT_NIGHT
|
||||
return
|
||||
|
||||
pipeline.on()
|
||||
@ -314,7 +325,7 @@ class LimitlessLEDGroup(Light):
|
||||
if ATTR_EFFECT in kwargs and self._effect_list:
|
||||
if kwargs[ATTR_EFFECT] == EFFECT_COLORLOOP:
|
||||
from limitlessled.presets import COLORLOOP
|
||||
self.repeating = True
|
||||
self._effect = EFFECT_COLORLOOP
|
||||
pipeline.append(COLORLOOP)
|
||||
if kwargs[ATTR_EFFECT] == EFFECT_WHITE:
|
||||
pipeline.white()
|
||||
|
@ -4,7 +4,6 @@ Support for MQTT lights.
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/light.mqtt/
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
@ -17,12 +16,13 @@ from homeassistant.components.light import (
|
||||
SUPPORT_EFFECT, SUPPORT_COLOR, SUPPORT_WHITE_VALUE)
|
||||
from homeassistant.const import (
|
||||
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)
|
||||
from homeassistant.components.mqtt import (
|
||||
CONF_AVAILABILITY_TOPIC, CONF_COMMAND_TOPIC, CONF_PAYLOAD_AVAILABLE,
|
||||
CONF_PAYLOAD_NOT_AVAILABLE, CONF_QOS, CONF_RETAIN, CONF_STATE_TOPIC,
|
||||
MqttAvailability)
|
||||
from homeassistant.helpers.restore_state import async_get_last_state
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
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)
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
async def async_setup_platform(hass, config, async_add_devices,
|
||||
discovery_info=None):
|
||||
"""Set up a MQTT Light."""
|
||||
if discovery_info is not None:
|
||||
config = PLATFORM_SCHEMA(discovery_info)
|
||||
@ -213,10 +213,9 @@ class MqttLight(MqttAvailability, Light):
|
||||
self._supported_features |= (
|
||||
topic[CONF_XY_COMMAND_TOPIC] is not None and SUPPORT_COLOR)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_added_to_hass(self):
|
||||
async def async_added_to_hass(self):
|
||||
"""Subscribe to MQTT events."""
|
||||
yield from super().async_added_to_hass()
|
||||
await super().async_added_to_hass()
|
||||
|
||||
templates = {}
|
||||
for key, tpl in list(self._templates.items()):
|
||||
@ -226,6 +225,8 @@ class MqttLight(MqttAvailability, Light):
|
||||
tpl.hass = self.hass
|
||||
templates[key] = tpl.async_render_with_possible_json_value
|
||||
|
||||
last_state = await async_get_last_state(self.hass, self.entity_id)
|
||||
|
||||
@callback
|
||||
def state_received(topic, payload, qos):
|
||||
"""Handle new MQTT messages."""
|
||||
@ -237,9 +238,11 @@ class MqttLight(MqttAvailability, Light):
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
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._qos)
|
||||
elif self._optimistic and last_state:
|
||||
self._state = last_state.state == STATE_ON
|
||||
|
||||
@callback
|
||||
def brightness_received(topic, payload, qos):
|
||||
@ -250,10 +253,13 @@ class MqttLight(MqttAvailability, Light):
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
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],
|
||||
brightness_received, self._qos)
|
||||
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:
|
||||
self._brightness = 255
|
||||
else:
|
||||
@ -268,11 +274,14 @@ class MqttLight(MqttAvailability, Light):
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
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._qos)
|
||||
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)
|
||||
|
||||
@callback
|
||||
@ -282,11 +291,14 @@ class MqttLight(MqttAvailability, Light):
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
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],
|
||||
color_temp_received, self._qos)
|
||||
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
|
||||
else:
|
||||
self._color_temp = None
|
||||
@ -298,11 +310,14 @@ class MqttLight(MqttAvailability, Light):
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
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],
|
||||
effect_received, self._qos)
|
||||
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'
|
||||
else:
|
||||
self._effect = None
|
||||
@ -316,10 +331,13 @@ class MqttLight(MqttAvailability, Light):
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
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],
|
||||
white_value_received, self._qos)
|
||||
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:
|
||||
self._white_value = 255
|
||||
else:
|
||||
@ -334,11 +352,14 @@ class MqttLight(MqttAvailability, Light):
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
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._qos)
|
||||
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)
|
||||
|
||||
@property
|
||||
@ -396,8 +417,7 @@ class MqttLight(MqttAvailability, Light):
|
||||
"""Flag supported features."""
|
||||
return self._supported_features
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_turn_on(self, **kwargs):
|
||||
async def async_turn_on(self, **kwargs):
|
||||
"""Turn the device on.
|
||||
|
||||
This method is a coroutine.
|
||||
@ -517,8 +537,7 @@ class MqttLight(MqttAvailability, Light):
|
||||
if should_update:
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_turn_off(self, **kwargs):
|
||||
async def async_turn_off(self, **kwargs):
|
||||
"""Turn the device off.
|
||||
|
||||
This method is a coroutine.
|
||||
|
@ -18,7 +18,7 @@ from homeassistant.components.light import (
|
||||
SUPPORT_TRANSITION, SUPPORT_WHITE_VALUE)
|
||||
from homeassistant.components.light.mqtt import CONF_BRIGHTNESS_SCALE
|
||||
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)
|
||||
from homeassistant.components.mqtt import (
|
||||
CONF_AVAILABILITY_TOPIC, CONF_STATE_TOPIC, CONF_COMMAND_TOPIC,
|
||||
@ -26,6 +26,7 @@ from homeassistant.components.mqtt import (
|
||||
MqttAvailability)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.typing import HomeAssistantType, ConfigType
|
||||
from homeassistant.helpers.restore_state import async_get_last_state
|
||||
import homeassistant.util.color as color_util
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@ -177,6 +178,8 @@ class MqttJson(MqttAvailability, Light):
|
||||
"""Subscribe to MQTT events."""
|
||||
await super().async_added_to_hass()
|
||||
|
||||
last_state = await async_get_last_state(self.hass, self.entity_id)
|
||||
|
||||
@callback
|
||||
def state_received(topic, payload, qos):
|
||||
"""Handle new MQTT messages."""
|
||||
@ -260,6 +263,19 @@ class MqttJson(MqttAvailability, Light):
|
||||
self.hass, self._topic[CONF_STATE_TOPIC], state_received,
|
||||
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
|
||||
def brightness(self):
|
||||
"""Return the brightness of this light between 0..255."""
|
||||
|
@ -4,7 +4,6 @@ Support for MQTT Template lights.
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/light.mqtt_template/
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
import voluptuous as vol
|
||||
|
||||
@ -22,6 +21,7 @@ from homeassistant.components.mqtt import (
|
||||
MqttAvailability)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
import homeassistant.util.color as color_util
|
||||
from homeassistant.helpers.restore_state import async_get_last_state
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@ -66,8 +66,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
}).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema)
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
async def async_setup_platform(hass, config, async_add_devices,
|
||||
discovery_info=None):
|
||||
"""Set up a MQTT Template light."""
|
||||
if discovery_info is not None:
|
||||
config = PLATFORM_SCHEMA(discovery_info)
|
||||
@ -152,10 +152,11 @@ class MqttTemplate(MqttAvailability, Light):
|
||||
if tpl is not None:
|
||||
tpl.hass = hass
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_added_to_hass(self):
|
||||
async def async_added_to_hass(self):
|
||||
"""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
|
||||
def state_received(topic, payload, qos):
|
||||
@ -223,10 +224,23 @@ class MqttTemplate(MqttAvailability, Light):
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
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._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
|
||||
def brightness(self):
|
||||
"""Return the brightness of this light between 0..255."""
|
||||
@ -280,8 +294,7 @@ class MqttTemplate(MqttAvailability, Light):
|
||||
"""Return the current effect."""
|
||||
return self._effect
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_turn_on(self, **kwargs):
|
||||
async def async_turn_on(self, **kwargs):
|
||||
"""Turn the entity on.
|
||||
|
||||
This method is a coroutine.
|
||||
@ -339,8 +352,7 @@ class MqttTemplate(MqttAvailability, Light):
|
||||
if self._optimistic:
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_turn_off(self, **kwargs):
|
||||
async def async_turn_off(self, **kwargs):
|
||||
"""Turn the entity off.
|
||||
|
||||
This method is a coroutine.
|
||||
|
@ -130,7 +130,7 @@ class MySensorsLight(mysensors.MySensorsEntity, Light):
|
||||
self._white = white
|
||||
self._values[self.value_type] = hex_color
|
||||
|
||||
def turn_off(self, **kwargs):
|
||||
async def async_turn_off(self, **kwargs):
|
||||
"""Turn the device off."""
|
||||
value_type = self.gateway.const.SetReq.V_LIGHT
|
||||
self.gateway.set_child_value(
|
||||
@ -139,7 +139,7 @@ class MySensorsLight(mysensors.MySensorsEntity, Light):
|
||||
# optimistically assume that light has changed state
|
||||
self._state = False
|
||||
self._values[value_type] = STATE_OFF
|
||||
self.schedule_update_ha_state()
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
def _async_update_light(self):
|
||||
"""Update the controller with values from light child."""
|
||||
@ -171,12 +171,12 @@ class MySensorsLightDimmer(MySensorsLight):
|
||||
"""Flag supported features."""
|
||||
return SUPPORT_BRIGHTNESS
|
||||
|
||||
def turn_on(self, **kwargs):
|
||||
async def async_turn_on(self, **kwargs):
|
||||
"""Turn the device on."""
|
||||
self._turn_on_light()
|
||||
self._turn_on_dimmer(**kwargs)
|
||||
if self.gateway.optimistic:
|
||||
self.schedule_update_ha_state()
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
async def async_update(self):
|
||||
"""Update the controller with the latest value from a sensor."""
|
||||
@ -196,13 +196,13 @@ class MySensorsLightRGB(MySensorsLight):
|
||||
return SUPPORT_BRIGHTNESS | SUPPORT_COLOR
|
||||
return SUPPORT_COLOR
|
||||
|
||||
def turn_on(self, **kwargs):
|
||||
async def async_turn_on(self, **kwargs):
|
||||
"""Turn the device on."""
|
||||
self._turn_on_light()
|
||||
self._turn_on_dimmer(**kwargs)
|
||||
self._turn_on_rgb_and_w('%02x%02x%02x', **kwargs)
|
||||
if self.gateway.optimistic:
|
||||
self.schedule_update_ha_state()
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
async def async_update(self):
|
||||
"""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_MYSENSORS_RGBW
|
||||
|
||||
def turn_on(self, **kwargs):
|
||||
async def async_turn_on(self, **kwargs):
|
||||
"""Turn the device on."""
|
||||
self._turn_on_light()
|
||||
self._turn_on_dimmer(**kwargs)
|
||||
self._turn_on_rgb_and_w('%02x%02x%02x%02x', **kwargs)
|
||||
if self.gateway.optimistic:
|
||||
self.schedule_update_ha_state()
|
||||
self.async_schedule_update_ha_state()
|
||||
|
@ -92,6 +92,16 @@ class AuroraLight(Light):
|
||||
"""Return the list of supported effects."""
|
||||
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
|
||||
def name(self):
|
||||
"""Return the display name of this light."""
|
||||
|
@ -16,8 +16,6 @@ from homeassistant.util.color import \
|
||||
|
||||
DEPENDENCIES = ['wink']
|
||||
|
||||
SUPPORT_WINK = SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP | SUPPORT_COLOR
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the Wink lights."""
|
||||
@ -78,7 +76,14 @@ class WinkLight(WinkDevice, Light):
|
||||
@property
|
||||
def supported_features(self):
|
||||
"""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):
|
||||
"""Turn the switch on."""
|
||||
|
@ -6,7 +6,6 @@ at https://home-assistant.io/components/light.zha/
|
||||
"""
|
||||
import logging
|
||||
from homeassistant.components import light, zha
|
||||
from homeassistant.const import STATE_UNKNOWN
|
||||
import homeassistant.util.color as color_util
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@ -76,7 +75,7 @@ class Light(zha.Entity, light.Light):
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
"""Return true if entity is on."""
|
||||
if self._state == STATE_UNKNOWN:
|
||||
if self._state is None:
|
||||
return False
|
||||
return bool(self._state)
|
||||
|
||||
|
@ -11,7 +11,8 @@ import voluptuous as vol
|
||||
|
||||
from homeassistant.components.lock import LockDevice
|
||||
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
|
||||
|
||||
DEPENDENCIES = ['wink']
|
||||
@ -28,7 +29,6 @@ SERVICE_ADD_KEY = 'wink_add_new_lock_key_code'
|
||||
ATTR_ENABLED = 'enabled'
|
||||
ATTR_SENSITIVITY = 'sensitivity'
|
||||
ATTR_MODE = 'mode'
|
||||
ATTR_NAME = 'name'
|
||||
|
||||
ALARM_SENSITIVITY_MAP = {
|
||||
'low': 0.2,
|
||||
|
@ -4,44 +4,49 @@ Event parser and human readable log generator.
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/logbook/
|
||||
"""
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
from itertools import groupby
|
||||
import logging
|
||||
|
||||
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.http import HomeAssistantView
|
||||
from homeassistant.const import (
|
||||
EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, EVENT_STATE_CHANGED,
|
||||
STATE_NOT_HOME, STATE_OFF, STATE_ON, ATTR_HIDDEN, HTTP_BAD_REQUEST,
|
||||
EVENT_LOGBOOK_ENTRY)
|
||||
from homeassistant.core import State, split_entity_id, DOMAIN as HA_DOMAIN
|
||||
|
||||
DOMAIN = 'logbook'
|
||||
DEPENDENCIES = ['recorder', 'frontend']
|
||||
ATTR_DOMAIN, ATTR_ENTITY_ID, ATTR_HIDDEN, ATTR_NAME, CONF_EXCLUDE,
|
||||
CONF_INCLUDE, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP,
|
||||
EVENT_LOGBOOK_ENTRY, EVENT_STATE_CHANGED, HTTP_BAD_REQUEST, STATE_NOT_HOME,
|
||||
STATE_OFF, STATE_ON)
|
||||
from homeassistant.core import DOMAIN as HA_DOMAIN
|
||||
from homeassistant.core import State, callback, split_entity_id
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
import homeassistant.util.dt as dt_util
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_EXCLUDE = 'exclude'
|
||||
CONF_INCLUDE = 'include'
|
||||
CONF_ENTITIES = 'entities'
|
||||
ATTR_MESSAGE = 'message'
|
||||
|
||||
CONF_DOMAINS = 'domains'
|
||||
CONF_ENTITIES = 'entities'
|
||||
CONTINUOUS_DOMAINS = ['proximity', 'sensor']
|
||||
|
||||
DEPENDENCIES = ['recorder', 'frontend']
|
||||
|
||||
DOMAIN = 'logbook'
|
||||
|
||||
GROUP_BY_MINUTES = 15
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.Schema({
|
||||
CONF_EXCLUDE: vol.Schema({
|
||||
vol.Optional(CONF_ENTITIES, default=[]): cv.entity_ids,
|
||||
vol.Optional(CONF_DOMAINS, default=[]): vol.All(cv.ensure_list,
|
||||
[cv.string])
|
||||
vol.Optional(CONF_DOMAINS, default=[]):
|
||||
vol.All(cv.ensure_list, [cv.string])
|
||||
}),
|
||||
CONF_INCLUDE: vol.Schema({
|
||||
vol.Optional(CONF_ENTITIES, default=[]): cv.entity_ids,
|
||||
vol.Optional(CONF_DOMAINS, default=[]): vol.All(cv.ensure_list,
|
||||
[cv.string])
|
||||
vol.Optional(CONF_DOMAINS, default=[]):
|
||||
vol.All(cv.ensure_list, [cv.string])
|
||||
})
|
||||
}),
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
@ -51,15 +56,6 @@ ALL_EVENT_TYPES = [
|
||||
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({
|
||||
vol.Required(ATTR_NAME): cv.string,
|
||||
vol.Required(ATTR_MESSAGE): cv.template,
|
||||
|
@ -14,7 +14,7 @@ from homeassistant.components.media_player import (
|
||||
SERVICE_PLAY_MEDIA)
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['youtube_dl==2018.04.25']
|
||||
REQUIREMENTS = ['youtube_dl==2018.05.09']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
@ -19,7 +19,7 @@ from homeassistant.const import (
|
||||
CONF_PASSWORD)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['pycmus==0.1.0']
|
||||
REQUIREMENTS = ['pycmus==0.1.1']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
@ -24,7 +24,6 @@ _LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_SOURCES = 'sources'
|
||||
CONF_MAX_VOLUME = 'max_volume'
|
||||
CONF_ZONE2 = 'zone2'
|
||||
|
||||
DEFAULT_NAME = 'Onkyo Receiver'
|
||||
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.Optional(CONF_SOURCES, default=DEFAULT_SOURCES):
|
||||
{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):
|
||||
"""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:
|
||||
try:
|
||||
receiver = eiscp.eISCP(host)
|
||||
hosts.append(OnkyoDevice(
|
||||
eiscp.eISCP(host), config.get(CONF_SOURCES),
|
||||
receiver,
|
||||
config.get(CONF_SOURCES),
|
||||
name=config.get(CONF_NAME),
|
||||
max_volume=config.get(CONF_MAX_VOLUME),
|
||||
))
|
||||
KNOWN_HOSTS.append(host)
|
||||
|
||||
# Add Zone2 if configured
|
||||
if config.get(CONF_ZONE2):
|
||||
zones = determine_zones(receiver)
|
||||
|
||||
# Add Zone2 if available
|
||||
if zones["zone2"]:
|
||||
_LOGGER.debug("Setting up zone 2")
|
||||
hosts.append(OnkyoDeviceZone2(eiscp.eISCP(host),
|
||||
config.get(CONF_SOURCES),
|
||||
name=config.get(CONF_NAME) +
|
||||
" Zone 2"))
|
||||
hosts.append(OnkyoDeviceZone(
|
||||
"2", receiver,
|
||||
config.get(CONF_SOURCES),
|
||||
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:
|
||||
_LOGGER.error("Unable to connect to receiver at %s", host)
|
||||
else:
|
||||
@ -227,12 +264,17 @@ class OnkyoDevice(MediaPlayerDevice):
|
||||
self.command('input-selector {}'.format(source))
|
||||
|
||||
|
||||
class OnkyoDeviceZone2(OnkyoDevice):
|
||||
"""Representation of an Onkyo device's zone 2."""
|
||||
class OnkyoDeviceZone(OnkyoDevice):
|
||||
"""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):
|
||||
"""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:
|
||||
return
|
||||
@ -242,9 +284,10 @@ class OnkyoDeviceZone2(OnkyoDevice):
|
||||
self._pwstate = STATE_OFF
|
||||
return
|
||||
|
||||
volume_raw = self.command('zone2.volume=query')
|
||||
mute_raw = self.command('zone2.muting=query')
|
||||
current_source_raw = self.command('zone2.selector=query')
|
||||
volume_raw = self.command('zone{}.volume=query'.format(self._zone))
|
||||
mute_raw = self.command('zone{}.muting=query'.format(self._zone))
|
||||
current_source_raw = self.command(
|
||||
'zone{}.selector=query'.format(self._zone))
|
||||
|
||||
if not (volume_raw and mute_raw and current_source_raw):
|
||||
return
|
||||
@ -268,33 +311,33 @@ class OnkyoDeviceZone2(OnkyoDevice):
|
||||
|
||||
def turn_off(self):
|
||||
"""Turn the media player off."""
|
||||
self.command('zone2.power=standby')
|
||||
self.command('zone{}.power=standby'.format(self._zone))
|
||||
|
||||
def set_volume_level(self, volume):
|
||||
"""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):
|
||||
"""Increase volume by 1 step."""
|
||||
self.command('zone2.volume=level-up')
|
||||
self.command('zone{}.volume=level-up'.format(self._zone))
|
||||
|
||||
def volume_down(self):
|
||||
"""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):
|
||||
"""Mute (true) or unmute (false) media player."""
|
||||
if mute:
|
||||
self.command('zone2.muting=on')
|
||||
self.command('zone{}.muting=on'.format(self._zone))
|
||||
else:
|
||||
self.command('zone2.muting=off')
|
||||
self.command('zone{}.muting=off'.format(self._zone))
|
||||
|
||||
def turn_on(self):
|
||||
"""Turn the media player on."""
|
||||
self.command('zone2.power=on')
|
||||
self.command('zone{}.power=on'.format(self._zone))
|
||||
|
||||
def select_source(self, source):
|
||||
"""Set the input source."""
|
||||
if source in self._source_list:
|
||||
source = self._reverse_mapping[source]
|
||||
self.command('zone2.selector={}'.format(source))
|
||||
self.command('zone{}.selector={}'.format(self._zone, source))
|
||||
|
@ -146,6 +146,11 @@ class RokuDevice(MediaPlayerDevice):
|
||||
"""Flag media player features that are supported."""
|
||||
return SUPPORT_ROKU
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return a unique, HASS-friendly identifier for this entity."""
|
||||
return self.device_info.sernum
|
||||
|
||||
@property
|
||||
def media_content_type(self):
|
||||
"""Content type of current playing media."""
|
||||
|
@ -151,8 +151,8 @@ class SongpalDevice(MediaPlayerDevice):
|
||||
return
|
||||
|
||||
if len(volumes) > 1:
|
||||
_LOGGER.warning("Got %s volume controls, using the first one",
|
||||
volumes)
|
||||
_LOGGER.debug("Got %s volume controls, using the first one",
|
||||
volumes)
|
||||
|
||||
volume = volumes[0]
|
||||
_LOGGER.debug("Current volume: %s", volume)
|
||||
|
@ -682,11 +682,15 @@ class SonosDevice(MediaPlayerDevice):
|
||||
if group:
|
||||
# New group information is pushed
|
||||
coordinator_uid, *slave_uids = group.split(',')
|
||||
else:
|
||||
elif self.soco.group:
|
||||
# Use SoCo cache for existing topology
|
||||
coordinator_uid = self.soco.group.coordinator.uid
|
||||
slave_uids = [p.uid for p in self.soco.group.members
|
||||
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:
|
||||
sonos_group = []
|
||||
|
@ -30,7 +30,8 @@ from homeassistant.const import (
|
||||
SERVICE_MEDIA_PLAY, SERVICE_MEDIA_PLAY_PAUSE, SERVICE_MEDIA_PREVIOUS_TRACK,
|
||||
SERVICE_MEDIA_SEEK, SERVICE_TURN_OFF, SERVICE_TURN_ON, SERVICE_VOLUME_DOWN,
|
||||
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.service import async_call_from_config
|
||||
|
||||
@ -45,7 +46,7 @@ CONF_SERVICE_DATA = 'service_data'
|
||||
ATTR_DATA = 'data'
|
||||
CONF_STATE = 'state'
|
||||
|
||||
OFF_STATES = [STATE_IDLE, STATE_OFF]
|
||||
OFF_STATES = [STATE_IDLE, STATE_OFF, STATE_UNAVAILABLE]
|
||||
REQUIREMENTS = []
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
@ -222,7 +222,7 @@ class YamahaDevice(MediaPlayerDevice):
|
||||
|
||||
@property
|
||||
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)
|
||||
|
||||
@property
|
||||
|
@ -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
|
||||
https://home-assistant.io/components/microsoft_face/
|
||||
@ -13,7 +13,7 @@ from aiohttp.hdrs import CONTENT_TYPE
|
||||
import async_timeout
|
||||
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.helpers.aiohttp_client import async_get_clientsession
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
@ -22,28 +22,25 @@ from homeassistant.util import slugify
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DOMAIN = 'microsoft_face'
|
||||
DEPENDENCIES = ['camera']
|
||||
|
||||
FACE_API_URL = "api.cognitive.microsoft.com/face/v1.0/{0}"
|
||||
|
||||
DATA_MICROSOFT_FACE = 'microsoft_face'
|
||||
ATTR_CAMERA_ENTITY = 'camera_entity'
|
||||
ATTR_GROUP = 'group'
|
||||
ATTR_PERSON = 'person'
|
||||
|
||||
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_DELETE_GROUP = 'delete_group'
|
||||
SERVICE_TRAIN_GROUP = 'train_group'
|
||||
SERVICE_CREATE_PERSON = 'create_person'
|
||||
SERVICE_DELETE_GROUP = 'delete_group'
|
||||
SERVICE_DELETE_PERSON = 'delete_person'
|
||||
SERVICE_FACE_PERSON = 'face_person'
|
||||
|
||||
ATTR_GROUP = 'group'
|
||||
ATTR_PERSON = 'person'
|
||||
ATTR_CAMERA_ENTITY = 'camera_entity'
|
||||
ATTR_NAME = 'name'
|
||||
|
||||
DEFAULT_TIMEOUT = 10
|
||||
SERVICE_TRAIN_GROUP = 'train_group'
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.Schema({
|
||||
@ -111,7 +108,7 @@ def face_person(hass, group, person, camera_entity):
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup(hass, config):
|
||||
"""Set up microsoft face."""
|
||||
"""Set up Microsoft Face."""
|
||||
entities = {}
|
||||
face = MicrosoftFace(
|
||||
hass,
|
||||
|
@ -16,7 +16,7 @@ from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers import discovery
|
||||
from homeassistant.util import Throttle
|
||||
|
||||
REQUIREMENTS = ["mychevy==0.1.1"]
|
||||
REQUIREMENTS = ["mychevy==0.4.0"]
|
||||
|
||||
DOMAIN = 'mychevy'
|
||||
UPDATE_TOPIC = DOMAIN
|
||||
@ -73,9 +73,6 @@ def setup(hass, base_config):
|
||||
hass.data[DOMAIN] = MyChevyHub(mc.MyChevy(email, password), hass)
|
||||
hass.data[DOMAIN].start()
|
||||
|
||||
discovery.load_platform(hass, 'sensor', DOMAIN, {}, config)
|
||||
discovery.load_platform(hass, 'binary_sensor', DOMAIN, {}, config)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@ -98,8 +95,9 @@ class MyChevyHub(threading.Thread):
|
||||
super().__init__()
|
||||
self._client = client
|
||||
self.hass = hass
|
||||
self.car = None
|
||||
self.cars = []
|
||||
self.status = None
|
||||
self.ready = False
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
||||
def update(self):
|
||||
@ -109,7 +107,22 @@ class MyChevyHub(threading.Thread):
|
||||
(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):
|
||||
"""Thread run loop."""
|
||||
|
@ -4,6 +4,7 @@ Connect to a MySensors gateway via pymysensors API.
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/mysensors/
|
||||
"""
|
||||
import asyncio
|
||||
from collections import defaultdict
|
||||
import logging
|
||||
import os
|
||||
@ -11,22 +12,23 @@ import socket
|
||||
import sys
|
||||
from timeit import default_timer as timer
|
||||
|
||||
import async_timeout
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.mqtt import (
|
||||
valid_publish_topic, valid_subscribe_topic)
|
||||
from homeassistant.const import (
|
||||
ATTR_BATTERY_LEVEL, CONF_NAME, CONF_OPTIMISTIC, EVENT_HOMEASSISTANT_START,
|
||||
EVENT_HOMEASSISTANT_STOP, STATE_OFF, STATE_ON)
|
||||
ATTR_BATTERY_LEVEL, CONF_NAME, CONF_OPTIMISTIC, EVENT_HOMEASSISTANT_STOP,
|
||||
STATE_OFF, STATE_ON)
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers import discovery
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.dispatcher import (
|
||||
async_dispatcher_connect, dispatcher_send)
|
||||
async_dispatcher_connect, async_dispatcher_send)
|
||||
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__)
|
||||
|
||||
@ -56,9 +58,11 @@ DEFAULT_TCP_PORT = 5003
|
||||
DEFAULT_VERSION = '1.4'
|
||||
DOMAIN = 'mysensors'
|
||||
|
||||
GATEWAY_READY_TIMEOUT = 15.0
|
||||
MQTT_COMPONENT = 'mqtt'
|
||||
MYSENSORS_GATEWAYS = 'mysensors_gateways'
|
||||
MYSENSORS_PLATFORM_DEVICES = 'mysensors_devices_{}'
|
||||
MYSENSORS_GATEWAY_READY = 'mysensors_gateway_ready_{}'
|
||||
PLATFORM = 'platform'
|
||||
SCHEMA = 'schema'
|
||||
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."""
|
||||
import mysensors.mysensors as mysensors
|
||||
|
||||
version = config[DOMAIN].get(CONF_VERSION)
|
||||
persistence = config[DOMAIN].get(CONF_PERSISTENCE)
|
||||
|
||||
def setup_gateway(device, persistence_file, baud_rate, tcp_port, in_prefix,
|
||||
out_prefix):
|
||||
async def setup_gateway(
|
||||
device, persistence_file, baud_rate, tcp_port, in_prefix,
|
||||
out_prefix):
|
||||
"""Return gateway after setup of the gateway."""
|
||||
if device == MQTT_COMPONENT:
|
||||
if not setup_component(hass, MQTT_COMPONENT, config):
|
||||
return
|
||||
if not await async_setup_component(hass, MQTT_COMPONENT, config):
|
||||
return None
|
||||
mqtt = hass.components.mqtt
|
||||
retain = config[DOMAIN].get(CONF_RETAIN)
|
||||
|
||||
def pub_callback(topic, payload, qos, retain):
|
||||
"""Call MQTT publish function."""
|
||||
mqtt.publish(topic, payload, qos, retain)
|
||||
mqtt.async_publish(topic, payload, qos, retain)
|
||||
|
||||
def sub_callback(topic, sub_cb, qos):
|
||||
"""Call MQTT subscribe function."""
|
||||
mqtt.subscribe(topic, sub_cb, qos)
|
||||
gateway = mysensors.MQTTGateway(
|
||||
pub_callback, sub_callback,
|
||||
@callback
|
||||
def internal_callback(*args):
|
||||
"""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,
|
||||
persistence_file=persistence_file,
|
||||
protocol_version=version, in_prefix=in_prefix,
|
||||
out_prefix=out_prefix, retain=retain)
|
||||
protocol_version=version)
|
||||
else:
|
||||
try:
|
||||
is_serial_port(device)
|
||||
gateway = mysensors.SerialGateway(
|
||||
device, event_callback=None, persistence=persistence,
|
||||
await hass.async_add_job(is_serial_port, device)
|
||||
gateway = mysensors.AsyncSerialGateway(
|
||||
device, baud=baud_rate, loop=hass.loop,
|
||||
event_callback=None, persistence=persistence,
|
||||
persistence_file=persistence_file,
|
||||
protocol_version=version, baud=baud_rate)
|
||||
protocol_version=version)
|
||||
except vol.Invalid:
|
||||
try:
|
||||
socket.getaddrinfo(device, None)
|
||||
# valid ip address
|
||||
gateway = mysensors.TCPGateway(
|
||||
device, event_callback=None, persistence=persistence,
|
||||
persistence_file=persistence_file,
|
||||
protocol_version=version, port=tcp_port)
|
||||
except OSError:
|
||||
# invalid ip address
|
||||
return
|
||||
gateway = mysensors.AsyncTCPGateway(
|
||||
device, port=tcp_port, loop=hass.loop, event_callback=None,
|
||||
persistence=persistence, persistence_file=persistence_file,
|
||||
protocol_version=version)
|
||||
gateway.metric = hass.config.units.is_metric
|
||||
gateway.optimistic = config[DOMAIN].get(CONF_OPTIMISTIC)
|
||||
gateway.device = device
|
||||
gateway.event_callback = gw_callback_factory(hass)
|
||||
|
||||
def gw_start(event):
|
||||
"""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)
|
||||
if persistence:
|
||||
await gateway.start_persistence()
|
||||
|
||||
return gateway
|
||||
|
||||
@ -357,12 +356,12 @@ def setup(hass, config):
|
||||
tcp_port = gway.get(CONF_TCP_PORT)
|
||||
in_prefix = gway.get(CONF_TOPIC_IN_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,
|
||||
out_prefix)
|
||||
if ready_gateway is not None:
|
||||
ready_gateway.nodes_config = gway.get(CONF_NODES)
|
||||
gateways[id(ready_gateway)] = ready_gateway
|
||||
if gateway is not None:
|
||||
gateway.nodes_config = gway.get(CONF_NODES)
|
||||
gateways[id(gateway)] = gateway
|
||||
|
||||
if not gateways:
|
||||
_LOGGER.error(
|
||||
@ -371,9 +370,65 @@ def setup(hass, config):
|
||||
|
||||
hass.data[MYSENSORS_GATEWAYS] = gateways
|
||||
|
||||
hass.async_add_job(finish_setup(hass, gateways))
|
||||
|
||||
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):
|
||||
"""Validate that a child has the correct values according to schema.
|
||||
|
||||
@ -431,14 +486,18 @@ def validate_child(gateway, node_id, child):
|
||||
return validated
|
||||
|
||||
|
||||
@callback
|
||||
def discover_mysensors_platform(hass, platform, new_devices):
|
||||
"""Discover a MySensors platform."""
|
||||
discovery.load_platform(
|
||||
hass, platform, DOMAIN, {ATTR_DEVICES: new_devices, CONF_NAME: DOMAIN})
|
||||
task = hass.async_add_job(discovery.async_load_platform(
|
||||
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."""
|
||||
tasks = []
|
||||
new_devices = defaultdict(list)
|
||||
for node_id in gateway.sensors:
|
||||
node = gateway.sensors[node_id]
|
||||
@ -447,7 +506,9 @@ def discover_persistent_devices(hass, gateway):
|
||||
for platform, dev_ids in validated.items():
|
||||
new_devices[platform].extend(dev_ids)
|
||||
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):
|
||||
@ -459,14 +520,18 @@ def get_mysensors_devices(hass, domain):
|
||||
|
||||
def gw_callback_factory(hass):
|
||||
"""Return a new callback for the gateway."""
|
||||
@callback
|
||||
def mysensors_callback(msg):
|
||||
"""Handle messages from a MySensors gateway."""
|
||||
start = timer()
|
||||
_LOGGER.debug(
|
||||
"Node update: node %s child %s", msg.node_id, msg.child_id)
|
||||
|
||||
child = msg.gateway.sensors[msg.node_id].children.get(msg.child_id)
|
||||
if child is None:
|
||||
set_gateway_ready(hass, msg)
|
||||
|
||||
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)
|
||||
return
|
||||
|
||||
@ -489,7 +554,7 @@ def gw_callback_factory(hass):
|
||||
# Only one signal per device is needed.
|
||||
# A device can have multiple platforms, ie multiple schemas.
|
||||
# FOR LATER: Add timer to not signal if another update comes in.
|
||||
dispatcher_send(hass, signal)
|
||||
async_dispatcher_send(hass, signal)
|
||||
end = timer()
|
||||
if end - start > 0.1:
|
||||
_LOGGER.debug(
|
||||
|
@ -12,8 +12,8 @@ import voluptuous as vol
|
||||
from homeassistant.helpers.event import track_state_change
|
||||
from homeassistant.config import load_yaml_config_file
|
||||
from homeassistant.components.notify import (
|
||||
ATTR_TARGET, ATTR_DATA, BaseNotificationService, DOMAIN)
|
||||
from homeassistant.const import CONF_NAME, CONF_PLATFORM
|
||||
ATTR_TARGET, ATTR_DATA, BaseNotificationService, DOMAIN, PLATFORM_SCHEMA)
|
||||
from homeassistant.const import CONF_NAME, CONF_PLATFORM, ATTR_NAME
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers import template as template_helper
|
||||
|
||||
@ -27,9 +27,8 @@ DEVICE_TRACKER_DOMAIN = 'device_tracker'
|
||||
SERVICE_REGISTER = 'apns_register'
|
||||
|
||||
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_NAME): cv.string,
|
||||
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):
|
||||
"""Initialize Apns Device."""
|
||||
"""Initialize APNS Device."""
|
||||
self.device_push_id = push_id
|
||||
self.device_name = name
|
||||
self.tracking_id = tracking_device_id
|
||||
@ -104,7 +103,7 @@ class ApnsDevice(object):
|
||||
|
||||
@property
|
||||
def disabled(self):
|
||||
"""Return the ."""
|
||||
"""Return the state of the service."""
|
||||
return self.device_disabled
|
||||
|
||||
def disable(self):
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user