Black format code

This commit is contained in:
Daniel Iversen 2018-09-15 14:38:18 +02:00
parent 34deaf8849
commit d732a7e670
1368 changed files with 69529 additions and 53340 deletions

View file

@ -25,6 +25,7 @@ def attempt_use_uvloop() -> None:
try:
import uvloop
asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
except ImportError:
pass
@ -33,28 +34,40 @@ def attempt_use_uvloop() -> None:
def validate_python() -> None:
"""Validate that the right Python version is running."""
if sys.version_info[:3] < REQUIRED_PYTHON_VER:
print("Home Assistant requires at least Python {}.{}.{}".format(
*REQUIRED_PYTHON_VER))
print(
"Home Assistant requires at least Python {}.{}.{}".format(
*REQUIRED_PYTHON_VER
)
)
sys.exit(1)
def ensure_config_path(config_dir: str) -> None:
"""Validate the configuration directory."""
import homeassistant.config as config_util
lib_dir = os.path.join(config_dir, 'deps')
lib_dir = os.path.join(config_dir, "deps")
# Test if configuration directory exists
if not os.path.isdir(config_dir):
if config_dir != config_util.get_default_config_dir():
print(('Fatal Error: Specified configuration directory does '
'not exist {} ').format(config_dir))
print(
(
"Fatal Error: Specified configuration directory does "
"not exist {} "
).format(config_dir)
)
sys.exit(1)
try:
os.mkdir(config_dir)
except OSError:
print(('Fatal Error: Unable to create default configuration '
'directory {} ').format(config_dir))
print(
(
"Fatal Error: Unable to create default configuration "
"directory {} "
).format(config_dir)
)
sys.exit(1)
# Test if library directory exists
@ -62,18 +75,22 @@ def ensure_config_path(config_dir: str) -> None:
try:
os.mkdir(lib_dir)
except OSError:
print(('Fatal Error: Unable to create library '
'directory {} ').format(lib_dir))
print(
("Fatal Error: Unable to create library " "directory {} ").format(
lib_dir
)
)
sys.exit(1)
def ensure_config_file(config_dir: str) -> str:
"""Ensure configuration file exists."""
import homeassistant.config as config_util
config_path = config_util.ensure_config_exists(config_dir)
if config_path is None:
print('Error getting configuration path')
print("Error getting configuration path")
sys.exit(1)
return config_path
@ -82,71 +99,72 @@ def ensure_config_file(config_dir: str) -> str:
def get_arguments() -> argparse.Namespace:
"""Get parsed passed in arguments."""
import homeassistant.config as config_util
parser = argparse.ArgumentParser(
description="Home Assistant: Observe, Control, Automate.")
parser.add_argument('--version', action='version', version=__version__)
description="Home Assistant: Observe, Control, Automate."
)
parser.add_argument("--version", action="version", version=__version__)
parser.add_argument(
'-c', '--config',
metavar='path_to_config_dir',
"-c",
"--config",
metavar="path_to_config_dir",
default=config_util.get_default_config_dir(),
help="Directory that contains the Home Assistant configuration")
help="Directory that contains the Home Assistant configuration",
)
parser.add_argument(
'--demo-mode',
action='store_true',
help='Start Home Assistant in demo mode')
"--demo-mode", action="store_true", help="Start Home Assistant in demo mode"
)
parser.add_argument(
'--debug',
action='store_true',
help='Start Home Assistant in debug mode')
"--debug", action="store_true", help="Start Home Assistant in debug mode"
)
parser.add_argument(
'--open-ui',
action='store_true',
help='Open the webinterface in a browser')
"--open-ui", action="store_true", help="Open the webinterface in a browser"
)
parser.add_argument(
'--skip-pip',
action='store_true',
help='Skips pip install of required packages on startup')
"--skip-pip",
action="store_true",
help="Skips pip install of required packages on startup",
)
parser.add_argument(
'-v', '--verbose',
action='store_true',
help="Enable verbose logging to file.")
"-v", "--verbose", action="store_true", help="Enable verbose logging to file."
)
parser.add_argument(
'--pid-file',
metavar='path_to_pid_file',
"--pid-file",
metavar="path_to_pid_file",
default=None,
help='Path to PID file useful for running as daemon')
help="Path to PID file useful for running as daemon",
)
parser.add_argument(
'--log-rotate-days',
"--log-rotate-days",
type=int,
default=None,
help='Enables daily log rotation and keeps up to the specified days')
help="Enables daily log rotation and keeps up to the specified days",
)
parser.add_argument(
'--log-file',
"--log-file",
type=str,
default=None,
help='Log file to write to. If not set, CONFIG/home-assistant.log '
'is used')
help="Log file to write to. If not set, CONFIG/home-assistant.log " "is used",
)
parser.add_argument(
'--log-no-color',
action='store_true',
help="Disable color logs")
"--log-no-color", action="store_true", help="Disable color logs"
)
parser.add_argument(
'--runner',
action='store_true',
help='On restart exit with code {}'.format(RESTART_EXIT_CODE))
"--runner",
action="store_true",
help="On restart exit with code {}".format(RESTART_EXIT_CODE),
)
parser.add_argument(
'--script',
nargs=argparse.REMAINDER,
help='Run one of the embedded scripts')
"--script", nargs=argparse.REMAINDER, help="Run one of the embedded scripts"
)
if os.name == "posix":
parser.add_argument(
'--daemon',
action='store_true',
help='Run Home Assistant as daemon')
"--daemon", action="store_true", help="Run Home Assistant as daemon"
)
arguments = parser.parse_args()
if os.name != "posix" or arguments.debug or arguments.runner:
setattr(arguments, 'daemon', False)
setattr(arguments, "daemon", False)
return arguments
@ -167,8 +185,8 @@ def daemonize() -> None:
sys.exit(0)
# redirect standard file descriptors to devnull
infd = open(os.devnull, 'r')
outfd = open(os.devnull, 'a+')
infd = open(os.devnull, "r")
outfd = open(os.devnull, "a+")
sys.stdout.flush()
sys.stderr.flush()
os.dup2(infd.fileno(), sys.stdin.fileno())
@ -180,7 +198,7 @@ def check_pid(pid_file: str) -> None:
"""Check that Home Assistant is not already running."""
# Check pid file
try:
with open(pid_file, 'r') as file:
with open(pid_file, "r") as file:
pid = int(file.readline())
except IOError:
# PID File does not exist
@ -195,7 +213,7 @@ def check_pid(pid_file: str) -> None:
except OSError:
# PID does not exist
return
print('Fatal Error: HomeAssistant is already running.')
print("Fatal Error: HomeAssistant is already running.")
sys.exit(1)
@ -203,10 +221,10 @@ def write_pid(pid_file: str) -> None:
"""Create a PID File."""
pid = os.getpid()
try:
with open(pid_file, 'w') as file:
with open(pid_file, "w") as file:
file.write(str(pid))
except IOError:
print('Fatal Error: Unable to write pid file {}'.format(pid_file))
print("Fatal Error: Unable to write pid file {}".format(pid_file))
sys.exit(1)
@ -230,23 +248,21 @@ def closefds_osx(min_fd: int, max_fd: int) -> None:
def cmdline() -> List[str]:
"""Collect path and arguments to re-execute the current hass instance."""
if os.path.basename(sys.argv[0]) == '__main__.py':
if os.path.basename(sys.argv[0]) == "__main__.py":
modulepath = os.path.dirname(sys.argv[0])
os.environ['PYTHONPATH'] = os.path.dirname(modulepath)
return [sys.executable] + [arg for arg in sys.argv if
arg != '--daemon']
os.environ["PYTHONPATH"] = os.path.dirname(modulepath)
return [sys.executable] + [arg for arg in sys.argv if arg != "--daemon"]
return [arg for arg in sys.argv if arg != '--daemon']
return [arg for arg in sys.argv if arg != "--daemon"]
def setup_and_run_hass(config_dir: str,
args: argparse.Namespace) -> int:
def setup_and_run_hass(config_dir: str, args: argparse.Namespace) -> int:
"""Set up HASS and run."""
from homeassistant import bootstrap
# Run a simple daemon runner process on Windows to handle restarts
if os.name == 'nt' and '--runner' not in sys.argv:
nt_args = cmdline() + ['--runner']
if os.name == "nt" and "--runner" not in sys.argv:
nt_args = cmdline() + ["--runner"]
while True:
try:
subprocess.check_call(nt_args)
@ -256,21 +272,27 @@ def setup_and_run_hass(config_dir: str,
sys.exit(exc.returncode)
if args.demo_mode:
config = {
'frontend': {},
'demo': {}
} # type: Dict[str, Any]
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,
log_file=args.log_file, log_no_color=args.log_no_color)
config,
config_dir=config_dir,
verbose=args.verbose,
skip_pip=args.skip_pip,
log_rotate_days=args.log_rotate_days,
log_file=args.log_file,
log_no_color=args.log_no_color,
)
else:
config_file = ensure_config_file(config_dir)
print('Config directory:', config_dir)
print("Config directory:", config_dir)
hass = bootstrap.from_config_file(
config_file, verbose=args.verbose, skip_pip=args.skip_pip,
log_rotate_days=args.log_rotate_days, log_file=args.log_file,
log_no_color=args.log_no_color)
config_file,
verbose=args.verbose,
skip_pip=args.skip_pip,
log_rotate_days=args.log_rotate_days,
log_file=args.log_file,
log_no_color=args.log_no_color,
)
if hass is None:
return -1
@ -283,12 +305,14 @@ def setup_and_run_hass(config_dir: str,
"""Open the web interface in a browser."""
if hass.config.api is not None: # type: ignore
import webbrowser
webbrowser.open(hass.config.api.base_url) # type: ignore
run_callback_threadsafe(
hass.loop,
hass.bus.async_listen_once,
EVENT_HOMEASSISTANT_START, open_browser
EVENT_HOMEASSISTANT_START,
open_browser,
)
return hass.start()
@ -298,17 +322,17 @@ def try_to_restart() -> None:
"""Attempt to clean up state and start a new Home Assistant instance."""
# Things should be mostly shut down already at this point, now just try
# to clean up things that may have been left behind.
sys.stderr.write('Home Assistant attempting to restart.\n')
sys.stderr.write("Home Assistant attempting to restart.\n")
# Count remaining threads, ideally there should only be one non-daemonized
# thread left (which is us). Nothing we really do with it, but it might be
# useful when debugging shutdown/restart issues.
try:
nthreads = sum(thread.is_alive() and not thread.daemon
for thread in threading.enumerate())
nthreads = sum(
thread.is_alive() and not thread.daemon for thread in threading.enumerate()
)
if nthreads > 1:
sys.stderr.write(
"Found {} non-daemonic threads.\n".format(nthreads))
sys.stderr.write("Found {} non-daemonic threads.\n".format(nthreads))
# Somehow we sometimes seem to trigger an assertion in the python threading
# module. It seems we find threads that have no associated OS level thread
@ -322,7 +346,7 @@ def try_to_restart() -> None:
except ValueError:
max_fd = 256
if platform.system() == 'Darwin':
if platform.system() == "Darwin":
closefds_osx(3, max_fd)
else:
os.closerange(3, max_fd)
@ -341,7 +365,7 @@ def main() -> int:
validate_python()
monkey_patch_needed = sys.version_info[:3] < (3, 6, 3)
if monkey_patch_needed and os.environ.get('HASS_NO_MONKEY') != '1':
if monkey_patch_needed and os.environ.get("HASS_NO_MONKEY") != "1":
if sys.version_info[:2] >= (3, 6):
monkey_patch.disable_c_asyncio()
monkey_patch.patch_weakref_tasks()
@ -352,6 +376,7 @@ def main() -> int:
if args.script is not None:
from homeassistant import scripts
return scripts.run(args.script)
config_dir = os.path.join(os.getcwd(), args.config)

View file

@ -23,9 +23,10 @@ _ProviderDict = Dict[_ProviderKey, AuthProvider]
async def auth_manager_from_config(
hass: HomeAssistant,
provider_configs: List[Dict[str, Any]],
module_configs: List[Dict[str, Any]]) -> 'AuthManager':
hass: HomeAssistant,
provider_configs: List[Dict[str, Any]],
module_configs: List[Dict[str, Any]],
) -> "AuthManager":
"""Initialize an auth manager from config.
CORE_CONFIG_SCHEMA will make sure do duplicated auth providers or
@ -34,8 +35,11 @@ async def auth_manager_from_config(
store = auth_store.AuthStore(hass)
if provider_configs:
providers = await asyncio.gather(
*[auth_provider_from_config(hass, store, config)
for config in provider_configs])
*[
auth_provider_from_config(hass, store, config)
for config in provider_configs
]
)
else:
providers = ()
# So returned auth providers are in same order as config
@ -46,8 +50,8 @@ async def auth_manager_from_config(
if module_configs:
modules = await asyncio.gather(
*[auth_mfa_module_from_config(hass, config)
for config in module_configs])
*[auth_mfa_module_from_config(hass, config) for config in module_configs]
)
else:
modules = ()
# So returned auth modules are in same order as config
@ -62,17 +66,21 @@ async def auth_manager_from_config(
class AuthManager:
"""Manage the authentication for Home Assistant."""
def __init__(self, hass: HomeAssistant, store: auth_store.AuthStore,
providers: _ProviderDict, mfa_modules: _MfaModuleDict) \
-> None:
def __init__(
self,
hass: HomeAssistant,
store: auth_store.AuthStore,
providers: _ProviderDict,
mfa_modules: _MfaModuleDict,
) -> None:
"""Initialize the auth manager."""
self.hass = hass
self._store = store
self._providers = providers
self._mfa_modules = mfa_modules
self.login_flow = data_entry_flow.FlowManager(
hass, self._async_create_login_flow,
self._async_finish_login_flow)
hass, self._async_create_login_flow, self._async_finish_login_flow
)
@property
def active(self) -> bool:
@ -87,7 +95,7 @@ class AuthManager:
Should be removed when we removed legacy_api_password auth providers.
"""
for provider_type, _ in self._providers:
if provider_type == 'legacy_api_password':
if provider_type == "legacy_api_password":
return True
return False
@ -101,8 +109,7 @@ class AuthManager:
"""Return a list of available auth modules."""
return list(self._mfa_modules.values())
def get_auth_mfa_module(self, module_id: str) \
-> Optional[MultiFactorAuthModule]:
def get_auth_mfa_module(self, module_id: str) -> Optional[MultiFactorAuthModule]:
"""Return an multi-factor auth module, None if not found."""
return self._mfa_modules.get(module_id)
@ -115,7 +122,8 @@ class AuthManager:
return await self._store.async_get_user(user_id)
async def async_get_user_by_credentials(
self, credentials: models.Credentials) -> Optional[models.User]:
self, credentials: models.Credentials
) -> Optional[models.User]:
"""Get a user by credential, return None if not found."""
for user in await self.async_get_users():
for creds in user.credentials:
@ -127,49 +135,43 @@ class AuthManager:
async def async_create_system_user(self, name: str) -> models.User:
"""Create a system user."""
return await self._store.async_create_user(
name=name,
system_generated=True,
is_active=True,
name=name, system_generated=True, is_active=True
)
async def async_create_user(self, name: str) -> models.User:
"""Create a user."""
kwargs = {
'name': name,
'is_active': True,
} # type: Dict[str, Any]
kwargs = {"name": name, "is_active": True} # type: Dict[str, Any]
if await self._user_should_be_owner():
kwargs['is_owner'] = True
kwargs["is_owner"] = True
return await self._store.async_create_user(**kwargs)
async def async_get_or_create_user(self, credentials: models.Credentials) \
-> models.User:
async def async_get_or_create_user(
self, credentials: models.Credentials
) -> models.User:
"""Get or create a user."""
if not credentials.is_new:
user = await self.async_get_user_by_credentials(credentials)
if user is None:
raise ValueError('Unable to find the user.')
raise ValueError("Unable to find the user.")
else:
return user
auth_provider = self._async_get_auth_provider(credentials)
if auth_provider is None:
raise RuntimeError('Credential with unknown provider encountered')
raise RuntimeError("Credential with unknown provider encountered")
info = await auth_provider.async_user_meta_for_credentials(
credentials)
info = await auth_provider.async_user_meta_for_credentials(credentials)
return await self._store.async_create_user(
credentials=credentials,
name=info.name,
is_active=info.is_active,
credentials=credentials, name=info.name, is_active=info.is_active
)
async def async_link_user(self, user: models.User,
credentials: models.Credentials) -> None:
async def async_link_user(
self, user: models.User, credentials: models.Credentials
) -> None:
"""Link credentials to an existing user."""
await self._store.async_link_user(user, credentials)
@ -192,47 +194,50 @@ class AuthManager:
async def async_deactivate_user(self, user: models.User) -> None:
"""Deactivate a user."""
if user.is_owner:
raise ValueError('Unable to deactive the owner')
raise ValueError("Unable to deactive the owner")
await self._store.async_deactivate_user(user)
async def async_remove_credentials(
self, credentials: models.Credentials) -> None:
async def async_remove_credentials(self, credentials: models.Credentials) -> None:
"""Remove credentials."""
provider = self._async_get_auth_provider(credentials)
if (provider is not None and
hasattr(provider, 'async_will_remove_credentials')):
if provider is not None and hasattr(provider, "async_will_remove_credentials"):
# https://github.com/python/mypy/issues/1424
await provider.async_will_remove_credentials( # type: ignore
credentials)
await provider.async_will_remove_credentials(credentials) # type: ignore
await self._store.async_remove_credentials(credentials)
async def async_enable_user_mfa(self, user: models.User,
mfa_module_id: str, data: Any) -> None:
async def async_enable_user_mfa(
self, user: models.User, mfa_module_id: str, data: Any
) -> None:
"""Enable a multi-factor auth module for user."""
if user.system_generated:
raise ValueError('System generated users cannot enable '
'multi-factor auth module.')
raise ValueError(
"System generated users cannot enable " "multi-factor auth module."
)
module = self.get_auth_mfa_module(mfa_module_id)
if module is None:
raise ValueError('Unable find multi-factor auth module: {}'
.format(mfa_module_id))
raise ValueError(
"Unable find multi-factor auth module: {}".format(mfa_module_id)
)
await module.async_setup_user(user.id, data)
async def async_disable_user_mfa(self, user: models.User,
mfa_module_id: str) -> None:
async def async_disable_user_mfa(
self, user: models.User, mfa_module_id: str
) -> None:
"""Disable a multi-factor auth module for user."""
if user.system_generated:
raise ValueError('System generated users cannot disable '
'multi-factor auth module.')
raise ValueError(
"System generated users cannot disable " "multi-factor auth module."
)
module = self.get_auth_mfa_module(mfa_module_id)
if module is None:
raise ValueError('Unable find multi-factor auth module: {}'
.format(mfa_module_id))
raise ValueError(
"Unable find multi-factor auth module: {}".format(mfa_module_id)
)
await module.async_depose_user(user.id)
@ -245,20 +250,23 @@ class AuthManager:
return modules
async def async_create_refresh_token(
self, user: models.User, client_id: Optional[str] = None,
client_name: Optional[str] = None,
client_icon: Optional[str] = None,
token_type: Optional[str] = None,
access_token_expiration: timedelta = ACCESS_TOKEN_EXPIRATION) \
-> models.RefreshToken:
self,
user: models.User,
client_id: Optional[str] = None,
client_name: Optional[str] = None,
client_icon: Optional[str] = None,
token_type: Optional[str] = None,
access_token_expiration: timedelta = ACCESS_TOKEN_EXPIRATION,
) -> models.RefreshToken:
"""Create a new refresh token for a user."""
if not user.is_active:
raise ValueError('User is not active')
raise ValueError("User is not active")
if user.system_generated and client_id is not None:
raise ValueError(
'System generated users cannot have refresh tokens connected '
'to a client.')
"System generated users cannot have refresh tokens connected "
"to a client."
)
if token_type is None:
if user.system_generated:
@ -268,62 +276,77 @@ class AuthManager:
if user.system_generated != (token_type == models.TOKEN_TYPE_SYSTEM):
raise ValueError(
'System generated users can only have system type '
'refresh tokens')
"System generated users can only have system type " "refresh tokens"
)
if token_type == models.TOKEN_TYPE_NORMAL and client_id is None:
raise ValueError('Client is required to generate a refresh token.')
raise ValueError("Client is required to generate a refresh token.")
if (token_type == models.TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN and
client_name is None):
raise ValueError('Client_name is required for long-lived access '
'token')
if (
token_type == models.TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN
and client_name is None
):
raise ValueError("Client_name is required for long-lived access " "token")
if token_type == models.TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN:
for token in user.refresh_tokens.values():
if (token.client_name == client_name and token.token_type ==
models.TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN):
if (
token.client_name == client_name
and token.token_type == models.TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN
):
# Each client_name can only have one
# long_lived_access_token type of refresh token
raise ValueError('{} already exists'.format(client_name))
raise ValueError("{} already exists".format(client_name))
return await self._store.async_create_refresh_token(
user, client_id, client_name, client_icon,
token_type, access_token_expiration)
user,
client_id,
client_name,
client_icon,
token_type,
access_token_expiration,
)
async def async_get_refresh_token(
self, token_id: str) -> Optional[models.RefreshToken]:
self, token_id: str
) -> Optional[models.RefreshToken]:
"""Get refresh token by id."""
return await self._store.async_get_refresh_token(token_id)
async def async_get_refresh_token_by_token(
self, token: str) -> Optional[models.RefreshToken]:
self, token: str
) -> Optional[models.RefreshToken]:
"""Get refresh token by token."""
return await self._store.async_get_refresh_token_by_token(token)
async def async_remove_refresh_token(self,
refresh_token: models.RefreshToken) \
-> None:
async def async_remove_refresh_token(
self, refresh_token: models.RefreshToken
) -> None:
"""Delete a refresh token."""
await self._store.async_remove_refresh_token(refresh_token)
@callback
def async_create_access_token(self,
refresh_token: models.RefreshToken,
remote_ip: Optional[str] = None) -> str:
def async_create_access_token(
self, refresh_token: models.RefreshToken, remote_ip: Optional[str] = None
) -> str:
"""Create a new access token."""
self._store.async_log_refresh_token_usage(refresh_token, remote_ip)
# pylint: disable=no-self-use
now = dt_util.utcnow()
return jwt.encode({
'iss': refresh_token.id,
'iat': now,
'exp': now + refresh_token.access_token_expiration,
}, refresh_token.jwt_key, algorithm='HS256').decode()
return jwt.encode(
{
"iss": refresh_token.id,
"iat": now,
"exp": now + refresh_token.access_token_expiration,
},
refresh_token.jwt_key,
algorithm="HS256",
).decode()
async def async_validate_access_token(
self, token: str) -> Optional[models.RefreshToken]:
self, token: str
) -> Optional[models.RefreshToken]:
"""Return refresh token if an access token is valid."""
try:
unverif_claims = jwt.decode(token, verify=False)
@ -331,23 +354,18 @@ class AuthManager:
return None
refresh_token = await self.async_get_refresh_token(
cast(str, unverif_claims.get('iss')))
cast(str, unverif_claims.get("iss"))
)
if refresh_token is None:
jwt_key = ''
issuer = ''
jwt_key = ""
issuer = ""
else:
jwt_key = refresh_token.jwt_key
issuer = refresh_token.id
try:
jwt.decode(
token,
jwt_key,
leeway=10,
issuer=issuer,
algorithms=['HS256']
)
jwt.decode(token, jwt_key, leeway=10, issuer=issuer, algorithms=["HS256"])
except jwt.InvalidTokenError:
return None
@ -357,31 +375,32 @@ class AuthManager:
return refresh_token
async def _async_create_login_flow(
self, handler: _ProviderKey, *, context: Optional[Dict],
data: Optional[Any]) -> data_entry_flow.FlowHandler:
self, handler: _ProviderKey, *, context: Optional[Dict], data: Optional[Any]
) -> data_entry_flow.FlowHandler:
"""Create a login flow."""
auth_provider = self._providers[handler]
return await auth_provider.async_login_flow(context)
async def _async_finish_login_flow(
self, flow: LoginFlow, result: Dict[str, Any]) \
-> Dict[str, Any]:
self, flow: LoginFlow, result: Dict[str, Any]
) -> Dict[str, Any]:
"""Return a user as result of login flow."""
if result['type'] != data_entry_flow.RESULT_TYPE_CREATE_ENTRY:
if result["type"] != data_entry_flow.RESULT_TYPE_CREATE_ENTRY:
return result
# we got final result
if isinstance(result['data'], models.User):
result['result'] = result['data']
if isinstance(result["data"], models.User):
result["result"] = result["data"]
return result
auth_provider = self._providers[result['handler']]
auth_provider = self._providers[result["handler"]]
credentials = await auth_provider.async_get_or_create_credentials(
result['data'])
result["data"]
)
if flow.context is not None and flow.context.get('credential_only'):
result['result'] = credentials
if flow.context is not None and flow.context.get("credential_only"):
result["result"] = credentials
return result
# multi-factor module cannot enabled for new credential
@ -396,15 +415,18 @@ class AuthManager:
flow.available_mfa_modules = modules
return await flow.async_step_select_mfa_module()
result['result'] = await self.async_get_or_create_user(credentials)
result["result"] = await self.async_get_or_create_user(credentials)
return result
@callback
def _async_get_auth_provider(
self, credentials: models.Credentials) -> Optional[AuthProvider]:
self, credentials: models.Credentials
) -> Optional[AuthProvider]:
"""Get auth provider from a set of credentials."""
auth_provider_key = (credentials.auth_provider_type,
credentials.auth_provider_id)
auth_provider_key = (
credentials.auth_provider_type,
credentials.auth_provider_id,
)
return self._providers.get(auth_provider_key)
async def _user_should_be_owner(self) -> bool:

View file

@ -12,7 +12,7 @@ from homeassistant.util import dt as dt_util
from . import models
STORAGE_VERSION = 1
STORAGE_KEY = 'auth'
STORAGE_KEY = "auth"
class AuthStore:
@ -47,27 +47,28 @@ class AuthStore:
return self._users.get(user_id)
async def async_create_user(
self, name: Optional[str], is_owner: Optional[bool] = None,
is_active: Optional[bool] = None,
system_generated: Optional[bool] = None,
credentials: Optional[models.Credentials] = None) -> models.User:
self,
name: Optional[str],
is_owner: Optional[bool] = None,
is_active: Optional[bool] = None,
system_generated: Optional[bool] = None,
credentials: Optional[models.Credentials] = None,
) -> models.User:
"""Create a new user."""
if self._users is None:
await self._async_load()
assert self._users is not None
kwargs = {
'name': name
} # type: Dict[str, Any]
kwargs = {"name": name} # type: Dict[str, Any]
if is_owner is not None:
kwargs['is_owner'] = is_owner
kwargs["is_owner"] = is_owner
if is_active is not None:
kwargs['is_active'] = is_active
kwargs["is_active"] = is_active
if system_generated is not None:
kwargs['system_generated'] = system_generated
kwargs["system_generated"] = system_generated
new_user = models.User(**kwargs)
@ -81,8 +82,9 @@ class AuthStore:
await self.async_link_user(new_user, credentials)
return new_user
async def async_link_user(self, user: models.User,
credentials: models.Credentials) -> None:
async def async_link_user(
self, user: models.User, credentials: models.Credentials
) -> None:
"""Add credentials to an existing user."""
user.credentials.append(credentials)
self._async_schedule_save()
@ -107,8 +109,7 @@ class AuthStore:
user.is_active = False
self._async_schedule_save()
async def async_remove_credentials(
self, credentials: models.Credentials) -> None:
async def async_remove_credentials(self, credentials: models.Credentials) -> None:
"""Remove credentials."""
if self._users is None:
await self._async_load()
@ -129,23 +130,25 @@ class AuthStore:
self._async_schedule_save()
async def async_create_refresh_token(
self, user: models.User, client_id: Optional[str] = None,
client_name: Optional[str] = None,
client_icon: Optional[str] = None,
token_type: str = models.TOKEN_TYPE_NORMAL,
access_token_expiration: timedelta = ACCESS_TOKEN_EXPIRATION) \
-> models.RefreshToken:
self,
user: models.User,
client_id: Optional[str] = None,
client_name: Optional[str] = None,
client_icon: Optional[str] = None,
token_type: str = models.TOKEN_TYPE_NORMAL,
access_token_expiration: timedelta = ACCESS_TOKEN_EXPIRATION,
) -> models.RefreshToken:
"""Create a new token for a user."""
kwargs = {
'user': user,
'client_id': client_id,
'token_type': token_type,
'access_token_expiration': access_token_expiration
"user": user,
"client_id": client_id,
"token_type": token_type,
"access_token_expiration": access_token_expiration,
} # type: Dict[str, Any]
if client_name:
kwargs['client_name'] = client_name
kwargs["client_name"] = client_name
if client_icon:
kwargs['client_icon'] = client_icon
kwargs["client_icon"] = client_icon
refresh_token = models.RefreshToken(**kwargs)
user.refresh_tokens[refresh_token.id] = refresh_token
@ -154,7 +157,8 @@ class AuthStore:
return refresh_token
async def async_remove_refresh_token(
self, refresh_token: models.RefreshToken) -> None:
self, refresh_token: models.RefreshToken
) -> None:
"""Remove a refresh token."""
if self._users is None:
await self._async_load()
@ -166,7 +170,8 @@ class AuthStore:
break
async def async_get_refresh_token(
self, token_id: str) -> Optional[models.RefreshToken]:
self, token_id: str
) -> Optional[models.RefreshToken]:
"""Get refresh token by id."""
if self._users is None:
await self._async_load()
@ -180,7 +185,8 @@ class AuthStore:
return None
async def async_get_refresh_token_by_token(
self, token: str) -> Optional[models.RefreshToken]:
self, token: str
) -> Optional[models.RefreshToken]:
"""Get refresh token by token."""
if self._users is None:
await self._async_load()
@ -197,8 +203,8 @@ class AuthStore:
@callback
def async_log_refresh_token_usage(
self, refresh_token: models.RefreshToken,
remote_ip: Optional[str] = None) -> None:
self, refresh_token: models.RefreshToken, remote_ip: Optional[str] = None
) -> None:
"""Update refresh token last used information."""
refresh_token.last_used_at = dt_util.utcnow()
refresh_token.last_used_ip = remote_ip
@ -219,61 +225,66 @@ class AuthStore:
self._users = users
return
for user_dict in data['users']:
users[user_dict['id']] = models.User(**user_dict)
for user_dict in data["users"]:
users[user_dict["id"]] = models.User(**user_dict)
for cred_dict in data['credentials']:
users[cred_dict['user_id']].credentials.append(models.Credentials(
id=cred_dict['id'],
is_new=False,
auth_provider_type=cred_dict['auth_provider_type'],
auth_provider_id=cred_dict['auth_provider_id'],
data=cred_dict['data'],
))
for cred_dict in data["credentials"]:
users[cred_dict["user_id"]].credentials.append(
models.Credentials(
id=cred_dict["id"],
is_new=False,
auth_provider_type=cred_dict["auth_provider_type"],
auth_provider_id=cred_dict["auth_provider_id"],
data=cred_dict["data"],
)
)
for rt_dict in data['refresh_tokens']:
for rt_dict in data["refresh_tokens"]:
# Filter out the old keys that don't have jwt_key (pre-0.76)
if 'jwt_key' not in rt_dict:
if "jwt_key" not in rt_dict:
continue
created_at = dt_util.parse_datetime(rt_dict['created_at'])
created_at = dt_util.parse_datetime(rt_dict["created_at"])
if created_at is None:
getLogger(__name__).error(
'Ignoring refresh token %(id)s with invalid created_at '
'%(created_at)s for user_id %(user_id)s', rt_dict)
"Ignoring refresh token %(id)s with invalid created_at "
"%(created_at)s for user_id %(user_id)s",
rt_dict,
)
continue
token_type = rt_dict.get('token_type')
token_type = rt_dict.get("token_type")
if token_type is None:
if rt_dict['client_id'] is None:
if rt_dict["client_id"] is None:
token_type = models.TOKEN_TYPE_SYSTEM
else:
token_type = models.TOKEN_TYPE_NORMAL
# old refresh_token don't have last_used_at (pre-0.78)
last_used_at_str = rt_dict.get('last_used_at')
last_used_at_str = rt_dict.get("last_used_at")
if last_used_at_str:
last_used_at = dt_util.parse_datetime(last_used_at_str)
else:
last_used_at = None
token = models.RefreshToken(
id=rt_dict['id'],
user=users[rt_dict['user_id']],
client_id=rt_dict['client_id'],
id=rt_dict["id"],
user=users[rt_dict["user_id"]],
client_id=rt_dict["client_id"],
# use dict.get to keep backward compatibility
client_name=rt_dict.get('client_name'),
client_icon=rt_dict.get('client_icon'),
client_name=rt_dict.get("client_name"),
client_icon=rt_dict.get("client_icon"),
token_type=token_type,
created_at=created_at,
access_token_expiration=timedelta(
seconds=rt_dict['access_token_expiration']),
token=rt_dict['token'],
jwt_key=rt_dict['jwt_key'],
seconds=rt_dict["access_token_expiration"]
),
token=rt_dict["token"],
jwt_key=rt_dict["jwt_key"],
last_used_at=last_used_at,
last_used_ip=rt_dict.get('last_used_ip'),
last_used_ip=rt_dict.get("last_used_ip"),
)
users[rt_dict['user_id']].refresh_tokens[token.id] = token
users[rt_dict["user_id"]].refresh_tokens[token.id] = token
self._users = users
@ -292,22 +303,22 @@ class AuthStore:
users = [
{
'id': user.id,
'is_owner': user.is_owner,
'is_active': user.is_active,
'name': user.name,
'system_generated': user.system_generated,
"id": user.id,
"is_owner": user.is_owner,
"is_active": user.is_active,
"name": user.name,
"system_generated": user.system_generated,
}
for user in self._users.values()
]
credentials = [
{
'id': credential.id,
'user_id': user.id,
'auth_provider_type': credential.auth_provider_type,
'auth_provider_id': credential.auth_provider_id,
'data': credential.data,
"id": credential.id,
"user_id": user.id,
"auth_provider_type": credential.auth_provider_type,
"auth_provider_id": credential.auth_provider_id,
"data": credential.data,
}
for user in self._users.values()
for credential in user.credentials
@ -315,28 +326,27 @@ class AuthStore:
refresh_tokens = [
{
'id': refresh_token.id,
'user_id': user.id,
'client_id': refresh_token.client_id,
'client_name': refresh_token.client_name,
'client_icon': refresh_token.client_icon,
'token_type': refresh_token.token_type,
'created_at': refresh_token.created_at.isoformat(),
'access_token_expiration':
refresh_token.access_token_expiration.total_seconds(),
'token': refresh_token.token,
'jwt_key': refresh_token.jwt_key,
'last_used_at':
refresh_token.last_used_at.isoformat()
if refresh_token.last_used_at else None,
'last_used_ip': refresh_token.last_used_ip,
"id": refresh_token.id,
"user_id": user.id,
"client_id": refresh_token.client_id,
"client_name": refresh_token.client_name,
"client_icon": refresh_token.client_icon,
"token_type": refresh_token.token_type,
"created_at": refresh_token.created_at.isoformat(),
"access_token_expiration": refresh_token.access_token_expiration.total_seconds(),
"token": refresh_token.token,
"jwt_key": refresh_token.jwt_key,
"last_used_at": refresh_token.last_used_at.isoformat()
if refresh_token.last_used_at
else None,
"last_used_ip": refresh_token.last_used_ip,
}
for user in self._users.values()
for refresh_token in user.refresh_tokens.values()
]
return {
'users': users,
'credentials': credentials,
'refresh_tokens': refresh_tokens,
"users": users,
"credentials": credentials,
"refresh_tokens": refresh_tokens,
}

View file

@ -16,16 +16,19 @@ from homeassistant.util.decorator import Registry
MULTI_FACTOR_AUTH_MODULES = Registry()
MULTI_FACTOR_AUTH_MODULE_SCHEMA = vol.Schema({
vol.Required(CONF_TYPE): str,
vol.Optional(CONF_NAME): str,
# Specify ID if you have two mfa auth module for same type.
vol.Optional(CONF_ID): str,
}, extra=vol.ALLOW_EXTRA)
MULTI_FACTOR_AUTH_MODULE_SCHEMA = vol.Schema(
{
vol.Required(CONF_TYPE): str,
vol.Optional(CONF_NAME): str,
# Specify ID if you have two mfa auth module for same type.
vol.Optional(CONF_ID): str,
},
extra=vol.ALLOW_EXTRA,
)
SESSION_EXPIRATION = timedelta(minutes=5)
DATA_REQS = 'mfa_auth_module_reqs_processed'
DATA_REQS = "mfa_auth_module_reqs_processed"
_LOGGER = logging.getLogger(__name__)
@ -33,7 +36,7 @@ _LOGGER = logging.getLogger(__name__)
class MultiFactorAuthModule:
"""Multi-factor Auth Module of validation function."""
DEFAULT_TITLE = 'Unnamed auth module'
DEFAULT_TITLE = "Unnamed auth module"
def __init__(self, hass: HomeAssistant, config: Dict[str, Any]) -> None:
"""Initialize an auth module."""
@ -65,7 +68,7 @@ class MultiFactorAuthModule:
"""Return a voluptuous schema to define mfa auth module's input."""
raise NotImplementedError
async def async_setup_flow(self, user_id: str) -> 'SetupFlow':
async def async_setup_flow(self, user_id: str) -> "SetupFlow":
"""Return a data entry flow handler for setup module.
Mfa module should extend SetupFlow
@ -84,8 +87,7 @@ class MultiFactorAuthModule:
"""Return whether user is setup."""
raise NotImplementedError
async def async_validation(
self, user_id: str, user_input: Dict[str, Any]) -> bool:
async def async_validation(self, user_id: str, user_input: Dict[str, Any]) -> bool:
"""Return True if validation passed."""
raise NotImplementedError
@ -93,17 +95,17 @@ class MultiFactorAuthModule:
class SetupFlow(data_entry_flow.FlowHandler):
"""Handler for the setup flow."""
def __init__(self, auth_module: MultiFactorAuthModule,
setup_schema: vol.Schema,
user_id: str) -> None:
def __init__(
self, auth_module: MultiFactorAuthModule, setup_schema: vol.Schema, user_id: str
) -> None:
"""Initialize the setup flow."""
self._auth_module = auth_module
self._setup_schema = setup_schema
self._user_id = user_id
async def async_step_init(
self, user_input: Optional[Dict[str, str]] = None) \
-> Dict[str, Any]:
self, user_input: Optional[Dict[str, str]] = None
) -> Dict[str, Any]:
"""Handle the first step of setup flow.
Return self.async_show_form(step_id='init') if user_input == None.
@ -112,23 +114,19 @@ class SetupFlow(data_entry_flow.FlowHandler):
errors = {} # type: Dict[str, str]
if user_input:
result = await self._auth_module.async_setup_user(
self._user_id, user_input)
result = await self._auth_module.async_setup_user(self._user_id, user_input)
return self.async_create_entry(
title=self._auth_module.name,
data={'result': result}
title=self._auth_module.name, data={"result": result}
)
return self.async_show_form(
step_id='init',
data_schema=self._setup_schema,
errors=errors
step_id="init", data_schema=self._setup_schema, errors=errors
)
async def auth_mfa_module_from_config(
hass: HomeAssistant, config: Dict[str, Any]) \
-> MultiFactorAuthModule:
hass: HomeAssistant, config: Dict[str, Any]
) -> MultiFactorAuthModule:
"""Initialize an auth module from a config."""
module_name = config[CONF_TYPE]
module = await _load_mfa_module(hass, module_name)
@ -136,26 +134,29 @@ async def auth_mfa_module_from_config(
try:
config = module.CONFIG_SCHEMA(config) # type: ignore
except vol.Invalid as err:
_LOGGER.error('Invalid configuration for multi-factor module %s: %s',
module_name, humanize_error(config, err))
_LOGGER.error(
"Invalid configuration for multi-factor module %s: %s",
module_name,
humanize_error(config, err),
)
raise
return MULTI_FACTOR_AUTH_MODULES[module_name](hass, config) # type: ignore
async def _load_mfa_module(hass: HomeAssistant, module_name: str) \
-> types.ModuleType:
async def _load_mfa_module(hass: HomeAssistant, module_name: str) -> types.ModuleType:
"""Load an mfa auth module."""
module_path = 'homeassistant.auth.mfa_modules.{}'.format(module_name)
module_path = "homeassistant.auth.mfa_modules.{}".format(module_name)
try:
module = importlib.import_module(module_path)
except ImportError as err:
_LOGGER.error('Unable to load mfa module %s: %s', module_name, err)
raise HomeAssistantError('Unable to load mfa module {}: {}'.format(
module_name, err))
_LOGGER.error("Unable to load mfa module %s: %s", module_name, err)
raise HomeAssistantError(
"Unable to load mfa module {}: {}".format(module_name, err)
)
if hass.config.skip_pip or not hasattr(module, 'REQUIREMENTS'):
if hass.config.skip_pip or not hasattr(module, "REQUIREMENTS"):
return module
processed = hass.data.get(DATA_REQS)
@ -166,12 +167,13 @@ async def _load_mfa_module(hass: HomeAssistant, module_name: str) \
# https://github.com/python/mypy/issues/1424
req_success = await requirements.async_process_requirements(
hass, module_path, module.REQUIREMENTS) # type: ignore
hass, module_path, module.REQUIREMENTS
) # type: ignore
if not req_success:
raise HomeAssistantError(
'Unable to process requirements of mfa module {}'.format(
module_name))
"Unable to process requirements of mfa module {}".format(module_name)
)
processed.add(module_name)
return module

View file

@ -6,39 +6,45 @@ import voluptuous as vol
from homeassistant.core import HomeAssistant
from . import MultiFactorAuthModule, MULTI_FACTOR_AUTH_MODULES, \
MULTI_FACTOR_AUTH_MODULE_SCHEMA, SetupFlow
from . import (
MultiFactorAuthModule,
MULTI_FACTOR_AUTH_MODULES,
MULTI_FACTOR_AUTH_MODULE_SCHEMA,
SetupFlow,
)
CONFIG_SCHEMA = MULTI_FACTOR_AUTH_MODULE_SCHEMA.extend({
vol.Required('data'): [vol.Schema({
vol.Required('user_id'): str,
vol.Required('pin'): str,
})]
}, extra=vol.PREVENT_EXTRA)
CONFIG_SCHEMA = MULTI_FACTOR_AUTH_MODULE_SCHEMA.extend(
{
vol.Required("data"): [
vol.Schema({vol.Required("user_id"): str, vol.Required("pin"): str})
]
},
extra=vol.PREVENT_EXTRA,
)
_LOGGER = logging.getLogger(__name__)
@MULTI_FACTOR_AUTH_MODULES.register('insecure_example')
@MULTI_FACTOR_AUTH_MODULES.register("insecure_example")
class InsecureExampleModule(MultiFactorAuthModule):
"""Example auth module validate pin."""
DEFAULT_TITLE = 'Insecure Personal Identify Number'
DEFAULT_TITLE = "Insecure Personal Identify Number"
def __init__(self, hass: HomeAssistant, config: Dict[str, Any]) -> None:
"""Initialize the user data store."""
super().__init__(hass, config)
self._data = config['data']
self._data = config["data"]
@property
def input_schema(self) -> vol.Schema:
"""Validate login flow input data."""
return vol.Schema({'pin': str})
return vol.Schema({"pin": str})
@property
def setup_schema(self) -> vol.Schema:
"""Validate async_setup_user input data."""
return vol.Schema({'pin': str})
return vol.Schema({"pin": str})
async def async_setup_flow(self, user_id: str) -> SetupFlow:
"""Return a data entry flow handler for setup module.
@ -50,21 +56,21 @@ class InsecureExampleModule(MultiFactorAuthModule):
async def async_setup_user(self, user_id: str, setup_data: Any) -> Any:
"""Set up user to use mfa module."""
# data shall has been validate in caller
pin = setup_data['pin']
pin = setup_data["pin"]
for data in self._data:
if data['user_id'] == user_id:
if data["user_id"] == user_id:
# already setup, override
data['pin'] = pin
data["pin"] = pin
return
self._data.append({'user_id': user_id, 'pin': pin})
self._data.append({"user_id": user_id, "pin": pin})
async def async_depose_user(self, user_id: str) -> None:
"""Remove user from mfa module."""
found = None
for data in self._data:
if data['user_id'] == user_id:
if data["user_id"] == user_id:
found = data
break
if found:
@ -73,17 +79,16 @@ class InsecureExampleModule(MultiFactorAuthModule):
async def async_is_user_setup(self, user_id: str) -> bool:
"""Return whether user is setup."""
for data in self._data:
if data['user_id'] == user_id:
if data["user_id"] == user_id:
return True
return False
async def async_validation(
self, user_id: str, user_input: Dict[str, Any]) -> bool:
async def async_validation(self, user_id: str, user_input: Dict[str, Any]) -> bool:
"""Return True if validation passed."""
for data in self._data:
if data['user_id'] == user_id:
if data["user_id"] == user_id:
# user_input has been validate in caller
if data['pin'] == user_input['pin']:
if data["pin"] == user_input["pin"]:
return True
return False

View file

@ -8,23 +8,26 @@ import voluptuous as vol
from homeassistant.auth.models import User
from homeassistant.core import HomeAssistant
from . import MultiFactorAuthModule, MULTI_FACTOR_AUTH_MODULES, \
MULTI_FACTOR_AUTH_MODULE_SCHEMA, SetupFlow
from . import (
MultiFactorAuthModule,
MULTI_FACTOR_AUTH_MODULES,
MULTI_FACTOR_AUTH_MODULE_SCHEMA,
SetupFlow,
)
REQUIREMENTS = ['pyotp==2.2.6', 'PyQRCode==1.2.1']
REQUIREMENTS = ["pyotp==2.2.6", "PyQRCode==1.2.1"]
CONFIG_SCHEMA = MULTI_FACTOR_AUTH_MODULE_SCHEMA.extend({
}, extra=vol.PREVENT_EXTRA)
CONFIG_SCHEMA = MULTI_FACTOR_AUTH_MODULE_SCHEMA.extend({}, extra=vol.PREVENT_EXTRA)
STORAGE_VERSION = 1
STORAGE_KEY = 'auth_module.totp'
STORAGE_USERS = 'users'
STORAGE_USER_ID = 'user_id'
STORAGE_OTA_SECRET = 'ota_secret'
STORAGE_KEY = "auth_module.totp"
STORAGE_USERS = "users"
STORAGE_USER_ID = "user_id"
STORAGE_OTA_SECRET = "ota_secret"
INPUT_FIELD_CODE = 'code'
INPUT_FIELD_CODE = "code"
DUMMY_SECRET = 'FPPTH34D4E3MI2HG'
DUMMY_SECRET = "FPPTH34D4E3MI2HG"
_LOGGER = logging.getLogger(__name__)
@ -37,10 +40,15 @@ def _generate_qr_code(data: str) -> str:
with BytesIO() as buffer:
qr_code.svg(file=buffer, scale=4)
return '{}'.format(
buffer.getvalue().decode("ascii").replace('\n', '')
.replace('<?xml version="1.0" encoding="UTF-8"?>'
'<svg xmlns="http://www.w3.org/2000/svg"', '<svg')
return "{}".format(
buffer.getvalue()
.decode("ascii")
.replace("\n", "")
.replace(
'<?xml version="1.0" encoding="UTF-8"?>'
'<svg xmlns="http://www.w3.org/2000/svg"',
"<svg",
)
)
@ -50,23 +58,23 @@ def _generate_secret_and_qr_code(username: str) -> Tuple[str, str, str]:
ota_secret = pyotp.random_base32()
url = pyotp.totp.TOTP(ota_secret).provisioning_uri(
username, issuer_name="Home Assistant")
username, issuer_name="Home Assistant"
)
image = _generate_qr_code(url)
return ota_secret, url, image
@MULTI_FACTOR_AUTH_MODULES.register('totp')
@MULTI_FACTOR_AUTH_MODULES.register("totp")
class TotpAuthModule(MultiFactorAuthModule):
"""Auth module validate time-based one time password."""
DEFAULT_TITLE = 'Time-based One Time Password'
DEFAULT_TITLE = "Time-based One Time Password"
def __init__(self, hass: HomeAssistant, config: Dict[str, Any]) -> None:
"""Initialize the user data store."""
super().__init__(hass, config)
self._users = None # type: Optional[Dict[str, str]]
self._user_store = hass.helpers.storage.Store(
STORAGE_VERSION, STORAGE_KEY)
self._user_store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY)
@property
def input_schema(self) -> vol.Schema:
@ -86,14 +94,13 @@ class TotpAuthModule(MultiFactorAuthModule):
"""Save data."""
await self._user_store.async_save({STORAGE_USERS: self._users})
def _add_ota_secret(self, user_id: str,
secret: Optional[str] = None) -> str:
def _add_ota_secret(self, user_id: str, secret: Optional[str] = None) -> str:
"""Create a ota_secret for user."""
import pyotp
ota_secret = secret or pyotp.random_base32() # type: str
self._users[user_id] = ota_secret # type: ignore
self._users[user_id] = ota_secret # type: ignore
return ota_secret
async def async_setup_flow(self, user_id: str) -> SetupFlow:
@ -101,7 +108,7 @@ class TotpAuthModule(MultiFactorAuthModule):
Mfa module should extend SetupFlow
"""
user = await self.hass.auth.async_get_user(user_id) # type: ignore
user = await self.hass.auth.async_get_user(user_id) # type: ignore
return TotpSetupFlow(self, self.input_schema, user)
async def async_setup_user(self, user_id: str, setup_data: Any) -> str:
@ -110,7 +117,8 @@ class TotpAuthModule(MultiFactorAuthModule):
await self._async_load()
result = await self.hass.async_add_executor_job(
self._add_ota_secret, user_id, setup_data.get('secret'))
self._add_ota_secret, user_id, setup_data.get("secret")
)
await self._async_save()
return result
@ -120,7 +128,7 @@ class TotpAuthModule(MultiFactorAuthModule):
if self._users is None:
await self._async_load()
if self._users.pop(user_id, None): # type: ignore
if self._users.pop(user_id, None): # type: ignore
await self._async_save()
async def async_is_user_setup(self, user_id: str) -> bool:
@ -128,10 +136,9 @@ class TotpAuthModule(MultiFactorAuthModule):
if self._users is None:
await self._async_load()
return user_id in self._users # type: ignore
return user_id in self._users # type: ignore
async def async_validation(
self, user_id: str, user_input: Dict[str, Any]) -> bool:
async def async_validation(self, user_id: str, user_input: Dict[str, Any]) -> bool:
"""Return True if validation passed."""
if self._users is None:
await self._async_load()
@ -139,7 +146,8 @@ class TotpAuthModule(MultiFactorAuthModule):
# user_input has been validate in caller
# set INPUT_FIELD_CODE as vol.Required is not user friendly
return await self.hass.async_add_executor_job(
self._validate_2fa, user_id, user_input.get(INPUT_FIELD_CODE, ''))
self._validate_2fa, user_id, user_input.get(INPUT_FIELD_CODE, "")
)
def _validate_2fa(self, user_id: str, code: str) -> bool:
"""Validate two factor authentication code."""
@ -158,9 +166,9 @@ class TotpAuthModule(MultiFactorAuthModule):
class TotpSetupFlow(SetupFlow):
"""Handler for the setup flow."""
def __init__(self, auth_module: TotpAuthModule,
setup_schema: vol.Schema,
user: User) -> None:
def __init__(
self, auth_module: TotpAuthModule, setup_schema: vol.Schema, user: User
) -> None:
"""Initialize the setup flow."""
super().__init__(auth_module, setup_schema, user.id)
# to fix typing complaint
@ -171,8 +179,8 @@ class TotpSetupFlow(SetupFlow):
self._image = None # type Optional[str]
async def async_step_init(
self, user_input: Optional[Dict[str, str]] = None) \
-> Dict[str, Any]:
self, user_input: Optional[Dict[str, str]] = None
) -> Dict[str, Any]:
"""Handle the first step of setup flow.
Return self.async_show_form(step_id='init') if user_input == None.
@ -184,30 +192,31 @@ class TotpSetupFlow(SetupFlow):
if user_input:
verified = await self.hass.async_add_executor_job( # type: ignore
pyotp.TOTP(self._ota_secret).verify, user_input['code'])
pyotp.TOTP(self._ota_secret).verify, user_input["code"]
)
if verified:
result = await self._auth_module.async_setup_user(
self._user_id, {'secret': self._ota_secret})
self._user_id, {"secret": self._ota_secret}
)
return self.async_create_entry(
title=self._auth_module.name,
data={'result': result}
title=self._auth_module.name, data={"result": result}
)
errors['base'] = 'invalid_code'
errors["base"] = "invalid_code"
else:
hass = self._auth_module.hass
self._ota_secret, self._url, self._image = \
await hass.async_add_executor_job( # type: ignore
_generate_secret_and_qr_code, str(self._user.name))
self._ota_secret, self._url, self._image = await hass.async_add_executor_job( # type: ignore
_generate_secret_and_qr_code, str(self._user.name)
)
return self.async_show_form(
step_id='init',
step_id="init",
data_schema=self._setup_schema,
description_placeholders={
'code': self._ota_secret,
'url': self._url,
'qr_code': self._image
"code": self._ota_secret,
"url": self._url,
"qr_code": self._image,
},
errors=errors
errors=errors,
)

View file

@ -9,9 +9,9 @@ from homeassistant.util import dt as dt_util
from .util import generate_secret
TOKEN_TYPE_NORMAL = 'normal'
TOKEN_TYPE_SYSTEM = 'system'
TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN = 'long_lived_access_token'
TOKEN_TYPE_NORMAL = "normal"
TOKEN_TYPE_SYSTEM = "system"
TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN = "long_lived_access_token"
@attr.s(slots=True)
@ -44,16 +44,17 @@ class RefreshToken:
access_token_expiration = attr.ib(type=timedelta)
client_name = attr.ib(type=Optional[str], default=None)
client_icon = attr.ib(type=Optional[str], default=None)
token_type = attr.ib(type=str, default=TOKEN_TYPE_NORMAL,
validator=attr.validators.in_((
TOKEN_TYPE_NORMAL, TOKEN_TYPE_SYSTEM,
TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN)))
token_type = attr.ib(
type=str,
default=TOKEN_TYPE_NORMAL,
validator=attr.validators.in_(
(TOKEN_TYPE_NORMAL, TOKEN_TYPE_SYSTEM, TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN)
),
)
id = attr.ib(type=str, default=attr.Factory(lambda: uuid.uuid4().hex))
created_at = attr.ib(type=datetime, default=attr.Factory(dt_util.utcnow))
token = attr.ib(type=str,
default=attr.Factory(lambda: generate_secret(64)))
jwt_key = attr.ib(type=str,
default=attr.Factory(lambda: generate_secret(64)))
token = attr.ib(type=str, default=attr.Factory(lambda: generate_secret(64)))
jwt_key = attr.ib(type=str, default=attr.Factory(lambda: generate_secret(64)))
last_used_at = attr.ib(type=Optional[datetime], default=None)
last_used_ip = attr.ib(type=Optional[str], default=None)
@ -73,5 +74,4 @@ class Credentials:
is_new = attr.ib(type=bool, default=True)
UserMeta = NamedTuple("UserMeta",
[('name', Optional[str]), ('is_active', bool)])
UserMeta = NamedTuple("UserMeta", [("name", Optional[str]), ("is_active", bool)])

View file

@ -19,25 +19,29 @@ from ..models import Credentials, User, UserMeta # noqa: F401
from ..mfa_modules import SESSION_EXPIRATION
_LOGGER = logging.getLogger(__name__)
DATA_REQS = 'auth_prov_reqs_processed'
DATA_REQS = "auth_prov_reqs_processed"
AUTH_PROVIDERS = Registry()
AUTH_PROVIDER_SCHEMA = vol.Schema({
vol.Required(CONF_TYPE): str,
vol.Optional(CONF_NAME): str,
# Specify ID if you have two auth providers for same type.
vol.Optional(CONF_ID): str,
}, extra=vol.ALLOW_EXTRA)
AUTH_PROVIDER_SCHEMA = vol.Schema(
{
vol.Required(CONF_TYPE): str,
vol.Optional(CONF_NAME): str,
# Specify ID if you have two auth providers for same type.
vol.Optional(CONF_ID): str,
},
extra=vol.ALLOW_EXTRA,
)
class AuthProvider:
"""Provider of user authentication."""
DEFAULT_TITLE = 'Unnamed auth provider'
DEFAULT_TITLE = "Unnamed auth provider"
def __init__(self, hass: HomeAssistant, store: AuthStore,
config: Dict[str, Any]) -> None:
def __init__(
self, hass: HomeAssistant, store: AuthStore, config: Dict[str, Any]
) -> None:
"""Initialize an auth provider."""
self.hass = hass
self.store = store
@ -73,22 +77,22 @@ class AuthProvider:
credentials
for user in users
for credentials in user.credentials
if (credentials.auth_provider_type == self.type and
credentials.auth_provider_id == self.id)
if (
credentials.auth_provider_type == self.type
and credentials.auth_provider_id == self.id
)
]
@callback
def async_create_credentials(self, data: Dict[str, str]) -> Credentials:
"""Create credentials."""
return Credentials(
auth_provider_type=self.type,
auth_provider_id=self.id,
data=data,
auth_provider_type=self.type, auth_provider_id=self.id, data=data
)
# Implement by extending class
async def async_login_flow(self, context: Optional[Dict]) -> 'LoginFlow':
async def async_login_flow(self, context: Optional[Dict]) -> "LoginFlow":
"""Return the data flow for logging in with auth provider.
Auth provider should extend LoginFlow and return an instance.
@ -96,12 +100,14 @@ class AuthProvider:
raise NotImplementedError
async def async_get_or_create_credentials(
self, flow_result: Dict[str, str]) -> Credentials:
self, flow_result: Dict[str, str]
) -> Credentials:
"""Get credentials based on the flow result."""
raise NotImplementedError
async def async_user_meta_for_credentials(
self, credentials: Credentials) -> UserMeta:
self, credentials: Credentials
) -> UserMeta:
"""Return extra user metadata for credentials.
Will be used to populate info when creating a new user.
@ -110,8 +116,8 @@ class AuthProvider:
async def auth_provider_from_config(
hass: HomeAssistant, store: AuthStore,
config: Dict[str, Any]) -> AuthProvider:
hass: HomeAssistant, store: AuthStore, config: Dict[str, Any]
) -> AuthProvider:
"""Initialize an auth provider from a config."""
provider_name = config[CONF_TYPE]
module = await load_auth_provider_module(hass, provider_name)
@ -119,25 +125,31 @@ async def auth_provider_from_config(
try:
config = module.CONFIG_SCHEMA(config) # type: ignore
except vol.Invalid as err:
_LOGGER.error('Invalid configuration for auth provider %s: %s',
provider_name, humanize_error(config, err))
_LOGGER.error(
"Invalid configuration for auth provider %s: %s",
provider_name,
humanize_error(config, err),
)
raise
return AUTH_PROVIDERS[provider_name](hass, store, config) # type: ignore
async def load_auth_provider_module(
hass: HomeAssistant, provider: str) -> types.ModuleType:
hass: HomeAssistant, provider: str
) -> types.ModuleType:
"""Load an auth provider."""
try:
module = importlib.import_module(
'homeassistant.auth.providers.{}'.format(provider))
"homeassistant.auth.providers.{}".format(provider)
)
except ImportError as err:
_LOGGER.error('Unable to load auth provider %s: %s', provider, err)
raise HomeAssistantError('Unable to load auth provider {}: {}'.format(
provider, err))
_LOGGER.error("Unable to load auth provider %s: %s", provider, err)
raise HomeAssistantError(
"Unable to load auth provider {}: {}".format(provider, err)
)
if hass.config.skip_pip or not hasattr(module, 'REQUIREMENTS'):
if hass.config.skip_pip or not hasattr(module, "REQUIREMENTS"):
return module
processed = hass.data.get(DATA_REQS)
@ -150,12 +162,13 @@ async def load_auth_provider_module(
# https://github.com/python/mypy/issues/1424
reqs = module.REQUIREMENTS # type: ignore
req_success = await requirements.async_process_requirements(
hass, 'auth provider {}'.format(provider), reqs)
hass, "auth provider {}".format(provider), reqs
)
if not req_success:
raise HomeAssistantError(
'Unable to process requirements of auth provider {}'.format(
provider))
"Unable to process requirements of auth provider {}".format(provider)
)
processed.add(provider)
return module
@ -174,8 +187,8 @@ class LoginFlow(data_entry_flow.FlowHandler):
self.user = None # type: Optional[User]
async def async_step_init(
self, user_input: Optional[Dict[str, str]] = None) \
-> Dict[str, Any]:
self, user_input: Optional[Dict[str, str]] = None
) -> Dict[str, Any]:
"""Handle the first step of login flow.
Return self.async_show_form(step_id='init') if user_input == None.
@ -184,38 +197,37 @@ class LoginFlow(data_entry_flow.FlowHandler):
raise NotImplementedError
async def async_step_select_mfa_module(
self, user_input: Optional[Dict[str, str]] = None) \
-> Dict[str, Any]:
self, user_input: Optional[Dict[str, str]] = None
) -> Dict[str, Any]:
"""Handle the step of select mfa module."""
errors = {}
if user_input is not None:
auth_module = user_input.get('multi_factor_auth_module')
auth_module = user_input.get("multi_factor_auth_module")
if auth_module in self.available_mfa_modules:
self._auth_module_id = auth_module
return await self.async_step_mfa()
errors['base'] = 'invalid_auth_module'
errors["base"] = "invalid_auth_module"
if len(self.available_mfa_modules) == 1:
self._auth_module_id = list(self.available_mfa_modules.keys())[0]
return await self.async_step_mfa()
return self.async_show_form(
step_id='select_mfa_module',
data_schema=vol.Schema({
'multi_factor_auth_module': vol.In(self.available_mfa_modules)
}),
step_id="select_mfa_module",
data_schema=vol.Schema(
{"multi_factor_auth_module": vol.In(self.available_mfa_modules)}
),
errors=errors,
)
async def async_step_mfa(
self, user_input: Optional[Dict[str, str]] = None) \
-> Dict[str, Any]:
self, user_input: Optional[Dict[str, str]] = None
) -> Dict[str, Any]:
"""Handle the step of mfa validation."""
errors = {}
auth_module = self._auth_manager.get_auth_mfa_module(
self._auth_module_id)
auth_module = self._auth_manager.get_auth_mfa_module(self._auth_module_id)
if auth_module is None:
# Given an invalid input to async_step_select_mfa_module
# will show invalid_auth_module error
@ -224,25 +236,24 @@ class LoginFlow(data_entry_flow.FlowHandler):
if user_input is not None:
expires = self.created_at + SESSION_EXPIRATION
if dt_util.utcnow() > expires:
return self.async_abort(
reason='login_expired'
)
return self.async_abort(reason="login_expired")
result = await auth_module.async_validation(
self.user.id, user_input) # type: ignore
self.user.id, user_input
) # type: ignore
if not result:
errors['base'] = 'invalid_code'
errors["base"] = "invalid_code"
if not errors:
return await self.async_finish(self.user)
description_placeholders = {
'mfa_module_name': auth_module.name,
'mfa_module_id': auth_module.id
"mfa_module_name": auth_module.name,
"mfa_module_id": auth_module.id,
} # type: Dict[str, str]
return self.async_show_form(
step_id='mfa',
step_id="mfa",
data_schema=auth_module.input_schema,
description_placeholders=description_placeholders,
errors=errors,
@ -250,7 +261,4 @@ class LoginFlow(data_entry_flow.FlowHandler):
async def async_finish(self, flow_result: Any) -> Dict:
"""Handle the pass of login flow."""
return self.async_create_entry(
title=self._auth_provider.name,
data=flow_result
)
return self.async_create_entry(title=self._auth_provider.name, data=flow_result)

View file

@ -20,14 +20,13 @@ from ..util import generate_secret
STORAGE_VERSION = 1
STORAGE_KEY = 'auth_provider.homeassistant'
STORAGE_KEY = "auth_provider.homeassistant"
def _disallow_id(conf: Dict[str, Any]) -> Dict[str, Any]:
"""Disallow ID in config."""
if CONF_ID in conf:
raise vol.Invalid(
'ID is not allowed for the homeassistant auth provider.')
raise vol.Invalid("ID is not allowed for the homeassistant auth provider.")
return conf
@ -60,68 +59,62 @@ class Data:
data = await self._store.async_load()
if data is None:
data = {
'salt': generate_secret(),
'users': []
}
data = {"salt": generate_secret(), "users": []}
self._data = data
@property
def users(self) -> List[Dict[str, str]]:
"""Return users."""
return self._data['users'] # type: ignore
return self._data["users"] # type: ignore
def validate_login(self, username: str, password: str) -> None:
"""Validate a username and password.
Raises InvalidAuth if auth invalid.
"""
dummy = b'$2b$12$CiuFGszHx9eNHxPuQcwBWez4CwDTOcLTX5CbOpV6gef2nYuXkY7BO'
dummy = b"$2b$12$CiuFGszHx9eNHxPuQcwBWez4CwDTOcLTX5CbOpV6gef2nYuXkY7BO"
found = None
# Compare all users to avoid timing attacks.
for user in self.users:
if username == user['username']:
if username == user["username"]:
found = user
if found is None:
# check a hash to make timing the same as if user was found
bcrypt.checkpw(b'foo',
dummy)
bcrypt.checkpw(b"foo", dummy)
raise InvalidAuth
user_hash = base64.b64decode(found['password'])
user_hash = base64.b64decode(found["password"])
# if the hash is not a bcrypt hash...
# provide a transparant upgrade for old pbkdf2 hash format
if not (user_hash.startswith(b'$2a$')
or user_hash.startswith(b'$2b$')
or user_hash.startswith(b'$2x$')
or user_hash.startswith(b'$2y$')):
if not (
user_hash.startswith(b"$2a$")
or user_hash.startswith(b"$2b$")
or user_hash.startswith(b"$2x$")
or user_hash.startswith(b"$2y$")
):
# IMPORTANT! validate the login, bail if invalid
hashed = self.legacy_hash_password(password)
if not hmac.compare_digest(hashed, user_hash):
raise InvalidAuth
# then re-hash the valid password with bcrypt
self.change_password(found['username'], password)
run_coroutine_threadsafe(
self.async_save(), self.hass.loop
).result()
user_hash = base64.b64decode(found['password'])
self.change_password(found["username"], password)
run_coroutine_threadsafe(self.async_save(), self.hass.loop).result()
user_hash = base64.b64decode(found["password"])
# bcrypt.checkpw is timing-safe
if not bcrypt.checkpw(password.encode(),
user_hash):
if not bcrypt.checkpw(password.encode(), user_hash):
raise InvalidAuth
def legacy_hash_password(self, password: str,
for_storage: bool = False) -> bytes:
def legacy_hash_password(self, password: str, for_storage: bool = False) -> bytes:
"""LEGACY password encoding."""
# We're no longer storing salts in data, but if one exists we
# should be able to retrieve it.
salt = self._data['salt'].encode() # type: ignore
hashed = hashlib.pbkdf2_hmac('sha512', password.encode(), salt, 100000)
salt = self._data["salt"].encode() # type: ignore
hashed = hashlib.pbkdf2_hmac("sha512", password.encode(), salt, 100000)
if for_storage:
hashed = base64.b64encode(hashed)
return hashed
@ -129,28 +122,30 @@ class Data:
# pylint: disable=no-self-use
def hash_password(self, password: str, for_storage: bool = False) -> bytes:
"""Encode a password."""
hashed = bcrypt.hashpw(password.encode(), bcrypt.gensalt(rounds=12)) \
# type: bytes
hashed = bcrypt.hashpw(password.encode(), bcrypt.gensalt(rounds=12))
# type: bytes
if for_storage:
hashed = base64.b64encode(hashed)
return hashed
def add_auth(self, username: str, password: str) -> None:
"""Add a new authenticated user/pass."""
if any(user['username'] == username for user in self.users):
if any(user["username"] == username for user in self.users):
raise InvalidUser
self.users.append({
'username': username,
'password': self.hash_password(password, True).decode(),
})
self.users.append(
{
"username": username,
"password": self.hash_password(password, True).decode(),
}
)
@callback
def async_remove_auth(self, username: str) -> None:
"""Remove authentication."""
index = None
for i, user in enumerate(self.users):
if user['username'] == username:
if user["username"] == username:
index = i
break
@ -165,9 +160,8 @@ class Data:
Raises InvalidUser if user cannot be found.
"""
for user in self.users:
if user['username'] == username:
user['password'] = self.hash_password(
new_password, True).decode()
if user["username"] == username:
user["password"] = self.hash_password(new_password, True).decode()
break
else:
raise InvalidUser
@ -177,11 +171,11 @@ class Data:
await self._store.async_save(self._data)
@AUTH_PROVIDERS.register('homeassistant')
@AUTH_PROVIDERS.register("homeassistant")
class HassAuthProvider(AuthProvider):
"""Auth provider based on a local storage of users in HASS config dir."""
DEFAULT_TITLE = 'Home Assistant Local'
DEFAULT_TITLE = "Home Assistant Local"
data = None
@ -193,8 +187,7 @@ class HassAuthProvider(AuthProvider):
self.data = Data(self.hass)
await self.data.async_load()
async def async_login_flow(
self, context: Optional[Dict]) -> LoginFlow:
async def async_login_flow(self, context: Optional[Dict]) -> LoginFlow:
"""Return a flow to login."""
return HassLoginFlow(self)
@ -205,36 +198,36 @@ class HassAuthProvider(AuthProvider):
assert self.data is not None
await self.hass.async_add_executor_job(
self.data.validate_login, username, password)
self.data.validate_login, username, password
)
async def async_get_or_create_credentials(
self, flow_result: Dict[str, str]) -> Credentials:
self, flow_result: Dict[str, str]
) -> Credentials:
"""Get credentials based on the flow result."""
username = flow_result['username']
username = flow_result["username"]
for credential in await self.async_credentials():
if credential.data['username'] == username:
if credential.data["username"] == username:
return credential
# Create new credentials.
return self.async_create_credentials({
'username': username
})
return self.async_create_credentials({"username": username})
async def async_user_meta_for_credentials(
self, credentials: Credentials) -> UserMeta:
self, credentials: Credentials
) -> UserMeta:
"""Get extra info for this credential."""
return UserMeta(name=credentials.data['username'], is_active=True)
return UserMeta(name=credentials.data["username"], is_active=True)
async def async_will_remove_credentials(
self, credentials: Credentials) -> None:
async def async_will_remove_credentials(self, credentials: Credentials) -> None:
"""When credentials get removed, also remove the auth."""
if self.data is None:
await self.async_initialize()
assert self.data is not None
try:
self.data.async_remove_auth(credentials.data['username'])
self.data.async_remove_auth(credentials.data["username"])
await self.data.async_save()
except InvalidUser:
# Can happen if somehow we didn't clean up a credential
@ -245,29 +238,27 @@ class HassLoginFlow(LoginFlow):
"""Handler for the login flow."""
async def async_step_init(
self, user_input: Optional[Dict[str, str]] = None) \
-> Dict[str, Any]:
self, user_input: Optional[Dict[str, str]] = None
) -> Dict[str, Any]:
"""Handle the step of the form."""
errors = {}
if user_input is not None:
try:
await cast(HassAuthProvider, self._auth_provider)\
.async_validate_login(user_input['username'],
user_input['password'])
await cast(HassAuthProvider, self._auth_provider).async_validate_login(
user_input["username"], user_input["password"]
)
except InvalidAuth:
errors['base'] = 'invalid_auth'
errors["base"] = "invalid_auth"
if not errors:
user_input.pop('password')
user_input.pop("password")
return await self.async_finish(user_input)
schema = OrderedDict() # type: Dict[str, type]
schema['username'] = str
schema['password'] = str
schema["username"] = str
schema["password"] = str
return self.async_show_form(
step_id='init',
data_schema=vol.Schema(schema),
errors=errors,
step_id="init", data_schema=vol.Schema(schema), errors=errors
)

View file

@ -12,23 +12,25 @@ from . import AuthProvider, AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS, LoginFlow
from ..models import Credentials, UserMeta
USER_SCHEMA = vol.Schema({
vol.Required('username'): str,
vol.Required('password'): str,
vol.Optional('name'): str,
})
USER_SCHEMA = vol.Schema(
{
vol.Required("username"): str,
vol.Required("password"): str,
vol.Optional("name"): str,
}
)
CONFIG_SCHEMA = AUTH_PROVIDER_SCHEMA.extend({
vol.Required('users'): [USER_SCHEMA]
}, extra=vol.PREVENT_EXTRA)
CONFIG_SCHEMA = AUTH_PROVIDER_SCHEMA.extend(
{vol.Required("users"): [USER_SCHEMA]}, extra=vol.PREVENT_EXTRA
)
class InvalidAuthError(HomeAssistantError):
"""Raised when submitting invalid authentication."""
@AUTH_PROVIDERS.register('insecure_example')
@AUTH_PROVIDERS.register("insecure_example")
class ExampleAuthProvider(AuthProvider):
"""Example auth provider based on hardcoded usernames and passwords."""
@ -42,47 +44,48 @@ class ExampleAuthProvider(AuthProvider):
user = None
# Compare all users to avoid timing attacks.
for usr in self.config['users']:
if hmac.compare_digest(username.encode('utf-8'),
usr['username'].encode('utf-8')):
for usr in self.config["users"]:
if hmac.compare_digest(
username.encode("utf-8"), usr["username"].encode("utf-8")
):
user = usr
if user is None:
# 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'))
hmac.compare_digest(password.encode("utf-8"), password.encode("utf-8"))
raise InvalidAuthError
if not hmac.compare_digest(user['password'].encode('utf-8'),
password.encode('utf-8')):
if not hmac.compare_digest(
user["password"].encode("utf-8"), password.encode("utf-8")
):
raise InvalidAuthError
async def async_get_or_create_credentials(
self, flow_result: Dict[str, str]) -> Credentials:
self, flow_result: Dict[str, str]
) -> Credentials:
"""Get credentials based on the flow result."""
username = flow_result['username']
username = flow_result["username"]
for credential in await self.async_credentials():
if credential.data['username'] == username:
if credential.data["username"] == username:
return credential
# Create new credentials.
return self.async_create_credentials({
'username': username
})
return self.async_create_credentials({"username": username})
async def async_user_meta_for_credentials(
self, credentials: Credentials) -> UserMeta:
self, credentials: Credentials
) -> UserMeta:
"""Return extra user metadata for credentials.
Will be used to populate info when creating a new user.
"""
username = credentials.data['username']
username = credentials.data["username"]
name = None
for user in self.config['users']:
if user['username'] == username:
name = user.get('name')
for user in self.config["users"]:
if user["username"] == username:
name = user.get("name")
break
return UserMeta(name=name, is_active=True)
@ -92,29 +95,27 @@ class ExampleLoginFlow(LoginFlow):
"""Handler for the login flow."""
async def async_step_init(
self, user_input: Optional[Dict[str, str]] = None) \
-> Dict[str, Any]:
self, user_input: Optional[Dict[str, str]] = None
) -> Dict[str, Any]:
"""Handle the step of the form."""
errors = {}
if user_input is not None:
try:
cast(ExampleAuthProvider, self._auth_provider)\
.async_validate_login(user_input['username'],
user_input['password'])
cast(ExampleAuthProvider, self._auth_provider).async_validate_login(
user_input["username"], user_input["password"]
)
except InvalidAuthError:
errors['base'] = 'invalid_auth'
errors["base"] = "invalid_auth"
if not errors:
user_input.pop('password')
user_input.pop("password")
return await self.async_finish(user_input)
schema = OrderedDict() # type: Dict[str, type]
schema['username'] = str
schema['password'] = str
schema["username"] = str
schema["password"] = str
return self.async_show_form(
step_id='init',
data_schema=vol.Schema(schema),
errors=errors,
step_id="init", data_schema=vol.Schema(schema), errors=errors
)

View file

@ -16,26 +16,23 @@ from . import AuthProvider, AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS, LoginFlow
from ..models import Credentials, UserMeta
USER_SCHEMA = vol.Schema({
vol.Required('username'): str,
})
USER_SCHEMA = vol.Schema({vol.Required("username"): str})
CONFIG_SCHEMA = AUTH_PROVIDER_SCHEMA.extend({
}, extra=vol.PREVENT_EXTRA)
CONFIG_SCHEMA = AUTH_PROVIDER_SCHEMA.extend({}, extra=vol.PREVENT_EXTRA)
LEGACY_USER_NAME = 'Legacy API password user'
LEGACY_USER_NAME = "Legacy API password user"
class InvalidAuthError(HomeAssistantError):
"""Raised when submitting invalid authentication."""
@AUTH_PROVIDERS.register('legacy_api_password')
@AUTH_PROVIDERS.register("legacy_api_password")
class LegacyApiPasswordAuthProvider(AuthProvider):
"""Example auth provider based on hardcoded usernames and passwords."""
DEFAULT_TITLE = 'Legacy API Password'
DEFAULT_TITLE = "Legacy API Password"
async def async_login_flow(self, context: Optional[Dict]) -> LoginFlow:
"""Return a flow to login."""
@ -44,14 +41,16 @@ class LegacyApiPasswordAuthProvider(AuthProvider):
@callback
def async_validate_login(self, password: str) -> None:
"""Validate a username and password."""
hass_http = getattr(self.hass, 'http', None) # type: HomeAssistantHTTP
hass_http = getattr(self.hass, "http", None) # type: HomeAssistantHTTP
if not hmac.compare_digest(hass_http.api_password.encode('utf-8'),
password.encode('utf-8')):
if not hmac.compare_digest(
hass_http.api_password.encode("utf-8"), password.encode("utf-8")
):
raise InvalidAuthError
async def async_get_or_create_credentials(
self, flow_result: Dict[str, str]) -> Credentials:
self, flow_result: Dict[str, str]
) -> Credentials:
"""Return credentials for this login."""
credentials = await self.async_credentials()
if credentials:
@ -60,7 +59,8 @@ class LegacyApiPasswordAuthProvider(AuthProvider):
return self.async_create_credentials({})
async def async_user_meta_for_credentials(
self, credentials: Credentials) -> UserMeta:
self, credentials: Credentials
) -> UserMeta:
"""
Return info for the user.
@ -73,29 +73,26 @@ class LegacyLoginFlow(LoginFlow):
"""Handler for the login flow."""
async def async_step_init(
self, user_input: Optional[Dict[str, str]] = None) \
-> Dict[str, Any]:
self, user_input: Optional[Dict[str, str]] = None
) -> Dict[str, Any]:
"""Handle the step of the form."""
errors = {}
hass_http = getattr(self.hass, 'http', None)
hass_http = getattr(self.hass, "http", None)
if hass_http is None or not hass_http.api_password:
return self.async_abort(
reason='no_api_password_set'
)
return self.async_abort(reason="no_api_password_set")
if user_input is not None:
try:
cast(LegacyApiPasswordAuthProvider, self._auth_provider)\
.async_validate_login(user_input['password'])
cast(
LegacyApiPasswordAuthProvider, self._auth_provider
).async_validate_login(user_input["password"])
except InvalidAuthError:
errors['base'] = 'invalid_auth'
errors["base"] = "invalid_auth"
if not errors:
return await self.async_finish({})
return self.async_show_form(
step_id='init',
data_schema=vol.Schema({'password': str}),
errors=errors,
step_id="init", data_schema=vol.Schema({"password": str}), errors=errors
)

View file

@ -14,8 +14,7 @@ from homeassistant.exceptions import HomeAssistantError
from . import AuthProvider, AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS, LoginFlow
from ..models import Credentials, UserMeta
CONFIG_SCHEMA = AUTH_PROVIDER_SCHEMA.extend({
}, extra=vol.PREVENT_EXTRA)
CONFIG_SCHEMA = AUTH_PROVIDER_SCHEMA.extend({}, extra=vol.PREVENT_EXTRA)
class InvalidAuthError(HomeAssistantError):
@ -26,14 +25,14 @@ class InvalidUserError(HomeAssistantError):
"""Raised when try to login as invalid user."""
@AUTH_PROVIDERS.register('trusted_networks')
@AUTH_PROVIDERS.register("trusted_networks")
class TrustedNetworksAuthProvider(AuthProvider):
"""Trusted Networks auth provider.
Allow passwordless access from trusted network.
"""
DEFAULT_TITLE = 'Trusted Networks'
DEFAULT_TITLE = "Trusted Networks"
@property
def support_mfa(self) -> bool:
@ -44,27 +43,29 @@ class TrustedNetworksAuthProvider(AuthProvider):
"""Return a flow to login."""
assert context is not None
users = await self.store.async_get_users()
available_users = {user.id: user.name
for user in users
if not user.system_generated and user.is_active}
available_users = {
user.id: user.name
for user in users
if not user.system_generated and user.is_active
}
return TrustedNetworksLoginFlow(
self, cast(str, context.get('ip_address')), available_users)
self, cast(str, context.get("ip_address")), available_users
)
async def async_get_or_create_credentials(
self, flow_result: Dict[str, str]) -> Credentials:
self, flow_result: Dict[str, str]
) -> Credentials:
"""Get credentials based on the flow result."""
user_id = flow_result['user']
user_id = flow_result["user"]
users = await self.store.async_get_users()
for user in users:
if (not user.system_generated and
user.is_active and
user.id == user_id):
if not user.system_generated and user.is_active and user.id == user_id:
for credential in await self.async_credentials():
if credential.data['user_id'] == user_id:
if credential.data["user_id"] == user_id:
return credential
cred = self.async_create_credentials({'user_id': user_id})
cred = self.async_create_credentials({"user_id": user_id})
await self.store.async_link_user(user, cred)
return cred
@ -72,7 +73,8 @@ class TrustedNetworksAuthProvider(AuthProvider):
raise InvalidUserError
async def async_user_meta_for_credentials(
self, credentials: Credentials) -> UserMeta:
self, credentials: Credentials
) -> UserMeta:
"""Return extra user metadata for credentials.
Trusted network auth provider should never create new user.
@ -86,44 +88,48 @@ class TrustedNetworksAuthProvider(AuthProvider):
Raise InvalidAuthError if not.
Raise InvalidAuthError if trusted_networks is not configured.
"""
hass_http = getattr(self.hass, 'http', None) # type: HomeAssistantHTTP
hass_http = getattr(self.hass, "http", None) # type: HomeAssistantHTTP
if not hass_http or not hass_http.trusted_networks:
raise InvalidAuthError('trusted_networks is not configured')
raise InvalidAuthError("trusted_networks is not configured")
if not any(ip_address in trusted_network for trusted_network
in hass_http.trusted_networks):
raise InvalidAuthError('Not in trusted_networks')
if not any(
ip_address in trusted_network
for trusted_network in hass_http.trusted_networks
):
raise InvalidAuthError("Not in trusted_networks")
class TrustedNetworksLoginFlow(LoginFlow):
"""Handler for the login flow."""
def __init__(self, auth_provider: TrustedNetworksAuthProvider,
ip_address: str, available_users: Dict[str, Optional[str]]) \
-> None:
def __init__(
self,
auth_provider: TrustedNetworksAuthProvider,
ip_address: str,
available_users: Dict[str, Optional[str]],
) -> None:
"""Initialize the login flow."""
super().__init__(auth_provider)
self._available_users = available_users
self._ip_address = ip_address
async def async_step_init(
self, user_input: Optional[Dict[str, str]] = None) \
-> Dict[str, Any]:
self, user_input: Optional[Dict[str, str]] = None
) -> Dict[str, Any]:
"""Handle the step of the form."""
try:
cast(TrustedNetworksAuthProvider, self._auth_provider)\
.async_validate_access(self._ip_address)
cast(
TrustedNetworksAuthProvider, self._auth_provider
).async_validate_access(self._ip_address)
except InvalidAuthError:
return self.async_abort(
reason='not_whitelisted'
)
return self.async_abort(reason="not_whitelisted")
if user_input is not None:
return await self.async_finish(user_input)
return self.async_show_form(
step_id='init',
data_schema=vol.Schema({'user': vol.In(self._available_users)}),
step_id="init",
data_schema=vol.Schema({"user": vol.In(self._available_users)}),
)

View file

@ -10,4 +10,4 @@ def generate_secret(entropy: int = 32) -> str:
Event loop friendly.
"""
return binascii.hexlify(os.urandom(entropy)).decode('ascii')
return binascii.hexlify(os.urandom(entropy)).decode("ascii")

View file

@ -10,7 +10,11 @@ from typing import Any, Optional, Dict
import voluptuous as vol
from homeassistant import (
core, config as conf_util, config_entries, components as core_components)
core,
config as conf_util,
config_entries,
components as core_components,
)
from homeassistant.components import persistent_notification
from homeassistant.const import EVENT_HOMEASSISTANT_CLOSE
from homeassistant.setup import async_setup_component
@ -22,25 +26,34 @@ from homeassistant.helpers.signal import async_register_signal_handling
_LOGGER = logging.getLogger(__name__)
ERROR_LOG_FILENAME = 'home-assistant.log'
ERROR_LOG_FILENAME = "home-assistant.log"
# hass.data key for logging information.
DATA_LOGGING = 'logging'
DATA_LOGGING = "logging"
FIRST_INIT_COMPONENT = {'system_log', 'recorder', 'mqtt', 'mqtt_eventstream',
'logger', 'introduction', 'frontend', 'history'}
FIRST_INIT_COMPONENT = {
"system_log",
"recorder",
"mqtt",
"mqtt_eventstream",
"logger",
"introduction",
"frontend",
"history",
}
def from_config_dict(config: Dict[str, Any],
hass: Optional[core.HomeAssistant] = None,
config_dir: Optional[str] = None,
enable_log: bool = True,
verbose: bool = False,
skip_pip: bool = False,
log_rotate_days: Any = None,
log_file: Any = None,
log_no_color: bool = False) \
-> Optional[core.HomeAssistant]:
def from_config_dict(
config: Dict[str, Any],
hass: Optional[core.HomeAssistant] = None,
config_dir: Optional[str] = None,
enable_log: bool = True,
verbose: bool = False,
skip_pip: bool = False,
log_rotate_days: Any = None,
log_file: Any = None,
log_no_color: bool = False,
) -> Optional[core.HomeAssistant]:
"""Try to configure Home Assistant from a configuration dictionary.
Dynamically loads required components and its dependencies.
@ -51,28 +64,36 @@ def from_config_dict(config: Dict[str, Any],
config_dir = os.path.abspath(config_dir)
hass.config.config_dir = config_dir
if not is_virtual_env():
hass.loop.run_until_complete(
async_mount_local_lib_path(config_dir))
hass.loop.run_until_complete(async_mount_local_lib_path(config_dir))
# run task
hass = hass.loop.run_until_complete(
async_from_config_dict(
config, hass, config_dir, enable_log, verbose, skip_pip,
log_rotate_days, log_file, log_no_color)
config,
hass,
config_dir,
enable_log,
verbose,
skip_pip,
log_rotate_days,
log_file,
log_no_color,
)
)
return hass
async def async_from_config_dict(config: Dict[str, Any],
hass: core.HomeAssistant,
config_dir: Optional[str] = None,
enable_log: bool = True,
verbose: bool = False,
skip_pip: bool = False,
log_rotate_days: Any = None,
log_file: Any = None,
log_no_color: bool = False) \
-> Optional[core.HomeAssistant]:
async def async_from_config_dict(
config: Dict[str, Any],
hass: core.HomeAssistant,
config_dir: Optional[str] = None,
enable_log: bool = True,
verbose: bool = False,
skip_pip: bool = False,
log_rotate_days: Any = None,
log_file: Any = None,
log_no_color: bool = False,
) -> Optional[core.HomeAssistant]:
"""Try to configure Home Assistant from a configuration dictionary.
Dynamically loads required components and its dependencies.
@ -81,40 +102,41 @@ async def async_from_config_dict(config: Dict[str, Any],
start = time()
if enable_log:
async_enable_logging(hass, verbose, log_rotate_days, log_file,
log_no_color)
async_enable_logging(hass, verbose, log_rotate_days, log_file, log_no_color)
core_config = config.get(core.DOMAIN, {})
has_api_password = bool((config.get('http') or {}).get('api_password'))
has_trusted_networks = bool((config.get('http') or {})
.get('trusted_networks'))
has_api_password = bool((config.get("http") or {}).get("api_password"))
has_trusted_networks = bool((config.get("http") or {}).get("trusted_networks"))
try:
await conf_util.async_process_ha_core_config(
hass, core_config, has_api_password, has_trusted_networks)
hass, core_config, has_api_password, has_trusted_networks
)
except vol.Invalid as config_err:
conf_util.async_log_exception(
config_err, 'homeassistant', core_config, hass)
conf_util.async_log_exception(config_err, "homeassistant", core_config, hass)
return None
except HomeAssistantError:
_LOGGER.error("Home Assistant core failed to initialize. "
"Further initialization aborted")
_LOGGER.error(
"Home Assistant core failed to initialize. "
"Further initialization aborted"
)
return None
await hass.async_add_executor_job(
conf_util.process_ha_config_upgrade, hass)
await hass.async_add_executor_job(conf_util.process_ha_config_upgrade, hass)
hass.config.skip_pip = skip_pip
if skip_pip:
_LOGGER.warning("Skipping pip installation of required modules. "
"This may cause issues")
_LOGGER.warning(
"Skipping pip installation of required modules. " "This may cause issues"
)
# Make a copy because we are mutating it.
config = OrderedDict(config)
# Merge packages
conf_util.merge_packages_config(
hass, config, core_config.get(conf_util.CONF_PACKAGES, {}))
hass, config, core_config.get(conf_util.CONF_PACKAGES, {})
)
# Ensure we have no None values after merge
for key, value in config.items():
@ -125,15 +147,16 @@ async def async_from_config_dict(config: Dict[str, Any],
await hass.config_entries.async_load()
# Filter out the repeating and common config section [homeassistant]
components = set(key.split(' ')[0] for key in config.keys()
if key != core.DOMAIN)
components = set(key.split(" ")[0] for key in config.keys() if key != core.DOMAIN)
components.update(hass.config_entries.async_domains())
# setup components
res = await core_components.async_setup(hass, config)
if not res:
_LOGGER.error("Home Assistant core failed to initialize. "
"Further initialization aborted")
_LOGGER.error(
"Home Assistant core failed to initialize. "
"Further initialization aborted"
)
return hass
await persistent_notification.async_setup(hass, config)
@ -157,20 +180,21 @@ async def async_from_config_dict(config: Dict[str, Any],
await hass.async_block_till_done()
stop = time()
_LOGGER.info("Home Assistant initialized in %.2fs", stop-start)
_LOGGER.info("Home Assistant initialized in %.2fs", stop - start)
async_register_signal_handling(hass)
return hass
def from_config_file(config_path: str,
hass: Optional[core.HomeAssistant] = None,
verbose: bool = False,
skip_pip: bool = True,
log_rotate_days: Any = None,
log_file: Any = None,
log_no_color: bool = False)\
-> Optional[core.HomeAssistant]:
def from_config_file(
config_path: str,
hass: Optional[core.HomeAssistant] = None,
verbose: bool = False,
skip_pip: bool = True,
log_rotate_days: Any = None,
log_file: Any = None,
log_no_color: bool = False,
) -> Optional[core.HomeAssistant]:
"""Read the configuration file and try to start all the functionality.
Will add functionality to 'hass' parameter if given,
@ -182,21 +206,28 @@ def from_config_file(config_path: str,
# run task
hass = hass.loop.run_until_complete(
async_from_config_file(
config_path, hass, verbose, skip_pip,
log_rotate_days, log_file, log_no_color)
config_path,
hass,
verbose,
skip_pip,
log_rotate_days,
log_file,
log_no_color,
)
)
return hass
async def async_from_config_file(config_path: str,
hass: core.HomeAssistant,
verbose: bool = False,
skip_pip: bool = True,
log_rotate_days: Any = None,
log_file: Any = None,
log_no_color: bool = False)\
-> Optional[core.HomeAssistant]:
async def async_from_config_file(
config_path: str,
hass: core.HomeAssistant,
verbose: bool = False,
skip_pip: bool = True,
log_rotate_days: Any = None,
log_file: Any = None,
log_no_color: bool = False,
) -> Optional[core.HomeAssistant]:
"""Read the configuration file and try to start all the functionality.
Will add functionality to 'hass' parameter.
@ -209,12 +240,12 @@ async def async_from_config_file(config_path: str,
if not is_virtual_env():
await async_mount_local_lib_path(config_dir)
async_enable_logging(hass, verbose, log_rotate_days, log_file,
log_no_color)
async_enable_logging(hass, verbose, log_rotate_days, log_file, log_no_color)
try:
config_dict = await hass.async_add_executor_job(
conf_util.load_yaml_config_file, config_path)
conf_util.load_yaml_config_file, config_path
)
except HomeAssistantError as err:
_LOGGER.error("Error loading %s: %s", config_path, err)
return None
@ -222,43 +253,48 @@ async def async_from_config_file(config_path: str,
clear_secret_cache()
return await async_from_config_dict(
config_dict, hass, enable_log=False, skip_pip=skip_pip)
config_dict, hass, enable_log=False, skip_pip=skip_pip
)
@core.callback
def async_enable_logging(hass: core.HomeAssistant,
verbose: bool = False,
log_rotate_days: Optional[int] = None,
log_file: Optional[str] = None,
log_no_color: bool = False) -> None:
def async_enable_logging(
hass: core.HomeAssistant,
verbose: bool = False,
log_rotate_days: Optional[int] = None,
log_file: Optional[str] = None,
log_no_color: bool = False,
) -> None:
"""Set up the logging.
This method must be run in the event loop.
"""
fmt = ("%(asctime)s %(levelname)s (%(threadName)s) "
"[%(name)s] %(message)s")
datefmt = '%Y-%m-%d %H:%M:%S'
fmt = "%(asctime)s %(levelname)s (%(threadName)s) " "[%(name)s] %(message)s"
datefmt = "%Y-%m-%d %H:%M:%S"
if not log_no_color:
try:
from colorlog import ColoredFormatter
# basicConfig must be called after importing colorlog in order to
# ensure that the handlers it sets up wraps the correct streams.
logging.basicConfig(level=logging.INFO)
colorfmt = "%(log_color)s{}%(reset)s".format(fmt)
logging.getLogger().handlers[0].setFormatter(ColoredFormatter(
colorfmt,
datefmt=datefmt,
reset=True,
log_colors={
'DEBUG': 'cyan',
'INFO': 'green',
'WARNING': 'yellow',
'ERROR': 'red',
'CRITICAL': 'red',
}
))
logging.getLogger().handlers[0].setFormatter(
ColoredFormatter(
colorfmt,
datefmt=datefmt,
reset=True,
log_colors={
"DEBUG": "cyan",
"INFO": "green",
"WARNING": "yellow",
"ERROR": "red",
"CRITICAL": "red",
},
)
)
except ImportError:
pass
@ -267,9 +303,9 @@ def async_enable_logging(hass: core.HomeAssistant,
logging.basicConfig(format=fmt, datefmt=datefmt, level=logging.INFO)
# Suppress overly verbose logs from libraries that aren't helpful
logging.getLogger('requests').setLevel(logging.WARNING)
logging.getLogger('urllib3').setLevel(logging.WARNING)
logging.getLogger('aiohttp.access').setLevel(logging.WARNING)
logging.getLogger("requests").setLevel(logging.WARNING)
logging.getLogger("urllib3").setLevel(logging.WARNING)
logging.getLogger("aiohttp.access").setLevel(logging.WARNING)
# Log errors to a file if we have write access to file or config dir
if log_file is None:
@ -282,16 +318,16 @@ def async_enable_logging(hass: core.HomeAssistant,
# Check if we can write to the error log if it exists or that
# we can create files in the containing directory if not.
if (err_path_exists and os.access(err_log_path, os.W_OK)) or \
(not err_path_exists and os.access(err_dir, os.W_OK)):
if (err_path_exists and os.access(err_log_path, os.W_OK)) or (
not err_path_exists and os.access(err_dir, os.W_OK)
):
if log_rotate_days:
err_handler = logging.handlers.TimedRotatingFileHandler(
err_log_path, when='midnight',
backupCount=log_rotate_days) # type: logging.FileHandler
err_log_path, when="midnight", backupCount=log_rotate_days
) # type: logging.FileHandler
else:
err_handler = logging.FileHandler(
err_log_path, mode='w', delay=True)
err_handler = logging.FileHandler(err_log_path, mode="w", delay=True)
err_handler.setLevel(logging.INFO if verbose else logging.WARNING)
err_handler.setFormatter(logging.Formatter(fmt, datefmt=datefmt))
@ -300,21 +336,19 @@ def async_enable_logging(hass: core.HomeAssistant,
async def async_stop_async_handler(_: Any) -> None:
"""Cleanup async handler."""
logging.getLogger('').removeHandler(async_handler) # type: ignore
logging.getLogger("").removeHandler(async_handler) # type: ignore
await async_handler.async_close(blocking=True)
hass.bus.async_listen_once(
EVENT_HOMEASSISTANT_CLOSE, async_stop_async_handler)
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_CLOSE, async_stop_async_handler)
logger = logging.getLogger('')
logger = logging.getLogger("")
logger.addHandler(async_handler) # type: ignore
logger.setLevel(logging.INFO)
# Save the log file location for access by other components.
hass.data[DATA_LOGGING] = err_log_path
else:
_LOGGER.error(
"Unable to set up error log %s (access denied)", err_log_path)
_LOGGER.error("Unable to set up error log %s (access denied)", err_log_path)
async def async_mount_local_lib_path(config_dir: str) -> str:
@ -322,7 +356,7 @@ async def async_mount_local_lib_path(config_dir: str) -> str:
This function is a coroutine.
"""
deps_dir = os.path.join(config_dir, 'deps')
deps_dir = os.path.join(config_dir, "deps")
lib_dir = await async_get_user_site(deps_dir)
if lib_dir not in sys.path:
sys.path.insert(0, lib_dir)

View file

@ -18,14 +18,19 @@ from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.service import extract_entity_ids
from homeassistant.helpers import intent
from homeassistant.const import (
ATTR_ENTITY_ID, SERVICE_TURN_ON, SERVICE_TURN_OFF, SERVICE_TOGGLE,
SERVICE_HOMEASSISTANT_STOP, SERVICE_HOMEASSISTANT_RESTART,
RESTART_EXIT_CODE)
ATTR_ENTITY_ID,
SERVICE_TURN_ON,
SERVICE_TURN_OFF,
SERVICE_TOGGLE,
SERVICE_HOMEASSISTANT_STOP,
SERVICE_HOMEASSISTANT_RESTART,
RESTART_EXIT_CODE,
)
_LOGGER = logging.getLogger(__name__)
SERVICE_RELOAD_CORE_CONFIG = 'reload_core_config'
SERVICE_CHECK_CONFIG = 'check_config'
SERVICE_RELOAD_CORE_CONFIG = "reload_core_config"
SERVICE_CHECK_CONFIG = "check_config"
def is_on(hass, entity_id=None):
@ -45,11 +50,10 @@ def is_on(hass, entity_id=None):
component = getattr(hass.components, domain)
except ImportError:
_LOGGER.error('Failed to call %s.is_on: component not found',
domain)
_LOGGER.error("Failed to call %s.is_on: component not found", domain)
continue
if not hasattr(component, 'is_on'):
if not hasattr(component, "is_on"):
_LOGGER.warning("Component %s has no is_on method.", domain)
continue
@ -112,6 +116,7 @@ def async_reload_core_config(hass):
@asyncio.coroutine
def async_setup(hass: ha.HomeAssistant, config: dict) -> Awaitable[bool]:
"""Set up general services related to Home Assistant."""
@asyncio.coroutine
def async_handle_turn_service(service):
"""Handle calls to homeassistant.turn_on/off."""
@ -120,13 +125,14 @@ def async_setup(hass: ha.HomeAssistant, config: dict) -> Awaitable[bool]:
# Generic turn on/off method requires entity id
if not entity_ids:
_LOGGER.error(
"homeassistant/%s cannot be called without entity_id",
service.service)
"homeassistant/%s cannot be called without entity_id", service.service
)
return
# Group entity_ids by domain. groupby requires sorted data.
by_domain = it.groupby(sorted(entity_ids),
lambda item: ha.split_entity_id(item)[0])
by_domain = it.groupby(
sorted(entity_ids), lambda item: ha.split_entity_id(item)[0]
)
tasks = []
@ -145,24 +151,30 @@ def async_setup(hass: ha.HomeAssistant, config: dict) -> Awaitable[bool]:
# ent_ids is a generator, convert it to a list.
data[ATTR_ENTITY_ID] = list(ent_ids)
tasks.append(hass.services.async_call(
domain, service.service, data, blocking))
tasks.append(
hass.services.async_call(domain, service.service, data, blocking)
)
yield from asyncio.wait(tasks, loop=hass.loop)
hass.services.async_register(
ha.DOMAIN, SERVICE_TURN_OFF, async_handle_turn_service)
hass.services.async_register(
ha.DOMAIN, SERVICE_TURN_ON, async_handle_turn_service)
hass.services.async_register(
ha.DOMAIN, SERVICE_TOGGLE, async_handle_turn_service)
hass.helpers.intent.async_register(intent.ServiceIntentHandler(
intent.INTENT_TURN_ON, ha.DOMAIN, SERVICE_TURN_ON, "Turned {} on"))
hass.helpers.intent.async_register(intent.ServiceIntentHandler(
intent.INTENT_TURN_OFF, ha.DOMAIN, SERVICE_TURN_OFF,
"Turned {} off"))
hass.helpers.intent.async_register(intent.ServiceIntentHandler(
intent.INTENT_TOGGLE, ha.DOMAIN, SERVICE_TOGGLE, "Toggled {}"))
hass.services.async_register(ha.DOMAIN, SERVICE_TURN_OFF, async_handle_turn_service)
hass.services.async_register(ha.DOMAIN, SERVICE_TURN_ON, async_handle_turn_service)
hass.services.async_register(ha.DOMAIN, SERVICE_TOGGLE, async_handle_turn_service)
hass.helpers.intent.async_register(
intent.ServiceIntentHandler(
intent.INTENT_TURN_ON, ha.DOMAIN, SERVICE_TURN_ON, "Turned {} on"
)
)
hass.helpers.intent.async_register(
intent.ServiceIntentHandler(
intent.INTENT_TURN_OFF, ha.DOMAIN, SERVICE_TURN_OFF, "Turned {} off"
)
)
hass.helpers.intent.async_register(
intent.ServiceIntentHandler(
intent.INTENT_TOGGLE, ha.DOMAIN, SERVICE_TOGGLE, "Toggled {}"
)
)
@asyncio.coroutine
def async_handle_core_service(call):
@ -180,18 +192,23 @@ def async_setup(hass: ha.HomeAssistant, config: dict) -> Awaitable[bool]:
_LOGGER.error(errors)
hass.components.persistent_notification.async_create(
"Config error. See dev-info panel for details.",
"Config validating", "{0}.check_config".format(ha.DOMAIN))
"Config validating",
"{0}.check_config".format(ha.DOMAIN),
)
return
if call.service == SERVICE_HOMEASSISTANT_RESTART:
hass.async_create_task(hass.async_stop(RESTART_EXIT_CODE))
hass.services.async_register(
ha.DOMAIN, SERVICE_HOMEASSISTANT_STOP, async_handle_core_service)
ha.DOMAIN, SERVICE_HOMEASSISTANT_STOP, async_handle_core_service
)
hass.services.async_register(
ha.DOMAIN, SERVICE_HOMEASSISTANT_RESTART, async_handle_core_service)
ha.DOMAIN, SERVICE_HOMEASSISTANT_RESTART, async_handle_core_service
)
hass.services.async_register(
ha.DOMAIN, SERVICE_CHECK_CONFIG, async_handle_core_service)
ha.DOMAIN, SERVICE_CHECK_CONFIG, async_handle_core_service
)
@asyncio.coroutine
def async_handle_reload_config(call):
@ -203,9 +220,11 @@ def async_setup(hass: ha.HomeAssistant, config: dict) -> Awaitable[bool]:
return
yield from conf_util.async_process_ha_core_config(
hass, conf.get(ha.DOMAIN) or {})
hass, conf.get(ha.DOMAIN) or {}
)
hass.services.async_register(
ha.DOMAIN, SERVICE_RELOAD_CORE_CONFIG, async_handle_reload_config)
ha.DOMAIN, SERVICE_RELOAD_CORE_CONFIG, async_handle_reload_config
)
return True

View file

@ -12,89 +12,109 @@ from requests.exceptions import HTTPError, ConnectTimeout
import voluptuous as vol
from homeassistant.const import (
ATTR_ATTRIBUTION, ATTR_DATE, ATTR_TIME, ATTR_ENTITY_ID, CONF_USERNAME,
CONF_PASSWORD, CONF_EXCLUDE, CONF_NAME, CONF_LIGHTS,
EVENT_HOMEASSISTANT_STOP, EVENT_HOMEASSISTANT_START)
ATTR_ATTRIBUTION,
ATTR_DATE,
ATTR_TIME,
ATTR_ENTITY_ID,
CONF_USERNAME,
CONF_PASSWORD,
CONF_EXCLUDE,
CONF_NAME,
CONF_LIGHTS,
EVENT_HOMEASSISTANT_STOP,
EVENT_HOMEASSISTANT_START,
)
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers import discovery
from homeassistant.helpers.entity import Entity
REQUIREMENTS = ['abodepy==0.13.1']
REQUIREMENTS = ["abodepy==0.13.1"]
_LOGGER = logging.getLogger(__name__)
CONF_ATTRIBUTION = "Data provided by goabode.com"
CONF_POLLING = 'polling'
CONF_POLLING = "polling"
DOMAIN = 'abode'
DEFAULT_CACHEDB = './abodepy_cache.pickle'
DOMAIN = "abode"
DEFAULT_CACHEDB = "./abodepy_cache.pickle"
NOTIFICATION_ID = 'abode_notification'
NOTIFICATION_TITLE = 'Abode Security Setup'
NOTIFICATION_ID = "abode_notification"
NOTIFICATION_TITLE = "Abode Security Setup"
EVENT_ABODE_ALARM = 'abode_alarm'
EVENT_ABODE_ALARM_END = 'abode_alarm_end'
EVENT_ABODE_AUTOMATION = 'abode_automation'
EVENT_ABODE_FAULT = 'abode_panel_fault'
EVENT_ABODE_RESTORE = 'abode_panel_restore'
EVENT_ABODE_ALARM = "abode_alarm"
EVENT_ABODE_ALARM_END = "abode_alarm_end"
EVENT_ABODE_AUTOMATION = "abode_automation"
EVENT_ABODE_FAULT = "abode_panel_fault"
EVENT_ABODE_RESTORE = "abode_panel_restore"
SERVICE_SETTINGS = 'change_setting'
SERVICE_CAPTURE_IMAGE = 'capture_image'
SERVICE_TRIGGER = 'trigger_quick_action'
SERVICE_SETTINGS = "change_setting"
SERVICE_CAPTURE_IMAGE = "capture_image"
SERVICE_TRIGGER = "trigger_quick_action"
ATTR_DEVICE_ID = 'device_id'
ATTR_DEVICE_NAME = 'device_name'
ATTR_DEVICE_TYPE = 'device_type'
ATTR_EVENT_CODE = 'event_code'
ATTR_EVENT_NAME = 'event_name'
ATTR_EVENT_TYPE = 'event_type'
ATTR_EVENT_UTC = 'event_utc'
ATTR_SETTING = 'setting'
ATTR_USER_NAME = 'user_name'
ATTR_VALUE = 'value'
ATTR_DEVICE_ID = "device_id"
ATTR_DEVICE_NAME = "device_name"
ATTR_DEVICE_TYPE = "device_type"
ATTR_EVENT_CODE = "event_code"
ATTR_EVENT_NAME = "event_name"
ATTR_EVENT_TYPE = "event_type"
ATTR_EVENT_UTC = "event_utc"
ATTR_SETTING = "setting"
ATTR_USER_NAME = "user_name"
ATTR_VALUE = "value"
ABODE_DEVICE_ID_LIST_SCHEMA = vol.Schema([str])
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_PASSWORD): cv.string,
vol.Optional(CONF_NAME): cv.string,
vol.Optional(CONF_POLLING, default=False): cv.boolean,
vol.Optional(CONF_EXCLUDE, default=[]): ABODE_DEVICE_ID_LIST_SCHEMA,
vol.Optional(CONF_LIGHTS, default=[]): ABODE_DEVICE_ID_LIST_SCHEMA
}),
}, extra=vol.ALLOW_EXTRA)
CONFIG_SCHEMA = vol.Schema(
{
DOMAIN: vol.Schema(
{
vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_PASSWORD): cv.string,
vol.Optional(CONF_NAME): cv.string,
vol.Optional(CONF_POLLING, default=False): cv.boolean,
vol.Optional(CONF_EXCLUDE, default=[]): ABODE_DEVICE_ID_LIST_SCHEMA,
vol.Optional(CONF_LIGHTS, default=[]): ABODE_DEVICE_ID_LIST_SCHEMA,
}
)
},
extra=vol.ALLOW_EXTRA,
)
CHANGE_SETTING_SCHEMA = vol.Schema({
vol.Required(ATTR_SETTING): cv.string,
vol.Required(ATTR_VALUE): cv.string
})
CHANGE_SETTING_SCHEMA = vol.Schema(
{vol.Required(ATTR_SETTING): cv.string, vol.Required(ATTR_VALUE): cv.string}
)
CAPTURE_IMAGE_SCHEMA = vol.Schema({
ATTR_ENTITY_ID: cv.entity_ids,
})
CAPTURE_IMAGE_SCHEMA = vol.Schema({ATTR_ENTITY_ID: cv.entity_ids})
TRIGGER_SCHEMA = vol.Schema({
ATTR_ENTITY_ID: cv.entity_ids,
})
TRIGGER_SCHEMA = vol.Schema({ATTR_ENTITY_ID: cv.entity_ids})
ABODE_PLATFORMS = [
'alarm_control_panel', 'binary_sensor', 'lock', 'switch', 'cover',
'camera', 'light', 'sensor'
"alarm_control_panel",
"binary_sensor",
"lock",
"switch",
"cover",
"camera",
"light",
"sensor",
]
class AbodeSystem:
"""Abode System class."""
def __init__(self, username, password, cache,
name, polling, exclude, lights):
def __init__(self, username, password, cache, name, polling, exclude, lights):
"""Initialize the system."""
import abodepy
self.abode = abodepy.Abode(
username, password, auto_login=True, get_devices=True,
get_automations=True, cache_path=cache)
username,
password,
auto_login=True,
get_devices=True,
get_automations=True,
cache_path=cache,
)
self.name = name
self.polling = polling
self.exclude = exclude
@ -113,9 +133,9 @@ class AbodeSystem:
"""Check if a switch device is configured as a light."""
import abodepy.helpers.constants as CONST
return (device.generic_type == CONST.TYPE_LIGHT or
(device.generic_type == CONST.TYPE_SWITCH and
device.device_id in self.lights))
return device.generic_type == CONST.TYPE_LIGHT or (
device.generic_type == CONST.TYPE_SWITCH and device.device_id in self.lights
)
def setup(hass, config):
@ -133,16 +153,18 @@ def setup(hass, config):
try:
cache = hass.config.path(DEFAULT_CACHEDB)
hass.data[DOMAIN] = AbodeSystem(
username, password, cache, name, polling, exclude, lights)
username, password, cache, name, polling, exclude, lights
)
except (AbodeException, ConnectTimeout, HTTPError) as ex:
_LOGGER.error("Unable to connect to Abode: %s", str(ex))
hass.components.persistent_notification.create(
'Error: {}<br />'
'You will need to restart hass after fixing.'
''.format(ex),
"Error: {}<br />"
"You will need to restart hass after fixing."
"".format(ex),
title=NOTIFICATION_TITLE,
notification_id=NOTIFICATION_ID)
notification_id=NOTIFICATION_ID,
)
return False
setup_hass_services(hass)
@ -173,8 +195,11 @@ def setup_hass_services(hass):
"""Capture a new image."""
entity_ids = call.data.get(ATTR_ENTITY_ID)
target_devices = [device for device in hass.data[DOMAIN].devices
if device.entity_id in entity_ids]
target_devices = [
device
for device in hass.data[DOMAIN].devices
if device.entity_id in entity_ids
]
for device in target_devices:
device.capture()
@ -183,27 +208,31 @@ def setup_hass_services(hass):
"""Trigger a quick action."""
entity_ids = call.data.get(ATTR_ENTITY_ID, None)
target_devices = [device for device in hass.data[DOMAIN].devices
if device.entity_id in entity_ids]
target_devices = [
device
for device in hass.data[DOMAIN].devices
if device.entity_id in entity_ids
]
for device in target_devices:
device.trigger()
hass.services.register(
DOMAIN, SERVICE_SETTINGS, change_setting,
schema=CHANGE_SETTING_SCHEMA)
DOMAIN, SERVICE_SETTINGS, change_setting, schema=CHANGE_SETTING_SCHEMA
)
hass.services.register(
DOMAIN, SERVICE_CAPTURE_IMAGE, capture_image,
schema=CAPTURE_IMAGE_SCHEMA)
DOMAIN, SERVICE_CAPTURE_IMAGE, capture_image, schema=CAPTURE_IMAGE_SCHEMA
)
hass.services.register(
DOMAIN, SERVICE_TRIGGER, trigger_quick_action,
schema=TRIGGER_SCHEMA)
DOMAIN, SERVICE_TRIGGER, trigger_quick_action, schema=TRIGGER_SCHEMA
)
def setup_hass_events(hass):
"""Home Assistant start and stop callbacks."""
def startup(event):
"""Listen for push events."""
hass.data[DOMAIN].abode.events.start()
@ -229,28 +258,32 @@ def setup_abode_events(hass):
def event_callback(event, event_json):
"""Handle an event callback from Abode."""
data = {
ATTR_DEVICE_ID: event_json.get(ATTR_DEVICE_ID, ''),
ATTR_DEVICE_NAME: event_json.get(ATTR_DEVICE_NAME, ''),
ATTR_DEVICE_TYPE: event_json.get(ATTR_DEVICE_TYPE, ''),
ATTR_EVENT_CODE: event_json.get(ATTR_EVENT_CODE, ''),
ATTR_EVENT_NAME: event_json.get(ATTR_EVENT_NAME, ''),
ATTR_EVENT_TYPE: event_json.get(ATTR_EVENT_TYPE, ''),
ATTR_EVENT_UTC: event_json.get(ATTR_EVENT_UTC, ''),
ATTR_USER_NAME: event_json.get(ATTR_USER_NAME, ''),
ATTR_DATE: event_json.get(ATTR_DATE, ''),
ATTR_TIME: event_json.get(ATTR_TIME, ''),
ATTR_DEVICE_ID: event_json.get(ATTR_DEVICE_ID, ""),
ATTR_DEVICE_NAME: event_json.get(ATTR_DEVICE_NAME, ""),
ATTR_DEVICE_TYPE: event_json.get(ATTR_DEVICE_TYPE, ""),
ATTR_EVENT_CODE: event_json.get(ATTR_EVENT_CODE, ""),
ATTR_EVENT_NAME: event_json.get(ATTR_EVENT_NAME, ""),
ATTR_EVENT_TYPE: event_json.get(ATTR_EVENT_TYPE, ""),
ATTR_EVENT_UTC: event_json.get(ATTR_EVENT_UTC, ""),
ATTR_USER_NAME: event_json.get(ATTR_USER_NAME, ""),
ATTR_DATE: event_json.get(ATTR_DATE, ""),
ATTR_TIME: event_json.get(ATTR_TIME, ""),
}
hass.bus.fire(event, data)
events = [TIMELINE.ALARM_GROUP, TIMELINE.ALARM_END_GROUP,
TIMELINE.PANEL_FAULT_GROUP, TIMELINE.PANEL_RESTORE_GROUP,
TIMELINE.AUTOMATION_GROUP]
events = [
TIMELINE.ALARM_GROUP,
TIMELINE.ALARM_END_GROUP,
TIMELINE.PANEL_FAULT_GROUP,
TIMELINE.PANEL_RESTORE_GROUP,
TIMELINE.AUTOMATION_GROUP,
]
for event in events:
hass.data[DOMAIN].abode.events.add_event_callback(
event,
partial(event_callback, event))
event, partial(event_callback, event)
)
class AbodeDevice(Entity):
@ -266,7 +299,8 @@ class AbodeDevice(Entity):
"""Subscribe Abode events."""
self.hass.async_add_job(
self._data.abode.events.add_device_callback,
self._device.device_id, self._update_callback
self._device.device_id,
self._update_callback,
)
@property
@ -288,10 +322,10 @@ class AbodeDevice(Entity):
"""Return the state attributes."""
return {
ATTR_ATTRIBUTION: CONF_ATTRIBUTION,
'device_id': self._device.device_id,
'battery_low': self._device.battery_low,
'no_response': self._device.no_response,
'device_type': self._device.type
"device_id": self._device.device_id,
"battery_low": self._device.battery_low,
"no_response": self._device.no_response,
"device_type": self._device.type,
}
def _update_callback(self, device):
@ -314,7 +348,8 @@ class AbodeAutomation(Entity):
if self._event:
self.hass.async_add_job(
self._data.abode.events.add_event_callback,
self._event, self._update_callback
self._event,
self._update_callback,
)
@property
@ -336,9 +371,9 @@ class AbodeAutomation(Entity):
"""Return the state attributes."""
return {
ATTR_ATTRIBUTION: CONF_ATTRIBUTION,
'automation_id': self._automation.automation_id,
'type': self._automation.type,
'sub_type': self._automation.sub_type
"automation_id": self._automation.automation_id,
"type": self._automation.type,
"sub_type": self._automation.sub_type,
}
def _update_callback(self, device):

View file

@ -10,51 +10,62 @@ import logging
import ctypes
from collections import namedtuple
import voluptuous as vol
from homeassistant.const import CONF_DEVICE, CONF_PORT, CONF_IP_ADDRESS, \
EVENT_HOMEASSISTANT_STOP
from homeassistant.const import (
CONF_DEVICE,
CONF_PORT,
CONF_IP_ADDRESS,
EVENT_HOMEASSISTANT_STOP,
)
import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['pyads==2.2.6']
REQUIREMENTS = ["pyads==2.2.6"]
_LOGGER = logging.getLogger(__name__)
DATA_ADS = 'data_ads'
DATA_ADS = "data_ads"
# Supported Types
ADSTYPE_INT = 'int'
ADSTYPE_UINT = 'uint'
ADSTYPE_BYTE = 'byte'
ADSTYPE_BOOL = 'bool'
ADSTYPE_INT = "int"
ADSTYPE_UINT = "uint"
ADSTYPE_BYTE = "byte"
ADSTYPE_BOOL = "bool"
DOMAIN = 'ads'
DOMAIN = "ads"
CONF_ADS_VAR = 'adsvar'
CONF_ADS_VAR_BRIGHTNESS = 'adsvar_brightness'
CONF_ADS_TYPE = 'adstype'
CONF_ADS_FACTOR = 'factor'
CONF_ADS_VALUE = 'value'
CONF_ADS_VAR = "adsvar"
CONF_ADS_VAR_BRIGHTNESS = "adsvar_brightness"
CONF_ADS_TYPE = "adstype"
CONF_ADS_FACTOR = "factor"
CONF_ADS_VALUE = "value"
SERVICE_WRITE_DATA_BY_NAME = 'write_data_by_name'
SERVICE_WRITE_DATA_BY_NAME = "write_data_by_name"
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
vol.Required(CONF_DEVICE): cv.string,
vol.Required(CONF_PORT): cv.port,
vol.Optional(CONF_IP_ADDRESS): cv.string,
})
}, extra=vol.ALLOW_EXTRA)
CONFIG_SCHEMA = vol.Schema(
{
DOMAIN: vol.Schema(
{
vol.Required(CONF_DEVICE): cv.string,
vol.Required(CONF_PORT): cv.port,
vol.Optional(CONF_IP_ADDRESS): cv.string,
}
)
},
extra=vol.ALLOW_EXTRA,
)
SCHEMA_SERVICE_WRITE_DATA_BY_NAME = vol.Schema({
vol.Required(CONF_ADS_TYPE):
vol.In([ADSTYPE_INT, ADSTYPE_UINT, ADSTYPE_BYTE]),
vol.Required(CONF_ADS_VALUE): cv.match_all,
vol.Required(CONF_ADS_VAR): cv.string,
})
SCHEMA_SERVICE_WRITE_DATA_BY_NAME = vol.Schema(
{
vol.Required(CONF_ADS_TYPE): vol.In([ADSTYPE_INT, ADSTYPE_UINT, ADSTYPE_BYTE]),
vol.Required(CONF_ADS_VALUE): cv.match_all,
vol.Required(CONF_ADS_VAR): cv.string,
}
)
def setup(hass, config):
"""Set up the ADS component."""
import pyads
conf = config[DOMAIN]
net_id = conf.get(CONF_DEVICE)
@ -79,8 +90,7 @@ def setup(hass, config):
try:
ads = AdsHub(client)
except pyads.pyads.ADSError:
_LOGGER.error(
"Could not connect to ADS host (netid=%s, port=%s)", net_id, port)
_LOGGER.error("Could not connect to ADS host (netid=%s, port=%s)", net_id, port)
return False
hass.data[DATA_ADS] = ads
@ -98,15 +108,18 @@ def setup(hass, config):
_LOGGER.error(err)
hass.services.register(
DOMAIN, SERVICE_WRITE_DATA_BY_NAME, handle_write_data_by_name,
schema=SCHEMA_SERVICE_WRITE_DATA_BY_NAME)
DOMAIN,
SERVICE_WRITE_DATA_BY_NAME,
handle_write_data_by_name,
schema=SCHEMA_SERVICE_WRITE_DATA_BY_NAME,
)
return True
# Tuple to hold data needed for notification
NotificationItem = namedtuple(
'NotificationItem', 'hnotify huser name plc_datatype callback'
"NotificationItem", "hnotify huser name plc_datatype callback"
)
@ -128,12 +141,13 @@ class AdsHub:
_LOGGER.debug("Shutting down ADS")
for notification_item in self._notification_items.values():
self._client.del_device_notification(
notification_item.hnotify,
notification_item.huser
notification_item.hnotify, notification_item.huser
)
_LOGGER.debug(
"Deleting device notification %d, %d",
notification_item.hnotify, notification_item.huser)
notification_item.hnotify,
notification_item.huser,
)
self._client.close()
def register_device(self, device):
@ -153,18 +167,20 @@ class AdsHub:
def add_device_notification(self, name, plc_datatype, callback):
"""Add a notification to the ADS devices."""
from pyads import NotificationAttrib
attr = NotificationAttrib(ctypes.sizeof(plc_datatype))
with self._lock:
hnotify, huser = self._client.add_device_notification(
name, attr, self._device_notification_callback)
name, attr, self._device_notification_callback
)
hnotify = int(hnotify)
_LOGGER.debug(
"Added device notification %d for variable %s", hnotify, name)
_LOGGER.debug("Added device notification %d for variable %s", hnotify, name)
self._notification_items[hnotify] = NotificationItem(
hnotify, huser, name, plc_datatype, callback)
hnotify, huser, name, plc_datatype, callback
)
def _device_notification_callback(self, addr, notification, huser):
"""Handle device notifications."""
@ -182,13 +198,13 @@ class AdsHub:
# Parse data to desired datatype
if notification_item.plc_datatype == self.PLCTYPE_BOOL:
value = bool(struct.unpack('<?', bytearray(data)[:1])[0])
value = bool(struct.unpack("<?", bytearray(data)[:1])[0])
elif notification_item.plc_datatype == self.PLCTYPE_INT:
value = struct.unpack('<h', bytearray(data)[:2])[0]
value = struct.unpack("<h", bytearray(data)[:2])[0]
elif notification_item.plc_datatype == self.PLCTYPE_BYTE:
value = struct.unpack('<B', bytearray(data)[:1])[0]
value = struct.unpack("<B", bytearray(data)[:1])[0]
elif notification_item.plc_datatype == self.PLCTYPE_UINT:
value = struct.unpack('<H', bytearray(data)[:2])[0]
value = struct.unpack("<H", bytearray(data)[:2])[0]
else:
value = bytearray(data)
_LOGGER.warning("No callback available for this datatype")

View file

@ -11,25 +11,31 @@ import logging
import voluptuous as vol
from homeassistant.const import (
ATTR_CODE, ATTR_CODE_FORMAT, ATTR_ENTITY_ID, SERVICE_ALARM_TRIGGER,
SERVICE_ALARM_DISARM, SERVICE_ALARM_ARM_HOME, SERVICE_ALARM_ARM_AWAY,
SERVICE_ALARM_ARM_NIGHT, SERVICE_ALARM_ARM_CUSTOM_BYPASS)
ATTR_CODE,
ATTR_CODE_FORMAT,
ATTR_ENTITY_ID,
SERVICE_ALARM_TRIGGER,
SERVICE_ALARM_DISARM,
SERVICE_ALARM_ARM_HOME,
SERVICE_ALARM_ARM_AWAY,
SERVICE_ALARM_ARM_NIGHT,
SERVICE_ALARM_ARM_CUSTOM_BYPASS,
)
from homeassistant.loader import bind_hass
from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.entity_component import EntityComponent
DOMAIN = 'alarm_control_panel'
DOMAIN = "alarm_control_panel"
SCAN_INTERVAL = timedelta(seconds=30)
ATTR_CHANGED_BY = 'changed_by'
ATTR_CHANGED_BY = "changed_by"
ENTITY_ID_FORMAT = DOMAIN + '.{}'
ENTITY_ID_FORMAT = DOMAIN + ".{}"
ALARM_SERVICE_SCHEMA = vol.Schema({
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
vol.Optional(ATTR_CODE): cv.string,
})
ALARM_SERVICE_SCHEMA = vol.Schema(
{vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, vol.Optional(ATTR_CODE): cv.string}
)
@bind_hass
@ -108,33 +114,30 @@ def alarm_arm_custom_bypass(hass, code=None, entity_id=None):
def async_setup(hass, config):
"""Track states and offer events for sensors."""
component = hass.data[DOMAIN] = EntityComponent(
logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL)
logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL
)
yield from component.async_setup(config)
component.async_register_entity_service(
SERVICE_ALARM_DISARM, ALARM_SERVICE_SCHEMA,
'async_alarm_disarm'
SERVICE_ALARM_DISARM, ALARM_SERVICE_SCHEMA, "async_alarm_disarm"
)
component.async_register_entity_service(
SERVICE_ALARM_ARM_HOME, ALARM_SERVICE_SCHEMA,
'async_alarm_arm_home'
SERVICE_ALARM_ARM_HOME, ALARM_SERVICE_SCHEMA, "async_alarm_arm_home"
)
component.async_register_entity_service(
SERVICE_ALARM_ARM_AWAY, ALARM_SERVICE_SCHEMA,
'async_alarm_arm_away'
SERVICE_ALARM_ARM_AWAY, ALARM_SERVICE_SCHEMA, "async_alarm_arm_away"
)
component.async_register_entity_service(
SERVICE_ALARM_ARM_NIGHT, ALARM_SERVICE_SCHEMA,
'async_alarm_arm_night'
SERVICE_ALARM_ARM_NIGHT, ALARM_SERVICE_SCHEMA, "async_alarm_arm_night"
)
component.async_register_entity_service(
SERVICE_ALARM_ARM_CUSTOM_BYPASS, ALARM_SERVICE_SCHEMA,
'async_alarm_arm_custom_bypass'
SERVICE_ALARM_ARM_CUSTOM_BYPASS,
ALARM_SERVICE_SCHEMA,
"async_alarm_arm_custom_bypass",
)
component.async_register_entity_service(
SERVICE_ALARM_TRIGGER, ALARM_SERVICE_SCHEMA,
'async_alarm_trigger'
SERVICE_ALARM_TRIGGER, ALARM_SERVICE_SCHEMA, "async_alarm_trigger"
)
return True
@ -228,14 +231,13 @@ class AlarmControlPanel(Entity):
This method must be run in the event loop and returns a coroutine.
"""
return self.hass.async_add_executor_job(
self.alarm_arm_custom_bypass, code)
return self.hass.async_add_executor_job(self.alarm_arm_custom_bypass, code)
@property
def state_attributes(self):
"""Return the state attributes."""
state_attr = {
ATTR_CODE_FORMAT: self.code_format,
ATTR_CHANGED_BY: self.changed_by
ATTR_CHANGED_BY: self.changed_by,
}
return state_attr

View file

@ -10,14 +10,17 @@ from homeassistant.components.abode import CONF_ATTRIBUTION, AbodeDevice
from homeassistant.components.abode import DOMAIN as ABODE_DOMAIN
from homeassistant.components.alarm_control_panel import AlarmControlPanel
from homeassistant.const import (
ATTR_ATTRIBUTION, STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME,
STATE_ALARM_DISARMED)
ATTR_ATTRIBUTION,
STATE_ALARM_ARMED_AWAY,
STATE_ALARM_ARMED_HOME,
STATE_ALARM_DISARMED,
)
DEPENDENCIES = ['abode']
DEPENDENCIES = ["abode"]
_LOGGER = logging.getLogger(__name__)
ICON = 'mdi:security'
ICON = "mdi:security"
def setup_platform(hass, config, add_entities, discovery_info=None):
@ -79,7 +82,7 @@ class AbodeAlarm(AbodeDevice, AlarmControlPanel):
"""Return the state attributes."""
return {
ATTR_ATTRIBUTION: CONF_ATTRIBUTION,
'device_id': self._device.device_id,
'battery_backup': self._device.battery,
'cellular_backup': self._device.is_cellular,
"device_id": self._device.device_id,
"battery_backup": self._device.battery,
"cellular_backup": self._device.is_cellular,
}

View file

@ -12,18 +12,20 @@ import voluptuous as vol
import homeassistant.components.alarm_control_panel as alarm
from homeassistant.components.alarmdecoder import DATA_AD, SIGNAL_PANEL_MESSAGE
from homeassistant.const import (
ATTR_CODE, STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME,
STATE_ALARM_DISARMED, STATE_ALARM_TRIGGERED)
ATTR_CODE,
STATE_ALARM_ARMED_AWAY,
STATE_ALARM_ARMED_HOME,
STATE_ALARM_DISARMED,
STATE_ALARM_TRIGGERED,
)
import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__)
DEPENDENCIES = ['alarmdecoder']
DEPENDENCIES = ["alarmdecoder"]
SERVICE_ALARM_TOGGLE_CHIME = 'alarmdecoder_alarm_toggle_chime'
ALARM_TOGGLE_CHIME_SCHEMA = vol.Schema({
vol.Required(ATTR_CODE): cv.string,
})
SERVICE_ALARM_TOGGLE_CHIME = "alarmdecoder_alarm_toggle_chime"
ALARM_TOGGLE_CHIME_SCHEMA = vol.Schema({vol.Required(ATTR_CODE): cv.string})
def setup_platform(hass, config, add_entities, discovery_info=None):
@ -37,8 +39,11 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
device.alarm_toggle_chime(code)
hass.services.register(
alarm.DOMAIN, SERVICE_ALARM_TOGGLE_CHIME, alarm_toggle_chime_handler,
schema=ALARM_TOGGLE_CHIME_SCHEMA)
alarm.DOMAIN,
SERVICE_ALARM_TOGGLE_CHIME,
alarm_toggle_chime_handler,
schema=ALARM_TOGGLE_CHIME_SCHEMA,
)
class AlarmDecoderAlarmPanel(alarm.AlarmControlPanel):
@ -63,7 +68,8 @@ class AlarmDecoderAlarmPanel(alarm.AlarmControlPanel):
def async_added_to_hass(self):
"""Register callbacks."""
self.hass.helpers.dispatcher.async_dispatcher_connect(
SIGNAL_PANEL_MESSAGE, self._message_callback)
SIGNAL_PANEL_MESSAGE, self._message_callback
)
def _message_callback(self, message):
"""Handle received messages."""
@ -101,7 +107,7 @@ class AlarmDecoderAlarmPanel(alarm.AlarmControlPanel):
@property
def code_format(self):
"""Return one or more digits/characters."""
return 'Number'
return "Number"
@property
def state(self):
@ -112,15 +118,15 @@ class AlarmDecoderAlarmPanel(alarm.AlarmControlPanel):
def device_state_attributes(self):
"""Return the state attributes."""
return {
'ac_power': self._ac_power,
'backlight_on': self._backlight_on,
'battery_low': self._battery_low,
'check_zone': self._check_zone,
'chime': self._chime,
'entry_delay_off': self._entry_delay_off,
'programming_mode': self._programming_mode,
'ready': self._ready,
'zone_bypassed': self._zone_bypassed,
"ac_power": self._ac_power,
"backlight_on": self._backlight_on,
"battery_low": self._battery_low,
"check_zone": self._check_zone,
"chime": self._chime,
"entry_delay_off": self._entry_delay_off,
"programming_mode": self._programming_mode,
"ready": self._ready,
"zone_bypassed": self._zone_bypassed,
}
def alarm_disarm(self, code=None):

View file

@ -13,28 +13,36 @@ import voluptuous as vol
import homeassistant.components.alarm_control_panel as alarm
from homeassistant.components.alarm_control_panel import PLATFORM_SCHEMA
from homeassistant.const import (
CONF_CODE, CONF_NAME, CONF_PASSWORD, CONF_USERNAME, STATE_ALARM_ARMED_AWAY,
STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED, STATE_UNKNOWN)
CONF_CODE,
CONF_NAME,
CONF_PASSWORD,
CONF_USERNAME,
STATE_ALARM_ARMED_AWAY,
STATE_ALARM_ARMED_HOME,
STATE_ALARM_DISARMED,
STATE_UNKNOWN,
)
from homeassistant.helpers.aiohttp_client import async_get_clientsession
import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['pyalarmdotcom==0.3.2']
REQUIREMENTS = ["pyalarmdotcom==0.3.2"]
_LOGGER = logging.getLogger(__name__)
DEFAULT_NAME = 'Alarm.com'
DEFAULT_NAME = "Alarm.com"
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_PASSWORD): cv.string,
vol.Required(CONF_USERNAME): cv.string,
vol.Optional(CONF_CODE): cv.positive_int,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
})
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
{
vol.Required(CONF_PASSWORD): cv.string,
vol.Required(CONF_USERNAME): cv.string,
vol.Optional(CONF_CODE): cv.positive_int,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
}
)
@asyncio.coroutine
def async_setup_platform(hass, config, async_add_entities,
discovery_info=None):
def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
"""Set up a Alarm.com control panel."""
name = config.get(CONF_NAME)
code = config.get(CONF_CODE)
@ -52,7 +60,8 @@ class AlarmDotCom(alarm.AlarmControlPanel):
def __init__(self, hass, name, code, username, password):
"""Initialize the Alarm.com status."""
from pyalarmdotcom import Alarmdotcom
_LOGGER.debug('Setting up Alarm.com...')
_LOGGER.debug("Setting up Alarm.com...")
self._hass = hass
self._name = name
self._code = str(code) if code else None
@ -60,8 +69,7 @@ class AlarmDotCom(alarm.AlarmControlPanel):
self._password = password
self._websession = async_get_clientsession(self._hass)
self._state = STATE_UNKNOWN
self._alarm = Alarmdotcom(
username, password, self._websession, hass.loop)
self._alarm = Alarmdotcom(username, password, self._websession, hass.loop)
@asyncio.coroutine
def async_login(self):
@ -84,27 +92,25 @@ class AlarmDotCom(alarm.AlarmControlPanel):
"""Return one or more digits/characters."""
if self._code is None:
return None
if isinstance(self._code, str) and re.search('^\\d+$', self._code):
return 'Number'
return 'Any'
if isinstance(self._code, str) and re.search("^\\d+$", self._code):
return "Number"
return "Any"
@property
def state(self):
"""Return the state of the device."""
if self._alarm.state.lower() == 'disarmed':
if self._alarm.state.lower() == "disarmed":
return STATE_ALARM_DISARMED
if self._alarm.state.lower() == 'armed stay':
if self._alarm.state.lower() == "armed stay":
return STATE_ALARM_ARMED_HOME
if self._alarm.state.lower() == 'armed away':
if self._alarm.state.lower() == "armed away":
return STATE_ALARM_ARMED_AWAY
return STATE_UNKNOWN
@property
def device_state_attributes(self):
"""Return the state attributes."""
return {
'sensor_status': self._alarm.sensor_status
}
return {"sensor_status": self._alarm.sensor_status}
@asyncio.coroutine
def async_alarm_disarm(self, code=None):

View file

@ -12,30 +12,40 @@ import homeassistant.helpers.config_validation as cv
from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.components.alarm_control_panel import (
AlarmControlPanel, PLATFORM_SCHEMA)
AlarmControlPanel,
PLATFORM_SCHEMA,
)
from homeassistant.components.arlo import (
DATA_ARLO, CONF_ATTRIBUTION, SIGNAL_UPDATE_ARLO)
DATA_ARLO,
CONF_ATTRIBUTION,
SIGNAL_UPDATE_ARLO,
)
from homeassistant.const import (
ATTR_ATTRIBUTION, STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME,
STATE_ALARM_DISARMED)
ATTR_ATTRIBUTION,
STATE_ALARM_ARMED_AWAY,
STATE_ALARM_ARMED_HOME,
STATE_ALARM_DISARMED,
)
_LOGGER = logging.getLogger(__name__)
ARMED = 'armed'
ARMED = "armed"
CONF_HOME_MODE_NAME = 'home_mode_name'
CONF_AWAY_MODE_NAME = 'away_mode_name'
CONF_HOME_MODE_NAME = "home_mode_name"
CONF_AWAY_MODE_NAME = "away_mode_name"
DEPENDENCIES = ['arlo']
DEPENDENCIES = ["arlo"]
DISARMED = 'disarmed'
DISARMED = "disarmed"
ICON = 'mdi:security'
ICON = "mdi:security"
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_HOME_MODE_NAME, default=ARMED): cv.string,
vol.Optional(CONF_AWAY_MODE_NAME, default=ARMED): cv.string,
})
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
{
vol.Optional(CONF_HOME_MODE_NAME, default=ARMED): cv.string,
vol.Optional(CONF_AWAY_MODE_NAME, default=ARMED): cv.string,
}
)
def setup_platform(hass, config, add_entities, discovery_info=None):
@ -49,8 +59,9 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
away_mode_name = config.get(CONF_AWAY_MODE_NAME)
base_stations = []
for base_station in arlo.base_stations:
base_stations.append(ArloBaseStation(base_station, home_mode_name,
away_mode_name))
base_stations.append(
ArloBaseStation(base_station, home_mode_name, away_mode_name)
)
add_entities(base_stations, True)
@ -71,8 +82,7 @@ class ArloBaseStation(AlarmControlPanel):
async def async_added_to_hass(self):
"""Register callbacks."""
async_dispatcher_connect(
self.hass, SIGNAL_UPDATE_ARLO, self._update_callback)
async_dispatcher_connect(self.hass, SIGNAL_UPDATE_ARLO, self._update_callback)
@callback
def _update_callback(self):
@ -115,7 +125,7 @@ class ArloBaseStation(AlarmControlPanel):
"""Return the state attributes."""
return {
ATTR_ATTRIBUTION: CONF_ATTRIBUTION,
'device_id': self._base_station.device_id
"device_id": self._base_station.device_id,
}
def _get_state_from_mode(self, mode):

View file

@ -8,10 +8,14 @@ import logging
from homeassistant.components.alarm_control_panel import AlarmControlPanel
from homeassistant.components.canary import DATA_CANARY
from homeassistant.const import STATE_ALARM_DISARMED, STATE_ALARM_ARMED_AWAY, \
STATE_ALARM_ARMED_NIGHT, STATE_ALARM_ARMED_HOME
from homeassistant.const import (
STATE_ALARM_DISARMED,
STATE_ALARM_ARMED_AWAY,
STATE_ALARM_ARMED_NIGHT,
STATE_ALARM_ARMED_HOME,
)
DEPENDENCIES = ['canary']
DEPENDENCIES = ["canary"]
_LOGGER = logging.getLogger(__name__)
@ -44,8 +48,11 @@ class CanaryAlarm(AlarmControlPanel):
@property
def state(self):
"""Return the state of the device."""
from canary.api import LOCATION_MODE_AWAY, LOCATION_MODE_HOME, \
LOCATION_MODE_NIGHT
from canary.api import (
LOCATION_MODE_AWAY,
LOCATION_MODE_HOME,
LOCATION_MODE_NIGHT,
)
location = self._data.get_location(self._location_id)
@ -65,27 +72,27 @@ class CanaryAlarm(AlarmControlPanel):
def device_state_attributes(self):
"""Return the state attributes."""
location = self._data.get_location(self._location_id)
return {
'private': location.is_private
}
return {"private": location.is_private}
def alarm_disarm(self, code=None):
"""Send disarm command."""
location = self._data.get_location(self._location_id)
self._data.set_location_mode(self._location_id, location.mode.name,
True)
self._data.set_location_mode(self._location_id, location.mode.name, True)
def alarm_arm_home(self, code=None):
"""Send arm home command."""
from canary.api import LOCATION_MODE_HOME
self._data.set_location_mode(self._location_id, LOCATION_MODE_HOME)
def alarm_arm_away(self, code=None):
"""Send arm away command."""
from canary.api import LOCATION_MODE_AWAY
self._data.set_location_mode(self._location_id, LOCATION_MODE_AWAY)
def alarm_arm_night(self, code=None):
"""Send arm night command."""
from canary.api import LOCATION_MODE_NIGHT
self._data.set_location_mode(self._location_id, LOCATION_MODE_NIGHT)

View file

@ -14,25 +14,33 @@ import voluptuous as vol
import homeassistant.components.alarm_control_panel as alarm
from homeassistant.components.alarm_control_panel import PLATFORM_SCHEMA
from homeassistant.const import (
CONF_HOST, CONF_NAME, CONF_PORT, STATE_ALARM_ARMED_AWAY,
STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED, STATE_UNKNOWN)
CONF_HOST,
CONF_NAME,
CONF_PORT,
STATE_ALARM_ARMED_AWAY,
STATE_ALARM_ARMED_HOME,
STATE_ALARM_DISARMED,
STATE_UNKNOWN,
)
import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['concord232==0.15']
REQUIREMENTS = ["concord232==0.15"]
_LOGGER = logging.getLogger(__name__)
DEFAULT_HOST = 'localhost'
DEFAULT_NAME = 'CONCORD232'
DEFAULT_HOST = "localhost"
DEFAULT_NAME = "CONCORD232"
DEFAULT_PORT = 5007
SCAN_INTERVAL = timedelta(seconds=10)
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
})
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
{
vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
}
)
def setup_platform(hass, config, add_entities, discovery_info=None):
@ -41,7 +49,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
host = config.get(CONF_HOST)
port = config.get(CONF_PORT)
url = 'http://{}:{}'.format(host, port)
url = "http://{}:{}".format(host, port)
try:
add_entities([Concord232Alarm(hass, url, name)])
@ -80,7 +88,7 @@ class Concord232Alarm(alarm.AlarmControlPanel):
@property
def code_format(self):
"""Return the characters if code is defined."""
return 'Number'
return "Number"
@property
def state(self):
@ -92,16 +100,18 @@ class Concord232Alarm(alarm.AlarmControlPanel):
try:
part = self._alarm.list_partitions()[0]
except requests.exceptions.ConnectionError as ex:
_LOGGER.error("Unable to connect to %(host)s: %(reason)s",
dict(host=self._url, reason=ex))
_LOGGER.error(
"Unable to connect to %(host)s: %(reason)s",
dict(host=self._url, reason=ex),
)
newstate = STATE_UNKNOWN
except IndexError:
_LOGGER.error("Concord232 reports no partitions")
newstate = STATE_UNKNOWN
if part['arming_level'] == 'Off':
if part["arming_level"] == "Off":
newstate = STATE_ALARM_DISARMED
elif 'Home' in part['arming_level']:
elif "Home" in part["arming_level"]:
newstate = STATE_ALARM_ARMED_HOME
else:
newstate = STATE_ALARM_ARMED_AWAY
@ -117,8 +127,8 @@ class Concord232Alarm(alarm.AlarmControlPanel):
def alarm_arm_home(self, code=None):
"""Send arm home command."""
self._alarm.arm('stay')
self._alarm.arm("stay")
def alarm_arm_away(self, code=None):
"""Send arm away command."""
self._alarm.arm('away')
self._alarm.arm("away")

View file

@ -7,42 +7,57 @@ https://home-assistant.io/components/demo/
import datetime
from homeassistant.components.alarm_control_panel import manual
from homeassistant.const import (
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_CUSTOM_BYPASS,
STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT,
STATE_ALARM_DISARMED, STATE_ALARM_TRIGGERED, CONF_DELAY_TIME,
CONF_PENDING_TIME, CONF_TRIGGER_TIME)
STATE_ALARM_ARMED_AWAY,
STATE_ALARM_ARMED_CUSTOM_BYPASS,
STATE_ALARM_ARMED_HOME,
STATE_ALARM_ARMED_NIGHT,
STATE_ALARM_DISARMED,
STATE_ALARM_TRIGGERED,
CONF_DELAY_TIME,
CONF_PENDING_TIME,
CONF_TRIGGER_TIME,
)
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the Demo alarm control panel platform."""
add_entities([
manual.ManualAlarm(hass, 'Alarm', '1234', None, False, {
STATE_ALARM_ARMED_AWAY: {
CONF_DELAY_TIME: datetime.timedelta(seconds=0),
CONF_PENDING_TIME: datetime.timedelta(seconds=5),
CONF_TRIGGER_TIME: datetime.timedelta(seconds=10),
},
STATE_ALARM_ARMED_HOME: {
CONF_DELAY_TIME: datetime.timedelta(seconds=0),
CONF_PENDING_TIME: datetime.timedelta(seconds=5),
CONF_TRIGGER_TIME: datetime.timedelta(seconds=10),
},
STATE_ALARM_ARMED_NIGHT: {
CONF_DELAY_TIME: datetime.timedelta(seconds=0),
CONF_PENDING_TIME: datetime.timedelta(seconds=5),
CONF_TRIGGER_TIME: datetime.timedelta(seconds=10),
},
STATE_ALARM_DISARMED: {
CONF_DELAY_TIME: datetime.timedelta(seconds=0),
CONF_TRIGGER_TIME: datetime.timedelta(seconds=10),
},
STATE_ALARM_ARMED_CUSTOM_BYPASS: {
CONF_DELAY_TIME: datetime.timedelta(seconds=0),
CONF_PENDING_TIME: datetime.timedelta(seconds=5),
CONF_TRIGGER_TIME: datetime.timedelta(seconds=10),
},
STATE_ALARM_TRIGGERED: {
CONF_PENDING_TIME: datetime.timedelta(seconds=5),
},
}),
])
add_entities(
[
manual.ManualAlarm(
hass,
"Alarm",
"1234",
None,
False,
{
STATE_ALARM_ARMED_AWAY: {
CONF_DELAY_TIME: datetime.timedelta(seconds=0),
CONF_PENDING_TIME: datetime.timedelta(seconds=5),
CONF_TRIGGER_TIME: datetime.timedelta(seconds=10),
},
STATE_ALARM_ARMED_HOME: {
CONF_DELAY_TIME: datetime.timedelta(seconds=0),
CONF_PENDING_TIME: datetime.timedelta(seconds=5),
CONF_TRIGGER_TIME: datetime.timedelta(seconds=10),
},
STATE_ALARM_ARMED_NIGHT: {
CONF_DELAY_TIME: datetime.timedelta(seconds=0),
CONF_PENDING_TIME: datetime.timedelta(seconds=5),
CONF_TRIGGER_TIME: datetime.timedelta(seconds=10),
},
STATE_ALARM_DISARMED: {
CONF_DELAY_TIME: datetime.timedelta(seconds=0),
CONF_TRIGGER_TIME: datetime.timedelta(seconds=10),
},
STATE_ALARM_ARMED_CUSTOM_BYPASS: {
CONF_DELAY_TIME: datetime.timedelta(seconds=0),
CONF_PENDING_TIME: datetime.timedelta(seconds=5),
CONF_TRIGGER_TIME: datetime.timedelta(seconds=10),
},
STATE_ALARM_TRIGGERED: {
CONF_PENDING_TIME: datetime.timedelta(seconds=5)
},
},
)
]
)

View file

@ -11,26 +11,33 @@ import requests
import homeassistant.components.alarm_control_panel as alarm
from homeassistant.const import (
STATE_ALARM_DISARMED, STATE_ALARM_ARMED_HOME,
STATE_ALARM_ARMED_AWAY, STATE_ALARM_TRIGGERED,
STATE_ALARM_ARMED_NIGHT)
STATE_ALARM_DISARMED,
STATE_ALARM_ARMED_HOME,
STATE_ALARM_ARMED_AWAY,
STATE_ALARM_TRIGGERED,
STATE_ALARM_ARMED_NIGHT,
)
from homeassistant.components.egardia import (
EGARDIA_DEVICE, EGARDIA_SERVER,
REPORT_SERVER_CODES_IGNORE, CONF_REPORT_SERVER_CODES,
CONF_REPORT_SERVER_ENABLED, CONF_REPORT_SERVER_PORT
)
DEPENDENCIES = ['egardia']
EGARDIA_DEVICE,
EGARDIA_SERVER,
REPORT_SERVER_CODES_IGNORE,
CONF_REPORT_SERVER_CODES,
CONF_REPORT_SERVER_ENABLED,
CONF_REPORT_SERVER_PORT,
)
DEPENDENCIES = ["egardia"]
_LOGGER = logging.getLogger(__name__)
STATES = {
'ARM': STATE_ALARM_ARMED_AWAY,
'DAY HOME': STATE_ALARM_ARMED_HOME,
'DISARM': STATE_ALARM_DISARMED,
'ARMHOME': STATE_ALARM_ARMED_HOME,
'HOME': STATE_ALARM_ARMED_HOME,
'NIGHT HOME': STATE_ALARM_ARMED_NIGHT,
'TRIGGERED': STATE_ALARM_TRIGGERED
"ARM": STATE_ALARM_ARMED_AWAY,
"DAY HOME": STATE_ALARM_ARMED_HOME,
"DISARM": STATE_ALARM_DISARMED,
"ARMHOME": STATE_ALARM_ARMED_HOME,
"HOME": STATE_ALARM_ARMED_HOME,
"NIGHT HOME": STATE_ALARM_ARMED_NIGHT,
"TRIGGERED": STATE_ALARM_TRIGGERED,
}
@ -39,11 +46,12 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
if discovery_info is None:
return
device = EgardiaAlarm(
discovery_info['name'],
discovery_info["name"],
hass.data[EGARDIA_DEVICE],
discovery_info[CONF_REPORT_SERVER_ENABLED],
discovery_info.get(CONF_REPORT_SERVER_CODES),
discovery_info[CONF_REPORT_SERVER_PORT])
discovery_info[CONF_REPORT_SERVER_PORT],
)
# add egardia alarm device
add_entities([device], True)
@ -51,8 +59,9 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
class EgardiaAlarm(alarm.AlarmControlPanel):
"""Representation of a Egardia alarm."""
def __init__(self, name, egardiasystem,
rs_enabled=False, rs_codes=None, rs_port=52010):
def __init__(
self, name, egardiasystem, rs_enabled=False, rs_codes=None, rs_port=52010
):
"""Initialize the Egardia alarm."""
self._name = name
self._egardiasystem = egardiasystem
@ -66,8 +75,7 @@ class EgardiaAlarm(alarm.AlarmControlPanel):
"""Add Egardiaserver callback if enabled."""
if self._rs_enabled:
_LOGGER.debug("Registering callback to Egardiaserver")
self.hass.data[EGARDIA_SERVER].register_callback(
self.handle_status_event)
self.hass.data[EGARDIA_SERVER].register_callback(self.handle_status_event)
@property
def name(self):
@ -88,7 +96,7 @@ class EgardiaAlarm(alarm.AlarmControlPanel):
def handle_status_event(self, event):
"""Handle the Egardia system status event."""
statuscode = event.get('status')
statuscode = event.get("status")
if statuscode is not None:
status = self.lookupstatusfromcode(statuscode)
self.parsestatus(status)
@ -96,10 +104,15 @@ class EgardiaAlarm(alarm.AlarmControlPanel):
def lookupstatusfromcode(self, statuscode):
"""Look at the rs_codes and returns the status from the code."""
status = next((
status_group.upper() for status_group, codes
in self._rs_codes.items() for code in codes
if statuscode == code), 'UNKNOWN')
status = next(
(
status_group.upper()
for status_group, codes in self._rs_codes.items()
for code in codes
if statuscode == code
),
"UNKNOWN",
)
return status
def parsestatus(self, status):
@ -124,21 +137,29 @@ class EgardiaAlarm(alarm.AlarmControlPanel):
try:
self._egardiasystem.alarm_disarm()
except requests.exceptions.RequestException as err:
_LOGGER.error("Egardia device exception occurred when "
"sending disarm command: %s", err)
_LOGGER.error(
"Egardia device exception occurred when " "sending disarm command: %s",
err,
)
def alarm_arm_home(self, code=None):
"""Send arm home command."""
try:
self._egardiasystem.alarm_arm_home()
except requests.exceptions.RequestException as err:
_LOGGER.error("Egardia device exception occurred when "
"sending arm home command: %s", err)
_LOGGER.error(
"Egardia device exception occurred when "
"sending arm home command: %s",
err,
)
def alarm_arm_away(self, code=None):
"""Send arm away command."""
try:
self._egardiasystem.alarm_arm_away()
except requests.exceptions.RequestException as err:
_LOGGER.error("Egardia device exception occurred when "
"sending arm away command: %s", err)
_LOGGER.error(
"Egardia device exception occurred when "
"sending arm away command: %s",
err,
)

View file

@ -14,29 +14,43 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect
import homeassistant.components.alarm_control_panel as alarm
import homeassistant.helpers.config_validation as cv
from homeassistant.components.envisalink import (
DATA_EVL, EnvisalinkDevice, PARTITION_SCHEMA, CONF_CODE, CONF_PANIC,
CONF_PARTITIONNAME, SIGNAL_KEYPAD_UPDATE, SIGNAL_PARTITION_UPDATE)
DATA_EVL,
EnvisalinkDevice,
PARTITION_SCHEMA,
CONF_CODE,
CONF_PANIC,
CONF_PARTITIONNAME,
SIGNAL_KEYPAD_UPDATE,
SIGNAL_PARTITION_UPDATE,
)
from homeassistant.const import (
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED,
STATE_UNKNOWN, STATE_ALARM_TRIGGERED, STATE_ALARM_PENDING, ATTR_ENTITY_ID)
STATE_ALARM_ARMED_AWAY,
STATE_ALARM_ARMED_HOME,
STATE_ALARM_DISARMED,
STATE_UNKNOWN,
STATE_ALARM_TRIGGERED,
STATE_ALARM_PENDING,
ATTR_ENTITY_ID,
)
_LOGGER = logging.getLogger(__name__)
DEPENDENCIES = ['envisalink']
DEPENDENCIES = ["envisalink"]
SERVICE_ALARM_KEYPRESS = 'envisalink_alarm_keypress'
ATTR_KEYPRESS = 'keypress'
ALARM_KEYPRESS_SCHEMA = vol.Schema({
vol.Required(ATTR_ENTITY_ID): cv.entity_ids,
vol.Required(ATTR_KEYPRESS): cv.string
})
SERVICE_ALARM_KEYPRESS = "envisalink_alarm_keypress"
ATTR_KEYPRESS = "keypress"
ALARM_KEYPRESS_SCHEMA = vol.Schema(
{
vol.Required(ATTR_ENTITY_ID): cv.entity_ids,
vol.Required(ATTR_KEYPRESS): cv.string,
}
)
@asyncio.coroutine
def async_setup_platform(hass, config, async_add_entities,
discovery_info=None):
def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
"""Perform the setup for Envisalink alarm panels."""
configured_partitions = discovery_info['partitions']
configured_partitions = discovery_info["partitions"]
code = discovery_info[CONF_CODE]
panic_type = discovery_info[CONF_PANIC]
@ -49,8 +63,8 @@ def async_setup_platform(hass, config, async_add_entities,
device_config_data[CONF_PARTITIONNAME],
code,
panic_type,
hass.data[DATA_EVL].alarm_state['partition'][part_num],
hass.data[DATA_EVL]
hass.data[DATA_EVL].alarm_state["partition"][part_num],
hass.data[DATA_EVL],
)
devices.append(device)
@ -62,15 +76,19 @@ def async_setup_platform(hass, config, async_add_entities,
entity_ids = service.data.get(ATTR_ENTITY_ID)
keypress = service.data.get(ATTR_KEYPRESS)
target_devices = [device for device in devices
if device.entity_id in entity_ids]
target_devices = [
device for device in devices if device.entity_id in entity_ids
]
for device in target_devices:
device.async_alarm_keypress(keypress)
hass.services.async_register(
alarm.DOMAIN, SERVICE_ALARM_KEYPRESS, alarm_keypress_handler,
schema=ALARM_KEYPRESS_SCHEMA)
alarm.DOMAIN,
SERVICE_ALARM_KEYPRESS,
alarm_keypress_handler,
schema=ALARM_KEYPRESS_SCHEMA,
)
return True
@ -78,8 +96,9 @@ def async_setup_platform(hass, config, async_add_entities,
class EnvisalinkAlarm(EnvisalinkDevice, alarm.AlarmControlPanel):
"""Representation of an Envisalink-based alarm panel."""
def __init__(self, hass, partition_number, alarm_name, code, panic_type,
info, controller):
def __init__(
self, hass, partition_number, alarm_name, code, panic_type, info, controller
):
"""Initialize the alarm panel."""
self._partition_number = partition_number
self._code = code
@ -91,10 +110,10 @@ class EnvisalinkAlarm(EnvisalinkDevice, alarm.AlarmControlPanel):
@asyncio.coroutine
def async_added_to_hass(self):
"""Register callbacks."""
async_dispatcher_connect(self.hass, SIGNAL_KEYPAD_UPDATE, self._update_callback)
async_dispatcher_connect(
self.hass, SIGNAL_KEYPAD_UPDATE, self._update_callback)
async_dispatcher_connect(
self.hass, SIGNAL_PARTITION_UPDATE, self._update_callback)
self.hass, SIGNAL_PARTITION_UPDATE, self._update_callback
)
@callback
def _update_callback(self, partition):
@ -107,24 +126,24 @@ class EnvisalinkAlarm(EnvisalinkDevice, alarm.AlarmControlPanel):
"""Regex for code format or None if no code is required."""
if self._code:
return None
return 'Number'
return "Number"
@property
def state(self):
"""Return the state of the device."""
state = STATE_UNKNOWN
if self._info['status']['alarm']:
if self._info["status"]["alarm"]:
state = STATE_ALARM_TRIGGERED
elif self._info['status']['armed_away']:
elif self._info["status"]["armed_away"]:
state = STATE_ALARM_ARMED_AWAY
elif self._info['status']['armed_stay']:
elif self._info["status"]["armed_stay"]:
state = STATE_ALARM_ARMED_HOME
elif self._info['status']['exit_delay']:
elif self._info["status"]["exit_delay"]:
state = STATE_ALARM_PENDING
elif self._info['status']['entry_delay']:
elif self._info["status"]["entry_delay"]:
state = STATE_ALARM_PENDING
elif self._info['status']['alpha']:
elif self._info["status"]["alpha"]:
state = STATE_ALARM_DISARMED
return state
@ -132,31 +151,35 @@ class EnvisalinkAlarm(EnvisalinkDevice, alarm.AlarmControlPanel):
def async_alarm_disarm(self, code=None):
"""Send disarm command."""
if code:
self.hass.data[DATA_EVL].disarm_partition(
str(code), self._partition_number)
self.hass.data[DATA_EVL].disarm_partition(str(code), self._partition_number)
else:
self.hass.data[DATA_EVL].disarm_partition(
str(self._code), self._partition_number)
str(self._code), self._partition_number
)
@asyncio.coroutine
def async_alarm_arm_home(self, code=None):
"""Send arm home command."""
if code:
self.hass.data[DATA_EVL].arm_stay_partition(
str(code), self._partition_number)
str(code), self._partition_number
)
else:
self.hass.data[DATA_EVL].arm_stay_partition(
str(self._code), self._partition_number)
str(self._code), self._partition_number
)
@asyncio.coroutine
def async_alarm_arm_away(self, code=None):
"""Send arm away command."""
if code:
self.hass.data[DATA_EVL].arm_away_partition(
str(code), self._partition_number)
str(code), self._partition_number
)
else:
self.hass.data[DATA_EVL].arm_away_partition(
str(self._code), self._partition_number)
str(self._code), self._partition_number
)
@asyncio.coroutine
def async_alarm_trigger(self, code=None):
@ -168,4 +191,5 @@ class EnvisalinkAlarm(EnvisalinkDevice, alarm.AlarmControlPanel):
"""Send custom keypress."""
if keypress:
self.hass.data[DATA_EVL].keypresses_to_partition(
self._partition_number, keypress)
self._partition_number, keypress
)

View file

@ -9,22 +9,26 @@ import logging
from homeassistant.components.alarm_control_panel import AlarmControlPanel
from homeassistant.components.homematicip_cloud import (
HMIPC_HAPID, HomematicipGenericDevice)
HMIPC_HAPID,
HomematicipGenericDevice,
)
from homeassistant.components.homematicip_cloud import DOMAIN as HMIPC_DOMAIN
from homeassistant.const import (
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED,
STATE_ALARM_TRIGGERED)
STATE_ALARM_ARMED_AWAY,
STATE_ALARM_ARMED_HOME,
STATE_ALARM_DISARMED,
STATE_ALARM_TRIGGERED,
)
_LOGGER = logging.getLogger(__name__)
DEPENDENCIES = ['homematicip_cloud']
DEPENDENCIES = ["homematicip_cloud"]
HMIP_ZONE_AWAY = 'EXTERNAL'
HMIP_ZONE_HOME = 'INTERNAL'
HMIP_ZONE_AWAY = "EXTERNAL"
HMIP_ZONE_HOME = "INTERNAL"
async def async_setup_platform(
hass, config, async_add_entities, discovery_info=None):
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
"""Set up the HomematicIP Cloud alarm control devices."""
pass
@ -48,8 +52,8 @@ class HomematicipSecurityZone(HomematicipGenericDevice, AlarmControlPanel):
def __init__(self, home, device):
"""Initialize the security zone group."""
device.modelType = 'Group-SecurityZone'
device.windowState = ''
device.modelType = "Group-SecurityZone"
device.windowState = ""
super().__init__(home, device)
@property
@ -58,8 +62,11 @@ class HomematicipSecurityZone(HomematicipGenericDevice, AlarmControlPanel):
from homematicip.base.enums import WindowState
if self._device.active:
if (self._device.sabotage or self._device.motionDetected or
self._device.windowState == WindowState.OPEN):
if (
self._device.sabotage
or self._device.motionDetected
or self._device.windowState == WindowState.OPEN
):
return STATE_ALARM_TRIGGERED
active = self._home.get_security_zones_activation()

View file

@ -11,33 +11,40 @@ import voluptuous as vol
import homeassistant.components.alarm_control_panel as alarm
from homeassistant.components.alarm_control_panel import PLATFORM_SCHEMA
from homeassistant.const import (
CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_USERNAME, STATE_ALARM_ARMED_AWAY,
STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED)
CONF_HOST,
CONF_NAME,
CONF_PASSWORD,
CONF_USERNAME,
STATE_ALARM_ARMED_AWAY,
STATE_ALARM_ARMED_HOME,
STATE_ALARM_DISARMED,
)
import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['pyialarm==0.2']
REQUIREMENTS = ["pyialarm==0.2"]
_LOGGER = logging.getLogger(__name__)
DEFAULT_NAME = 'iAlarm'
DEFAULT_NAME = "iAlarm"
def no_application_protocol(value):
"""Validate that value is without the application protocol."""
protocol_separator = "://"
if not value or protocol_separator in value:
raise vol.Invalid(
'Invalid host, {} is not allowed'.format(protocol_separator))
raise vol.Invalid("Invalid host, {} is not allowed".format(protocol_separator))
return value
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_HOST): vol.All(cv.string, no_application_protocol),
vol.Required(CONF_PASSWORD): cv.string,
vol.Required(CONF_USERNAME): cv.string,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
})
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
{
vol.Required(CONF_HOST): vol.All(cv.string, no_application_protocol),
vol.Required(CONF_PASSWORD): cv.string,
vol.Required(CONF_USERNAME): cv.string,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
}
)
def setup_platform(hass, config, add_entities, discovery_info=None):
@ -47,7 +54,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
password = config.get(CONF_PASSWORD)
host = config.get(CONF_HOST)
url = 'http://{}'.format(host)
url = "http://{}".format(host)
ialarm = IAlarmPanel(name, username, password, url)
add_entities([ialarm], True)
@ -79,7 +86,7 @@ class IAlarmPanel(alarm.AlarmControlPanel):
def update(self):
"""Return the state of the device."""
status = self._client.get_status()
_LOGGER.debug('iAlarm status: %s', status)
_LOGGER.debug("iAlarm status: %s", status)
if status:
status = int(status)

View file

@ -10,25 +10,37 @@ import re
import voluptuous as vol
import homeassistant.components.alarm_control_panel as alarm
from homeassistant.components.alarm_control_panel import (
DOMAIN, PLATFORM_SCHEMA)
from homeassistant.components.alarm_control_panel import DOMAIN, PLATFORM_SCHEMA
from homeassistant.components.ifttt import (
ATTR_EVENT, DOMAIN as IFTTT_DOMAIN, SERVICE_TRIGGER)
ATTR_EVENT,
DOMAIN as IFTTT_DOMAIN,
SERVICE_TRIGGER,
)
from homeassistant.const import (
ATTR_ENTITY_ID, ATTR_STATE, CONF_NAME, CONF_CODE,
CONF_OPTIMISTIC, STATE_ALARM_DISARMED, STATE_ALARM_ARMED_NIGHT,
STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_AWAY)
ATTR_ENTITY_ID,
ATTR_STATE,
CONF_NAME,
CONF_CODE,
CONF_OPTIMISTIC,
STATE_ALARM_DISARMED,
STATE_ALARM_ARMED_NIGHT,
STATE_ALARM_ARMED_HOME,
STATE_ALARM_ARMED_AWAY,
)
import homeassistant.helpers.config_validation as cv
DEPENDENCIES = ['ifttt']
DEPENDENCIES = ["ifttt"]
_LOGGER = logging.getLogger(__name__)
ALLOWED_STATES = [
STATE_ALARM_DISARMED, STATE_ALARM_ARMED_NIGHT,
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME]
STATE_ALARM_DISARMED,
STATE_ALARM_ARMED_NIGHT,
STATE_ALARM_ARMED_AWAY,
STATE_ALARM_ARMED_HOME,
]
DATA_IFTTT_ALARM = 'ifttt_alarm'
DATA_IFTTT_ALARM = "ifttt_alarm"
DEFAULT_NAME = "Home"
CONF_EVENT_AWAY = "event_arm_away"
@ -41,22 +53,23 @@ DEFAULT_EVENT_HOME = "alarm_arm_home"
DEFAULT_EVENT_NIGHT = "alarm_arm_night"
DEFAULT_EVENT_DISARM = "alarm_disarm"
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_CODE): cv.string,
vol.Optional(CONF_EVENT_AWAY, default=DEFAULT_EVENT_AWAY): cv.string,
vol.Optional(CONF_EVENT_HOME, default=DEFAULT_EVENT_HOME): cv.string,
vol.Optional(CONF_EVENT_NIGHT, default=DEFAULT_EVENT_NIGHT): cv.string,
vol.Optional(CONF_EVENT_DISARM, default=DEFAULT_EVENT_DISARM): cv.string,
vol.Optional(CONF_OPTIMISTIC, default=False): cv.boolean,
})
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
{
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_CODE): cv.string,
vol.Optional(CONF_EVENT_AWAY, default=DEFAULT_EVENT_AWAY): cv.string,
vol.Optional(CONF_EVENT_HOME, default=DEFAULT_EVENT_HOME): cv.string,
vol.Optional(CONF_EVENT_NIGHT, default=DEFAULT_EVENT_NIGHT): cv.string,
vol.Optional(CONF_EVENT_DISARM, default=DEFAULT_EVENT_DISARM): cv.string,
vol.Optional(CONF_OPTIMISTIC, default=False): cv.boolean,
}
)
SERVICE_PUSH_ALARM_STATE = "ifttt_push_alarm_state"
PUSH_ALARM_STATE_SERVICE_SCHEMA = vol.Schema({
vol.Required(ATTR_ENTITY_ID): cv.entity_ids,
vol.Required(ATTR_STATE): cv.string,
})
PUSH_ALARM_STATE_SERVICE_SCHEMA = vol.Schema(
{vol.Required(ATTR_ENTITY_ID): cv.entity_ids, vol.Required(ATTR_STATE): cv.string}
)
def setup_platform(hass, config, add_entities, discovery_info=None):
@ -72,8 +85,9 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
event_disarm = config.get(CONF_EVENT_DISARM)
optimistic = config.get(CONF_OPTIMISTIC)
alarmpanel = IFTTTAlarmPanel(name, code, event_away, event_home,
event_night, event_disarm, optimistic)
alarmpanel = IFTTTAlarmPanel(
name, code, event_away, event_home, event_night, event_disarm, optimistic
)
hass.data[DATA_IFTTT_ALARM].append(alarmpanel)
add_entities([alarmpanel])
@ -89,15 +103,20 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
device.push_alarm_state(state)
device.async_schedule_update_ha_state()
hass.services.register(DOMAIN, SERVICE_PUSH_ALARM_STATE, push_state_update,
schema=PUSH_ALARM_STATE_SERVICE_SCHEMA)
hass.services.register(
DOMAIN,
SERVICE_PUSH_ALARM_STATE,
push_state_update,
schema=PUSH_ALARM_STATE_SERVICE_SCHEMA,
)
class IFTTTAlarmPanel(alarm.AlarmControlPanel):
"""Representation of an alarm control panel controlled through IFTTT."""
def __init__(self, name, code, event_away, event_home, event_night,
event_disarm, optimistic):
def __init__(
self, name, code, event_away, event_home, event_night, event_disarm, optimistic
):
"""Initialize the alarm control panel."""
self._name = name
self._code = code
@ -128,9 +147,9 @@ class IFTTTAlarmPanel(alarm.AlarmControlPanel):
"""Return one or more digits/characters."""
if self._code is None:
return None
if isinstance(self._code, str) and re.search('^\\d+$', self._code):
return 'Number'
return 'Any'
if isinstance(self._code, str) and re.search("^\\d+$", self._code):
return "Number"
return "Any"
def alarm_disarm(self, code=None):
"""Send disarm command."""

View file

@ -13,37 +13,54 @@ import voluptuous as vol
import homeassistant.components.alarm_control_panel as alarm
from homeassistant.const import (
CONF_CODE, CONF_DELAY_TIME, CONF_DISARM_AFTER_TRIGGER, CONF_NAME,
CONF_PENDING_TIME, CONF_PLATFORM, CONF_TRIGGER_TIME,
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_CUSTOM_BYPASS,
STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT, STATE_ALARM_DISARMED,
STATE_ALARM_PENDING, STATE_ALARM_TRIGGERED)
CONF_CODE,
CONF_DELAY_TIME,
CONF_DISARM_AFTER_TRIGGER,
CONF_NAME,
CONF_PENDING_TIME,
CONF_PLATFORM,
CONF_TRIGGER_TIME,
STATE_ALARM_ARMED_AWAY,
STATE_ALARM_ARMED_CUSTOM_BYPASS,
STATE_ALARM_ARMED_HOME,
STATE_ALARM_ARMED_NIGHT,
STATE_ALARM_DISARMED,
STATE_ALARM_PENDING,
STATE_ALARM_TRIGGERED,
)
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.event import track_point_in_time
import homeassistant.util.dt as dt_util
_LOGGER = logging.getLogger(__name__)
CONF_CODE_TEMPLATE = 'code_template'
CONF_CODE_TEMPLATE = "code_template"
DEFAULT_ALARM_NAME = 'HA Alarm'
DEFAULT_ALARM_NAME = "HA Alarm"
DEFAULT_DELAY_TIME = datetime.timedelta(seconds=0)
DEFAULT_PENDING_TIME = datetime.timedelta(seconds=60)
DEFAULT_TRIGGER_TIME = datetime.timedelta(seconds=120)
DEFAULT_DISARM_AFTER_TRIGGER = False
SUPPORTED_STATES = [STATE_ALARM_DISARMED, STATE_ALARM_ARMED_AWAY,
STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT,
STATE_ALARM_ARMED_CUSTOM_BYPASS, STATE_ALARM_TRIGGERED]
SUPPORTED_STATES = [
STATE_ALARM_DISARMED,
STATE_ALARM_ARMED_AWAY,
STATE_ALARM_ARMED_HOME,
STATE_ALARM_ARMED_NIGHT,
STATE_ALARM_ARMED_CUSTOM_BYPASS,
STATE_ALARM_TRIGGERED,
]
SUPPORTED_PRETRIGGER_STATES = [state for state in SUPPORTED_STATES
if state != STATE_ALARM_TRIGGERED]
SUPPORTED_PRETRIGGER_STATES = [
state for state in SUPPORTED_STATES if state != STATE_ALARM_TRIGGERED
]
SUPPORTED_PENDING_STATES = [state for state in SUPPORTED_STATES
if state != STATE_ALARM_DISARMED]
SUPPORTED_PENDING_STATES = [
state for state in SUPPORTED_STATES if state != STATE_ALARM_DISARMED
]
ATTR_PRE_PENDING_STATE = 'pre_pending_state'
ATTR_POST_PENDING_STATE = 'post_pending_state'
ATTR_PRE_PENDING_STATE = "pre_pending_state"
ATTR_POST_PENDING_STATE = "post_pending_state"
def _state_validator(config):
@ -66,53 +83,75 @@ def _state_schema(state):
schema = {}
if state in SUPPORTED_PRETRIGGER_STATES:
schema[vol.Optional(CONF_DELAY_TIME)] = vol.All(
cv.time_period, cv.positive_timedelta)
cv.time_period, cv.positive_timedelta
)
schema[vol.Optional(CONF_TRIGGER_TIME)] = vol.All(
cv.time_period, cv.positive_timedelta)
cv.time_period, cv.positive_timedelta
)
if state in SUPPORTED_PENDING_STATES:
schema[vol.Optional(CONF_PENDING_TIME)] = vol.All(
cv.time_period, cv.positive_timedelta)
cv.time_period, cv.positive_timedelta
)
return vol.Schema(schema)
PLATFORM_SCHEMA = vol.Schema(vol.All({
vol.Required(CONF_PLATFORM): 'manual',
vol.Optional(CONF_NAME, default=DEFAULT_ALARM_NAME): cv.string,
vol.Exclusive(CONF_CODE, 'code validation'): cv.string,
vol.Exclusive(CONF_CODE_TEMPLATE, 'code validation'): cv.template,
vol.Optional(CONF_DELAY_TIME, default=DEFAULT_DELAY_TIME):
vol.All(cv.time_period, cv.positive_timedelta),
vol.Optional(CONF_PENDING_TIME, default=DEFAULT_PENDING_TIME):
vol.All(cv.time_period, cv.positive_timedelta),
vol.Optional(CONF_TRIGGER_TIME, default=DEFAULT_TRIGGER_TIME):
vol.All(cv.time_period, cv.positive_timedelta),
vol.Optional(CONF_DISARM_AFTER_TRIGGER,
default=DEFAULT_DISARM_AFTER_TRIGGER): cv.boolean,
vol.Optional(STATE_ALARM_ARMED_AWAY, default={}):
_state_schema(STATE_ALARM_ARMED_AWAY),
vol.Optional(STATE_ALARM_ARMED_HOME, default={}):
_state_schema(STATE_ALARM_ARMED_HOME),
vol.Optional(STATE_ALARM_ARMED_NIGHT, default={}):
_state_schema(STATE_ALARM_ARMED_NIGHT),
vol.Optional(STATE_ALARM_ARMED_CUSTOM_BYPASS, default={}):
_state_schema(STATE_ALARM_ARMED_CUSTOM_BYPASS),
vol.Optional(STATE_ALARM_DISARMED, default={}):
_state_schema(STATE_ALARM_DISARMED),
vol.Optional(STATE_ALARM_TRIGGERED, default={}):
_state_schema(STATE_ALARM_TRIGGERED),
}, _state_validator))
PLATFORM_SCHEMA = vol.Schema(
vol.All(
{
vol.Required(CONF_PLATFORM): "manual",
vol.Optional(CONF_NAME, default=DEFAULT_ALARM_NAME): cv.string,
vol.Exclusive(CONF_CODE, "code validation"): cv.string,
vol.Exclusive(CONF_CODE_TEMPLATE, "code validation"): cv.template,
vol.Optional(CONF_DELAY_TIME, default=DEFAULT_DELAY_TIME): vol.All(
cv.time_period, cv.positive_timedelta
),
vol.Optional(CONF_PENDING_TIME, default=DEFAULT_PENDING_TIME): vol.All(
cv.time_period, cv.positive_timedelta
),
vol.Optional(CONF_TRIGGER_TIME, default=DEFAULT_TRIGGER_TIME): vol.All(
cv.time_period, cv.positive_timedelta
),
vol.Optional(
CONF_DISARM_AFTER_TRIGGER, default=DEFAULT_DISARM_AFTER_TRIGGER
): cv.boolean,
vol.Optional(STATE_ALARM_ARMED_AWAY, default={}): _state_schema(
STATE_ALARM_ARMED_AWAY
),
vol.Optional(STATE_ALARM_ARMED_HOME, default={}): _state_schema(
STATE_ALARM_ARMED_HOME
),
vol.Optional(STATE_ALARM_ARMED_NIGHT, default={}): _state_schema(
STATE_ALARM_ARMED_NIGHT
),
vol.Optional(STATE_ALARM_ARMED_CUSTOM_BYPASS, default={}): _state_schema(
STATE_ALARM_ARMED_CUSTOM_BYPASS
),
vol.Optional(STATE_ALARM_DISARMED, default={}): _state_schema(
STATE_ALARM_DISARMED
),
vol.Optional(STATE_ALARM_TRIGGERED, default={}): _state_schema(
STATE_ALARM_TRIGGERED
),
},
_state_validator,
)
)
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the manual alarm platform."""
add_entities([ManualAlarm(
hass,
config[CONF_NAME],
config.get(CONF_CODE),
config.get(CONF_CODE_TEMPLATE),
config.get(CONF_DISARM_AFTER_TRIGGER, DEFAULT_DISARM_AFTER_TRIGGER),
config
)])
add_entities(
[
ManualAlarm(
hass,
config[CONF_NAME],
config.get(CONF_CODE),
config.get(CONF_CODE_TEMPLATE),
config.get(CONF_DISARM_AFTER_TRIGGER, DEFAULT_DISARM_AFTER_TRIGGER),
config,
)
]
)
class ManualAlarm(alarm.AlarmControlPanel):
@ -127,8 +166,7 @@ class ManualAlarm(alarm.AlarmControlPanel):
A trigger_time of zero disables the alarm_trigger service.
"""
def __init__(self, hass, name, code, code_template,
disarm_after_trigger, config):
def __init__(self, hass, name, code, code_template, disarm_after_trigger, config):
"""Init the manual alarm panel."""
self._state = STATE_ALARM_DISARMED
self._hass = hass
@ -144,13 +182,16 @@ class ManualAlarm(alarm.AlarmControlPanel):
self._delay_time_by_state = {
state: config[state][CONF_DELAY_TIME]
for state in SUPPORTED_PRETRIGGER_STATES}
for state in SUPPORTED_PRETRIGGER_STATES
}
self._trigger_time_by_state = {
state: config[state][CONF_TRIGGER_TIME]
for state in SUPPORTED_PRETRIGGER_STATES}
for state in SUPPORTED_PRETRIGGER_STATES
}
self._pending_time_by_state = {
state: config[state][CONF_PENDING_TIME]
for state in SUPPORTED_PENDING_STATES}
for state in SUPPORTED_PENDING_STATES
}
@property
def should_poll(self):
@ -169,15 +210,17 @@ class ManualAlarm(alarm.AlarmControlPanel):
if self._within_pending_time(self._state):
return STATE_ALARM_PENDING
trigger_time = self._trigger_time_by_state[self._previous_state]
if (self._state_ts + self._pending_time(self._state) +
trigger_time) < dt_util.utcnow():
if (
self._state_ts + self._pending_time(self._state) + trigger_time
) < dt_util.utcnow():
if self._disarm_after_trigger:
return STATE_ALARM_DISARMED
self._state = self._previous_state
return self._state
if self._state in SUPPORTED_PENDING_STATES and \
self._within_pending_time(self._state):
if self._state in SUPPORTED_PENDING_STATES and self._within_pending_time(
self._state
):
return STATE_ALARM_PENDING
return self._state
@ -205,9 +248,9 @@ class ManualAlarm(alarm.AlarmControlPanel):
"""Return one or more digits/characters."""
if self._code is None:
return None
if isinstance(self._code, str) and re.search('^\\d+$', self._code):
return 'Number'
return 'Any'
if isinstance(self._code, str) and re.search("^\\d+$", self._code):
return "Number"
return "Any"
def alarm_disarm(self, code=None):
"""Send disarm command."""
@ -270,17 +313,19 @@ class ManualAlarm(alarm.AlarmControlPanel):
pending_time = self._pending_time(state)
if state == STATE_ALARM_TRIGGERED:
track_point_in_time(
self._hass, self.async_update_ha_state,
self._state_ts + pending_time)
self._hass, self.async_update_ha_state, self._state_ts + pending_time
)
trigger_time = self._trigger_time_by_state[self._previous_state]
track_point_in_time(
self._hass, self.async_update_ha_state,
self._state_ts + pending_time + trigger_time)
self._hass,
self.async_update_ha_state,
self._state_ts + pending_time + trigger_time,
)
elif state in SUPPORTED_PENDING_STATES and pending_time:
track_point_in_time(
self._hass, self.async_update_ha_state,
self._state_ts + pending_time)
self._hass, self.async_update_ha_state, self._state_ts + pending_time
)
def _validate_code(self, code, state):
"""Validate given code."""
@ -289,8 +334,7 @@ class ManualAlarm(alarm.AlarmControlPanel):
if isinstance(self._code, str):
alarm_code = self._code
else:
alarm_code = self._code.render(from_state=self._state,
to_state=state)
alarm_code = self._code.render(from_state=self._state, to_state=state)
check = not alarm_code or code == alarm_code
if not check:
_LOGGER.warning("Invalid code given for %s", state)

View file

@ -15,10 +15,20 @@ import voluptuous as vol
import homeassistant.components.alarm_control_panel as alarm
import homeassistant.util.dt as dt_util
from homeassistant.const import (
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT,
STATE_ALARM_DISARMED, STATE_ALARM_PENDING, STATE_ALARM_TRIGGERED,
CONF_PLATFORM, CONF_NAME, CONF_CODE, CONF_DELAY_TIME, CONF_PENDING_TIME,
CONF_TRIGGER_TIME, CONF_DISARM_AFTER_TRIGGER)
STATE_ALARM_ARMED_AWAY,
STATE_ALARM_ARMED_HOME,
STATE_ALARM_ARMED_NIGHT,
STATE_ALARM_DISARMED,
STATE_ALARM_PENDING,
STATE_ALARM_TRIGGERED,
CONF_PLATFORM,
CONF_NAME,
CONF_CODE,
CONF_DELAY_TIME,
CONF_PENDING_TIME,
CONF_TRIGGER_TIME,
CONF_DISARM_AFTER_TRIGGER,
)
from homeassistant.components import mqtt
from homeassistant.helpers.event import async_track_state_change
@ -29,35 +39,41 @@ from homeassistant.helpers.event import track_point_in_time
_LOGGER = logging.getLogger(__name__)
CONF_CODE_TEMPLATE = 'code_template'
CONF_CODE_TEMPLATE = "code_template"
CONF_PAYLOAD_DISARM = 'payload_disarm'
CONF_PAYLOAD_ARM_HOME = 'payload_arm_home'
CONF_PAYLOAD_ARM_AWAY = 'payload_arm_away'
CONF_PAYLOAD_ARM_NIGHT = 'payload_arm_night'
CONF_PAYLOAD_DISARM = "payload_disarm"
CONF_PAYLOAD_ARM_HOME = "payload_arm_home"
CONF_PAYLOAD_ARM_AWAY = "payload_arm_away"
CONF_PAYLOAD_ARM_NIGHT = "payload_arm_night"
DEFAULT_ALARM_NAME = 'HA Alarm'
DEFAULT_ALARM_NAME = "HA Alarm"
DEFAULT_DELAY_TIME = datetime.timedelta(seconds=0)
DEFAULT_PENDING_TIME = datetime.timedelta(seconds=60)
DEFAULT_TRIGGER_TIME = datetime.timedelta(seconds=120)
DEFAULT_DISARM_AFTER_TRIGGER = False
DEFAULT_ARM_AWAY = 'ARM_AWAY'
DEFAULT_ARM_HOME = 'ARM_HOME'
DEFAULT_ARM_NIGHT = 'ARM_NIGHT'
DEFAULT_DISARM = 'DISARM'
DEFAULT_ARM_AWAY = "ARM_AWAY"
DEFAULT_ARM_HOME = "ARM_HOME"
DEFAULT_ARM_NIGHT = "ARM_NIGHT"
DEFAULT_DISARM = "DISARM"
SUPPORTED_STATES = [STATE_ALARM_DISARMED, STATE_ALARM_ARMED_AWAY,
STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT,
STATE_ALARM_TRIGGERED]
SUPPORTED_STATES = [
STATE_ALARM_DISARMED,
STATE_ALARM_ARMED_AWAY,
STATE_ALARM_ARMED_HOME,
STATE_ALARM_ARMED_NIGHT,
STATE_ALARM_TRIGGERED,
]
SUPPORTED_PRETRIGGER_STATES = [state for state in SUPPORTED_STATES
if state != STATE_ALARM_TRIGGERED]
SUPPORTED_PRETRIGGER_STATES = [
state for state in SUPPORTED_STATES if state != STATE_ALARM_TRIGGERED
]
SUPPORTED_PENDING_STATES = [state for state in SUPPORTED_STATES
if state != STATE_ALARM_DISARMED]
SUPPORTED_PENDING_STATES = [
state for state in SUPPORTED_STATES if state != STATE_ALARM_DISARMED
]
ATTR_PRE_PENDING_STATE = 'pre_pending_state'
ATTR_POST_PENDING_STATE = 'post_pending_state'
ATTR_PRE_PENDING_STATE = "pre_pending_state"
ATTR_POST_PENDING_STATE = "post_pending_state"
def _state_validator(config):
@ -80,65 +96,95 @@ def _state_schema(state):
schema = {}
if state in SUPPORTED_PRETRIGGER_STATES:
schema[vol.Optional(CONF_DELAY_TIME)] = vol.All(
cv.time_period, cv.positive_timedelta)
cv.time_period, cv.positive_timedelta
)
schema[vol.Optional(CONF_TRIGGER_TIME)] = vol.All(
cv.time_period, cv.positive_timedelta)
cv.time_period, cv.positive_timedelta
)
if state in SUPPORTED_PENDING_STATES:
schema[vol.Optional(CONF_PENDING_TIME)] = vol.All(
cv.time_period, cv.positive_timedelta)
cv.time_period, cv.positive_timedelta
)
return vol.Schema(schema)
DEPENDENCIES = ['mqtt']
DEPENDENCIES = ["mqtt"]
PLATFORM_SCHEMA = vol.Schema(vol.All(mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend({
vol.Required(CONF_PLATFORM): 'manual_mqtt',
vol.Optional(CONF_NAME, default=DEFAULT_ALARM_NAME): cv.string,
vol.Exclusive(CONF_CODE, 'code validation'): cv.string,
vol.Exclusive(CONF_CODE_TEMPLATE, 'code validation'): cv.template,
vol.Optional(CONF_DELAY_TIME, default=DEFAULT_DELAY_TIME):
vol.All(cv.time_period, cv.positive_timedelta),
vol.Optional(CONF_PENDING_TIME, default=DEFAULT_PENDING_TIME):
vol.All(cv.time_period, cv.positive_timedelta),
vol.Optional(CONF_TRIGGER_TIME, default=DEFAULT_TRIGGER_TIME):
vol.All(cv.time_period, cv.positive_timedelta),
vol.Optional(CONF_DISARM_AFTER_TRIGGER,
default=DEFAULT_DISARM_AFTER_TRIGGER): cv.boolean,
vol.Optional(STATE_ALARM_ARMED_AWAY, default={}):
_state_schema(STATE_ALARM_ARMED_AWAY),
vol.Optional(STATE_ALARM_ARMED_HOME, default={}):
_state_schema(STATE_ALARM_ARMED_HOME),
vol.Optional(STATE_ALARM_ARMED_NIGHT, default={}):
_state_schema(STATE_ALARM_ARMED_NIGHT),
vol.Optional(STATE_ALARM_DISARMED, default={}):
_state_schema(STATE_ALARM_DISARMED),
vol.Optional(STATE_ALARM_TRIGGERED, default={}):
_state_schema(STATE_ALARM_TRIGGERED),
vol.Required(mqtt.CONF_COMMAND_TOPIC): mqtt.valid_publish_topic,
vol.Required(mqtt.CONF_STATE_TOPIC): mqtt.valid_subscribe_topic,
vol.Optional(CONF_PAYLOAD_ARM_AWAY, default=DEFAULT_ARM_AWAY): cv.string,
vol.Optional(CONF_PAYLOAD_ARM_HOME, default=DEFAULT_ARM_HOME): cv.string,
vol.Optional(CONF_PAYLOAD_ARM_NIGHT, default=DEFAULT_ARM_NIGHT): cv.string,
vol.Optional(CONF_PAYLOAD_DISARM, default=DEFAULT_DISARM): cv.string,
}), _state_validator))
PLATFORM_SCHEMA = vol.Schema(
vol.All(
mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend(
{
vol.Required(CONF_PLATFORM): "manual_mqtt",
vol.Optional(CONF_NAME, default=DEFAULT_ALARM_NAME): cv.string,
vol.Exclusive(CONF_CODE, "code validation"): cv.string,
vol.Exclusive(CONF_CODE_TEMPLATE, "code validation"): cv.template,
vol.Optional(CONF_DELAY_TIME, default=DEFAULT_DELAY_TIME): vol.All(
cv.time_period, cv.positive_timedelta
),
vol.Optional(CONF_PENDING_TIME, default=DEFAULT_PENDING_TIME): vol.All(
cv.time_period, cv.positive_timedelta
),
vol.Optional(CONF_TRIGGER_TIME, default=DEFAULT_TRIGGER_TIME): vol.All(
cv.time_period, cv.positive_timedelta
),
vol.Optional(
CONF_DISARM_AFTER_TRIGGER, default=DEFAULT_DISARM_AFTER_TRIGGER
): cv.boolean,
vol.Optional(STATE_ALARM_ARMED_AWAY, default={}): _state_schema(
STATE_ALARM_ARMED_AWAY
),
vol.Optional(STATE_ALARM_ARMED_HOME, default={}): _state_schema(
STATE_ALARM_ARMED_HOME
),
vol.Optional(STATE_ALARM_ARMED_NIGHT, default={}): _state_schema(
STATE_ALARM_ARMED_NIGHT
),
vol.Optional(STATE_ALARM_DISARMED, default={}): _state_schema(
STATE_ALARM_DISARMED
),
vol.Optional(STATE_ALARM_TRIGGERED, default={}): _state_schema(
STATE_ALARM_TRIGGERED
),
vol.Required(mqtt.CONF_COMMAND_TOPIC): mqtt.valid_publish_topic,
vol.Required(mqtt.CONF_STATE_TOPIC): mqtt.valid_subscribe_topic,
vol.Optional(
CONF_PAYLOAD_ARM_AWAY, default=DEFAULT_ARM_AWAY
): cv.string,
vol.Optional(
CONF_PAYLOAD_ARM_HOME, default=DEFAULT_ARM_HOME
): cv.string,
vol.Optional(
CONF_PAYLOAD_ARM_NIGHT, default=DEFAULT_ARM_NIGHT
): cv.string,
vol.Optional(CONF_PAYLOAD_DISARM, default=DEFAULT_DISARM): cv.string,
}
),
_state_validator,
)
)
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the manual MQTT alarm platform."""
add_entities([ManualMQTTAlarm(
hass,
config[CONF_NAME],
config.get(CONF_CODE),
config.get(CONF_CODE_TEMPLATE),
config.get(CONF_DISARM_AFTER_TRIGGER, DEFAULT_DISARM_AFTER_TRIGGER),
config.get(mqtt.CONF_STATE_TOPIC),
config.get(mqtt.CONF_COMMAND_TOPIC),
config.get(mqtt.CONF_QOS),
config.get(CONF_PAYLOAD_DISARM),
config.get(CONF_PAYLOAD_ARM_HOME),
config.get(CONF_PAYLOAD_ARM_AWAY),
config.get(CONF_PAYLOAD_ARM_NIGHT),
config)])
add_entities(
[
ManualMQTTAlarm(
hass,
config[CONF_NAME],
config.get(CONF_CODE),
config.get(CONF_CODE_TEMPLATE),
config.get(CONF_DISARM_AFTER_TRIGGER, DEFAULT_DISARM_AFTER_TRIGGER),
config.get(mqtt.CONF_STATE_TOPIC),
config.get(mqtt.CONF_COMMAND_TOPIC),
config.get(mqtt.CONF_QOS),
config.get(CONF_PAYLOAD_DISARM),
config.get(CONF_PAYLOAD_ARM_HOME),
config.get(CONF_PAYLOAD_ARM_AWAY),
config.get(CONF_PAYLOAD_ARM_NIGHT),
config,
)
]
)
class ManualMQTTAlarm(alarm.AlarmControlPanel):
@ -153,10 +199,22 @@ class ManualMQTTAlarm(alarm.AlarmControlPanel):
A trigger_time of zero disables the alarm_trigger service.
"""
def __init__(self, hass, name, code, code_template, disarm_after_trigger,
state_topic, command_topic, qos, payload_disarm,
payload_arm_home, payload_arm_away, payload_arm_night,
config):
def __init__(
self,
hass,
name,
code,
code_template,
disarm_after_trigger,
state_topic,
command_topic,
qos,
payload_disarm,
payload_arm_home,
payload_arm_away,
payload_arm_night,
config,
):
"""Init the manual MQTT alarm panel."""
self._state = STATE_ALARM_DISARMED
self._hass = hass
@ -172,13 +230,16 @@ class ManualMQTTAlarm(alarm.AlarmControlPanel):
self._delay_time_by_state = {
state: config[state][CONF_DELAY_TIME]
for state in SUPPORTED_PRETRIGGER_STATES}
for state in SUPPORTED_PRETRIGGER_STATES
}
self._trigger_time_by_state = {
state: config[state][CONF_TRIGGER_TIME]
for state in SUPPORTED_PRETRIGGER_STATES}
for state in SUPPORTED_PRETRIGGER_STATES
}
self._pending_time_by_state = {
state: config[state][CONF_PENDING_TIME]
for state in SUPPORTED_PENDING_STATES}
for state in SUPPORTED_PENDING_STATES
}
self._state_topic = state_topic
self._command_topic = command_topic
@ -205,15 +266,17 @@ class ManualMQTTAlarm(alarm.AlarmControlPanel):
if self._within_pending_time(self._state):
return STATE_ALARM_PENDING
trigger_time = self._trigger_time_by_state[self._previous_state]
if (self._state_ts + self._pending_time(self._state) +
trigger_time) < dt_util.utcnow():
if (
self._state_ts + self._pending_time(self._state) + trigger_time
) < dt_util.utcnow():
if self._disarm_after_trigger:
return STATE_ALARM_DISARMED
self._state = self._previous_state
return self._state
if self._state in SUPPORTED_PENDING_STATES and \
self._within_pending_time(self._state):
if self._state in SUPPORTED_PENDING_STATES and self._within_pending_time(
self._state
):
return STATE_ALARM_PENDING
return self._state
@ -241,9 +304,9 @@ class ManualMQTTAlarm(alarm.AlarmControlPanel):
"""Return one or more digits/characters."""
if self._code is None:
return None
if isinstance(self._code, str) and re.search('^\\d+$', self._code):
return 'Number'
return 'Any'
if isinstance(self._code, str) and re.search("^\\d+$", self._code):
return "Number"
return "Any"
def alarm_disarm(self, code=None):
"""Send disarm command."""
@ -299,17 +362,19 @@ class ManualMQTTAlarm(alarm.AlarmControlPanel):
pending_time = self._pending_time(state)
if state == STATE_ALARM_TRIGGERED:
track_point_in_time(
self._hass, self.async_update_ha_state,
self._state_ts + pending_time)
self._hass, self.async_update_ha_state, self._state_ts + pending_time
)
trigger_time = self._trigger_time_by_state[self._previous_state]
track_point_in_time(
self._hass, self.async_update_ha_state,
self._state_ts + pending_time + trigger_time)
self._hass,
self.async_update_ha_state,
self._state_ts + pending_time + trigger_time,
)
elif state in SUPPORTED_PENDING_STATES and pending_time:
track_point_in_time(
self._hass, self.async_update_ha_state,
self._state_ts + pending_time)
self._hass, self.async_update_ha_state, self._state_ts + pending_time
)
def _validate_code(self, code, state):
"""Validate given code."""
@ -318,8 +383,7 @@ class ManualMQTTAlarm(alarm.AlarmControlPanel):
if isinstance(self._code, str):
alarm_code = self._code
else:
alarm_code = self._code.render(from_state=self._state,
to_state=state)
alarm_code = self._code.render(from_state=self._state, to_state=state)
check = not alarm_code or code == alarm_code
if not check:
_LOGGER.warning("Invalid code given for %s", state)
@ -361,10 +425,12 @@ class ManualMQTTAlarm(alarm.AlarmControlPanel):
return
return mqtt.async_subscribe(
self.hass, self._command_topic, message_received, self._qos)
self.hass, self._command_topic, message_received, self._qos
)
@asyncio.coroutine
def _async_state_changed_listener(self, entity_id, old_state, new_state):
"""Publish state change to MQTT."""
mqtt.async_publish(
self.hass, self._state_topic, new_state.state, self._qos, True)
self.hass, self._state_topic, new_state.state, self._qos, True
)

View file

@ -14,69 +14,100 @@ from homeassistant.core import callback
import homeassistant.components.alarm_control_panel as alarm
from homeassistant.components import mqtt
from homeassistant.const import (
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED,
STATE_ALARM_PENDING, STATE_ALARM_TRIGGERED, STATE_UNKNOWN,
CONF_NAME, CONF_CODE)
STATE_ALARM_ARMED_AWAY,
STATE_ALARM_ARMED_HOME,
STATE_ALARM_DISARMED,
STATE_ALARM_PENDING,
STATE_ALARM_TRIGGERED,
STATE_UNKNOWN,
CONF_NAME,
CONF_CODE,
)
from homeassistant.components.mqtt import (
CONF_AVAILABILITY_TOPIC, CONF_STATE_TOPIC, CONF_COMMAND_TOPIC,
CONF_PAYLOAD_AVAILABLE, CONF_PAYLOAD_NOT_AVAILABLE, CONF_QOS,
CONF_RETAIN, MqttAvailability)
CONF_AVAILABILITY_TOPIC,
CONF_STATE_TOPIC,
CONF_COMMAND_TOPIC,
CONF_PAYLOAD_AVAILABLE,
CONF_PAYLOAD_NOT_AVAILABLE,
CONF_QOS,
CONF_RETAIN,
MqttAvailability,
)
import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__)
CONF_PAYLOAD_DISARM = 'payload_disarm'
CONF_PAYLOAD_ARM_HOME = 'payload_arm_home'
CONF_PAYLOAD_ARM_AWAY = 'payload_arm_away'
CONF_PAYLOAD_DISARM = "payload_disarm"
CONF_PAYLOAD_ARM_HOME = "payload_arm_home"
CONF_PAYLOAD_ARM_AWAY = "payload_arm_away"
DEFAULT_ARM_AWAY = 'ARM_AWAY'
DEFAULT_ARM_HOME = 'ARM_HOME'
DEFAULT_DISARM = 'DISARM'
DEFAULT_NAME = 'MQTT Alarm'
DEPENDENCIES = ['mqtt']
DEFAULT_ARM_AWAY = "ARM_AWAY"
DEFAULT_ARM_HOME = "ARM_HOME"
DEFAULT_DISARM = "DISARM"
DEFAULT_NAME = "MQTT Alarm"
DEPENDENCIES = ["mqtt"]
PLATFORM_SCHEMA = mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend({
vol.Required(CONF_COMMAND_TOPIC): mqtt.valid_publish_topic,
vol.Required(CONF_STATE_TOPIC): mqtt.valid_subscribe_topic,
vol.Optional(CONF_CODE): cv.string,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_PAYLOAD_ARM_AWAY, default=DEFAULT_ARM_AWAY): cv.string,
vol.Optional(CONF_PAYLOAD_ARM_HOME, default=DEFAULT_ARM_HOME): cv.string,
vol.Optional(CONF_PAYLOAD_DISARM, default=DEFAULT_DISARM): cv.string,
}).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema)
PLATFORM_SCHEMA = mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend(
{
vol.Required(CONF_COMMAND_TOPIC): mqtt.valid_publish_topic,
vol.Required(CONF_STATE_TOPIC): mqtt.valid_subscribe_topic,
vol.Optional(CONF_CODE): cv.string,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_PAYLOAD_ARM_AWAY, default=DEFAULT_ARM_AWAY): cv.string,
vol.Optional(CONF_PAYLOAD_ARM_HOME, default=DEFAULT_ARM_HOME): cv.string,
vol.Optional(CONF_PAYLOAD_DISARM, default=DEFAULT_DISARM): cv.string,
}
).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema)
@asyncio.coroutine
def async_setup_platform(hass, config, async_add_entities,
discovery_info=None):
def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
"""Set up the MQTT Alarm Control Panel platform."""
if discovery_info is not None:
config = PLATFORM_SCHEMA(discovery_info)
async_add_entities([MqttAlarm(
config.get(CONF_NAME),
config.get(CONF_STATE_TOPIC),
config.get(CONF_COMMAND_TOPIC),
config.get(CONF_QOS),
config.get(CONF_RETAIN),
config.get(CONF_PAYLOAD_DISARM),
config.get(CONF_PAYLOAD_ARM_HOME),
config.get(CONF_PAYLOAD_ARM_AWAY),
config.get(CONF_CODE),
config.get(CONF_AVAILABILITY_TOPIC),
config.get(CONF_PAYLOAD_AVAILABLE),
config.get(CONF_PAYLOAD_NOT_AVAILABLE))])
async_add_entities(
[
MqttAlarm(
config.get(CONF_NAME),
config.get(CONF_STATE_TOPIC),
config.get(CONF_COMMAND_TOPIC),
config.get(CONF_QOS),
config.get(CONF_RETAIN),
config.get(CONF_PAYLOAD_DISARM),
config.get(CONF_PAYLOAD_ARM_HOME),
config.get(CONF_PAYLOAD_ARM_AWAY),
config.get(CONF_CODE),
config.get(CONF_AVAILABILITY_TOPIC),
config.get(CONF_PAYLOAD_AVAILABLE),
config.get(CONF_PAYLOAD_NOT_AVAILABLE),
)
]
)
class MqttAlarm(MqttAvailability, alarm.AlarmControlPanel):
"""Representation of a MQTT alarm status."""
def __init__(self, name, state_topic, command_topic, qos, retain,
payload_disarm, payload_arm_home, payload_arm_away, code,
availability_topic, payload_available, payload_not_available):
def __init__(
self,
name,
state_topic,
command_topic,
qos,
retain,
payload_disarm,
payload_arm_home,
payload_arm_away,
code,
availability_topic,
payload_available,
payload_not_available,
):
"""Init the MQTT Alarm Control Panel."""
super().__init__(availability_topic, qos, payload_available,
payload_not_available)
super().__init__(
availability_topic, qos, payload_available, payload_not_available
)
self._state = STATE_UNKNOWN
self._name = name
self._state_topic = state_topic
@ -96,16 +127,21 @@ class MqttAlarm(MqttAvailability, alarm.AlarmControlPanel):
@callback
def message_received(topic, payload, qos):
"""Run when new MQTT message has been received."""
if payload not in (STATE_ALARM_DISARMED, STATE_ALARM_ARMED_HOME,
STATE_ALARM_ARMED_AWAY, STATE_ALARM_PENDING,
STATE_ALARM_TRIGGERED):
if payload not in (
STATE_ALARM_DISARMED,
STATE_ALARM_ARMED_HOME,
STATE_ALARM_ARMED_AWAY,
STATE_ALARM_PENDING,
STATE_ALARM_TRIGGERED,
):
_LOGGER.warning("Received unexpected payload: %s", payload)
return
self._state = payload
self.async_schedule_update_ha_state()
yield from mqtt.async_subscribe(
self.hass, self._state_topic, message_received, self._qos)
self.hass, self._state_topic, message_received, self._qos
)
@property
def should_poll(self):
@ -127,9 +163,9 @@ class MqttAlarm(MqttAvailability, alarm.AlarmControlPanel):
"""Return one or more digits/characters."""
if self._code is None:
return None
if isinstance(self._code, str) and re.search('^\\d+$', self._code):
return 'Number'
return 'Any'
if isinstance(self._code, str) and re.search("^\\d+$", self._code):
return "Number"
return "Any"
@asyncio.coroutine
def async_alarm_disarm(self, code=None):
@ -137,11 +173,15 @@ class MqttAlarm(MqttAvailability, alarm.AlarmControlPanel):
This method is a coroutine.
"""
if not self._validate_code(code, 'disarming'):
if not self._validate_code(code, "disarming"):
return
mqtt.async_publish(
self.hass, self._command_topic, self._payload_disarm, self._qos,
self._retain)
self.hass,
self._command_topic,
self._payload_disarm,
self._qos,
self._retain,
)
@asyncio.coroutine
def async_alarm_arm_home(self, code=None):
@ -149,11 +189,15 @@ class MqttAlarm(MqttAvailability, alarm.AlarmControlPanel):
This method is a coroutine.
"""
if not self._validate_code(code, 'arming home'):
if not self._validate_code(code, "arming home"):
return
mqtt.async_publish(
self.hass, self._command_topic, self._payload_arm_home, self._qos,
self._retain)
self.hass,
self._command_topic,
self._payload_arm_home,
self._qos,
self._retain,
)
@asyncio.coroutine
def async_alarm_arm_away(self, code=None):
@ -161,15 +205,19 @@ class MqttAlarm(MqttAvailability, alarm.AlarmControlPanel):
This method is a coroutine.
"""
if not self._validate_code(code, 'arming away'):
if not self._validate_code(code, "arming away"):
return
mqtt.async_publish(
self.hass, self._command_topic, self._payload_arm_away, self._qos,
self._retain)
self.hass,
self._command_topic,
self._payload_arm_away,
self._qos,
self._retain,
)
def _validate_code(self, code, state):
"""Validate given code."""
check = self._code is None or code == self._code
if not check:
_LOGGER.warning('Wrong code entered for %s', state)
_LOGGER.warning("Wrong code entered for %s", state)
return check

View file

@ -12,23 +12,31 @@ import voluptuous as vol
import homeassistant.components.alarm_control_panel as alarm
from homeassistant.components.alarm_control_panel import PLATFORM_SCHEMA
from homeassistant.const import (
CONF_HOST, CONF_NAME, CONF_PORT, STATE_ALARM_ARMED_AWAY,
STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED, STATE_UNKNOWN)
CONF_HOST,
CONF_NAME,
CONF_PORT,
STATE_ALARM_ARMED_AWAY,
STATE_ALARM_ARMED_HOME,
STATE_ALARM_DISARMED,
STATE_UNKNOWN,
)
import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['pynx584==0.4']
REQUIREMENTS = ["pynx584==0.4"]
_LOGGER = logging.getLogger(__name__)
DEFAULT_HOST = 'localhost'
DEFAULT_NAME = 'NX584'
DEFAULT_HOST = "localhost"
DEFAULT_NAME = "NX584"
DEFAULT_PORT = 5007
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
})
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
{
vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
}
)
def setup_platform(hass, config, add_entities, discovery_info=None):
@ -37,7 +45,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
host = config.get(CONF_HOST)
port = config.get(CONF_PORT)
url = 'http://{}:{}'.format(host, port)
url = "http://{}:{}".format(host, port)
try:
add_entities([NX584Alarm(hass, url, name)])
@ -52,6 +60,7 @@ class NX584Alarm(alarm.AlarmControlPanel):
def __init__(self, hass, url, name):
"""Init the nx584 alarm panel."""
from nx584 import client
self._hass = hass
self._name = name
self._url = url
@ -70,7 +79,7 @@ class NX584Alarm(alarm.AlarmControlPanel):
@property
def code_format(self):
"""Return one or more digits/characters."""
return 'Number'
return "Number"
@property
def state(self):
@ -83,8 +92,10 @@ class NX584Alarm(alarm.AlarmControlPanel):
part = self._alarm.list_partitions()[0]
zones = self._alarm.list_zones()
except requests.exceptions.ConnectionError as ex:
_LOGGER.error("Unable to connect to %(host)s: %(reason)s",
dict(host=self._url, reason=ex))
_LOGGER.error(
"Unable to connect to %(host)s: %(reason)s",
dict(host=self._url, reason=ex),
)
self._state = STATE_UNKNOWN
zones = []
except IndexError:
@ -94,13 +105,15 @@ class NX584Alarm(alarm.AlarmControlPanel):
bypassed = False
for zone in zones:
if zone['bypassed']:
_LOGGER.debug("Zone %(zone)s is bypassed, assuming HOME",
dict(zone=zone['number']))
if zone["bypassed"]:
_LOGGER.debug(
"Zone %(zone)s is bypassed, assuming HOME",
dict(zone=zone["number"]),
)
bypassed = True
break
if not part['armed']:
if not part["armed"]:
self._state = STATE_ALARM_DISARMED
elif bypassed:
self._state = STATE_ALARM_ARMED_HOME
@ -113,8 +126,8 @@ class NX584Alarm(alarm.AlarmControlPanel):
def alarm_arm_home(self, code=None):
"""Send arm home command."""
self._alarm.arm('stay')
self._alarm.arm("stay")
def alarm_arm_away(self, code=None):
"""Send arm away command."""
self._alarm.arm('exit')
self._alarm.arm("exit")

View file

@ -9,24 +9,27 @@ import logging
import homeassistant.components.alarm_control_panel as alarm
from homeassistant.components.satel_integra import (
CONF_ARM_HOME_MODE, DATA_SATEL, SIGNAL_PANEL_MESSAGE)
CONF_ARM_HOME_MODE,
DATA_SATEL,
SIGNAL_PANEL_MESSAGE,
)
from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
_LOGGER = logging.getLogger(__name__)
DEPENDENCIES = ['satel_integra']
DEPENDENCIES = ["satel_integra"]
@asyncio.coroutine
def async_setup_platform(hass, config, async_add_entities,
discovery_info=None):
def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
"""Set up for Satel Integra alarm panels."""
if not discovery_info:
return
device = SatelIntegraAlarmPanel(
"Alarm Panel", discovery_info.get(CONF_ARM_HOME_MODE))
"Alarm Panel", discovery_info.get(CONF_ARM_HOME_MODE)
)
async_add_entities([device])
@ -43,7 +46,8 @@ class SatelIntegraAlarmPanel(alarm.AlarmControlPanel):
def async_added_to_hass(self):
"""Register callbacks."""
async_dispatcher_connect(
self.hass, SIGNAL_PANEL_MESSAGE, self._message_callback)
self.hass, SIGNAL_PANEL_MESSAGE, self._message_callback
)
@callback
def _message_callback(self, message):
@ -67,7 +71,7 @@ class SatelIntegraAlarmPanel(alarm.AlarmControlPanel):
@property
def code_format(self):
"""Return the regex for code format or None if no code is required."""
return 'Number'
return "Number"
@property
def state(self):
@ -90,5 +94,4 @@ class SatelIntegraAlarmPanel(alarm.AlarmControlPanel):
def async_alarm_arm_home(self, code=None):
"""Send arm home command."""
if code:
yield from self.hass.data[DATA_SATEL].arm(
code, self._arm_home_mode)
yield from self.hass.data[DATA_SATEL].arm(code, self._arm_home_mode)

View file

@ -10,33 +10,44 @@ import re
import voluptuous as vol
from homeassistant.components.alarm_control_panel import (
PLATFORM_SCHEMA, AlarmControlPanel)
PLATFORM_SCHEMA,
AlarmControlPanel,
)
from homeassistant.const import (
CONF_CODE, CONF_NAME, CONF_PASSWORD, CONF_USERNAME,
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME,
STATE_ALARM_DISARMED, STATE_UNKNOWN)
CONF_CODE,
CONF_NAME,
CONF_PASSWORD,
CONF_USERNAME,
STATE_ALARM_ARMED_AWAY,
STATE_ALARM_ARMED_HOME,
STATE_ALARM_DISARMED,
STATE_UNKNOWN,
)
import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['simplisafe-python==2.0.2']
REQUIREMENTS = ["simplisafe-python==2.0.2"]
_LOGGER = logging.getLogger(__name__)
DEFAULT_NAME = 'SimpliSafe'
DEFAULT_NAME = "SimpliSafe"
ATTR_ALARM_ACTIVE = "alarm_active"
ATTR_TEMPERATURE = "temperature"
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_PASSWORD): cv.string,
vol.Required(CONF_USERNAME): cv.string,
vol.Optional(CONF_CODE): cv.string,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
})
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
{
vol.Required(CONF_PASSWORD): cv.string,
vol.Required(CONF_USERNAME): cv.string,
vol.Optional(CONF_CODE): cv.string,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
}
)
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the SimpliSafe platform."""
from simplipy.api import SimpliSafeApiInterface, SimpliSafeAPIException
name = config.get(CONF_NAME)
code = config.get(CONF_CODE)
username = config.get(CONF_USERNAME)
@ -75,27 +86,30 @@ class SimpliSafeAlarm(AlarmControlPanel):
"""Return the name of the device."""
if self._name is not None:
return self._name
return 'Alarm {}'.format(self.simplisafe.location_id)
return "Alarm {}".format(self.simplisafe.location_id)
@property
def code_format(self):
"""Return one or more digits/characters."""
if self._code is None:
return None
if isinstance(self._code, str) and re.search('^\\d+$', self._code):
return 'Number'
return 'Any'
if isinstance(self._code, str) and re.search("^\\d+$", self._code):
return "Number"
return "Any"
@property
def state(self):
"""Return the state of the device."""
status = self.simplisafe.state
if status.lower() == 'off':
if status.lower() == "off":
state = STATE_ALARM_DISARMED
elif status.lower() == 'home' or status.lower() == 'home_count':
elif status.lower() == "home" or status.lower() == "home_count":
state = STATE_ALARM_ARMED_HOME
elif (status.lower() == 'away' or status.lower() == 'exitDelay' or
status.lower() == 'away_count'):
elif (
status.lower() == "away"
or status.lower() == "exitDelay"
or status.lower() == "away_count"
):
state = STATE_ALARM_ARMED_AWAY
else:
state = STATE_UNKNOWN
@ -118,23 +132,23 @@ class SimpliSafeAlarm(AlarmControlPanel):
def alarm_disarm(self, code=None):
"""Send disarm command."""
if not self._validate_code(code, 'disarming'):
if not self._validate_code(code, "disarming"):
return
self.simplisafe.set_state('off')
self.simplisafe.set_state("off")
_LOGGER.info("SimpliSafe alarm disarming")
def alarm_arm_home(self, code=None):
"""Send arm home command."""
if not self._validate_code(code, 'arming home'):
if not self._validate_code(code, "arming home"):
return
self.simplisafe.set_state('home')
self.simplisafe.set_state("home")
_LOGGER.info("SimpliSafe alarm arming home")
def alarm_arm_away(self, code=None):
"""Send arm away command."""
if not self._validate_code(code, 'arming away'):
if not self._validate_code(code, "arming away"):
return
self.simplisafe.set_state('away')
self.simplisafe.set_state("away")
_LOGGER.info("SimpliSafe alarm arming away")
def _validate_code(self, code, state):

View file

@ -9,17 +9,24 @@ import logging
import homeassistant.components.alarm_control_panel as alarm
from homeassistant.components.spc import (
ATTR_DISCOVER_AREAS, DATA_API, DATA_REGISTRY, SpcWebGateway)
ATTR_DISCOVER_AREAS,
DATA_API,
DATA_REGISTRY,
SpcWebGateway,
)
from homeassistant.const import (
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED,
STATE_UNKNOWN)
STATE_ALARM_ARMED_AWAY,
STATE_ALARM_ARMED_HOME,
STATE_ALARM_DISARMED,
STATE_UNKNOWN,
)
_LOGGER = logging.getLogger(__name__)
SPC_AREA_MODE_TO_STATE = {
'0': STATE_ALARM_DISARMED,
'1': STATE_ALARM_ARMED_HOME,
'3': STATE_ALARM_ARMED_AWAY,
"0": STATE_ALARM_DISARMED,
"1": STATE_ALARM_ARMED_HOME,
"3": STATE_ALARM_ARMED_AWAY,
}
@ -29,16 +36,13 @@ def _get_alarm_state(spc_mode):
@asyncio.coroutine
def async_setup_platform(hass, config, async_add_entities,
discovery_info=None):
def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
"""Set up the SPC alarm control panel platform."""
if (discovery_info is None or
discovery_info[ATTR_DISCOVER_AREAS] is None):
if discovery_info is None or discovery_info[ATTR_DISCOVER_AREAS] is None:
return
api = hass.data[DATA_API]
devices = [SpcAlarm(api, area)
for area in discovery_info[ATTR_DISCOVER_AREAS]]
devices = [SpcAlarm(api, area) for area in discovery_info[ATTR_DISCOVER_AREAS]]
async_add_entities(devices)
@ -48,26 +52,25 @@ class SpcAlarm(alarm.AlarmControlPanel):
def __init__(self, api, area):
"""Initialize the SPC alarm panel."""
self._area_id = area['id']
self._name = area['name']
self._state = _get_alarm_state(area['mode'])
self._area_id = area["id"]
self._name = area["name"]
self._state = _get_alarm_state(area["mode"])
if self._state == STATE_ALARM_DISARMED:
self._changed_by = area.get('last_unset_user_name', 'unknown')
self._changed_by = area.get("last_unset_user_name", "unknown")
else:
self._changed_by = area.get('last_set_user_name', 'unknown')
self._changed_by = area.get("last_set_user_name", "unknown")
self._api = api
@asyncio.coroutine
def async_added_to_hass(self):
"""Call for adding new entities."""
self.hass.data[DATA_REGISTRY].register_alarm_device(
self._area_id, self)
self.hass.data[DATA_REGISTRY].register_alarm_device(self._area_id, self)
@asyncio.coroutine
def async_update_from_spc(self, state, extra):
"""Update the alarm panel with a new state."""
self._state = state
self._changed_by = extra.get('changed_by', 'unknown')
self._changed_by = extra.get("changed_by", "unknown")
self.async_schedule_update_ha_state()
@property
@ -94,16 +97,19 @@ class SpcAlarm(alarm.AlarmControlPanel):
def async_alarm_disarm(self, code=None):
"""Send disarm command."""
yield from self._api.send_area_command(
self._area_id, SpcWebGateway.AREA_COMMAND_UNSET)
self._area_id, SpcWebGateway.AREA_COMMAND_UNSET
)
@asyncio.coroutine
def async_alarm_arm_home(self, code=None):
"""Send arm home command."""
yield from self._api.send_area_command(
self._area_id, SpcWebGateway.AREA_COMMAND_PART_SET)
self._area_id, SpcWebGateway.AREA_COMMAND_PART_SET
)
@asyncio.coroutine
def async_alarm_arm_away(self, code=None):
"""Send arm away command."""
yield from self._api.send_area_command(
self._area_id, SpcWebGateway.AREA_COMMAND_SET)
self._area_id, SpcWebGateway.AREA_COMMAND_SET
)

View file

@ -12,23 +12,33 @@ import homeassistant.helpers.config_validation as cv
import homeassistant.components.alarm_control_panel as alarm
from homeassistant.components.alarm_control_panel import PLATFORM_SCHEMA
from homeassistant.const import (
CONF_PASSWORD, CONF_USERNAME, STATE_ALARM_ARMED_AWAY,
STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT, STATE_ALARM_DISARMED,
STATE_ALARM_ARMING, STATE_ALARM_DISARMING, STATE_UNKNOWN, CONF_NAME,
STATE_ALARM_ARMED_CUSTOM_BYPASS)
CONF_PASSWORD,
CONF_USERNAME,
STATE_ALARM_ARMED_AWAY,
STATE_ALARM_ARMED_HOME,
STATE_ALARM_ARMED_NIGHT,
STATE_ALARM_DISARMED,
STATE_ALARM_ARMING,
STATE_ALARM_DISARMING,
STATE_UNKNOWN,
CONF_NAME,
STATE_ALARM_ARMED_CUSTOM_BYPASS,
)
REQUIREMENTS = ['total_connect_client==0.18']
REQUIREMENTS = ["total_connect_client==0.18"]
_LOGGER = logging.getLogger(__name__)
DEFAULT_NAME = 'Total Connect'
DEFAULT_NAME = "Total Connect"
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_PASSWORD): cv.string,
vol.Required(CONF_USERNAME): cv.string,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
})
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
{
vol.Required(CONF_PASSWORD): cv.string,
vol.Required(CONF_USERNAME): cv.string,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
}
)
def setup_platform(hass, config, add_entities, discovery_info=None):
@ -53,8 +63,7 @@ class TotalConnect(alarm.AlarmControlPanel):
self._username = username
self._password = password
self._state = STATE_UNKNOWN
self._client = TotalConnectClient.TotalConnectClient(
username, password)
self._client = TotalConnectClient.TotalConnectClient(username, password)
@property
def name(self):

View file

@ -11,8 +11,11 @@ import homeassistant.components.alarm_control_panel as alarm
from homeassistant.components.verisure import CONF_ALARM, CONF_CODE_DIGITS
from homeassistant.components.verisure import HUB as hub
from homeassistant.const import (
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED,
STATE_UNKNOWN)
STATE_ALARM_ARMED_AWAY,
STATE_ALARM_ARMED_HOME,
STATE_ALARM_DISARMED,
STATE_UNKNOWN,
)
_LOGGER = logging.getLogger(__name__)
@ -29,10 +32,11 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
def set_arm_state(state, code=None):
"""Send set arm state command."""
transaction_id = hub.session.set_arm_state(code, state)[
'armStateChangeTransactionId']
_LOGGER.info('verisure set arm state %s', state)
"armStateChangeTransactionId"
]
_LOGGER.info("verisure set arm state %s", state)
transaction = {}
while 'result' not in transaction:
while "result" not in transaction:
sleep(0.5)
transaction = hub.session.get_arm_state_transaction(transaction_id)
# pylint: disable=unexpected-keyword-arg
@ -51,7 +55,7 @@ class VerisureAlarm(alarm.AlarmControlPanel):
@property
def name(self):
"""Return the name of the device."""
return '{} alarm'.format(hub.session.installations[0]['alias'])
return "{} alarm".format(hub.session.installations[0]["alias"])
@property
def state(self):
@ -61,7 +65,7 @@ class VerisureAlarm(alarm.AlarmControlPanel):
@property
def code_format(self):
"""Return one or more digits/characters."""
return 'Number'
return "Number"
@property
def changed_by(self):
@ -72,24 +76,24 @@ class VerisureAlarm(alarm.AlarmControlPanel):
"""Update alarm status."""
hub.update_overview()
status = hub.get_first("$.armState.statusType")
if status == 'DISARMED':
if status == "DISARMED":
self._state = STATE_ALARM_DISARMED
elif status == 'ARMED_HOME':
elif status == "ARMED_HOME":
self._state = STATE_ALARM_ARMED_HOME
elif status == 'ARMED_AWAY':
elif status == "ARMED_AWAY":
self._state = STATE_ALARM_ARMED_AWAY
elif status != 'PENDING':
_LOGGER.error('Unknown alarm state %s', status)
elif status != "PENDING":
_LOGGER.error("Unknown alarm state %s", status)
self._changed_by = hub.get_first("$.armState.name")
def alarm_disarm(self, code=None):
"""Send disarm command."""
set_arm_state('DISARMED', code)
set_arm_state("DISARMED", code)
def alarm_arm_home(self, code=None):
"""Send arm home command."""
set_arm_state('ARMED_HOME', code)
set_arm_state("ARMED_HOME", code)
def alarm_arm_away(self, code=None):
"""Send arm away command."""
set_arm_state('ARMED_AWAY', code)
set_arm_state("ARMED_AWAY", code)

View file

@ -10,14 +10,17 @@ import logging
import homeassistant.components.alarm_control_panel as alarm
from homeassistant.components.wink import DOMAIN, WinkDevice
from homeassistant.const import (
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED,
STATE_UNKNOWN)
STATE_ALARM_ARMED_AWAY,
STATE_ALARM_ARMED_HOME,
STATE_ALARM_DISARMED,
STATE_UNKNOWN,
)
_LOGGER = logging.getLogger(__name__)
DEPENDENCIES = ['wink']
DEPENDENCIES = ["wink"]
STATE_ALARM_PRIVACY = 'Private'
STATE_ALARM_PRIVACY = "Private"
def setup_platform(hass, config, add_entities, discovery_info=None):
@ -31,7 +34,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
camera.capability()
except AttributeError:
_id = camera.object_id() + camera.name()
if _id not in hass.data[DOMAIN]['unique_ids']:
if _id not in hass.data[DOMAIN]["unique_ids"]:
add_entities([WinkCameraDevice(camera, hass)])
@ -41,7 +44,7 @@ class WinkCameraDevice(WinkDevice, alarm.AlarmControlPanel):
@asyncio.coroutine
def async_added_to_hass(self):
"""Call when entity is added to hass."""
self.hass.data[DOMAIN]['entities']['alarm_control_panel'].append(self)
self.hass.data[DOMAIN]["entities"]["alarm_control_panel"].append(self)
@property
def state(self):
@ -72,6 +75,4 @@ class WinkCameraDevice(WinkDevice, alarm.AlarmControlPanel):
@property
def device_state_attributes(self):
"""Return the state attributes."""
return {
'private': self.wink.private()
}
return {"private": self.wink.private()}

View file

@ -9,28 +9,37 @@ import logging
import voluptuous as vol
from homeassistant.components.alarm_control_panel import (
AlarmControlPanel, PLATFORM_SCHEMA)
AlarmControlPanel,
PLATFORM_SCHEMA,
)
from homeassistant.const import (
CONF_PASSWORD, CONF_USERNAME, CONF_NAME,
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED)
CONF_PASSWORD,
CONF_USERNAME,
CONF_NAME,
STATE_ALARM_ARMED_AWAY,
STATE_ALARM_ARMED_HOME,
STATE_ALARM_DISARMED,
)
import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['yalesmartalarmclient==0.1.4']
REQUIREMENTS = ["yalesmartalarmclient==0.1.4"]
CONF_AREA_ID = 'area_id'
CONF_AREA_ID = "area_id"
DEFAULT_NAME = 'Yale Smart Alarm'
DEFAULT_NAME = "Yale Smart Alarm"
DEFAULT_AREA_ID = '1'
DEFAULT_AREA_ID = "1"
_LOGGER = logging.getLogger(__name__)
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_PASSWORD): cv.string,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_AREA_ID, default=DEFAULT_AREA_ID): cv.string,
})
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
{
vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_PASSWORD): cv.string,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_AREA_ID, default=DEFAULT_AREA_ID): cv.string,
}
)
def setup_platform(hass, config, add_entities, discovery_info=None):
@ -40,8 +49,8 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
password = config[CONF_PASSWORD]
area_id = config[CONF_AREA_ID]
from yalesmartalarmclient.client import (
YaleSmartAlarmClient, AuthenticationError)
from yalesmartalarmclient.client import YaleSmartAlarmClient, AuthenticationError
try:
client = YaleSmartAlarmClient(username, password, area_id)
except AuthenticationError:
@ -60,13 +69,16 @@ class YaleAlarmDevice(AlarmControlPanel):
self._client = client
self._state = None
from yalesmartalarmclient.client import (YALE_STATE_DISARM,
YALE_STATE_ARM_PARTIAL,
YALE_STATE_ARM_FULL)
from yalesmartalarmclient.client import (
YALE_STATE_DISARM,
YALE_STATE_ARM_PARTIAL,
YALE_STATE_ARM_FULL,
)
self._state_map = {
YALE_STATE_DISARM: STATE_ALARM_DISARMED,
YALE_STATE_ARM_PARTIAL: STATE_ALARM_ARMED_HOME,
YALE_STATE_ARM_FULL: STATE_ALARM_ARMED_AWAY
YALE_STATE_ARM_FULL: STATE_ALARM_ARMED_AWAY,
}
@property

View file

@ -15,87 +15,108 @@ from homeassistant.helpers.discovery import load_platform
from homeassistant.util import dt as dt_util
from homeassistant.components.binary_sensor import DEVICE_CLASSES_SCHEMA
REQUIREMENTS = ['alarmdecoder==1.13.2']
REQUIREMENTS = ["alarmdecoder==1.13.2"]
_LOGGER = logging.getLogger(__name__)
DOMAIN = 'alarmdecoder'
DOMAIN = "alarmdecoder"
DATA_AD = 'alarmdecoder'
DATA_AD = "alarmdecoder"
CONF_DEVICE = 'device'
CONF_DEVICE_BAUD = 'baudrate'
CONF_DEVICE_HOST = 'host'
CONF_DEVICE_PATH = 'path'
CONF_DEVICE_PORT = 'port'
CONF_DEVICE_TYPE = 'type'
CONF_PANEL_DISPLAY = 'panel_display'
CONF_ZONE_NAME = 'name'
CONF_ZONE_TYPE = 'type'
CONF_ZONE_RFID = 'rfid'
CONF_ZONES = 'zones'
CONF_RELAY_ADDR = 'relayaddr'
CONF_RELAY_CHAN = 'relaychan'
CONF_DEVICE = "device"
CONF_DEVICE_BAUD = "baudrate"
CONF_DEVICE_HOST = "host"
CONF_DEVICE_PATH = "path"
CONF_DEVICE_PORT = "port"
CONF_DEVICE_TYPE = "type"
CONF_PANEL_DISPLAY = "panel_display"
CONF_ZONE_NAME = "name"
CONF_ZONE_TYPE = "type"
CONF_ZONE_RFID = "rfid"
CONF_ZONES = "zones"
CONF_RELAY_ADDR = "relayaddr"
CONF_RELAY_CHAN = "relaychan"
DEFAULT_DEVICE_TYPE = 'socket'
DEFAULT_DEVICE_HOST = 'localhost'
DEFAULT_DEVICE_TYPE = "socket"
DEFAULT_DEVICE_HOST = "localhost"
DEFAULT_DEVICE_PORT = 10000
DEFAULT_DEVICE_PATH = '/dev/ttyUSB0'
DEFAULT_DEVICE_PATH = "/dev/ttyUSB0"
DEFAULT_DEVICE_BAUD = 115200
DEFAULT_PANEL_DISPLAY = False
DEFAULT_ZONE_TYPE = 'opening'
DEFAULT_ZONE_TYPE = "opening"
SIGNAL_PANEL_MESSAGE = 'alarmdecoder.panel_message'
SIGNAL_PANEL_ARM_AWAY = 'alarmdecoder.panel_arm_away'
SIGNAL_PANEL_ARM_HOME = 'alarmdecoder.panel_arm_home'
SIGNAL_PANEL_DISARM = 'alarmdecoder.panel_disarm'
SIGNAL_PANEL_MESSAGE = "alarmdecoder.panel_message"
SIGNAL_PANEL_ARM_AWAY = "alarmdecoder.panel_arm_away"
SIGNAL_PANEL_ARM_HOME = "alarmdecoder.panel_arm_home"
SIGNAL_PANEL_DISARM = "alarmdecoder.panel_disarm"
SIGNAL_ZONE_FAULT = 'alarmdecoder.zone_fault'
SIGNAL_ZONE_RESTORE = 'alarmdecoder.zone_restore'
SIGNAL_RFX_MESSAGE = 'alarmdecoder.rfx_message'
SIGNAL_REL_MESSAGE = 'alarmdecoder.rel_message'
SIGNAL_ZONE_FAULT = "alarmdecoder.zone_fault"
SIGNAL_ZONE_RESTORE = "alarmdecoder.zone_restore"
SIGNAL_RFX_MESSAGE = "alarmdecoder.rfx_message"
SIGNAL_REL_MESSAGE = "alarmdecoder.rel_message"
DEVICE_SOCKET_SCHEMA = vol.Schema({
vol.Required(CONF_DEVICE_TYPE): 'socket',
vol.Optional(CONF_DEVICE_HOST, default=DEFAULT_DEVICE_HOST): cv.string,
vol.Optional(CONF_DEVICE_PORT, default=DEFAULT_DEVICE_PORT): cv.port})
DEVICE_SOCKET_SCHEMA = vol.Schema(
{
vol.Required(CONF_DEVICE_TYPE): "socket",
vol.Optional(CONF_DEVICE_HOST, default=DEFAULT_DEVICE_HOST): cv.string,
vol.Optional(CONF_DEVICE_PORT, default=DEFAULT_DEVICE_PORT): cv.port,
}
)
DEVICE_SERIAL_SCHEMA = vol.Schema({
vol.Required(CONF_DEVICE_TYPE): 'serial',
vol.Optional(CONF_DEVICE_PATH, default=DEFAULT_DEVICE_PATH): cv.string,
vol.Optional(CONF_DEVICE_BAUD, default=DEFAULT_DEVICE_BAUD): cv.string})
DEVICE_SERIAL_SCHEMA = vol.Schema(
{
vol.Required(CONF_DEVICE_TYPE): "serial",
vol.Optional(CONF_DEVICE_PATH, default=DEFAULT_DEVICE_PATH): cv.string,
vol.Optional(CONF_DEVICE_BAUD, default=DEFAULT_DEVICE_BAUD): cv.string,
}
)
DEVICE_USB_SCHEMA = vol.Schema({
vol.Required(CONF_DEVICE_TYPE): 'usb'})
DEVICE_USB_SCHEMA = vol.Schema({vol.Required(CONF_DEVICE_TYPE): "usb"})
ZONE_SCHEMA = vol.Schema({
vol.Required(CONF_ZONE_NAME): cv.string,
vol.Optional(CONF_ZONE_TYPE,
default=DEFAULT_ZONE_TYPE): vol.Any(DEVICE_CLASSES_SCHEMA),
vol.Optional(CONF_ZONE_RFID): cv.string,
vol.Inclusive(CONF_RELAY_ADDR, 'relaylocation',
'Relay address and channel must exist together'): cv.byte,
vol.Inclusive(CONF_RELAY_CHAN, 'relaylocation',
'Relay address and channel must exist together'): cv.byte})
ZONE_SCHEMA = vol.Schema(
{
vol.Required(CONF_ZONE_NAME): cv.string,
vol.Optional(CONF_ZONE_TYPE, default=DEFAULT_ZONE_TYPE): vol.Any(
DEVICE_CLASSES_SCHEMA
),
vol.Optional(CONF_ZONE_RFID): cv.string,
vol.Inclusive(
CONF_RELAY_ADDR,
"relaylocation",
"Relay address and channel must exist together",
): cv.byte,
vol.Inclusive(
CONF_RELAY_CHAN,
"relaylocation",
"Relay address and channel must exist together",
): cv.byte,
}
)
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
vol.Required(CONF_DEVICE): vol.Any(
DEVICE_SOCKET_SCHEMA, DEVICE_SERIAL_SCHEMA,
DEVICE_USB_SCHEMA),
vol.Optional(CONF_PANEL_DISPLAY,
default=DEFAULT_PANEL_DISPLAY): cv.boolean,
vol.Optional(CONF_ZONES): {vol.Coerce(int): ZONE_SCHEMA},
}),
}, extra=vol.ALLOW_EXTRA)
CONFIG_SCHEMA = vol.Schema(
{
DOMAIN: vol.Schema(
{
vol.Required(CONF_DEVICE): vol.Any(
DEVICE_SOCKET_SCHEMA, DEVICE_SERIAL_SCHEMA, DEVICE_USB_SCHEMA
),
vol.Optional(
CONF_PANEL_DISPLAY, default=DEFAULT_PANEL_DISPLAY
): cv.boolean,
vol.Optional(CONF_ZONES): {vol.Coerce(int): ZONE_SCHEMA},
}
)
},
extra=vol.ALLOW_EXTRA,
)
def setup(hass, config):
"""Set up for the AlarmDecoder devices."""
from alarmdecoder import AlarmDecoder
from alarmdecoder.devices import (SocketDevice, SerialDevice, USBDevice)
from alarmdecoder.devices import SocketDevice, SerialDevice, USBDevice
conf = config.get(DOMAIN)
@ -120,13 +141,15 @@ def setup(hass, config):
def open_connection(now=None):
"""Open a connection to AlarmDecoder."""
from alarmdecoder.util import NoDeviceError
nonlocal restart
try:
controller.open(baud)
except NoDeviceError:
_LOGGER.debug("Failed to connect. Retrying in 5 seconds")
hass.helpers.event.track_point_in_time(
open_connection, dt_util.utcnow() + timedelta(seconds=5))
open_connection, dt_util.utcnow() + timedelta(seconds=5)
)
return
_LOGGER.debug("Established a connection with the alarmdecoder")
restart = True
@ -142,39 +165,34 @@ def setup(hass, config):
def handle_message(sender, message):
"""Handle message from AlarmDecoder."""
hass.helpers.dispatcher.dispatcher_send(
SIGNAL_PANEL_MESSAGE, message)
hass.helpers.dispatcher.dispatcher_send(SIGNAL_PANEL_MESSAGE, message)
def handle_rfx_message(sender, message):
"""Handle RFX message from AlarmDecoder."""
hass.helpers.dispatcher.dispatcher_send(
SIGNAL_RFX_MESSAGE, message)
hass.helpers.dispatcher.dispatcher_send(SIGNAL_RFX_MESSAGE, message)
def zone_fault_callback(sender, zone):
"""Handle zone fault from AlarmDecoder."""
hass.helpers.dispatcher.dispatcher_send(
SIGNAL_ZONE_FAULT, zone)
hass.helpers.dispatcher.dispatcher_send(SIGNAL_ZONE_FAULT, zone)
def zone_restore_callback(sender, zone):
"""Handle zone restore from AlarmDecoder."""
hass.helpers.dispatcher.dispatcher_send(
SIGNAL_ZONE_RESTORE, zone)
hass.helpers.dispatcher.dispatcher_send(SIGNAL_ZONE_RESTORE, zone)
def handle_rel_message(sender, message):
"""Handle relay message from AlarmDecoder."""
hass.helpers.dispatcher.dispatcher_send(
SIGNAL_REL_MESSAGE, message)
hass.helpers.dispatcher.dispatcher_send(SIGNAL_REL_MESSAGE, message)
controller = False
if device_type == 'socket':
if device_type == "socket":
host = device.get(CONF_DEVICE_HOST)
port = device.get(CONF_DEVICE_PORT)
controller = AlarmDecoder(SocketDevice(interface=(host, port)))
elif device_type == 'serial':
elif device_type == "serial":
path = device.get(CONF_DEVICE_PATH)
baud = device.get(CONF_DEVICE_BAUD)
controller = AlarmDecoder(SerialDevice(interface=path))
elif device_type == 'usb':
elif device_type == "usb":
AlarmDecoder(USBDevice.find())
return False
@ -191,13 +209,12 @@ def setup(hass, config):
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, stop_alarmdecoder)
load_platform(hass, 'alarm_control_panel', DOMAIN, conf, config)
load_platform(hass, "alarm_control_panel", DOMAIN, conf, config)
if zones:
load_platform(
hass, 'binary_sensor', DOMAIN, {CONF_ZONES: zones}, config)
load_platform(hass, "binary_sensor", DOMAIN, {CONF_ZONES: zones}, config)
if display:
load_platform(hass, 'sensor', DOMAIN, conf, config)
load_platform(hass, "sensor", DOMAIN, conf, config)
return True

View file

@ -12,46 +12,54 @@ import voluptuous as vol
from homeassistant.core import callback
from homeassistant.const import (
CONF_ENTITY_ID, STATE_IDLE, CONF_NAME, CONF_STATE, STATE_ON, STATE_OFF,
SERVICE_TURN_ON, SERVICE_TURN_OFF, SERVICE_TOGGLE, ATTR_ENTITY_ID)
CONF_ENTITY_ID,
STATE_IDLE,
CONF_NAME,
CONF_STATE,
STATE_ON,
STATE_OFF,
SERVICE_TURN_ON,
SERVICE_TURN_OFF,
SERVICE_TOGGLE,
ATTR_ENTITY_ID,
)
from homeassistant.helpers.entity import ToggleEntity
from homeassistant.helpers import service, event
import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__)
DOMAIN = 'alert'
ENTITY_ID_FORMAT = DOMAIN + '.{}'
DOMAIN = "alert"
ENTITY_ID_FORMAT = DOMAIN + ".{}"
CONF_DONE_MESSAGE = 'done_message'
CONF_CAN_ACK = 'can_acknowledge'
CONF_NOTIFIERS = 'notifiers'
CONF_REPEAT = 'repeat'
CONF_SKIP_FIRST = 'skip_first'
CONF_DONE_MESSAGE = "done_message"
CONF_CAN_ACK = "can_acknowledge"
CONF_NOTIFIERS = "notifiers"
CONF_REPEAT = "repeat"
CONF_SKIP_FIRST = "skip_first"
DEFAULT_CAN_ACK = True
DEFAULT_SKIP_FIRST = False
ALERT_SCHEMA = vol.Schema({
vol.Required(CONF_NAME): cv.string,
vol.Optional(CONF_DONE_MESSAGE): cv.string,
vol.Required(CONF_ENTITY_ID): cv.entity_id,
vol.Required(CONF_STATE, default=STATE_ON): cv.string,
vol.Required(CONF_REPEAT): vol.All(cv.ensure_list, [vol.Coerce(float)]),
vol.Required(CONF_CAN_ACK, default=DEFAULT_CAN_ACK): cv.boolean,
vol.Required(CONF_SKIP_FIRST, default=DEFAULT_SKIP_FIRST): cv.boolean,
vol.Required(CONF_NOTIFIERS): cv.ensure_list})
ALERT_SCHEMA = vol.Schema(
{
vol.Required(CONF_NAME): cv.string,
vol.Optional(CONF_DONE_MESSAGE): cv.string,
vol.Required(CONF_ENTITY_ID): cv.entity_id,
vol.Required(CONF_STATE, default=STATE_ON): cv.string,
vol.Required(CONF_REPEAT): vol.All(cv.ensure_list, [vol.Coerce(float)]),
vol.Required(CONF_CAN_ACK, default=DEFAULT_CAN_ACK): cv.boolean,
vol.Required(CONF_SKIP_FIRST, default=DEFAULT_SKIP_FIRST): cv.boolean,
vol.Required(CONF_NOTIFIERS): cv.ensure_list,
}
)
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
cv.slug: ALERT_SCHEMA,
}),
}, extra=vol.ALLOW_EXTRA)
CONFIG_SCHEMA = vol.Schema(
{DOMAIN: vol.Schema({cv.slug: ALERT_SCHEMA})}, extra=vol.ALLOW_EXTRA
)
ALERT_SERVICE_SCHEMA = vol.Schema({
vol.Required(ATTR_ENTITY_ID): cv.entity_ids,
})
ALERT_SERVICE_SCHEMA = vol.Schema({vol.Required(ATTR_ENTITY_ID): cv.entity_ids})
def is_on(hass, entity_id):
@ -68,8 +76,7 @@ def turn_on(hass, entity_id):
def async_turn_on(hass, entity_id):
"""Async reset the alert."""
data = {ATTR_ENTITY_ID: entity_id}
hass.async_create_task(
hass.services.async_call(DOMAIN, SERVICE_TURN_ON, data))
hass.async_create_task(hass.services.async_call(DOMAIN, SERVICE_TURN_ON, data))
def turn_off(hass, entity_id):
@ -81,8 +88,7 @@ def turn_off(hass, entity_id):
def async_turn_off(hass, entity_id):
"""Async acknowledge the alert."""
data = {ATTR_ENTITY_ID: entity_id}
hass.async_create_task(
hass.services.async_call(DOMAIN, SERVICE_TURN_OFF, data))
hass.async_create_task(hass.services.async_call(DOMAIN, SERVICE_TURN_OFF, data))
def toggle(hass, entity_id):
@ -94,8 +100,7 @@ def toggle(hass, entity_id):
def async_toggle(hass, entity_id):
"""Async toggle acknowledgement of alert."""
data = {ATTR_ENTITY_ID: entity_id}
hass.async_create_task(
hass.services.async_call(DOMAIN, SERVICE_TOGGLE, data))
hass.async_create_task(hass.services.async_call(DOMAIN, SERVICE_TOGGLE, data))
@asyncio.coroutine
@ -121,23 +126,33 @@ def async_setup(hass, config):
# Setup alerts
for entity_id, alert in alerts.items():
entity = Alert(hass, entity_id,
alert[CONF_NAME], alert.get(CONF_DONE_MESSAGE),
alert[CONF_ENTITY_ID], alert[CONF_STATE],
alert[CONF_REPEAT], alert[CONF_SKIP_FIRST],
alert[CONF_NOTIFIERS], alert[CONF_CAN_ACK])
entity = Alert(
hass,
entity_id,
alert[CONF_NAME],
alert.get(CONF_DONE_MESSAGE),
alert[CONF_ENTITY_ID],
alert[CONF_STATE],
alert[CONF_REPEAT],
alert[CONF_SKIP_FIRST],
alert[CONF_NOTIFIERS],
alert[CONF_CAN_ACK],
)
all_alerts[entity.entity_id] = entity
# Setup service calls
hass.services.async_register(
DOMAIN, SERVICE_TURN_OFF, async_handle_alert_service,
schema=ALERT_SERVICE_SCHEMA)
DOMAIN,
SERVICE_TURN_OFF,
async_handle_alert_service,
schema=ALERT_SERVICE_SCHEMA,
)
hass.services.async_register(
DOMAIN, SERVICE_TURN_ON, async_handle_alert_service,
schema=ALERT_SERVICE_SCHEMA)
DOMAIN, SERVICE_TURN_ON, async_handle_alert_service, schema=ALERT_SERVICE_SCHEMA
)
hass.services.async_register(
DOMAIN, SERVICE_TOGGLE, async_handle_alert_service,
schema=ALERT_SERVICE_SCHEMA)
DOMAIN, SERVICE_TOGGLE, async_handle_alert_service, schema=ALERT_SERVICE_SCHEMA
)
tasks = [alert.async_update_ha_state() for alert in all_alerts.values()]
if tasks:
@ -149,8 +164,19 @@ def async_setup(hass, config):
class Alert(ToggleEntity):
"""Representation of an alert."""
def __init__(self, hass, entity_id, name, done_message, watched_entity_id,
state, repeat, skip_first, notifiers, can_ack):
def __init__(
self,
hass,
entity_id,
name,
done_message,
watched_entity_id,
state,
repeat,
skip_first,
notifiers,
can_ack,
):
"""Initialize the alert."""
self.hass = hass
self._name = name
@ -170,7 +196,8 @@ class Alert(ToggleEntity):
self.entity_id = ENTITY_ID_FORMAT.format(entity_id)
event.async_track_state_change(
hass, watched_entity_id, self.watched_entity_change)
hass, watched_entity_id, self.watched_entity_change
)
@property
def name(self):
@ -236,8 +263,9 @@ class Alert(ToggleEntity):
"""Schedule a notification."""
delay = self._delay[self._next_delay]
next_msg = datetime.now() + delay
self._cancel = \
event.async_track_point_in_time(self.hass, self._notify, next_msg)
self._cancel = event.async_track_point_in_time(
self.hass, self._notify, next_msg
)
self._next_delay = min(self._next_delay + 1, len(self._delay) - 1)
@asyncio.coroutine
@ -251,7 +279,8 @@ class Alert(ToggleEntity):
self._send_done_message = True
for target in self._notifiers:
yield from self.hass.services.async_call(
'notify', target, {'message': self._name})
"notify", target, {"message": self._name}
)
yield from self._schedule_notify()
@asyncio.coroutine
@ -261,7 +290,8 @@ class Alert(ToggleEntity):
self._send_done_message = False
for target in self._notifiers:
yield from self.hass.services.async_call(
'notify', target, {'message': self._done_message})
"notify", target, {"message": self._done_message}
)
@asyncio.coroutine
def async_turn_on(self, **kwargs):

View file

@ -14,43 +14,62 @@ from homeassistant.helpers import entityfilter
from . import flash_briefings, intent, smart_home
from .const import (
CONF_AUDIO, CONF_DISPLAY_URL, CONF_TEXT, CONF_TITLE, CONF_UID, DOMAIN,
CONF_FILTER, CONF_ENTITY_CONFIG)
CONF_AUDIO,
CONF_DISPLAY_URL,
CONF_TEXT,
CONF_TITLE,
CONF_UID,
DOMAIN,
CONF_FILTER,
CONF_ENTITY_CONFIG,
)
_LOGGER = logging.getLogger(__name__)
CONF_FLASH_BRIEFINGS = 'flash_briefings'
CONF_SMART_HOME = 'smart_home'
CONF_FLASH_BRIEFINGS = "flash_briefings"
CONF_SMART_HOME = "smart_home"
DEPENDENCIES = ['http']
DEPENDENCIES = ["http"]
ALEXA_ENTITY_SCHEMA = vol.Schema({
vol.Optional(smart_home.CONF_DESCRIPTION): cv.string,
vol.Optional(smart_home.CONF_DISPLAY_CATEGORIES): cv.string,
vol.Optional(smart_home.CONF_NAME): cv.string,
})
SMART_HOME_SCHEMA = vol.Schema({
vol.Optional(CONF_FILTER, default={}): entityfilter.FILTER_SCHEMA,
vol.Optional(CONF_ENTITY_CONFIG): {cv.entity_id: ALEXA_ENTITY_SCHEMA}
})
CONFIG_SCHEMA = vol.Schema({
DOMAIN: {
CONF_FLASH_BRIEFINGS: {
cv.string: vol.All(cv.ensure_list, [{
vol.Optional(CONF_UID): cv.string,
vol.Required(CONF_TITLE): cv.template,
vol.Optional(CONF_AUDIO): cv.template,
vol.Required(CONF_TEXT, default=""): cv.template,
vol.Optional(CONF_DISPLAY_URL): cv.template,
}]),
},
# vol.Optional here would mean we couldn't distinguish between an empty
# smart_home: and none at all.
CONF_SMART_HOME: vol.Any(SMART_HOME_SCHEMA, None),
ALEXA_ENTITY_SCHEMA = vol.Schema(
{
vol.Optional(smart_home.CONF_DESCRIPTION): cv.string,
vol.Optional(smart_home.CONF_DISPLAY_CATEGORIES): cv.string,
vol.Optional(smart_home.CONF_NAME): cv.string,
}
}, extra=vol.ALLOW_EXTRA)
)
SMART_HOME_SCHEMA = vol.Schema(
{
vol.Optional(CONF_FILTER, default={}): entityfilter.FILTER_SCHEMA,
vol.Optional(CONF_ENTITY_CONFIG): {cv.entity_id: ALEXA_ENTITY_SCHEMA},
}
)
CONFIG_SCHEMA = vol.Schema(
{
DOMAIN: {
CONF_FLASH_BRIEFINGS: {
cv.string: vol.All(
cv.ensure_list,
[
{
vol.Optional(CONF_UID): cv.string,
vol.Required(CONF_TITLE): cv.template,
vol.Optional(CONF_AUDIO): cv.template,
vol.Required(CONF_TEXT, default=""): cv.template,
vol.Optional(CONF_DISPLAY_URL): cv.template,
}
],
)
},
# vol.Optional here would mean we couldn't distinguish between an empty
# smart_home: and none at all.
CONF_SMART_HOME: vol.Any(SMART_HOME_SCHEMA, None),
}
},
extra=vol.ALLOW_EXTRA,
)
@asyncio.coroutine

View file

@ -1,23 +1,23 @@
"""Constants for the Alexa integration."""
DOMAIN = 'alexa'
DOMAIN = "alexa"
# Flash briefing constants
CONF_UID = 'uid'
CONF_TITLE = 'title'
CONF_AUDIO = 'audio'
CONF_TEXT = 'text'
CONF_DISPLAY_URL = 'display_url'
CONF_UID = "uid"
CONF_TITLE = "title"
CONF_AUDIO = "audio"
CONF_TEXT = "text"
CONF_DISPLAY_URL = "display_url"
CONF_FILTER = 'filter'
CONF_ENTITY_CONFIG = 'entity_config'
CONF_FILTER = "filter"
CONF_ENTITY_CONFIG = "entity_config"
ATTR_UID = 'uid'
ATTR_UPDATE_DATE = 'updateDate'
ATTR_TITLE_TEXT = 'titleText'
ATTR_STREAM_URL = 'streamUrl'
ATTR_MAIN_TEXT = 'mainText'
ATTR_REDIRECTION_URL = 'redirectionURL'
ATTR_UID = "uid"
ATTR_UPDATE_DATE = "updateDate"
ATTR_TITLE_TEXT = "titleText"
ATTR_STREAM_URL = "streamUrl"
ATTR_MAIN_TEXT = "mainText"
ATTR_REDIRECTION_URL = "redirectionURL"
SYN_RESOLUTION_MATCH = 'ER_SUCCESS_MATCH'
SYN_RESOLUTION_MATCH = "ER_SUCCESS_MATCH"
DATE_FORMAT = '%Y-%m-%dT%H:%M:%S.0Z'
DATE_FORMAT = "%Y-%m-%dT%H:%M:%S.0Z"

View file

@ -14,27 +14,36 @@ from homeassistant.core import callback
from homeassistant.helpers import template
from .const import (
ATTR_MAIN_TEXT, ATTR_REDIRECTION_URL, ATTR_STREAM_URL, ATTR_TITLE_TEXT,
ATTR_UID, ATTR_UPDATE_DATE, CONF_AUDIO, CONF_DISPLAY_URL, CONF_TEXT,
CONF_TITLE, CONF_UID, DATE_FORMAT)
ATTR_MAIN_TEXT,
ATTR_REDIRECTION_URL,
ATTR_STREAM_URL,
ATTR_TITLE_TEXT,
ATTR_UID,
ATTR_UPDATE_DATE,
CONF_AUDIO,
CONF_DISPLAY_URL,
CONF_TEXT,
CONF_TITLE,
CONF_UID,
DATE_FORMAT,
)
_LOGGER = logging.getLogger(__name__)
FLASH_BRIEFINGS_API_ENDPOINT = '/api/alexa/flash_briefings/{briefing_id}'
FLASH_BRIEFINGS_API_ENDPOINT = "/api/alexa/flash_briefings/{briefing_id}"
@callback
def async_setup(hass, flash_briefing_config):
"""Activate Alexa component."""
hass.http.register_view(
AlexaFlashBriefingView(hass, flash_briefing_config))
hass.http.register_view(AlexaFlashBriefingView(hass, flash_briefing_config))
class AlexaFlashBriefingView(http.HomeAssistantView):
"""Handle Alexa Flash Briefing skill requests."""
url = FLASH_BRIEFINGS_API_ENDPOINT
name = 'api:alexa:flash_briefings'
name = "api:alexa:flash_briefings"
def __init__(self, hass, flash_briefings):
"""Initialize Alexa view."""
@ -45,13 +54,12 @@ class AlexaFlashBriefingView(http.HomeAssistantView):
@callback
def get(self, request, briefing_id):
"""Handle Alexa Flash Briefing request."""
_LOGGER.debug("Received Alexa flash briefing request for: %s",
briefing_id)
_LOGGER.debug("Received Alexa flash briefing request for: %s", briefing_id)
if self.flash_briefings.get(briefing_id) is None:
err = "No configured Alexa flash briefing was found for: %s"
_LOGGER.error(err, briefing_id)
return b'', 404
return b"", 404
briefing = []
@ -81,10 +89,8 @@ class AlexaFlashBriefingView(http.HomeAssistantView):
output[ATTR_STREAM_URL] = item.get(CONF_AUDIO)
if item.get(CONF_DISPLAY_URL) is not None:
if isinstance(item.get(CONF_DISPLAY_URL),
template.Template):
output[ATTR_REDIRECTION_URL] = \
item[CONF_DISPLAY_URL].async_render()
if isinstance(item.get(CONF_DISPLAY_URL), template.Template):
output[ATTR_REDIRECTION_URL] = item[CONF_DISPLAY_URL].async_render()
else:
output[ATTR_REDIRECTION_URL] = item.get(CONF_DISPLAY_URL)

View file

@ -20,27 +20,24 @@ _LOGGER = logging.getLogger(__name__)
HANDLERS = Registry()
INTENTS_API_ENDPOINT = '/api/alexa'
INTENTS_API_ENDPOINT = "/api/alexa"
class SpeechType(enum.Enum):
"""The Alexa speech types."""
plaintext = 'PlainText'
ssml = 'SSML'
plaintext = "PlainText"
ssml = "SSML"
SPEECH_MAPPINGS = {
'plain': SpeechType.plaintext,
'ssml': SpeechType.ssml,
}
SPEECH_MAPPINGS = {"plain": SpeechType.plaintext, "ssml": SpeechType.ssml}
class CardType(enum.Enum):
"""The Alexa card types."""
simple = 'Simple'
link_account = 'LinkAccount'
simple = "Simple"
link_account = "LinkAccount"
@callback
@ -57,45 +54,51 @@ class AlexaIntentsView(http.HomeAssistantView):
"""Handle Alexa requests."""
url = INTENTS_API_ENDPOINT
name = 'api:alexa'
name = "api:alexa"
@asyncio.coroutine
def post(self, request):
"""Handle Alexa."""
hass = request.app['hass']
hass = request.app["hass"]
message = yield from request.json()
_LOGGER.debug("Received Alexa request: %s", message)
try:
response = yield from async_handle_message(hass, message)
return b'' if response is None else self.json(response)
return b"" if response is None else self.json(response)
except UnknownRequest as err:
_LOGGER.warning(str(err))
return self.json(intent_error_response(
hass, message, str(err)))
return self.json(intent_error_response(hass, message, str(err)))
except intent.UnknownIntent as err:
_LOGGER.warning(str(err))
return self.json(intent_error_response(
hass, message,
"This intent is not yet configured within Home Assistant."))
return self.json(
intent_error_response(
hass,
message,
"This intent is not yet configured within Home Assistant.",
)
)
except intent.InvalidSlotInfo as err:
_LOGGER.error("Received invalid slot data from Alexa: %s", err)
return self.json(intent_error_response(
hass, message,
"Invalid slot information received for this intent."))
return self.json(
intent_error_response(
hass, message, "Invalid slot information received for this intent."
)
)
except intent.IntentError as err:
_LOGGER.exception(str(err))
return self.json(intent_error_response(
hass, message, "Error handling intent."))
return self.json(
intent_error_response(hass, message, "Error handling intent.")
)
def intent_error_response(hass, message, error):
"""Return an Alexa response that will speak the error message."""
alexa_intent_info = message.get('request').get('intent')
alexa_intent_info = message.get("request").get("intent")
alexa_response = AlexaResponse(hass, alexa_intent_info)
alexa_response.add_speech(SpeechType.plaintext, error)
return alexa_response.as_dict()
@ -112,26 +115,26 @@ def async_handle_message(hass, message):
- intent.IntentError
"""
req = message.get('request')
req_type = req['type']
req = message.get("request")
req_type = req["type"]
handler = HANDLERS.get(req_type)
if not handler:
raise UnknownRequest('Received unknown request {}'.format(req_type))
raise UnknownRequest("Received unknown request {}".format(req_type))
return (yield from handler(hass, message))
@HANDLERS.register('SessionEndedRequest')
@HANDLERS.register("SessionEndedRequest")
@asyncio.coroutine
def async_handle_session_end(hass, message):
"""Handle a session end request."""
return None
@HANDLERS.register('IntentRequest')
@HANDLERS.register('LaunchRequest')
@HANDLERS.register("IntentRequest")
@HANDLERS.register("LaunchRequest")
@asyncio.coroutine
def async_handle_intent(hass, message):
"""Handle an intent request.
@ -142,33 +145,37 @@ def async_handle_intent(hass, message):
- intent.IntentError
"""
req = message.get('request')
alexa_intent_info = req.get('intent')
req = message.get("request")
alexa_intent_info = req.get("intent")
alexa_response = AlexaResponse(hass, alexa_intent_info)
if req['type'] == 'LaunchRequest':
intent_name = message.get('session', {}) \
.get('application', {}) \
.get('applicationId')
if req["type"] == "LaunchRequest":
intent_name = (
message.get("session", {}).get("application", {}).get("applicationId")
)
else:
intent_name = alexa_intent_info['name']
intent_name = alexa_intent_info["name"]
intent_response = yield from intent.async_handle(
hass, DOMAIN, intent_name,
{key: {'value': value} for key, value
in alexa_response.variables.items()})
hass,
DOMAIN,
intent_name,
{key: {"value": value} for key, value in alexa_response.variables.items()},
)
for intent_speech, alexa_speech in SPEECH_MAPPINGS.items():
if intent_speech in intent_response.speech:
alexa_response.add_speech(
alexa_speech,
intent_response.speech[intent_speech]['speech'])
alexa_speech, intent_response.speech[intent_speech]["speech"]
)
break
if 'simple' in intent_response.card:
if "simple" in intent_response.card:
alexa_response.add_card(
CardType.simple, intent_response.card['simple']['title'],
intent_response.card['simple']['content'])
CardType.simple,
intent_response.card["simple"]["title"],
intent_response.card["simple"]["content"],
)
return alexa_response.as_dict()
@ -178,23 +185,23 @@ def resolve_slot_synonyms(key, request):
# Default to the spoken slot value if more than one or none are found. For
# reference to the request object structure, see the Alexa docs:
# https://tinyurl.com/ybvm7jhs
resolved_value = request['value']
resolved_value = request["value"]
if ('resolutions' in request and
'resolutionsPerAuthority' in request['resolutions'] and
len(request['resolutions']['resolutionsPerAuthority']) >= 1):
if (
"resolutions" in request
and "resolutionsPerAuthority" in request["resolutions"]
and len(request["resolutions"]["resolutionsPerAuthority"]) >= 1
):
# Extract all of the possible values from each authority with a
# successful match
possible_values = []
for entry in request['resolutions']['resolutionsPerAuthority']:
if entry['status']['code'] != SYN_RESOLUTION_MATCH:
for entry in request["resolutions"]["resolutionsPerAuthority"]:
if entry["status"]["code"] != SYN_RESOLUTION_MATCH:
continue
possible_values.extend([item['value']['name']
for item
in entry['values']])
possible_values.extend([item["value"]["name"] for item in entry["values"]])
# If there is only one match use the resolved value, otherwise the
# resolution cannot be determined, so use the spoken slot value
@ -202,9 +209,9 @@ def resolve_slot_synonyms(key, request):
resolved_value = possible_values[0]
else:
_LOGGER.debug(
'Found multiple synonym resolutions for slot value: {%s: %s}',
"Found multiple synonym resolutions for slot value: {%s: %s}",
key,
request['value']
request["value"],
)
return resolved_value
@ -225,12 +232,12 @@ class AlexaResponse:
# Intent is None if request was a LaunchRequest or SessionEndedRequest
if intent_info is not None:
for key, value in intent_info.get('slots', {}).items():
for key, value in intent_info.get("slots", {}).items():
# Only include slots with values
if 'value' not in value:
if "value" not in value:
continue
_key = key.replace('.', '_')
_key = key.replace(".", "_")
self.variables[_key] = resolve_slot_synonyms(key, value)
@ -238,9 +245,7 @@ class AlexaResponse:
"""Add a card to the response."""
assert self.card is None
card = {
"type": card_type.value
}
card = {"type": card_type.value}
if card_type == CardType.link_account:
self.card = card
@ -254,43 +259,36 @@ class AlexaResponse:
"""Add speech to the response."""
assert self.speech is None
key = 'ssml' if speech_type == SpeechType.ssml else 'text'
key = "ssml" if speech_type == SpeechType.ssml else "text"
self.speech = {
'type': speech_type.value,
key: text
}
self.speech = {"type": speech_type.value, key: text}
def add_reprompt(self, speech_type, text):
"""Add reprompt if user does not answer."""
assert self.reprompt is None
key = 'ssml' if speech_type == SpeechType.ssml else 'text'
key = "ssml" if speech_type == SpeechType.ssml else "text"
self.reprompt = {
'type': speech_type.value,
key: text.async_render(self.variables)
"type": speech_type.value,
key: text.async_render(self.variables),
}
def as_dict(self):
"""Return response in an Alexa valid dict."""
response = {
'shouldEndSession': self.should_end_session
}
response = {"shouldEndSession": self.should_end_session}
if self.card is not None:
response['card'] = self.card
response["card"] = self.card
if self.speech is not None:
response['outputSpeech'] = self.speech
response["outputSpeech"] = self.speech
if self.reprompt is not None:
response['reprompt'] = {
'outputSpeech': self.reprompt
}
response["reprompt"] = {"outputSpeech": self.reprompt}
return {
'version': '1.0',
'sessionAttributes': self.session_attributes,
'response': response,
"version": "1.0",
"sessionAttributes": self.session_attributes,
"response": response,
}

File diff suppressed because it is too large Load diff

View file

@ -13,85 +13,100 @@ from requests.exceptions import HTTPError, ConnectTimeout
from requests.exceptions import ConnectionError as ConnectError
from homeassistant.const import (
CONF_NAME, CONF_HOST, CONF_PORT, CONF_USERNAME, CONF_PASSWORD,
CONF_SENSORS, CONF_SWITCHES, CONF_SCAN_INTERVAL, HTTP_BASIC_AUTHENTICATION)
CONF_NAME,
CONF_HOST,
CONF_PORT,
CONF_USERNAME,
CONF_PASSWORD,
CONF_SENSORS,
CONF_SWITCHES,
CONF_SCAN_INTERVAL,
HTTP_BASIC_AUTHENTICATION,
)
from homeassistant.helpers import discovery
import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['amcrest==1.2.3']
DEPENDENCIES = ['ffmpeg']
REQUIREMENTS = ["amcrest==1.2.3"]
DEPENDENCIES = ["ffmpeg"]
_LOGGER = logging.getLogger(__name__)
CONF_AUTHENTICATION = 'authentication'
CONF_RESOLUTION = 'resolution'
CONF_STREAM_SOURCE = 'stream_source'
CONF_FFMPEG_ARGUMENTS = 'ffmpeg_arguments'
CONF_AUTHENTICATION = "authentication"
CONF_RESOLUTION = "resolution"
CONF_STREAM_SOURCE = "stream_source"
CONF_FFMPEG_ARGUMENTS = "ffmpeg_arguments"
DEFAULT_NAME = 'Amcrest Camera'
DEFAULT_NAME = "Amcrest Camera"
DEFAULT_PORT = 80
DEFAULT_RESOLUTION = 'high'
DEFAULT_STREAM_SOURCE = 'snapshot'
DEFAULT_RESOLUTION = "high"
DEFAULT_STREAM_SOURCE = "snapshot"
TIMEOUT = 10
DATA_AMCREST = 'amcrest'
DOMAIN = 'amcrest'
DATA_AMCREST = "amcrest"
DOMAIN = "amcrest"
NOTIFICATION_ID = 'amcrest_notification'
NOTIFICATION_TITLE = 'Amcrest Camera Setup'
NOTIFICATION_ID = "amcrest_notification"
NOTIFICATION_TITLE = "Amcrest Camera Setup"
RESOLUTION_LIST = {
'high': 0,
'low': 1,
}
RESOLUTION_LIST = {"high": 0, "low": 1}
SCAN_INTERVAL = timedelta(seconds=10)
AUTHENTICATION_LIST = {
'basic': 'basic'
}
AUTHENTICATION_LIST = {"basic": "basic"}
STREAM_SOURCE_LIST = {
'mjpeg': 0,
'snapshot': 1,
'rtsp': 2,
}
STREAM_SOURCE_LIST = {"mjpeg": 0, "snapshot": 1, "rtsp": 2}
# Sensor types are defined like: Name, units, icon
SENSORS = {
'motion_detector': ['Motion Detected', None, 'mdi:run'],
'sdcard': ['SD Used', '%', 'mdi:sd'],
'ptz_preset': ['PTZ Preset', None, 'mdi:camera-iris'],
"motion_detector": ["Motion Detected", None, "mdi:run"],
"sdcard": ["SD Used", "%", "mdi:sd"],
"ptz_preset": ["PTZ Preset", None, "mdi:camera-iris"],
}
# Switch types are defined like: Name, icon
SWITCHES = {
'motion_detection': ['Motion Detection', 'mdi:run-fast'],
'motion_recording': ['Motion Recording', 'mdi:record-rec']
"motion_detection": ["Motion Detection", "mdi:run-fast"],
"motion_recording": ["Motion Recording", "mdi:record-rec"],
}
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.All(cv.ensure_list, [vol.Schema({
vol.Required(CONF_HOST): cv.string,
vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_PASSWORD): cv.string,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
vol.Optional(CONF_AUTHENTICATION, default=HTTP_BASIC_AUTHENTICATION):
vol.All(vol.In(AUTHENTICATION_LIST)),
vol.Optional(CONF_RESOLUTION, default=DEFAULT_RESOLUTION):
vol.All(vol.In(RESOLUTION_LIST)),
vol.Optional(CONF_STREAM_SOURCE, default=DEFAULT_STREAM_SOURCE):
vol.All(vol.In(STREAM_SOURCE_LIST)),
vol.Optional(CONF_FFMPEG_ARGUMENTS): cv.string,
vol.Optional(CONF_SCAN_INTERVAL, default=SCAN_INTERVAL):
cv.time_period,
vol.Optional(CONF_SENSORS):
vol.All(cv.ensure_list, [vol.In(SENSORS)]),
vol.Optional(CONF_SWITCHES):
vol.All(cv.ensure_list, [vol.In(SWITCHES)]),
})])
}, extra=vol.ALLOW_EXTRA)
CONFIG_SCHEMA = vol.Schema(
{
DOMAIN: vol.All(
cv.ensure_list,
[
vol.Schema(
{
vol.Required(CONF_HOST): cv.string,
vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_PASSWORD): cv.string,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
vol.Optional(
CONF_AUTHENTICATION, default=HTTP_BASIC_AUTHENTICATION
): vol.All(vol.In(AUTHENTICATION_LIST)),
vol.Optional(
CONF_RESOLUTION, default=DEFAULT_RESOLUTION
): vol.All(vol.In(RESOLUTION_LIST)),
vol.Optional(
CONF_STREAM_SOURCE, default=DEFAULT_STREAM_SOURCE
): vol.All(vol.In(STREAM_SOURCE_LIST)),
vol.Optional(CONF_FFMPEG_ARGUMENTS): cv.string,
vol.Optional(
CONF_SCAN_INTERVAL, default=SCAN_INTERVAL
): cv.time_period,
vol.Optional(CONF_SENSORS): vol.All(
cv.ensure_list, [vol.In(SENSORS)]
),
vol.Optional(CONF_SWITCHES): vol.All(
cv.ensure_list, [vol.In(SWITCHES)]
),
}
)
],
)
},
extra=vol.ALLOW_EXTRA,
)
def setup(hass, config):
@ -103,21 +118,24 @@ def setup(hass, config):
for device in amcrest_cams:
try:
camera = AmcrestCamera(device.get(CONF_HOST),
device.get(CONF_PORT),
device.get(CONF_USERNAME),
device.get(CONF_PASSWORD)).camera
camera = AmcrestCamera(
device.get(CONF_HOST),
device.get(CONF_PORT),
device.get(CONF_USERNAME),
device.get(CONF_PASSWORD),
).camera
# pylint: disable=pointless-statement
camera.current_time
except (ConnectError, ConnectTimeout, HTTPError) as ex:
_LOGGER.error("Unable to connect to Amcrest camera: %s", str(ex))
hass.components.persistent_notification.create(
'Error: {}<br />'
'You will need to restart hass after fixing.'
''.format(ex),
"Error: {}<br />"
"You will need to restart hass after fixing."
"".format(ex),
title=NOTIFICATION_TITLE,
notification_id=NOTIFICATION_ID)
notification_id=NOTIFICATION_ID,
)
continue
ffmpeg_arguments = device.get(CONF_FFMPEG_ARGUMENTS)
@ -139,27 +157,24 @@ def setup(hass, config):
authentication = None
hass.data[DATA_AMCREST][name] = AmcrestDevice(
camera, name, authentication, ffmpeg_arguments, stream_source,
resolution)
camera, name, authentication, ffmpeg_arguments, stream_source, resolution
)
discovery.load_platform(
hass, 'camera', DOMAIN, {
CONF_NAME: name,
}, config)
discovery.load_platform(hass, "camera", DOMAIN, {CONF_NAME: name}, config)
if sensors:
discovery.load_platform(
hass, 'sensor', DOMAIN, {
CONF_NAME: name,
CONF_SENSORS: sensors,
}, config)
hass, "sensor", DOMAIN, {CONF_NAME: name, CONF_SENSORS: sensors}, config
)
if switches:
discovery.load_platform(
hass, 'switch', DOMAIN, {
CONF_NAME: name,
CONF_SWITCHES: switches
}, config)
hass,
"switch",
DOMAIN,
{CONF_NAME: name, CONF_SWITCHES: switches},
config,
)
return True
@ -167,8 +182,9 @@ def setup(hass, config):
class AmcrestDevice:
"""Representation of a base Amcrest discovery device."""
def __init__(self, camera, name, authentication, ffmpeg_arguments,
stream_source, resolution):
def __init__(
self, camera, name, authentication, ffmpeg_arguments, stream_source, resolution
):
"""Initialize the entity."""
self.device = camera
self.name = name

View file

@ -12,141 +12,183 @@ import voluptuous as vol
from homeassistant.core import callback
from homeassistant.const import (
CONF_NAME, CONF_HOST, CONF_PORT, CONF_USERNAME, CONF_PASSWORD,
CONF_SENSORS, CONF_SWITCHES, CONF_TIMEOUT, CONF_SCAN_INTERVAL,
CONF_PLATFORM)
CONF_NAME,
CONF_HOST,
CONF_PORT,
CONF_USERNAME,
CONF_PASSWORD,
CONF_SENSORS,
CONF_SWITCHES,
CONF_TIMEOUT,
CONF_SCAN_INTERVAL,
CONF_PLATFORM,
)
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers import discovery
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.dispatcher import (
async_dispatcher_send, async_dispatcher_connect)
async_dispatcher_send,
async_dispatcher_connect,
)
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.event import async_track_point_in_utc_time
from homeassistant.util.dt import utcnow
from homeassistant.components.camera.mjpeg import (
CONF_MJPEG_URL, CONF_STILL_IMAGE_URL)
from homeassistant.components.camera.mjpeg import CONF_MJPEG_URL, CONF_STILL_IMAGE_URL
REQUIREMENTS = ['pydroid-ipcam==0.8']
REQUIREMENTS = ["pydroid-ipcam==0.8"]
_LOGGER = logging.getLogger(__name__)
ATTR_AUD_CONNS = 'Audio Connections'
ATTR_HOST = 'host'
ATTR_VID_CONNS = 'Video Connections'
ATTR_AUD_CONNS = "Audio Connections"
ATTR_HOST = "host"
ATTR_VID_CONNS = "Video Connections"
CONF_MOTION_SENSOR = 'motion_sensor'
CONF_MOTION_SENSOR = "motion_sensor"
DATA_IP_WEBCAM = 'android_ip_webcam'
DEFAULT_NAME = 'IP Webcam'
DATA_IP_WEBCAM = "android_ip_webcam"
DEFAULT_NAME = "IP Webcam"
DEFAULT_PORT = 8080
DEFAULT_TIMEOUT = 10
DOMAIN = 'android_ip_webcam'
DOMAIN = "android_ip_webcam"
SCAN_INTERVAL = timedelta(seconds=10)
SIGNAL_UPDATE_DATA = 'android_ip_webcam_update'
SIGNAL_UPDATE_DATA = "android_ip_webcam_update"
KEY_MAP = {
'audio_connections': 'Audio Connections',
'adet_limit': 'Audio Trigger Limit',
'antibanding': 'Anti-banding',
'audio_only': 'Audio Only',
'battery_level': 'Battery Level',
'battery_temp': 'Battery Temperature',
'battery_voltage': 'Battery Voltage',
'coloreffect': 'Color Effect',
'exposure': 'Exposure Level',
'exposure_lock': 'Exposure Lock',
'ffc': 'Front-facing Camera',
'flashmode': 'Flash Mode',
'focus': 'Focus',
'focus_homing': 'Focus Homing',
'focus_region': 'Focus Region',
'focusmode': 'Focus Mode',
'gps_active': 'GPS Active',
'idle': 'Idle',
'ip_address': 'IPv4 Address',
'ipv6_address': 'IPv6 Address',
'ivideon_streaming': 'Ivideon Streaming',
'light': 'Light Level',
'mirror_flip': 'Mirror Flip',
'motion': 'Motion',
'motion_active': 'Motion Active',
'motion_detect': 'Motion Detection',
'motion_event': 'Motion Event',
'motion_limit': 'Motion Limit',
'night_vision': 'Night Vision',
'night_vision_average': 'Night Vision Average',
'night_vision_gain': 'Night Vision Gain',
'orientation': 'Orientation',
'overlay': 'Overlay',
'photo_size': 'Photo Size',
'pressure': 'Pressure',
'proximity': 'Proximity',
'quality': 'Quality',
'scenemode': 'Scene Mode',
'sound': 'Sound',
'sound_event': 'Sound Event',
'sound_timeout': 'Sound Timeout',
'torch': 'Torch',
'video_connections': 'Video Connections',
'video_chunk_len': 'Video Chunk Length',
'video_recording': 'Video Recording',
'video_size': 'Video Size',
'whitebalance': 'White Balance',
'whitebalance_lock': 'White Balance Lock',
'zoom': 'Zoom'
"audio_connections": "Audio Connections",
"adet_limit": "Audio Trigger Limit",
"antibanding": "Anti-banding",
"audio_only": "Audio Only",
"battery_level": "Battery Level",
"battery_temp": "Battery Temperature",
"battery_voltage": "Battery Voltage",
"coloreffect": "Color Effect",
"exposure": "Exposure Level",
"exposure_lock": "Exposure Lock",
"ffc": "Front-facing Camera",
"flashmode": "Flash Mode",
"focus": "Focus",
"focus_homing": "Focus Homing",
"focus_region": "Focus Region",
"focusmode": "Focus Mode",
"gps_active": "GPS Active",
"idle": "Idle",
"ip_address": "IPv4 Address",
"ipv6_address": "IPv6 Address",
"ivideon_streaming": "Ivideon Streaming",
"light": "Light Level",
"mirror_flip": "Mirror Flip",
"motion": "Motion",
"motion_active": "Motion Active",
"motion_detect": "Motion Detection",
"motion_event": "Motion Event",
"motion_limit": "Motion Limit",
"night_vision": "Night Vision",
"night_vision_average": "Night Vision Average",
"night_vision_gain": "Night Vision Gain",
"orientation": "Orientation",
"overlay": "Overlay",
"photo_size": "Photo Size",
"pressure": "Pressure",
"proximity": "Proximity",
"quality": "Quality",
"scenemode": "Scene Mode",
"sound": "Sound",
"sound_event": "Sound Event",
"sound_timeout": "Sound Timeout",
"torch": "Torch",
"video_connections": "Video Connections",
"video_chunk_len": "Video Chunk Length",
"video_recording": "Video Recording",
"video_size": "Video Size",
"whitebalance": "White Balance",
"whitebalance_lock": "White Balance Lock",
"zoom": "Zoom",
}
ICON_MAP = {
'audio_connections': 'mdi:speaker',
'battery_level': 'mdi:battery',
'battery_temp': 'mdi:thermometer',
'battery_voltage': 'mdi:battery-charging-100',
'exposure_lock': 'mdi:camera',
'ffc': 'mdi:camera-front-variant',
'focus': 'mdi:image-filter-center-focus',
'gps_active': 'mdi:crosshairs-gps',
'light': 'mdi:flashlight',
'motion': 'mdi:run',
'night_vision': 'mdi:weather-night',
'overlay': 'mdi:monitor',
'pressure': 'mdi:gauge',
'proximity': 'mdi:map-marker-radius',
'quality': 'mdi:quality-high',
'sound': 'mdi:speaker',
'sound_event': 'mdi:speaker',
'sound_timeout': 'mdi:speaker',
'torch': 'mdi:white-balance-sunny',
'video_chunk_len': 'mdi:video',
'video_connections': 'mdi:eye',
'video_recording': 'mdi:record-rec',
'whitebalance_lock': 'mdi:white-balance-auto'
"audio_connections": "mdi:speaker",
"battery_level": "mdi:battery",
"battery_temp": "mdi:thermometer",
"battery_voltage": "mdi:battery-charging-100",
"exposure_lock": "mdi:camera",
"ffc": "mdi:camera-front-variant",
"focus": "mdi:image-filter-center-focus",
"gps_active": "mdi:crosshairs-gps",
"light": "mdi:flashlight",
"motion": "mdi:run",
"night_vision": "mdi:weather-night",
"overlay": "mdi:monitor",
"pressure": "mdi:gauge",
"proximity": "mdi:map-marker-radius",
"quality": "mdi:quality-high",
"sound": "mdi:speaker",
"sound_event": "mdi:speaker",
"sound_timeout": "mdi:speaker",
"torch": "mdi:white-balance-sunny",
"video_chunk_len": "mdi:video",
"video_connections": "mdi:eye",
"video_recording": "mdi:record-rec",
"whitebalance_lock": "mdi:white-balance-auto",
}
SWITCHES = ['exposure_lock', 'ffc', 'focus', 'gps_active', 'night_vision',
'overlay', 'torch', 'whitebalance_lock', 'video_recording']
SWITCHES = [
"exposure_lock",
"ffc",
"focus",
"gps_active",
"night_vision",
"overlay",
"torch",
"whitebalance_lock",
"video_recording",
]
SENSORS = ['audio_connections', 'battery_level', 'battery_temp',
'battery_voltage', 'light', 'motion', 'pressure', 'proximity',
'sound', 'video_connections']
SENSORS = [
"audio_connections",
"battery_level",
"battery_temp",
"battery_voltage",
"light",
"motion",
"pressure",
"proximity",
"sound",
"video_connections",
]
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.All(cv.ensure_list, [vol.Schema({
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Required(CONF_HOST): cv.string,
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int,
vol.Optional(CONF_SCAN_INTERVAL, default=SCAN_INTERVAL):
cv.time_period,
vol.Inclusive(CONF_USERNAME, 'authentication'): cv.string,
vol.Inclusive(CONF_PASSWORD, 'authentication'): cv.string,
vol.Optional(CONF_SWITCHES):
vol.All(cv.ensure_list, [vol.In(SWITCHES)]),
vol.Optional(CONF_SENSORS):
vol.All(cv.ensure_list, [vol.In(SENSORS)]),
vol.Optional(CONF_MOTION_SENSOR): cv.boolean,
})])
}, extra=vol.ALLOW_EXTRA)
CONFIG_SCHEMA = vol.Schema(
{
DOMAIN: vol.All(
cv.ensure_list,
[
vol.Schema(
{
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Required(CONF_HOST): cv.string,
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
vol.Optional(
CONF_TIMEOUT, default=DEFAULT_TIMEOUT
): cv.positive_int,
vol.Optional(
CONF_SCAN_INTERVAL, default=SCAN_INTERVAL
): cv.time_period,
vol.Inclusive(CONF_USERNAME, "authentication"): cv.string,
vol.Inclusive(CONF_PASSWORD, "authentication"): cv.string,
vol.Optional(CONF_SWITCHES): vol.All(
cv.ensure_list, [vol.In(SWITCHES)]
),
vol.Optional(CONF_SENSORS): vol.All(
cv.ensure_list, [vol.In(SENSORS)]
),
vol.Optional(CONF_MOTION_SENSOR): cv.boolean,
}
)
],
)
},
extra=vol.ALLOW_EXTRA,
)
@asyncio.coroutine
@ -171,22 +213,26 @@ def async_setup(hass, config):
# Init ip webcam
cam = PyDroidIPCam(
hass.loop, websession, host, cam_config[CONF_PORT],
username=username, password=password,
timeout=cam_config[CONF_TIMEOUT]
hass.loop,
websession,
host,
cam_config[CONF_PORT],
username=username,
password=password,
timeout=cam_config[CONF_TIMEOUT],
)
if switches is None:
switches = [setting for setting in cam.enabled_settings
if setting in SWITCHES]
switches = [
setting for setting in cam.enabled_settings if setting in SWITCHES
]
if sensors is None:
sensors = [sensor for sensor in cam.enabled_sensors
if sensor in SENSORS]
sensors.extend(['audio_connections', 'video_connections'])
sensors = [sensor for sensor in cam.enabled_sensors if sensor in SENSORS]
sensors.extend(["audio_connections", "video_connections"])
if motion is None:
motion = 'motion_active' in cam.enabled_sensors
motion = "motion_active" in cam.enabled_sensors
@asyncio.coroutine
def async_update_data(now):
@ -194,8 +240,7 @@ def async_setup(hass, config):
yield from cam.update()
async_dispatcher_send(hass, SIGNAL_UPDATE_DATA, host)
async_track_point_in_utc_time(
hass, async_update_data, utcnow() + interval)
async_track_point_in_utc_time(hass, async_update_data, utcnow() + interval)
yield from async_update_data(None)
@ -203,42 +248,50 @@ def async_setup(hass, config):
webcams[host] = cam
mjpeg_camera = {
CONF_PLATFORM: 'mjpeg',
CONF_PLATFORM: "mjpeg",
CONF_MJPEG_URL: cam.mjpeg_url,
CONF_STILL_IMAGE_URL: cam.image_url,
CONF_NAME: name,
}
if username and password:
mjpeg_camera.update({
CONF_USERNAME: username,
CONF_PASSWORD: password
})
mjpeg_camera.update({CONF_USERNAME: username, CONF_PASSWORD: password})
hass.async_create_task(discovery.async_load_platform(
hass, 'camera', 'mjpeg', mjpeg_camera, config))
hass.async_create_task(
discovery.async_load_platform(hass, "camera", "mjpeg", mjpeg_camera, config)
)
if sensors:
hass.async_create_task(discovery.async_load_platform(
hass, 'sensor', DOMAIN, {
CONF_NAME: name,
CONF_HOST: host,
CONF_SENSORS: sensors,
}, config))
hass.async_create_task(
discovery.async_load_platform(
hass,
"sensor",
DOMAIN,
{CONF_NAME: name, CONF_HOST: host, CONF_SENSORS: sensors},
config,
)
)
if switches:
hass.async_create_task(discovery.async_load_platform(
hass, 'switch', DOMAIN, {
CONF_NAME: name,
CONF_HOST: host,
CONF_SWITCHES: switches,
}, config))
hass.async_create_task(
discovery.async_load_platform(
hass,
"switch",
DOMAIN,
{CONF_NAME: name, CONF_HOST: host, CONF_SWITCHES: switches},
config,
)
)
if motion:
hass.async_create_task(discovery.async_load_platform(
hass, 'binary_sensor', DOMAIN, {
CONF_HOST: host,
CONF_NAME: name,
}, config))
hass.async_create_task(
discovery.async_load_platform(
hass,
"binary_sensor",
DOMAIN,
{CONF_HOST: host, CONF_NAME: name},
config,
)
)
tasks = [async_setup_ipcamera(conf) for conf in config[DOMAIN]]
if tasks:
@ -258,6 +311,7 @@ class AndroidIPCamEntity(Entity):
@asyncio.coroutine
def async_added_to_hass(self):
"""Register update dispatcher."""
@callback
def async_ipcam_update(host):
"""Update callback."""
@ -265,8 +319,7 @@ class AndroidIPCamEntity(Entity):
return
self.async_schedule_update_ha_state(True)
async_dispatcher_connect(
self.hass, SIGNAL_UPDATE_DATA, async_ipcam_update)
async_dispatcher_connect(self.hass, SIGNAL_UPDATE_DATA, async_ipcam_update)
@property
def should_poll(self):
@ -285,9 +338,7 @@ class AndroidIPCamEntity(Entity):
if self._ipcam.status_data is None:
return state_attr
state_attr[ATTR_VID_CONNS] = \
self._ipcam.status_data.get('video_connections')
state_attr[ATTR_AUD_CONNS] = \
self._ipcam.status_data.get('audio_connections')
state_attr[ATTR_VID_CONNS] = self._ipcam.status_data.get("video_connections")
state_attr[ATTR_AUD_CONNS] = self._ipcam.status_data.get("audio_connections")
return state_attr

View file

@ -9,33 +9,38 @@ from datetime import timedelta
import voluptuous as vol
from homeassistant.const import (CONF_HOST, CONF_PORT)
from homeassistant.const import CONF_HOST, CONF_PORT
import homeassistant.helpers.config_validation as cv
from homeassistant.util import Throttle
REQUIREMENTS = ['apcaccess==0.0.13']
REQUIREMENTS = ["apcaccess==0.0.13"]
_LOGGER = logging.getLogger(__name__)
CONF_TYPE = 'type'
CONF_TYPE = "type"
DATA = None
DEFAULT_HOST = 'localhost'
DEFAULT_HOST = "localhost"
DEFAULT_PORT = 3551
DOMAIN = 'apcupsd'
DOMAIN = "apcupsd"
KEY_STATUS = 'STATUS'
KEY_STATUS = "STATUS"
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60)
VALUE_ONLINE = 'ONLINE'
VALUE_ONLINE = "ONLINE"
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string,
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
}),
}, extra=vol.ALLOW_EXTRA)
CONFIG_SCHEMA = vol.Schema(
{
DOMAIN: vol.Schema(
{
vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string,
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
}
)
},
extra=vol.ALLOW_EXTRA,
)
def setup(hass, config):
@ -68,6 +73,7 @@ class APCUPSdData:
def __init__(self, host, port):
"""Initialize the data object."""
from apcaccess import status
self._host = host
self._port = port
self._status = None

View file

@ -14,11 +14,25 @@ import async_timeout
from homeassistant.bootstrap import DATA_LOGGING
from homeassistant.components.http import HomeAssistantView
from homeassistant.const import (
EVENT_HOMEASSISTANT_STOP, EVENT_TIME_CHANGED, HTTP_BAD_REQUEST,
HTTP_CREATED, HTTP_NOT_FOUND, MATCH_ALL, URL_API, URL_API_COMPONENTS,
URL_API_CONFIG, URL_API_DISCOVERY_INFO, URL_API_ERROR_LOG, URL_API_EVENTS,
URL_API_SERVICES, URL_API_STATES, URL_API_STATES_ENTITY, URL_API_STREAM,
URL_API_TEMPLATE, __version__)
EVENT_HOMEASSISTANT_STOP,
EVENT_TIME_CHANGED,
HTTP_BAD_REQUEST,
HTTP_CREATED,
HTTP_NOT_FOUND,
MATCH_ALL,
URL_API,
URL_API_COMPONENTS,
URL_API_CONFIG,
URL_API_DISCOVERY_INFO,
URL_API_ERROR_LOG,
URL_API_EVENTS,
URL_API_SERVICES,
URL_API_STATES,
URL_API_STATES_ENTITY,
URL_API_STREAM,
URL_API_TEMPLATE,
__version__,
)
import homeassistant.core as ha
from homeassistant.exceptions import TemplateError
from homeassistant.helpers import template
@ -28,15 +42,15 @@ from homeassistant.helpers.json import JSONEncoder
_LOGGER = logging.getLogger(__name__)
ATTR_BASE_URL = 'base_url'
ATTR_LOCATION_NAME = 'location_name'
ATTR_REQUIRES_API_PASSWORD = 'requires_api_password'
ATTR_VERSION = 'version'
ATTR_BASE_URL = "base_url"
ATTR_LOCATION_NAME = "location_name"
ATTR_REQUIRES_API_PASSWORD = "requires_api_password"
ATTR_VERSION = "version"
DOMAIN = 'api'
DEPENDENCIES = ['http']
DOMAIN = "api"
DEPENDENCIES = ["http"]
STREAM_PING_PAYLOAD = 'ping'
STREAM_PING_PAYLOAD = "ping"
STREAM_PING_INTERVAL = 50 # seconds
@ -65,7 +79,7 @@ class APIStatusView(HomeAssistantView):
"""View to handle Status requests."""
url = URL_API
name = 'api:status'
name = "api:status"
@ha.callback
def get(self, request):
@ -77,17 +91,17 @@ class APIEventStream(HomeAssistantView):
"""View to handle EventStream requests."""
url = URL_API_STREAM
name = 'api:stream'
name = "api:stream"
async def get(self, request):
"""Provide a streaming interface for the event bus."""
hass = request.app['hass']
hass = request.app["hass"]
stop_obj = object()
to_write = asyncio.Queue(loop=hass.loop)
restrict = request.query.get('restrict')
restrict = request.query.get("restrict")
if restrict:
restrict = restrict.split(',') + [EVENT_HOMEASSISTANT_STOP]
restrict = restrict.split(",") + [EVENT_HOMEASSISTANT_STOP]
async def forward_events(event):
"""Forward events to the open request."""
@ -107,7 +121,7 @@ class APIEventStream(HomeAssistantView):
await to_write.put(data)
response = web.StreamResponse()
response.content_type = 'text/event-stream'
response.content_type = "text/event-stream"
await response.prepare(request)
unsub_stream = hass.bus.async_listen(MATCH_ALL, forward_events)
@ -120,17 +134,15 @@ class APIEventStream(HomeAssistantView):
while True:
try:
with async_timeout.timeout(STREAM_PING_INTERVAL,
loop=hass.loop):
with async_timeout.timeout(STREAM_PING_INTERVAL, loop=hass.loop):
payload = await to_write.get()
if payload is stop_obj:
break
msg = "data: {}\n\n".format(payload)
_LOGGER.debug(
"STREAM %s WRITING %s", id(stop_obj), msg.strip())
await response.write(msg.encode('UTF-8'))
_LOGGER.debug("STREAM %s WRITING %s", id(stop_obj), msg.strip())
await response.write(msg.encode("UTF-8"))
except asyncio.TimeoutError:
await to_write.put(STREAM_PING_PAYLOAD)
@ -146,12 +158,12 @@ class APIConfigView(HomeAssistantView):
"""View to handle Configuration requests."""
url = URL_API_CONFIG
name = 'api:config'
name = "api:config"
@ha.callback
def get(self, request):
"""Get current configuration."""
return self.json(request.app['hass'].config.as_dict())
return self.json(request.app["hass"].config.as_dict())
class APIDiscoveryView(HomeAssistantView):
@ -159,19 +171,21 @@ class APIDiscoveryView(HomeAssistantView):
requires_auth = False
url = URL_API_DISCOVERY_INFO
name = 'api:discovery'
name = "api:discovery"
@ha.callback
def get(self, request):
"""Get discovery information."""
hass = request.app['hass']
hass = request.app["hass"]
needs_auth = hass.config.api.api_password is not None
return self.json({
ATTR_BASE_URL: hass.config.api.base_url,
ATTR_LOCATION_NAME: hass.config.location_name,
ATTR_REQUIRES_API_PASSWORD: needs_auth,
ATTR_VERSION: __version__,
})
return self.json(
{
ATTR_BASE_URL: hass.config.api.base_url,
ATTR_LOCATION_NAME: hass.config.location_name,
ATTR_REQUIRES_API_PASSWORD: needs_auth,
ATTR_VERSION: __version__,
}
)
class APIStatesView(HomeAssistantView):
@ -183,58 +197,58 @@ class APIStatesView(HomeAssistantView):
@ha.callback
def get(self, request):
"""Get current states."""
return self.json(request.app['hass'].states.async_all())
return self.json(request.app["hass"].states.async_all())
class APIEntityStateView(HomeAssistantView):
"""View to handle EntityState requests."""
url = '/api/states/{entity_id}'
name = 'api:entity-state'
url = "/api/states/{entity_id}"
name = "api:entity-state"
@ha.callback
def get(self, request, entity_id):
"""Retrieve state of entity."""
state = request.app['hass'].states.get(entity_id)
state = request.app["hass"].states.get(entity_id)
if state:
return self.json(state)
return self.json_message("Entity not found.", HTTP_NOT_FOUND)
async def post(self, request, entity_id):
"""Update state of entity."""
hass = request.app['hass']
hass = request.app["hass"]
try:
data = await request.json()
except ValueError:
return self.json_message(
"Invalid JSON specified.", HTTP_BAD_REQUEST)
return self.json_message("Invalid JSON specified.", HTTP_BAD_REQUEST)
new_state = data.get('state')
new_state = data.get("state")
if new_state is None:
return self.json_message("No state specified.", HTTP_BAD_REQUEST)
attributes = data.get('attributes')
force_update = data.get('force_update', False)
attributes = data.get("attributes")
force_update = data.get("force_update", False)
is_new_state = hass.states.get(entity_id) is None
# Write state
hass.states.async_set(entity_id, new_state, attributes, force_update,
self.context(request))
hass.states.async_set(
entity_id, new_state, attributes, force_update, self.context(request)
)
# Read the state back for our response
status_code = HTTP_CREATED if is_new_state else 200
resp = self.json(hass.states.get(entity_id), status_code)
resp.headers.add('Location', URL_API_STATES_ENTITY.format(entity_id))
resp.headers.add("Location", URL_API_STATES_ENTITY.format(entity_id))
return resp
@ha.callback
def delete(self, request, entity_id):
"""Remove entity."""
if request.app['hass'].states.async_remove(entity_id):
if request.app["hass"].states.async_remove(entity_id):
return self.json_message("Entity removed.")
return self.json_message("Entity not found.", HTTP_NOT_FOUND)
@ -243,19 +257,19 @@ class APIEventListenersView(HomeAssistantView):
"""View to handle EventListeners requests."""
url = URL_API_EVENTS
name = 'api:event-listeners'
name = "api:event-listeners"
@ha.callback
def get(self, request):
"""Get event listeners."""
return self.json(async_events_json(request.app['hass']))
return self.json(async_events_json(request.app["hass"]))
class APIEventView(HomeAssistantView):
"""View to handle Event requests."""
url = '/api/events/{event_type}'
name = 'api:event'
url = "/api/events/{event_type}"
name = "api:event"
async def post(self, request, event_type):
"""Fire events."""
@ -264,24 +278,26 @@ class APIEventView(HomeAssistantView):
event_data = json.loads(body) if body else None
except ValueError:
return self.json_message(
"Event data should be valid JSON.", HTTP_BAD_REQUEST)
"Event data should be valid JSON.", HTTP_BAD_REQUEST
)
if event_data is not None and not isinstance(event_data, dict):
return self.json_message(
"Event data should be a JSON object", HTTP_BAD_REQUEST)
"Event data should be a JSON object", HTTP_BAD_REQUEST
)
# Special case handling for event STATE_CHANGED
# We will try to convert state dicts back to State objects
if event_type == ha.EVENT_STATE_CHANGED and event_data:
for key in ('old_state', 'new_state'):
for key in ("old_state", "new_state"):
state = ha.State.from_dict(event_data.get(key))
if state:
event_data[key] = state
request.app['hass'].bus.async_fire(
event_type, event_data, ha.EventOrigin.remote,
self.context(request))
request.app["hass"].bus.async_fire(
event_type, event_data, ha.EventOrigin.remote, self.context(request)
)
return self.json_message("Event {} fired.".format(event_type))
@ -290,36 +306,36 @@ class APIServicesView(HomeAssistantView):
"""View to handle Services requests."""
url = URL_API_SERVICES
name = 'api:services'
name = "api:services"
async def get(self, request):
"""Get registered services."""
services = await async_services_json(request.app['hass'])
services = await async_services_json(request.app["hass"])
return self.json(services)
class APIDomainServicesView(HomeAssistantView):
"""View to handle DomainServices requests."""
url = '/api/services/{domain}/{service}'
name = 'api:domain-services'
url = "/api/services/{domain}/{service}"
name = "api:domain-services"
async def post(self, request, domain, service):
"""Call a service.
Returns a list of changed states.
"""
hass = request.app['hass']
hass = request.app["hass"]
body = await request.text()
try:
data = json.loads(body) if body else None
except ValueError:
return self.json_message(
"Data should be valid JSON.", HTTP_BAD_REQUEST)
return self.json_message("Data should be valid JSON.", HTTP_BAD_REQUEST)
with AsyncTrackStates(hass) as changed_states:
await hass.services.async_call(
domain, service, data, True, self.context(request))
domain, service, data, True, self.context(request)
)
return self.json(changed_states)
@ -328,50 +344,52 @@ class APIComponentsView(HomeAssistantView):
"""View to handle Components requests."""
url = URL_API_COMPONENTS
name = 'api:components'
name = "api:components"
@ha.callback
def get(self, request):
"""Get current loaded components."""
return self.json(request.app['hass'].config.components)
return self.json(request.app["hass"].config.components)
class APITemplateView(HomeAssistantView):
"""View to handle Template requests."""
url = URL_API_TEMPLATE
name = 'api:template'
name = "api:template"
async def post(self, request):
"""Render a template."""
try:
data = await request.json()
tpl = template.Template(data['template'], request.app['hass'])
return tpl.async_render(data.get('variables'))
tpl = template.Template(data["template"], request.app["hass"])
return tpl.async_render(data.get("variables"))
except (ValueError, TemplateError) as ex:
return self.json_message(
"Error rendering template: {}".format(ex), HTTP_BAD_REQUEST)
"Error rendering template: {}".format(ex), HTTP_BAD_REQUEST
)
class APIErrorLog(HomeAssistantView):
"""View to fetch the API error log."""
url = URL_API_ERROR_LOG
name = 'api:error_log'
name = "api:error_log"
async def get(self, request):
"""Retrieve API error log."""
return web.FileResponse(request.app['hass'].data[DATA_LOGGING])
return web.FileResponse(request.app["hass"].data[DATA_LOGGING])
async def async_services_json(hass):
"""Generate services data to JSONify."""
descriptions = await async_get_all_descriptions(hass)
return [{'domain': key, 'services': value}
for key, value in descriptions.items()]
return [{"domain": key, "services": value} for key, value in descriptions.items()]
def async_events_json(hass):
"""Generate event data to JSONify."""
return [{'event': key, 'listener_count': value}
for key, value in hass.bus.async_listeners().items()]
return [
{"event": key, "listener_count": value}
for key, value in hass.bus.async_listeners().items()
]

View file

@ -16,35 +16,35 @@ from homeassistant.helpers import discovery
from homeassistant.helpers.aiohttp_client import async_get_clientsession
import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['pyatv==0.3.10']
REQUIREMENTS = ["pyatv==0.3.10"]
_LOGGER = logging.getLogger(__name__)
DOMAIN = 'apple_tv'
DOMAIN = "apple_tv"
SERVICE_SCAN = 'apple_tv_scan'
SERVICE_AUTHENTICATE = 'apple_tv_authenticate'
SERVICE_SCAN = "apple_tv_scan"
SERVICE_AUTHENTICATE = "apple_tv_authenticate"
ATTR_ATV = 'atv'
ATTR_POWER = 'power'
ATTR_ATV = "atv"
ATTR_POWER = "power"
CONF_LOGIN_ID = 'login_id'
CONF_START_OFF = 'start_off'
CONF_CREDENTIALS = 'credentials'
CONF_LOGIN_ID = "login_id"
CONF_START_OFF = "start_off"
CONF_CREDENTIALS = "credentials"
DEFAULT_NAME = 'Apple TV'
DEFAULT_NAME = "Apple TV"
DATA_APPLE_TV = 'data_apple_tv'
DATA_ENTITIES = 'data_apple_tv_entities'
DATA_APPLE_TV = "data_apple_tv"
DATA_ENTITIES = "data_apple_tv_entities"
KEY_CONFIG = 'apple_tv_configuring'
KEY_CONFIG = "apple_tv_configuring"
NOTIFICATION_AUTH_ID = 'apple_tv_auth_notification'
NOTIFICATION_AUTH_TITLE = 'Apple TV Authentication'
NOTIFICATION_SCAN_ID = 'apple_tv_scan_notification'
NOTIFICATION_SCAN_TITLE = 'Apple TV Scan'
NOTIFICATION_AUTH_ID = "apple_tv_auth_notification"
NOTIFICATION_AUTH_TITLE = "Apple TV Authentication"
NOTIFICATION_SCAN_ID = "apple_tv_scan_notification"
NOTIFICATION_SCAN_TITLE = "Apple TV Scan"
T = TypeVar('T') # pylint: disable=invalid-name
T = TypeVar("T") # pylint: disable=invalid-name
# This version of ensure_list interprets an empty dict as no value
@ -55,22 +55,30 @@ def ensure_list(value: Union[T, Sequence[T]]) -> Sequence[T]:
return value if isinstance(value, list) else [value]
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.All(ensure_list, [vol.Schema({
vol.Required(CONF_HOST): cv.string,
vol.Required(CONF_LOGIN_ID): cv.string,
vol.Optional(CONF_CREDENTIALS): cv.string,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_START_OFF, default=False): cv.boolean,
})])
}, extra=vol.ALLOW_EXTRA)
CONFIG_SCHEMA = vol.Schema(
{
DOMAIN: vol.All(
ensure_list,
[
vol.Schema(
{
vol.Required(CONF_HOST): cv.string,
vol.Required(CONF_LOGIN_ID): cv.string,
vol.Optional(CONF_CREDENTIALS): cv.string,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_START_OFF, default=False): cv.boolean,
}
)
],
)
},
extra=vol.ALLOW_EXTRA,
)
# Currently no attributes but it might change later
APPLE_TV_SCAN_SCHEMA = vol.Schema({})
APPLE_TV_AUTHENTICATE_SCHEMA = vol.Schema({
ATTR_ENTITY_ID: cv.entity_ids,
})
APPLE_TV_AUTHENTICATE_SCHEMA = vol.Schema({ATTR_ENTITY_ID: cv.entity_ids})
def request_configuration(hass, config, atv, credentials):
@ -81,30 +89,34 @@ def request_configuration(hass, config, atv, credentials):
def configuration_callback(callback_data):
"""Handle the submitted configuration."""
from pyatv import exceptions
pin = callback_data.get('pin')
pin = callback_data.get("pin")
try:
yield from atv.airplay.finish_authentication(pin)
hass.components.persistent_notification.async_create(
'Authentication succeeded!<br /><br />Add the following '
'to credentials: in your apple_tv configuration:<br /><br />'
'{0}'.format(credentials),
"Authentication succeeded!<br /><br />Add the following "
"to credentials: in your apple_tv configuration:<br /><br />"
"{0}".format(credentials),
title=NOTIFICATION_AUTH_TITLE,
notification_id=NOTIFICATION_AUTH_ID)
notification_id=NOTIFICATION_AUTH_ID,
)
except exceptions.DeviceAuthenticationError as ex:
hass.components.persistent_notification.async_create(
'Authentication failed! Did you enter correct PIN?<br /><br />'
'Details: {0}'.format(ex),
"Authentication failed! Did you enter correct PIN?<br /><br />"
"Details: {0}".format(ex),
title=NOTIFICATION_AUTH_TITLE,
notification_id=NOTIFICATION_AUTH_ID)
notification_id=NOTIFICATION_AUTH_ID,
)
hass.async_add_job(configurator.request_done, instance)
instance = configurator.request_config(
'Apple TV Authentication', configuration_callback,
description='Please enter PIN code shown on screen.',
submit_caption='Confirm',
fields=[{'id': 'pin', 'name': 'PIN Code', 'type': 'password'}]
"Apple TV Authentication",
configuration_callback,
description="Please enter PIN code shown on screen.",
submit_caption="Confirm",
fields=[{"id": "pin", "name": "PIN Code", "type": "password"}],
)
@ -112,24 +124,28 @@ def request_configuration(hass, config, atv, credentials):
def scan_for_apple_tvs(hass):
"""Scan for devices and present a notification of the ones found."""
import pyatv
atvs = yield from pyatv.scan_for_apple_tvs(hass.loop, timeout=3)
devices = []
for atv in atvs:
login_id = atv.login_id
if login_id is None:
login_id = 'Home Sharing disabled'
devices.append('Name: {0}<br />Host: {1}<br />Login ID: {2}'.format(
atv.name, atv.address, login_id))
login_id = "Home Sharing disabled"
devices.append(
"Name: {0}<br />Host: {1}<br />Login ID: {2}".format(
atv.name, atv.address, login_id
)
)
if not devices:
devices = ['No device(s) found']
devices = ["No device(s) found"]
hass.components.persistent_notification.async_create(
'The following devices were found:<br /><br />' +
'<br /><br />'.join(devices),
"The following devices were found:<br /><br />" + "<br /><br />".join(devices),
title=NOTIFICATION_SCAN_TITLE,
notification_id=NOTIFICATION_SCAN_ID)
notification_id=NOTIFICATION_SCAN_ID,
)
@asyncio.coroutine
@ -148,8 +164,11 @@ def async_setup(hass, config):
return
if entity_ids:
devices = [device for device in hass.data[DATA_ENTITIES]
if device.entity_id in entity_ids]
devices = [
device
for device in hass.data[DATA_ENTITIES]
if device.entity_id in entity_ids
]
else:
devices = hass.data[DATA_ENTITIES]
@ -160,20 +179,22 @@ def async_setup(hass, config):
atv = device.atv
credentials = yield from atv.airplay.generate_credentials()
yield from atv.airplay.load_credentials(credentials)
_LOGGER.debug('Generated new credentials: %s', credentials)
_LOGGER.debug("Generated new credentials: %s", credentials)
yield from atv.airplay.start_authentication()
hass.async_add_job(request_configuration,
hass, config, atv, credentials)
hass.async_add_job(request_configuration, hass, config, atv, credentials)
@asyncio.coroutine
def atv_discovered(service, info):
"""Set up an Apple TV that was auto discovered."""
yield from _setup_atv(hass, {
CONF_NAME: info['name'],
CONF_HOST: info['host'],
CONF_LOGIN_ID: info['properties']['hG'],
CONF_START_OFF: False
})
yield from _setup_atv(
hass,
{
CONF_NAME: info["name"],
CONF_HOST: info["host"],
CONF_LOGIN_ID: info["properties"]["hG"],
CONF_START_OFF: False,
},
)
discovery.async_listen(hass, SERVICE_APPLE_TV, atv_discovered)
@ -182,12 +203,15 @@ def async_setup(hass, config):
yield from asyncio.wait(tasks, loop=hass.loop)
hass.services.async_register(
DOMAIN, SERVICE_SCAN, async_service_handler,
schema=APPLE_TV_SCAN_SCHEMA)
DOMAIN, SERVICE_SCAN, async_service_handler, schema=APPLE_TV_SCAN_SCHEMA
)
hass.services.async_register(
DOMAIN, SERVICE_AUTHENTICATE, async_service_handler,
schema=APPLE_TV_AUTHENTICATE_SCHEMA)
DOMAIN,
SERVICE_AUTHENTICATE,
async_service_handler,
schema=APPLE_TV_AUTHENTICATE_SCHEMA,
)
return True
@ -196,6 +220,7 @@ def async_setup(hass, config):
def _setup_atv(hass, atv_config):
"""Set up an Apple TV."""
import pyatv
name = atv_config.get(CONF_NAME)
host = atv_config.get(CONF_HOST)
login_id = atv_config.get(CONF_LOGIN_ID)
@ -212,16 +237,15 @@ def _setup_atv(hass, atv_config):
yield from atv.airplay.load_credentials(credentials)
power = AppleTVPowerManager(hass, atv, start_off)
hass.data[DATA_APPLE_TV][host] = {
ATTR_ATV: atv,
ATTR_POWER: power
}
hass.data[DATA_APPLE_TV][host] = {ATTR_ATV: atv, ATTR_POWER: power}
hass.async_create_task(discovery.async_load_platform(
hass, 'media_player', DOMAIN, atv_config))
hass.async_create_task(
discovery.async_load_platform(hass, "media_player", DOMAIN, atv_config)
)
hass.async_create_task(discovery.async_load_platform(
hass, 'remote', DOMAIN, atv_config))
hass.async_create_task(
discovery.async_load_platform(hass, "remote", DOMAIN, atv_config)
)
class AppleTVPowerManager:

View file

@ -8,24 +8,21 @@ import logging
import voluptuous as vol
from homeassistant.const import (
EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP)
from homeassistant.const import EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP
from homeassistant.const import CONF_PORT
import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['PyMata==2.14']
REQUIREMENTS = ["PyMata==2.14"]
_LOGGER = logging.getLogger(__name__)
BOARD = None
DOMAIN = 'arduino'
DOMAIN = "arduino"
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
vol.Required(CONF_PORT): cv.string,
}),
}, extra=vol.ALLOW_EXTRA)
CONFIG_SCHEMA = vol.Schema(
{DOMAIN: vol.Schema({vol.Required(CONF_PORT): cv.string})}, extra=vol.ALLOW_EXTRA
)
def setup(hass, config):
@ -46,8 +43,10 @@ def setup(hass, config):
_LOGGER.error("The StandardFirmata sketch should be 2.2 or newer")
return False
except IndexError:
_LOGGER.warning("The version of the StandardFirmata sketch was not"
"detected. This may lead to side effects")
_LOGGER.warning(
"The version of the StandardFirmata sketch was not"
"detected. This may lead to side effects"
)
def stop_arduino(event):
"""Stop the Arduino service."""
@ -68,26 +67,22 @@ class ArduinoBoard:
def __init__(self, port):
"""Initialize the board."""
from PyMata.pymata import PyMata
self._port = port
self._board = PyMata(self._port, verbose=False)
def set_mode(self, pin, direction, mode):
"""Set the mode and the direction of a given pin."""
if mode == 'analog' and direction == 'in':
self._board.set_pin_mode(
pin, self._board.INPUT, self._board.ANALOG)
elif mode == 'analog' and direction == 'out':
self._board.set_pin_mode(
pin, self._board.OUTPUT, self._board.ANALOG)
elif mode == 'digital' and direction == 'in':
self._board.set_pin_mode(
pin, self._board.INPUT, self._board.DIGITAL)
elif mode == 'digital' and direction == 'out':
self._board.set_pin_mode(
pin, self._board.OUTPUT, self._board.DIGITAL)
elif mode == 'pwm':
self._board.set_pin_mode(
pin, self._board.OUTPUT, self._board.PWM)
if mode == "analog" and direction == "in":
self._board.set_pin_mode(pin, self._board.INPUT, self._board.ANALOG)
elif mode == "analog" and direction == "out":
self._board.set_pin_mode(pin, self._board.OUTPUT, self._board.ANALOG)
elif mode == "digital" and direction == "in":
self._board.set_pin_mode(pin, self._board.INPUT, self._board.DIGITAL)
elif mode == "digital" and direction == "out":
self._board.set_pin_mode(pin, self._board.OUTPUT, self._board.DIGITAL)
elif mode == "pwm":
self._board.set_pin_mode(pin, self._board.OUTPUT, self._board.PWM)
def get_analog_inputs(self):
"""Get the values from the pins."""

View file

@ -11,36 +11,39 @@ import voluptuous as vol
from requests.exceptions import HTTPError, ConnectTimeout
from homeassistant.helpers import config_validation as cv
from homeassistant.const import (
CONF_USERNAME, CONF_PASSWORD, CONF_SCAN_INTERVAL)
from homeassistant.const import CONF_USERNAME, CONF_PASSWORD, CONF_SCAN_INTERVAL
from homeassistant.helpers.event import track_time_interval
from homeassistant.helpers.dispatcher import dispatcher_send
REQUIREMENTS = ['pyarlo==0.2.0']
REQUIREMENTS = ["pyarlo==0.2.0"]
_LOGGER = logging.getLogger(__name__)
CONF_ATTRIBUTION = "Data provided by arlo.netgear.com"
DATA_ARLO = 'data_arlo'
DEFAULT_BRAND = 'Netgear Arlo'
DOMAIN = 'arlo'
DATA_ARLO = "data_arlo"
DEFAULT_BRAND = "Netgear Arlo"
DOMAIN = "arlo"
NOTIFICATION_ID = 'arlo_notification'
NOTIFICATION_TITLE = 'Arlo Component Setup'
NOTIFICATION_ID = "arlo_notification"
NOTIFICATION_TITLE = "Arlo Component Setup"
SCAN_INTERVAL = timedelta(seconds=60)
SIGNAL_UPDATE_ARLO = "arlo_update"
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_PASSWORD): cv.string,
vol.Optional(CONF_SCAN_INTERVAL, default=SCAN_INTERVAL):
cv.time_period,
}),
}, extra=vol.ALLOW_EXTRA)
CONFIG_SCHEMA = vol.Schema(
{
DOMAIN: vol.Schema(
{
vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_PASSWORD): cv.string,
vol.Optional(CONF_SCAN_INTERVAL, default=SCAN_INTERVAL): cv.time_period,
}
)
},
extra=vol.ALLOW_EXTRA,
)
def setup(hass, config):
@ -58,8 +61,7 @@ def setup(hass, config):
return False
# assign refresh period to base station thread
arlo_base_station = next((
station for station in arlo.base_stations), None)
arlo_base_station = next((station for station in arlo.base_stations), None)
if arlo_base_station is not None:
arlo_base_station.refresh_rate = scan_interval.total_seconds()
@ -72,22 +74,22 @@ def setup(hass, config):
except (ConnectTimeout, HTTPError) as ex:
_LOGGER.error("Unable to connect to Netgear Arlo: %s", str(ex))
hass.components.persistent_notification.create(
'Error: {}<br />'
'You will need to restart hass after fixing.'
''.format(ex),
"Error: {}<br />"
"You will need to restart hass after fixing."
"".format(ex),
title=NOTIFICATION_TITLE,
notification_id=NOTIFICATION_ID)
notification_id=NOTIFICATION_ID,
)
return False
def hub_refresh(event_time):
"""Call ArloHub to refresh information."""
_LOGGER.info("Updating Arlo Hub component")
hass.data[DATA_ARLO].update(update_cameras=True,
update_base_station=True)
hass.data[DATA_ARLO].update(update_cameras=True, update_base_station=True)
dispatcher_send(hass, SIGNAL_UPDATE_ARLO)
# register service
hass.services.register(DOMAIN, 'update', hub_refresh)
hass.services.register(DOMAIN, "update", hub_refresh)
# register scan interval for ArloHub
track_time_interval(hass, hub_refresh, scan_interval)

View file

@ -13,24 +13,31 @@ 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, async_dispatcher_send)
async_dispatcher_connect,
async_dispatcher_send,
)
REQUIREMENTS = ['asterisk_mbox==0.5.0']
REQUIREMENTS = ["asterisk_mbox==0.5.0"]
_LOGGER = logging.getLogger(__name__)
DOMAIN = 'asterisk_mbox'
DOMAIN = "asterisk_mbox"
SIGNAL_MESSAGE_REQUEST = 'asterisk_mbox.message_request'
SIGNAL_MESSAGE_UPDATE = 'asterisk_mbox.message_updated'
SIGNAL_MESSAGE_REQUEST = "asterisk_mbox.message_request"
SIGNAL_MESSAGE_UPDATE = "asterisk_mbox.message_updated"
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
vol.Required(CONF_HOST): cv.string,
vol.Required(CONF_PASSWORD): cv.string,
vol.Required(CONF_PORT): int,
}),
}, extra=vol.ALLOW_EXTRA)
CONFIG_SCHEMA = vol.Schema(
{
DOMAIN: vol.Schema(
{
vol.Required(CONF_HOST): cv.string,
vol.Required(CONF_PASSWORD): cv.string,
vol.Required(CONF_PORT): int,
}
)
},
extra=vol.ALLOW_EXTRA,
)
def setup(hass, config):
@ -43,7 +50,7 @@ def setup(hass, config):
hass.data[DOMAIN] = AsteriskData(hass, host, port, password)
discovery.load_platform(hass, 'mailbox', DOMAIN, {}, config)
discovery.load_platform(hass, "mailbox", DOMAIN, {}, config)
return True
@ -60,7 +67,8 @@ class AsteriskData:
self.messages = []
async_dispatcher_connect(
self.hass, SIGNAL_MESSAGE_REQUEST, self._request_messages)
self.hass, SIGNAL_MESSAGE_REQUEST, self._request_messages
)
@callback
def handle_data(self, command, msg):
@ -70,9 +78,9 @@ class AsteriskData:
if command == CMD_MESSAGE_LIST:
_LOGGER.debug("AsteriskVM sent updated message list")
self.messages = sorted(
msg, key=lambda item: item['info']['origtime'], reverse=True)
async_dispatcher_send(
self.hass, SIGNAL_MESSAGE_UPDATE, self.messages)
msg, key=lambda item: item["info"]["origtime"], reverse=True
)
async_dispatcher_send(self.hass, SIGNAL_MESSAGE_UPDATE, self.messages)
@callback
def _request_messages(self):

View file

@ -12,8 +12,7 @@ import voluptuous as vol
from requests import RequestException
import homeassistant.helpers.config_validation as cv
from homeassistant.const import (
CONF_PASSWORD, CONF_USERNAME, CONF_TIMEOUT)
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, CONF_TIMEOUT
from homeassistant.helpers import discovery
from homeassistant.util import Throttle
@ -21,40 +20,43 @@ _LOGGER = logging.getLogger(__name__)
_CONFIGURING = {}
REQUIREMENTS = ['py-august==0.6.0']
REQUIREMENTS = ["py-august==0.6.0"]
DEFAULT_TIMEOUT = 10
ACTIVITY_FETCH_LIMIT = 10
ACTIVITY_INITIAL_FETCH_LIMIT = 20
CONF_LOGIN_METHOD = 'login_method'
CONF_INSTALL_ID = 'install_id'
CONF_LOGIN_METHOD = "login_method"
CONF_INSTALL_ID = "install_id"
NOTIFICATION_ID = 'august_notification'
NOTIFICATION_ID = "august_notification"
NOTIFICATION_TITLE = "August Setup"
AUGUST_CONFIG_FILE = '.august.conf'
AUGUST_CONFIG_FILE = ".august.conf"
DATA_AUGUST = 'august'
DOMAIN = 'august'
DEFAULT_ENTITY_NAMESPACE = 'august'
DATA_AUGUST = "august"
DOMAIN = "august"
DEFAULT_ENTITY_NAMESPACE = "august"
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=5)
DEFAULT_SCAN_INTERVAL = timedelta(seconds=5)
LOGIN_METHODS = ['phone', 'email']
LOGIN_METHODS = ["phone", "email"]
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
vol.Required(CONF_LOGIN_METHOD): vol.In(LOGIN_METHODS),
vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_PASSWORD): cv.string,
vol.Optional(CONF_INSTALL_ID): cv.string,
vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int,
})
}, extra=vol.ALLOW_EXTRA)
CONFIG_SCHEMA = vol.Schema(
{
DOMAIN: vol.Schema(
{
vol.Required(CONF_LOGIN_METHOD): vol.In(LOGIN_METHODS),
vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_PASSWORD): cv.string,
vol.Optional(CONF_INSTALL_ID): cv.string,
vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int,
}
)
},
extra=vol.ALLOW_EXTRA,
)
AUGUST_COMPONENTS = [
'camera', 'binary_sensor', 'lock'
]
AUGUST_COMPONENTS = ["camera", "binary_sensor", "lock"]
def request_configuration(hass, config, api, authenticator):
@ -65,12 +67,12 @@ def request_configuration(hass, config, api, authenticator):
"""Run when the configuration callback is called."""
from august.authenticator import ValidationResult
result = authenticator.validate_verification_code(
data.get('verification_code'))
result = authenticator.validate_verification_code(data.get("verification_code"))
if result == ValidationResult.INVALID_VERIFICATION_CODE:
configurator.notify_errors(_CONFIGURING[DOMAIN],
"Invalid verification code")
configurator.notify_errors(
_CONFIGURING[DOMAIN], "Invalid verification code"
)
elif result == ValidationResult.VALIDATED:
setup_august(hass, config, api, authenticator)
@ -85,12 +87,11 @@ def request_configuration(hass, config, api, authenticator):
NOTIFICATION_TITLE,
august_configuration_callback,
description="Please check your {} ({}) and enter the verification "
"code below".format(login_method, username),
submit_caption='Verify',
fields=[{
'id': 'verification_code',
'name': "Verification code",
'type': 'string'}]
"code below".format(login_method, username),
submit_caption="Verify",
fields=[
{"id": "verification_code", "name": "Verification code", "type": "string"}
],
)
@ -109,7 +110,8 @@ def setup_august(hass, config, api, authenticator):
"You will need to restart hass after fixing."
"".format(ex),
title=NOTIFICATION_TITLE,
notification_id=NOTIFICATION_ID)
notification_id=NOTIFICATION_ID,
)
state = authentication.state
@ -146,7 +148,8 @@ def setup(hass, config):
conf.get(CONF_USERNAME),
conf.get(CONF_PASSWORD),
install_id=conf.get(CONF_INSTALL_ID),
access_token_cache_file=hass.config.path(AUGUST_CONFIG_FILE))
access_token_cache_file=hass.config.path(AUGUST_CONFIG_FILE),
)
return setup_august(hass, config, api, authenticator)
@ -200,14 +203,15 @@ class AugustData:
def _update_device_activities(self, limit=ACTIVITY_FETCH_LIMIT):
"""Update data object with latest from August API."""
for house_id in self.house_ids:
activities = self._api.get_house_activities(self._access_token,
house_id,
limit=limit)
activities = self._api.get_house_activities(
self._access_token, house_id, limit=limit
)
device_ids = {a.device_id for a in activities}
for device_id in device_ids:
self._activities_by_id[device_id] = [a for a in activities if
a.device_id == device_id]
self._activities_by_id[device_id] = [
a for a in activities if a.device_id == device_id
]
def get_doorbell_detail(self, doorbell_id):
"""Return doorbell detail."""
@ -220,7 +224,8 @@ class AugustData:
for doorbell in self._doorbells:
detail_by_id[doorbell.device_id] = self._api.get_doorbell_detail(
self._access_token, doorbell.device_id)
self._access_token, doorbell.device_id
)
self._doorbell_detail_by_id = detail_by_id
@ -241,9 +246,11 @@ class AugustData:
for lock in self._locks:
status_by_id[lock.device_id] = self._api.get_lock_status(
self._access_token, lock.device_id)
self._access_token, lock.device_id
)
detail_by_id[lock.device_id] = self._api.get_lock_detail(
self._access_token, lock.device_id)
self._access_token, lock.device_id
)
self._lock_status_by_id = status_by_id
self._lock_detail_by_id = detail_by_id

View file

@ -126,8 +126,11 @@ from datetime import timedelta
from aiohttp import web
import voluptuous as vol
from homeassistant.auth.models import User, Credentials, \
TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN
from homeassistant.auth.models import (
User,
Credentials,
TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN,
)
from homeassistant.components import websocket_api
from homeassistant.components.http import KEY_REAL_IP
from homeassistant.components.http.ban import log_invalid_auth
@ -140,38 +143,39 @@ from . import indieauth
from . import login_flow
from . import mfa_setup_flow
DOMAIN = 'auth'
DEPENDENCIES = ['http']
DOMAIN = "auth"
DEPENDENCIES = ["http"]
WS_TYPE_CURRENT_USER = 'auth/current_user'
SCHEMA_WS_CURRENT_USER = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
vol.Required('type'): WS_TYPE_CURRENT_USER,
})
WS_TYPE_CURRENT_USER = "auth/current_user"
SCHEMA_WS_CURRENT_USER = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend(
{vol.Required("type"): WS_TYPE_CURRENT_USER}
)
WS_TYPE_LONG_LIVED_ACCESS_TOKEN = 'auth/long_lived_access_token'
SCHEMA_WS_LONG_LIVED_ACCESS_TOKEN = \
websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
vol.Required('type'): WS_TYPE_LONG_LIVED_ACCESS_TOKEN,
vol.Required('lifespan'): int, # days
vol.Required('client_name'): str,
vol.Optional('client_icon'): str,
})
WS_TYPE_LONG_LIVED_ACCESS_TOKEN = "auth/long_lived_access_token"
SCHEMA_WS_LONG_LIVED_ACCESS_TOKEN = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend(
{
vol.Required("type"): WS_TYPE_LONG_LIVED_ACCESS_TOKEN,
vol.Required("lifespan"): int, # days
vol.Required("client_name"): str,
vol.Optional("client_icon"): str,
}
)
WS_TYPE_REFRESH_TOKENS = 'auth/refresh_tokens'
SCHEMA_WS_REFRESH_TOKENS = \
websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
vol.Required('type'): WS_TYPE_REFRESH_TOKENS,
})
WS_TYPE_REFRESH_TOKENS = "auth/refresh_tokens"
SCHEMA_WS_REFRESH_TOKENS = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend(
{vol.Required("type"): WS_TYPE_REFRESH_TOKENS}
)
WS_TYPE_DELETE_REFRESH_TOKEN = 'auth/delete_refresh_token'
SCHEMA_WS_DELETE_REFRESH_TOKEN = \
websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
vol.Required('type'): WS_TYPE_DELETE_REFRESH_TOKEN,
vol.Required('refresh_token_id'): str,
})
WS_TYPE_DELETE_REFRESH_TOKEN = "auth/delete_refresh_token"
SCHEMA_WS_DELETE_REFRESH_TOKEN = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend(
{
vol.Required("type"): WS_TYPE_DELETE_REFRESH_TOKEN,
vol.Required("refresh_token_id"): str,
}
)
RESULT_TYPE_CREDENTIALS = 'credentials'
RESULT_TYPE_USER = 'user'
RESULT_TYPE_CREDENTIALS = "credentials"
RESULT_TYPE_USER = "user"
_LOGGER = logging.getLogger(__name__)
@ -184,23 +188,20 @@ async def async_setup(hass, config):
hass.http.register_view(LinkUserView(retrieve_result))
hass.components.websocket_api.async_register_command(
WS_TYPE_CURRENT_USER, websocket_current_user,
SCHEMA_WS_CURRENT_USER
WS_TYPE_CURRENT_USER, websocket_current_user, SCHEMA_WS_CURRENT_USER
)
hass.components.websocket_api.async_register_command(
WS_TYPE_LONG_LIVED_ACCESS_TOKEN,
websocket_create_long_lived_access_token,
SCHEMA_WS_LONG_LIVED_ACCESS_TOKEN
SCHEMA_WS_LONG_LIVED_ACCESS_TOKEN,
)
hass.components.websocket_api.async_register_command(
WS_TYPE_REFRESH_TOKENS,
websocket_refresh_tokens,
SCHEMA_WS_REFRESH_TOKENS
WS_TYPE_REFRESH_TOKENS, websocket_refresh_tokens, SCHEMA_WS_REFRESH_TOKENS
)
hass.components.websocket_api.async_register_command(
WS_TYPE_DELETE_REFRESH_TOKEN,
websocket_delete_refresh_token,
SCHEMA_WS_DELETE_REFRESH_TOKEN
SCHEMA_WS_DELETE_REFRESH_TOKEN,
)
await login_flow.async_setup(hass, store_result)
@ -212,8 +213,8 @@ async def async_setup(hass, config):
class TokenView(HomeAssistantView):
"""View to issue or revoke tokens."""
url = '/auth/token'
name = 'api:auth:token'
url = "/auth/token"
name = "api:auth:token"
requires_auth = False
cors_allowed = True
@ -224,29 +225,29 @@ class TokenView(HomeAssistantView):
@log_invalid_auth
async def post(self, request):
"""Grant a token."""
hass = request.app['hass']
hass = request.app["hass"]
data = await request.post()
grant_type = data.get('grant_type')
grant_type = data.get("grant_type")
# IndieAuth 6.3.5
# The revocation endpoint is the same as the token endpoint.
# The revocation request includes an additional parameter,
# action=revoke.
if data.get('action') == 'revoke':
if data.get("action") == "revoke":
return await self._async_handle_revoke_token(hass, data)
if grant_type == 'authorization_code':
if grant_type == "authorization_code":
return await self._async_handle_auth_code(
hass, data, str(request[KEY_REAL_IP]))
hass, data, str(request[KEY_REAL_IP])
)
if grant_type == 'refresh_token':
if grant_type == "refresh_token":
return await self._async_handle_refresh_token(
hass, data, str(request[KEY_REAL_IP]))
hass, data, str(request[KEY_REAL_IP])
)
return self.json({
'error': 'unsupported_grant_type',
}, status_code=400)
return self.json({"error": "unsupported_grant_type"}, status_code=400)
async def _async_handle_revoke_token(self, hass, data):
"""Handle revoke token request."""
@ -254,7 +255,7 @@ class TokenView(HomeAssistantView):
# 2.2 The authorization server responds with HTTP status code 200
# if the token has been revoked successfully or if the client
# submitted an invalid token.
token = data.get('token')
token = data.get("token")
if token is None:
return web.Response(status=200)
@ -269,117 +270,112 @@ class TokenView(HomeAssistantView):
async def _async_handle_auth_code(self, hass, data, remote_addr):
"""Handle authorization code request."""
client_id = data.get('client_id')
client_id = data.get("client_id")
if client_id is None or not indieauth.verify_client_id(client_id):
return self.json({
'error': 'invalid_request',
'error_description': 'Invalid client id',
}, status_code=400)
return self.json(
{"error": "invalid_request", "error_description": "Invalid client id"},
status_code=400,
)
code = data.get('code')
code = data.get("code")
if code is None:
return self.json({
'error': 'invalid_request',
'error_description': 'Invalid code',
}, status_code=400)
return self.json(
{"error": "invalid_request", "error_description": "Invalid code"},
status_code=400,
)
user = self._retrieve_user(client_id, RESULT_TYPE_USER, code)
if user is None or not isinstance(user, User):
return self.json({
'error': 'invalid_request',
'error_description': 'Invalid code',
}, status_code=400)
return self.json(
{"error": "invalid_request", "error_description": "Invalid code"},
status_code=400,
)
# refresh user
user = await hass.auth.async_get_user(user.id)
if not user.is_active:
return self.json({
'error': 'access_denied',
'error_description': 'User is not active',
}, status_code=403)
return self.json(
{"error": "access_denied", "error_description": "User is not active"},
status_code=403,
)
refresh_token = await hass.auth.async_create_refresh_token(user,
client_id)
access_token = hass.auth.async_create_access_token(
refresh_token, remote_addr)
refresh_token = await hass.auth.async_create_refresh_token(user, client_id)
access_token = hass.auth.async_create_access_token(refresh_token, remote_addr)
return self.json({
'access_token': access_token,
'token_type': 'Bearer',
'refresh_token': refresh_token.token,
'expires_in':
int(refresh_token.access_token_expiration.total_seconds()),
})
return self.json(
{
"access_token": access_token,
"token_type": "Bearer",
"refresh_token": refresh_token.token,
"expires_in": int(
refresh_token.access_token_expiration.total_seconds()
),
}
)
async def _async_handle_refresh_token(self, hass, data, remote_addr):
"""Handle authorization code request."""
client_id = data.get('client_id')
client_id = data.get("client_id")
if client_id is not None and not indieauth.verify_client_id(client_id):
return self.json({
'error': 'invalid_request',
'error_description': 'Invalid client id',
}, status_code=400)
return self.json(
{"error": "invalid_request", "error_description": "Invalid client id"},
status_code=400,
)
token = data.get('refresh_token')
token = data.get("refresh_token")
if token is None:
return self.json({
'error': 'invalid_request',
}, status_code=400)
return self.json({"error": "invalid_request"}, status_code=400)
refresh_token = await hass.auth.async_get_refresh_token_by_token(token)
if refresh_token is None:
return self.json({
'error': 'invalid_grant',
}, status_code=400)
return self.json({"error": "invalid_grant"}, status_code=400)
if refresh_token.client_id != client_id:
return self.json({
'error': 'invalid_request',
}, status_code=400)
return self.json({"error": "invalid_request"}, status_code=400)
access_token = hass.auth.async_create_access_token(
refresh_token, remote_addr)
access_token = hass.auth.async_create_access_token(refresh_token, remote_addr)
return self.json({
'access_token': access_token,
'token_type': 'Bearer',
'expires_in':
int(refresh_token.access_token_expiration.total_seconds()),
})
return self.json(
{
"access_token": access_token,
"token_type": "Bearer",
"expires_in": int(
refresh_token.access_token_expiration.total_seconds()
),
}
)
class LinkUserView(HomeAssistantView):
"""View to link existing users to new credentials."""
url = '/auth/link_user'
name = 'api:auth:link_user'
url = "/auth/link_user"
name = "api:auth:link_user"
def __init__(self, retrieve_credentials):
"""Initialize the link user view."""
self._retrieve_credentials = retrieve_credentials
@RequestDataValidator(vol.Schema({
'code': str,
'client_id': str,
}))
@RequestDataValidator(vol.Schema({"code": str, "client_id": str}))
async def post(self, request, data):
"""Link a user."""
hass = request.app['hass']
user = request['hass_user']
hass = request.app["hass"]
user = request["hass_user"]
credentials = self._retrieve_credentials(
data['client_id'], RESULT_TYPE_CREDENTIALS, data['code'])
data["client_id"], RESULT_TYPE_CREDENTIALS, data["code"]
)
if credentials is None:
return self.json_message('Invalid code', status_code=400)
return self.json_message("Invalid code", status_code=400)
await hass.auth.async_link_user(user, credentials)
return self.json_message('User linked')
return self.json_message("User linked")
@callback
@ -395,11 +391,14 @@ def _create_auth_code_store():
elif isinstance(result, Credentials):
result_type = RESULT_TYPE_CREDENTIALS
else:
raise ValueError('result has to be either User or Credentials')
raise ValueError("result has to be either User or Credentials")
code = uuid.uuid4().hex
temp_results[(client_id, result_type, code)] = \
(dt_util.utcnow(), result_type, result)
temp_results[(client_id, result_type, code)] = (
dt_util.utcnow(),
result_type,
result,
)
return code
@callback
@ -427,26 +426,39 @@ def _create_auth_code_store():
@websocket_api.ws_require_user()
@callback
def websocket_current_user(
hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg):
hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg
):
"""Return the current user."""
async def async_get_current_user(user):
"""Get current user."""
enabled_modules = await hass.auth.async_get_enabled_mfa(user)
connection.send_message_outside(
websocket_api.result_message(msg['id'], {
'id': user.id,
'name': user.name,
'is_owner': user.is_owner,
'credentials': [{'auth_provider_type': c.auth_provider_type,
'auth_provider_id': c.auth_provider_id}
for c in user.credentials],
'mfa_modules': [{
'id': module.id,
'name': module.name,
'enabled': module.id in enabled_modules,
} for module in hass.auth.auth_mfa_modules],
}))
websocket_api.result_message(
msg["id"],
{
"id": user.id,
"name": user.name,
"is_owner": user.is_owner,
"credentials": [
{
"auth_provider_type": c.auth_provider_type,
"auth_provider_id": c.auth_provider_id,
}
for c in user.credentials
],
"mfa_modules": [
{
"id": module.id,
"name": module.name,
"enabled": module.id in enabled_modules,
}
for module in hass.auth.auth_mfa_modules
],
},
)
)
hass.async_create_task(async_get_current_user(connection.user))
@ -454,63 +466,77 @@ def websocket_current_user(
@websocket_api.ws_require_user()
@callback
def websocket_create_long_lived_access_token(
hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg):
hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg
):
"""Create or a long-lived access token."""
async def async_create_long_lived_access_token(user):
"""Create or a long-lived access token."""
refresh_token = await hass.auth.async_create_refresh_token(
user,
client_name=msg['client_name'],
client_icon=msg.get('client_icon'),
client_name=msg["client_name"],
client_icon=msg.get("client_icon"),
token_type=TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN,
access_token_expiration=timedelta(days=msg['lifespan']))
access_token_expiration=timedelta(days=msg["lifespan"]),
)
access_token = hass.auth.async_create_access_token(
refresh_token)
access_token = hass.auth.async_create_access_token(refresh_token)
connection.send_message_outside(
websocket_api.result_message(msg['id'], access_token))
websocket_api.result_message(msg["id"], access_token)
)
hass.async_create_task(
async_create_long_lived_access_token(connection.user))
hass.async_create_task(async_create_long_lived_access_token(connection.user))
@websocket_api.ws_require_user()
@callback
def websocket_refresh_tokens(
hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg):
hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg
):
"""Return metadata of users refresh tokens."""
current_id = connection.request.get('refresh_token_id')
connection.to_write.put_nowait(websocket_api.result_message(msg['id'], [{
'id': refresh.id,
'client_id': refresh.client_id,
'client_name': refresh.client_name,
'client_icon': refresh.client_icon,
'type': refresh.token_type,
'created_at': refresh.created_at,
'is_current': refresh.id == current_id,
'last_used_at': refresh.last_used_at,
'last_used_ip': refresh.last_used_ip,
} for refresh in connection.user.refresh_tokens.values()]))
current_id = connection.request.get("refresh_token_id")
connection.to_write.put_nowait(
websocket_api.result_message(
msg["id"],
[
{
"id": refresh.id,
"client_id": refresh.client_id,
"client_name": refresh.client_name,
"client_icon": refresh.client_icon,
"type": refresh.token_type,
"created_at": refresh.created_at,
"is_current": refresh.id == current_id,
"last_used_at": refresh.last_used_at,
"last_used_ip": refresh.last_used_ip,
}
for refresh in connection.user.refresh_tokens.values()
],
)
)
@websocket_api.ws_require_user()
@callback
def websocket_delete_refresh_token(
hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg):
hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg
):
"""Handle a delete refresh token request."""
async def async_delete_refresh_token(user, refresh_token_id):
"""Delete a refresh token."""
refresh_token = connection.user.refresh_tokens.get(refresh_token_id)
if refresh_token is None:
return websocket_api.error_message(
msg['id'], 'invalid_token_id', 'Received invalid token')
msg["id"], "invalid_token_id", "Received invalid token"
)
await hass.auth.async_remove_refresh_token(refresh_token)
connection.send_message_outside(
websocket_api.result_message(msg['id'], {}))
connection.send_message_outside(websocket_api.result_message(msg["id"], {}))
hass.async_create_task(
async_delete_refresh_token(connection.user, msg['refresh_token_id']))
async_delete_refresh_token(connection.user, msg["refresh_token_id"])
)

View file

@ -8,16 +8,13 @@ import aiohttp
from aiohttp.client_exceptions import ClientError
# IP addresses of loopback interfaces
ALLOWED_IPS = (
ip_address('127.0.0.1'),
ip_address('::1'),
)
ALLOWED_IPS = (ip_address("127.0.0.1"), ip_address("::1"))
# RFC1918 - Address allocation for Private Internets
ALLOWED_NETWORKS = (
ip_network('10.0.0.0/8'),
ip_network('172.16.0.0/12'),
ip_network('192.168.0.0/16'),
ip_network("10.0.0.0/8"),
ip_network("172.16.0.0/12"),
ip_network("192.168.0.0/16"),
)
@ -32,8 +29,8 @@ async def verify_redirect_uri(hass, client_id, redirect_uri):
# Verify redirect url and client url have same scheme and domain.
is_valid = (
client_id_parts.scheme == redirect_parts.scheme and
client_id_parts.netloc == redirect_parts.netloc
client_id_parts.scheme == redirect_parts.scheme
and client_id_parts.netloc == redirect_parts.netloc
)
if is_valid:
@ -56,13 +53,13 @@ class LinkTagParser(HTMLParser):
def handle_starttag(self, tag, attrs):
"""Handle finding a start tag."""
if tag != 'link':
if tag != "link":
return
attrs = dict(attrs)
if attrs.get('rel') == self.rel:
self.found.append(attrs.get('href'))
if attrs.get("rel") == self.rel:
self.found.append(attrs.get("href"))
async def fetch_redirect_uris(hass, url):
@ -77,7 +74,7 @@ async def fetch_redirect_uris(hass, url):
We do not implement extracting redirect uris from headers.
"""
parser = LinkTagParser('redirect_uri')
parser = LinkTagParser("redirect_uri")
chunks = 0
try:
async with aiohttp.ClientSession() as session:
@ -119,8 +116,8 @@ def _parse_url(url):
# If a URL with no path component is ever encountered,
# it MUST be treated as if it had the path /.
if parts.path == '':
parts = parts._replace(path='/')
if parts.path == "":
parts = parts._replace(path="/")
return parts
@ -134,34 +131,35 @@ def _parse_client_id(client_id):
# Client identifier URLs
# MUST have either an https or http scheme
if parts.scheme not in ('http', 'https'):
if parts.scheme not in ("http", "https"):
raise ValueError()
# MUST contain a path component
# Handled by url canonicalization.
# MUST NOT contain single-dot or double-dot path segments
if any(segment in ('.', '..') for segment in parts.path.split('/')):
if any(segment in (".", "..") for segment in parts.path.split("/")):
raise ValueError(
'Client ID cannot contain single-dot or double-dot path segments')
"Client ID cannot contain single-dot or double-dot path segments"
)
# MUST NOT contain a fragment component
if parts.fragment != '':
raise ValueError('Client ID cannot contain a fragment')
if parts.fragment != "":
raise ValueError("Client ID cannot contain a fragment")
# MUST NOT contain a username or password component
if parts.username is not None:
raise ValueError('Client ID cannot contain username')
raise ValueError("Client ID cannot contain username")
if parts.password is not None:
raise ValueError('Client ID cannot contain password')
raise ValueError("Client ID cannot contain password")
# MAY contain a port
try:
# parts raises ValueError when port cannot be parsed as int
parts.port
except ValueError:
raise ValueError('Client ID contains invalid port')
raise ValueError("Client ID contains invalid port")
# Additionally, hostnames
# MUST be domain names or a loopback interface and
@ -177,7 +175,7 @@ def _parse_client_id(client_id):
netloc = parts.netloc
# Strip the [, ] from ipv6 addresses before parsing
if netloc[0] == '[' and netloc[-1] == ']':
if netloc[0] == "[" and netloc[-1] == "]":
netloc = netloc[1:-1]
address = ip_address(netloc)
@ -185,9 +183,11 @@ def _parse_client_id(client_id):
# Not an ip address
pass
if (address is None or
address in ALLOWED_IPS or
any(address in network for network in ALLOWED_NETWORKS)):
if (
address is None
or address in ALLOWED_IPS
or any(address in network for network in ALLOWED_NETWORKS)
):
return parts
raise ValueError('Hostname should be a domain name or local IP address')
raise ValueError("Hostname should be a domain name or local IP address")

View file

@ -71,8 +71,7 @@ import voluptuous as vol
from homeassistant import data_entry_flow
from homeassistant.components.http import KEY_REAL_IP
from homeassistant.components.http.ban import process_wrong_login, \
log_invalid_auth
from homeassistant.components.http.ban import process_wrong_login, log_invalid_auth
from homeassistant.components.http.data_validator import RequestDataValidator
from homeassistant.components.http.view import HomeAssistantView
from . import indieauth
@ -82,55 +81,55 @@ async def async_setup(hass, store_result):
"""Component to allow users to login."""
hass.http.register_view(AuthProvidersView)
hass.http.register_view(LoginFlowIndexView(hass.auth.login_flow))
hass.http.register_view(
LoginFlowResourceView(hass.auth.login_flow, store_result))
hass.http.register_view(LoginFlowResourceView(hass.auth.login_flow, store_result))
class AuthProvidersView(HomeAssistantView):
"""View to get available auth providers."""
url = '/auth/providers'
name = 'api:auth:providers'
url = "/auth/providers"
name = "api:auth:providers"
requires_auth = False
async def get(self, request):
"""Get available auth providers."""
hass = request.app['hass']
hass = request.app["hass"]
if not hass.components.onboarding.async_is_onboarded():
return self.json_message(
message='Onboarding not finished',
message="Onboarding not finished",
status_code=400,
message_code='onboarding_required'
message_code="onboarding_required",
)
return self.json([{
'name': provider.name,
'id': provider.id,
'type': provider.type,
} for provider in hass.auth.auth_providers])
return self.json(
[
{"name": provider.name, "id": provider.id, "type": provider.type}
for provider in hass.auth.auth_providers
]
)
def _prepare_result_json(result):
"""Convert result to JSON."""
if result['type'] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY:
if result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY:
data = result.copy()
data.pop('result')
data.pop('data')
data.pop("result")
data.pop("data")
return data
if result['type'] != data_entry_flow.RESULT_TYPE_FORM:
if result["type"] != data_entry_flow.RESULT_TYPE_FORM:
return result
import voluptuous_serialize
data = result.copy()
schema = data['data_schema']
schema = data["data_schema"]
if schema is None:
data['data_schema'] = []
data["data_schema"] = []
else:
data['data_schema'] = voluptuous_serialize.convert(schema)
data["data_schema"] = voluptuous_serialize.convert(schema)
return data
@ -138,8 +137,8 @@ def _prepare_result_json(result):
class LoginFlowIndexView(HomeAssistantView):
"""View to create a config flow."""
url = '/auth/login_flow'
name = 'api:auth:login_flow'
url = "/auth/login_flow"
name = "api:auth:login_flow"
requires_auth = False
def __init__(self, flow_mgr):
@ -150,34 +149,41 @@ class LoginFlowIndexView(HomeAssistantView):
"""Do not allow index of flows in progress."""
return web.Response(status=405)
@RequestDataValidator(vol.Schema({
vol.Required('client_id'): str,
vol.Required('handler'): vol.Any(str, list),
vol.Required('redirect_uri'): str,
vol.Optional('type', default='authorize'): str,
}))
@RequestDataValidator(
vol.Schema(
{
vol.Required("client_id"): str,
vol.Required("handler"): vol.Any(str, list),
vol.Required("redirect_uri"): str,
vol.Optional("type", default="authorize"): str,
}
)
)
@log_invalid_auth
async def post(self, request, data):
"""Create a new login flow."""
if not await indieauth.verify_redirect_uri(
request.app['hass'], data['client_id'], data['redirect_uri']):
return self.json_message('invalid client id or redirect uri', 400)
request.app["hass"], data["client_id"], data["redirect_uri"]
):
return self.json_message("invalid client id or redirect uri", 400)
if isinstance(data['handler'], list):
handler = tuple(data['handler'])
if isinstance(data["handler"], list):
handler = tuple(data["handler"])
else:
handler = data['handler']
handler = data["handler"]
try:
result = await self._flow_mgr.async_init(
handler, context={
'ip_address': request[KEY_REAL_IP],
'credential_only': data.get('type') == 'link_user',
})
handler,
context={
"ip_address": request[KEY_REAL_IP],
"credential_only": data.get("type") == "link_user",
},
)
except data_entry_flow.UnknownHandler:
return self.json_message('Invalid handler specified', 404)
return self.json_message("Invalid handler specified", 404)
except data_entry_flow.UnknownStep:
return self.json_message('Handler does not support init', 400)
return self.json_message("Handler does not support init", 400)
return self.json(_prepare_result_json(result))
@ -185,8 +191,8 @@ class LoginFlowIndexView(HomeAssistantView):
class LoginFlowResourceView(HomeAssistantView):
"""View to interact with the flow manager."""
url = '/auth/login_flow/{flow_id}'
name = 'api:auth:login_flow:resource'
url = "/auth/login_flow/{flow_id}"
name = "api:auth:login_flow:resource"
requires_auth = False
def __init__(self, flow_mgr, store_result):
@ -196,43 +202,43 @@ class LoginFlowResourceView(HomeAssistantView):
async def get(self, request):
"""Do not allow getting status of a flow in progress."""
return self.json_message('Invalid flow specified', 404)
return self.json_message("Invalid flow specified", 404)
@RequestDataValidator(vol.Schema({
'client_id': str
}, extra=vol.ALLOW_EXTRA))
@RequestDataValidator(vol.Schema({"client_id": str}, extra=vol.ALLOW_EXTRA))
@log_invalid_auth
async def post(self, request, flow_id, data):
"""Handle progressing a login flow request."""
client_id = data.pop('client_id')
client_id = data.pop("client_id")
if not indieauth.verify_client_id(client_id):
return self.json_message('Invalid client id', 400)
return self.json_message("Invalid client id", 400)
try:
# do not allow change ip during login flow
for flow in self._flow_mgr.async_progress():
if (flow['flow_id'] == flow_id and
flow['context']['ip_address'] !=
request.get(KEY_REAL_IP)):
return self.json_message('IP address changed', 400)
if flow["flow_id"] == flow_id and flow["context"][
"ip_address"
] != request.get(KEY_REAL_IP):
return self.json_message("IP address changed", 400)
result = await self._flow_mgr.async_configure(flow_id, data)
except data_entry_flow.UnknownFlow:
return self.json_message('Invalid flow specified', 404)
return self.json_message("Invalid flow specified", 404)
except vol.Invalid:
return self.json_message('User input malformed', 400)
return self.json_message("User input malformed", 400)
if result['type'] != data_entry_flow.RESULT_TYPE_CREATE_ENTRY:
if result["type"] != data_entry_flow.RESULT_TYPE_CREATE_ENTRY:
# @log_invalid_auth does not work here since it returns HTTP 200
# need manually log failed login attempts
if result['errors'] is not None and \
result['errors'].get('base') == 'invalid_auth':
if (
result["errors"] is not None
and result["errors"].get("base") == "invalid_auth"
):
await process_wrong_login(request)
return self.json(_prepare_result_json(result))
result.pop('data')
result['result'] = self._store_result(client_id, result['result'])
result.pop("data")
result["result"] = self._store_result(client_id, result["result"])
return self.json(result)
@ -241,6 +247,6 @@ class LoginFlowResourceView(HomeAssistantView):
try:
self._flow_mgr.async_abort(flow_id)
except data_entry_flow.UnknownFlow:
return self.json_message('Invalid flow specified', 404)
return self.json_message("Invalid flow specified", 404)
return self.json_message('Flow aborted')
return self.json_message("Flow aborted")

View file

@ -7,82 +7,93 @@ from homeassistant import data_entry_flow
from homeassistant.components import websocket_api
from homeassistant.core import callback, HomeAssistant
WS_TYPE_SETUP_MFA = 'auth/setup_mfa'
SCHEMA_WS_SETUP_MFA = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
vol.Required('type'): WS_TYPE_SETUP_MFA,
vol.Exclusive('mfa_module_id', 'module_or_flow_id'): str,
vol.Exclusive('flow_id', 'module_or_flow_id'): str,
vol.Optional('user_input'): object,
})
WS_TYPE_SETUP_MFA = "auth/setup_mfa"
SCHEMA_WS_SETUP_MFA = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend(
{
vol.Required("type"): WS_TYPE_SETUP_MFA,
vol.Exclusive("mfa_module_id", "module_or_flow_id"): str,
vol.Exclusive("flow_id", "module_or_flow_id"): str,
vol.Optional("user_input"): object,
}
)
WS_TYPE_DEPOSE_MFA = 'auth/depose_mfa'
SCHEMA_WS_DEPOSE_MFA = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
vol.Required('type'): WS_TYPE_DEPOSE_MFA,
vol.Required('mfa_module_id'): str,
})
WS_TYPE_DEPOSE_MFA = "auth/depose_mfa"
SCHEMA_WS_DEPOSE_MFA = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend(
{vol.Required("type"): WS_TYPE_DEPOSE_MFA, vol.Required("mfa_module_id"): str}
)
DATA_SETUP_FLOW_MGR = 'auth_mfa_setup_flow_manager'
DATA_SETUP_FLOW_MGR = "auth_mfa_setup_flow_manager"
_LOGGER = logging.getLogger(__name__)
async def async_setup(hass):
"""Init mfa setup flow manager."""
async def _async_create_setup_flow(handler, context, data):
"""Create a setup flow. hanlder is a mfa module."""
mfa_module = hass.auth.get_auth_mfa_module(handler)
if mfa_module is None:
raise ValueError('Mfa module {} is not found'.format(handler))
raise ValueError("Mfa module {} is not found".format(handler))
user_id = data.pop('user_id')
user_id = data.pop("user_id")
return await mfa_module.async_setup_flow(user_id)
async def _async_finish_setup_flow(flow, flow_result):
_LOGGER.debug('flow_result: %s', flow_result)
_LOGGER.debug("flow_result: %s", flow_result)
return flow_result
hass.data[DATA_SETUP_FLOW_MGR] = data_entry_flow.FlowManager(
hass, _async_create_setup_flow, _async_finish_setup_flow)
hass, _async_create_setup_flow, _async_finish_setup_flow
)
hass.components.websocket_api.async_register_command(
WS_TYPE_SETUP_MFA, websocket_setup_mfa, SCHEMA_WS_SETUP_MFA)
WS_TYPE_SETUP_MFA, websocket_setup_mfa, SCHEMA_WS_SETUP_MFA
)
hass.components.websocket_api.async_register_command(
WS_TYPE_DEPOSE_MFA, websocket_depose_mfa, SCHEMA_WS_DEPOSE_MFA)
WS_TYPE_DEPOSE_MFA, websocket_depose_mfa, SCHEMA_WS_DEPOSE_MFA
)
@callback
@websocket_api.ws_require_user(allow_system_user=False)
def websocket_setup_mfa(
hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg):
hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg
):
"""Return a setup flow for mfa auth module."""
async def async_setup_flow(msg):
"""Return a setup flow for mfa auth module."""
flow_manager = hass.data[DATA_SETUP_FLOW_MGR]
flow_id = msg.get('flow_id')
flow_id = msg.get("flow_id")
if flow_id is not None:
result = await flow_manager.async_configure(
flow_id, msg.get('user_input'))
result = await flow_manager.async_configure(flow_id, msg.get("user_input"))
connection.send_message_outside(
websocket_api.result_message(
msg['id'], _prepare_result_json(result)))
websocket_api.result_message(msg["id"], _prepare_result_json(result))
)
return
mfa_module_id = msg.get('mfa_module_id')
mfa_module_id = msg.get("mfa_module_id")
mfa_module = hass.auth.get_auth_mfa_module(mfa_module_id)
if mfa_module is None:
connection.send_message_outside(websocket_api.error_message(
msg['id'], 'no_module',
'MFA module {} is not found'.format(mfa_module_id)))
connection.send_message_outside(
websocket_api.error_message(
msg["id"],
"no_module",
"MFA module {} is not found".format(mfa_module_id),
)
)
return
result = await flow_manager.async_init(
mfa_module_id, data={'user_id': connection.user.id})
mfa_module_id, data={"user_id": connection.user.id}
)
connection.send_message_outside(
websocket_api.result_message(
msg['id'], _prepare_result_json(result)))
websocket_api.result_message(msg["id"], _prepare_result_json(result))
)
hass.async_create_task(async_setup_flow(msg))
@ -90,45 +101,49 @@ def websocket_setup_mfa(
@callback
@websocket_api.ws_require_user(allow_system_user=False)
def websocket_depose_mfa(
hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg):
hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg
):
"""Remove user from mfa module."""
async def async_depose(msg):
"""Remove user from mfa auth module."""
mfa_module_id = msg['mfa_module_id']
mfa_module_id = msg["mfa_module_id"]
try:
await hass.auth.async_disable_user_mfa(
connection.user, msg['mfa_module_id'])
connection.user, msg["mfa_module_id"]
)
except ValueError as err:
connection.send_message_outside(websocket_api.error_message(
msg['id'], 'disable_failed',
'Cannot disable MFA Module {}: {}'.format(
mfa_module_id, err)))
connection.send_message_outside(
websocket_api.error_message(
msg["id"],
"disable_failed",
"Cannot disable MFA Module {}: {}".format(mfa_module_id, err),
)
)
return
connection.send_message_outside(
websocket_api.result_message(
msg['id'], 'done'))
connection.send_message_outside(websocket_api.result_message(msg["id"], "done"))
hass.async_create_task(async_depose(msg))
def _prepare_result_json(result):
"""Convert result to JSON."""
if result['type'] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY:
if result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY:
data = result.copy()
return data
if result['type'] != data_entry_flow.RESULT_TYPE_FORM:
if result["type"] != data_entry_flow.RESULT_TYPE_FORM:
return result
import voluptuous_serialize
data = result.copy()
schema = data['data_schema']
schema = data["data_schema"]
if schema is None:
data['data_schema'] = []
data["data_schema"] = []
else:
data['data_schema'] = voluptuous_serialize.convert(schema)
data["data_schema"] = voluptuous_serialize.convert(schema)
return data

View file

@ -15,8 +15,16 @@ from homeassistant.setup import async_prepare_setup_platform
from homeassistant.core import CoreState
from homeassistant.loader import bind_hass
from homeassistant.const import (
ATTR_ENTITY_ID, CONF_PLATFORM, STATE_ON, SERVICE_TURN_ON, SERVICE_TURN_OFF,
SERVICE_TOGGLE, SERVICE_RELOAD, EVENT_HOMEASSISTANT_START, CONF_ID)
ATTR_ENTITY_ID,
CONF_PLATFORM,
STATE_ON,
SERVICE_TURN_ON,
SERVICE_TURN_OFF,
SERVICE_TOGGLE,
SERVICE_RELOAD,
EVENT_HOMEASSISTANT_START,
CONF_ID,
)
from homeassistant.components import logbook
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import extract_domain_configs, script, condition
@ -26,32 +34,32 @@ from homeassistant.helpers.restore_state import async_get_last_state
from homeassistant.util.dt import utcnow
import homeassistant.helpers.config_validation as cv
DOMAIN = 'automation'
DEPENDENCIES = ['group']
ENTITY_ID_FORMAT = DOMAIN + '.{}'
DOMAIN = "automation"
DEPENDENCIES = ["group"]
ENTITY_ID_FORMAT = DOMAIN + ".{}"
GROUP_NAME_ALL_AUTOMATIONS = 'all automations'
GROUP_NAME_ALL_AUTOMATIONS = "all automations"
CONF_ALIAS = 'alias'
CONF_HIDE_ENTITY = 'hide_entity'
CONF_ALIAS = "alias"
CONF_HIDE_ENTITY = "hide_entity"
CONF_CONDITION = 'condition'
CONF_ACTION = 'action'
CONF_TRIGGER = 'trigger'
CONF_CONDITION_TYPE = 'condition_type'
CONF_INITIAL_STATE = 'initial_state'
CONF_CONDITION = "condition"
CONF_ACTION = "action"
CONF_TRIGGER = "trigger"
CONF_CONDITION_TYPE = "condition_type"
CONF_INITIAL_STATE = "initial_state"
CONDITION_USE_TRIGGER_VALUES = 'use_trigger_values'
CONDITION_TYPE_AND = 'and'
CONDITION_TYPE_OR = 'or'
CONDITION_USE_TRIGGER_VALUES = "use_trigger_values"
CONDITION_TYPE_AND = "and"
CONDITION_TYPE_OR = "or"
DEFAULT_CONDITION_TYPE = CONDITION_TYPE_AND
DEFAULT_HIDE_ENTITY = False
DEFAULT_INITIAL_STATE = True
ATTR_LAST_TRIGGERED = 'last_triggered'
ATTR_VARIABLES = 'variables'
SERVICE_TRIGGER = 'trigger'
ATTR_LAST_TRIGGERED = "last_triggered"
ATTR_VARIABLES = "variables"
SERVICE_TRIGGER = "trigger"
_LOGGER = logging.getLogger(__name__)
@ -60,10 +68,10 @@ def _platform_validator(config):
"""Validate it is a valid platform."""
try:
platform = importlib.import_module(
'homeassistant.components.automation.{}'.format(
config[CONF_PLATFORM]))
"homeassistant.components.automation.{}".format(config[CONF_PLATFORM])
)
except ImportError:
raise vol.Invalid('Invalid platform specified') from None
raise vol.Invalid("Invalid platform specified") from None
return platform.TRIGGER_SCHEMA(config)
@ -72,35 +80,35 @@ _TRIGGER_SCHEMA = vol.All(
cv.ensure_list,
[
vol.All(
vol.Schema({
vol.Required(CONF_PLATFORM): str
}, extra=vol.ALLOW_EXTRA),
_platform_validator
),
]
vol.Schema({vol.Required(CONF_PLATFORM): str}, extra=vol.ALLOW_EXTRA),
_platform_validator,
)
],
)
_CONDITION_SCHEMA = vol.All(cv.ensure_list, [cv.CONDITION_SCHEMA])
PLATFORM_SCHEMA = vol.Schema({
# str on purpose
CONF_ID: str,
CONF_ALIAS: cv.string,
vol.Optional(CONF_INITIAL_STATE): cv.boolean,
vol.Optional(CONF_HIDE_ENTITY, default=DEFAULT_HIDE_ENTITY): cv.boolean,
vol.Required(CONF_TRIGGER): _TRIGGER_SCHEMA,
vol.Optional(CONF_CONDITION): _CONDITION_SCHEMA,
vol.Required(CONF_ACTION): cv.SCRIPT_SCHEMA,
})
PLATFORM_SCHEMA = vol.Schema(
{
# str on purpose
CONF_ID: str,
CONF_ALIAS: cv.string,
vol.Optional(CONF_INITIAL_STATE): cv.boolean,
vol.Optional(CONF_HIDE_ENTITY, default=DEFAULT_HIDE_ENTITY): cv.boolean,
vol.Required(CONF_TRIGGER): _TRIGGER_SCHEMA,
vol.Optional(CONF_CONDITION): _CONDITION_SCHEMA,
vol.Required(CONF_ACTION): cv.SCRIPT_SCHEMA,
}
)
SERVICE_SCHEMA = vol.Schema({
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
})
SERVICE_SCHEMA = vol.Schema({vol.Optional(ATTR_ENTITY_ID): cv.entity_ids})
TRIGGER_SERVICE_SCHEMA = vol.Schema({
vol.Required(ATTR_ENTITY_ID): cv.entity_ids,
vol.Optional(ATTR_VARIABLES, default={}): dict,
})
TRIGGER_SERVICE_SCHEMA = vol.Schema(
{
vol.Required(ATTR_ENTITY_ID): cv.entity_ids,
vol.Optional(ATTR_VARIABLES, default={}): dict,
}
)
RELOAD_SERVICE_SCHEMA = vol.Schema({})
@ -160,8 +168,9 @@ def async_reload(hass):
async def async_setup(hass, config):
"""Set up the automation."""
component = EntityComponent(_LOGGER, DOMAIN, hass,
group_name=GROUP_NAME_ALL_AUTOMATIONS)
component = EntityComponent(
_LOGGER, DOMAIN, hass, group_name=GROUP_NAME_ALL_AUTOMATIONS
)
await _async_process_config(hass, config, component)
@ -169,10 +178,13 @@ async def async_setup(hass, config):
"""Handle automation triggers."""
tasks = []
for entity in component.async_extract_from_service(service_call):
tasks.append(entity.async_trigger(
service_call.data.get(ATTR_VARIABLES),
skip_condition=True,
context=service_call.context))
tasks.append(
entity.async_trigger(
service_call.data.get(ATTR_VARIABLES),
skip_condition=True,
context=service_call.context,
)
)
if tasks:
await asyncio.wait(tasks, loop=hass.loop)
@ -180,7 +192,7 @@ async def async_setup(hass, config):
async def turn_onoff_service_handler(service_call):
"""Handle automation turn on/off service calls."""
tasks = []
method = 'async_{}'.format(service_call.service)
method = "async_{}".format(service_call.service)
for entity in component.async_extract_from_service(service_call):
tasks.append(getattr(entity, method)())
@ -207,21 +219,21 @@ async def async_setup(hass, config):
await _async_process_config(hass, conf, component)
hass.services.async_register(
DOMAIN, SERVICE_TRIGGER, trigger_service_handler,
schema=TRIGGER_SERVICE_SCHEMA)
DOMAIN, SERVICE_TRIGGER, trigger_service_handler, schema=TRIGGER_SERVICE_SCHEMA
)
hass.services.async_register(
DOMAIN, SERVICE_RELOAD, reload_service_handler,
schema=RELOAD_SERVICE_SCHEMA)
DOMAIN, SERVICE_RELOAD, reload_service_handler, schema=RELOAD_SERVICE_SCHEMA
)
hass.services.async_register(
DOMAIN, SERVICE_TOGGLE, toggle_service_handler,
schema=SERVICE_SCHEMA)
DOMAIN, SERVICE_TOGGLE, toggle_service_handler, schema=SERVICE_SCHEMA
)
for service in (SERVICE_TURN_ON, SERVICE_TURN_OFF):
hass.services.async_register(
DOMAIN, service, turn_onoff_service_handler,
schema=SERVICE_SCHEMA)
DOMAIN, service, turn_onoff_service_handler, schema=SERVICE_SCHEMA
)
return True
@ -229,8 +241,16 @@ async def async_setup(hass, config):
class AutomationEntity(ToggleEntity):
"""Entity to show status of entity."""
def __init__(self, automation_id, name, async_attach_triggers, cond_func,
async_action, hidden, initial_state):
def __init__(
self,
automation_id,
name,
async_attach_triggers,
cond_func,
async_action,
hidden,
initial_state,
):
"""Initialize an automation entity."""
self._id = automation_id
self._name = name
@ -255,9 +275,7 @@ class AutomationEntity(ToggleEntity):
@property
def state_attributes(self):
"""Return the entity state attributes."""
return {
ATTR_LAST_TRIGGERED: self._last_triggered
}
return {ATTR_LAST_TRIGGERED: self._last_triggered}
@property
def hidden(self) -> bool:
@ -273,33 +291,43 @@ class AutomationEntity(ToggleEntity):
"""Startup with initial state or previous state."""
if self._initial_state is not None:
enable_automation = self._initial_state
_LOGGER.debug("Automation %s initial state %s from config "
"initial_state", self.entity_id, enable_automation)
_LOGGER.debug(
"Automation %s initial state %s from config " "initial_state",
self.entity_id,
enable_automation,
)
else:
state = await async_get_last_state(self.hass, self.entity_id)
if state:
enable_automation = state.state == STATE_ON
self._last_triggered = state.attributes.get('last_triggered')
_LOGGER.debug("Automation %s initial state %s from recorder "
"last state %s", self.entity_id,
enable_automation, state)
self._last_triggered = state.attributes.get("last_triggered")
_LOGGER.debug(
"Automation %s initial state %s from recorder " "last state %s",
self.entity_id,
enable_automation,
state,
)
else:
enable_automation = DEFAULT_INITIAL_STATE
_LOGGER.debug("Automation %s initial state %s from default "
"initial state", self.entity_id,
enable_automation)
_LOGGER.debug(
"Automation %s initial state %s from default " "initial state",
self.entity_id,
enable_automation,
)
if not enable_automation:
return
# HomeAssistant is starting up
if self.hass.state == CoreState.not_running:
async def async_enable_automation(event):
"""Start automation on startup."""
await self.async_enable()
self.hass.bus.async_listen_once(
EVENT_HOMEASSISTANT_START, async_enable_automation)
EVENT_HOMEASSISTANT_START, async_enable_automation
)
# HomeAssistant is running
else:
@ -321,8 +349,7 @@ class AutomationEntity(ToggleEntity):
self._async_detach_triggers = None
await self.async_update_ha_state()
async def async_trigger(self, variables, skip_condition=False,
context=None):
async def async_trigger(self, variables, skip_condition=False, context=None):
"""Trigger automation.
This method is a coroutine.
@ -346,7 +373,8 @@ class AutomationEntity(ToggleEntity):
return
self._async_detach_triggers = await self._async_attach_triggers(
self.async_trigger)
self.async_trigger
)
await self.async_update_ha_state()
@property
@ -355,9 +383,7 @@ class AutomationEntity(ToggleEntity):
if self._id is None:
return None
return {
CONF_ID: self._id
}
return {CONF_ID: self._id}
async def _async_process_config(hass, config, component):
@ -372,14 +398,12 @@ async def _async_process_config(hass, config, component):
for list_no, config_block in enumerate(conf):
automation_id = config_block.get(CONF_ID)
name = config_block.get(CONF_ALIAS) or "{} {}".format(config_key,
list_no)
name = config_block.get(CONF_ALIAS) or "{} {}".format(config_key, list_no)
hidden = config_block[CONF_HIDE_ENTITY]
initial_state = config_block.get(CONF_INITIAL_STATE)
action = _async_get_action(hass, config_block.get(CONF_ACTION, {}),
name)
action = _async_get_action(hass, config_block.get(CONF_ACTION, {}), name)
if CONF_CONDITION in config_block:
cond_func = _async_process_if(hass, config, config_block)
@ -387,17 +411,27 @@ async def _async_process_config(hass, config, component):
if cond_func is None:
continue
else:
def cond_func(variables):
"""Condition will always pass."""
return True
async_attach_triggers = partial(
_async_process_trigger, hass, config,
config_block.get(CONF_TRIGGER, []), name
_async_process_trigger,
hass,
config,
config_block.get(CONF_TRIGGER, []),
name,
)
entity = AutomationEntity(
automation_id, name, async_attach_triggers, cond_func, action,
hidden, initial_state)
automation_id,
name,
async_attach_triggers,
cond_func,
action,
hidden,
initial_state,
)
entities.append(entity)
@ -411,9 +445,8 @@ def _async_get_action(hass, config, name):
async def action(entity_id, variables, context):
"""Execute an action."""
_LOGGER.info('Executing %s', name)
logbook.async_log_entry(
hass, name, 'has been triggered', DOMAIN, entity_id)
_LOGGER.info("Executing %s", name)
logbook.async_log_entry(hass, name, "has been triggered", DOMAIN, entity_id)
await script_obj.async_run(variables, context)
return action
@ -428,7 +461,7 @@ def _async_process_if(hass, config, p_config):
try:
checks.append(condition.async_from_config(if_config, False))
except HomeAssistantError as ex:
_LOGGER.warning('Invalid condition: %s', ex)
_LOGGER.warning("Invalid condition: %s", ex)
return None
def if_action(variables=None):
@ -447,7 +480,8 @@ async def _async_process_trigger(hass, config, trigger_configs, name, action):
for conf in trigger_configs:
platform = await async_prepare_setup_platform(
hass, config, DOMAIN, conf.get(CONF_PLATFORM))
hass, config, DOMAIN, conf.get(CONF_PLATFORM)
)
if platform is None:
return None

View file

@ -13,25 +13,29 @@ from homeassistant.core import callback
from homeassistant.const import CONF_PLATFORM
from homeassistant.helpers import config_validation as cv
CONF_EVENT_TYPE = 'event_type'
CONF_EVENT_DATA = 'event_data'
CONF_EVENT_TYPE = "event_type"
CONF_EVENT_DATA = "event_data"
_LOGGER = logging.getLogger(__name__)
TRIGGER_SCHEMA = vol.Schema({
vol.Required(CONF_PLATFORM): 'event',
vol.Required(CONF_EVENT_TYPE): cv.string,
vol.Optional(CONF_EVENT_DATA): dict,
})
TRIGGER_SCHEMA = vol.Schema(
{
vol.Required(CONF_PLATFORM): "event",
vol.Required(CONF_EVENT_TYPE): cv.string,
vol.Optional(CONF_EVENT_DATA): dict,
}
)
@asyncio.coroutine
def async_trigger(hass, config, action):
"""Listen for events based on configuration."""
event_type = config.get(CONF_EVENT_TYPE)
event_data_schema = vol.Schema(
config.get(CONF_EVENT_DATA),
extra=vol.ALLOW_EXTRA) if config.get(CONF_EVENT_DATA) else None
event_data_schema = (
vol.Schema(config.get(CONF_EVENT_DATA), extra=vol.ALLOW_EXTRA)
if config.get(CONF_EVENT_DATA)
else None
)
@callback
def handle_event(event):
@ -45,11 +49,11 @@ def async_trigger(hass, config, action):
# If event data doesn't match requested schema, skip event
return
hass.async_run_job(action({
'trigger': {
'platform': 'event',
'event': event,
},
}, context=event.context))
hass.async_run_job(
action(
{"trigger": {"platform": "event", "event": event}},
context=event.context,
)
)
return hass.bus.async_listen(event_type, handle_event)

View file

@ -10,17 +10,18 @@ import logging
import voluptuous as vol
from homeassistant.core import callback, CoreState
from homeassistant.const import (
CONF_PLATFORM, CONF_EVENT, EVENT_HOMEASSISTANT_STOP)
from homeassistant.const import CONF_PLATFORM, CONF_EVENT, EVENT_HOMEASSISTANT_STOP
EVENT_START = 'start'
EVENT_SHUTDOWN = 'shutdown'
EVENT_START = "start"
EVENT_SHUTDOWN = "shutdown"
_LOGGER = logging.getLogger(__name__)
TRIGGER_SCHEMA = vol.Schema({
vol.Required(CONF_PLATFORM): 'homeassistant',
vol.Required(CONF_EVENT): vol.Any(EVENT_START, EVENT_SHUTDOWN),
})
TRIGGER_SCHEMA = vol.Schema(
{
vol.Required(CONF_PLATFORM): "homeassistant",
vol.Required(CONF_EVENT): vol.Any(EVENT_START, EVENT_SHUTDOWN),
}
)
@asyncio.coroutine
@ -29,27 +30,24 @@ def async_trigger(hass, config, action):
event = config.get(CONF_EVENT)
if event == EVENT_SHUTDOWN:
@callback
def hass_shutdown(event):
"""Execute when Home Assistant is shutting down."""
hass.async_run_job(action({
'trigger': {
'platform': 'homeassistant',
'event': event,
},
}, context=event.context))
hass.async_run_job(
action(
{"trigger": {"platform": "homeassistant", "event": event}},
context=event.context,
)
)
return hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP,
hass_shutdown)
return hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, hass_shutdown)
# Automation are enabled while hass is starting up, fire right away
# Check state because a config reload shouldn't trigger it.
if hass.state == CoreState.starting:
hass.async_run_job(action({
'trigger': {
'platform': 'homeassistant',
'event': event,
},
}))
hass.async_run_job(
action({"trigger": {"platform": "homeassistant", "event": event}})
)
return lambda: None

View file

@ -15,22 +15,26 @@ import homeassistant.helpers.config_validation as cv
import homeassistant.util.dt as dt_util
from homeassistant.helpers.event import track_point_in_utc_time
DEPENDENCIES = ['litejet']
DEPENDENCIES = ["litejet"]
_LOGGER = logging.getLogger(__name__)
CONF_NUMBER = 'number'
CONF_HELD_MORE_THAN = 'held_more_than'
CONF_HELD_LESS_THAN = 'held_less_than'
CONF_NUMBER = "number"
CONF_HELD_MORE_THAN = "held_more_than"
CONF_HELD_LESS_THAN = "held_less_than"
TRIGGER_SCHEMA = vol.Schema({
vol.Required(CONF_PLATFORM): 'litejet',
vol.Required(CONF_NUMBER): cv.positive_int,
vol.Optional(CONF_HELD_MORE_THAN):
vol.All(cv.time_period, cv.positive_timedelta),
vol.Optional(CONF_HELD_LESS_THAN):
vol.All(cv.time_period, cv.positive_timedelta)
})
TRIGGER_SCHEMA = vol.Schema(
{
vol.Required(CONF_PLATFORM): "litejet",
vol.Required(CONF_NUMBER): cv.positive_int,
vol.Optional(CONF_HELD_MORE_THAN): vol.All(
cv.time_period, cv.positive_timedelta
),
vol.Optional(CONF_HELD_LESS_THAN): vol.All(
cv.time_period, cv.positive_timedelta
),
}
)
@asyncio.coroutine
@ -45,14 +49,17 @@ def async_trigger(hass, config, action):
@callback
def call_action():
"""Call action with right context."""
hass.async_run_job(action, {
'trigger': {
CONF_PLATFORM: 'litejet',
CONF_NUMBER: number,
CONF_HELD_MORE_THAN: held_more_than,
CONF_HELD_LESS_THAN: held_less_than
hass.async_run_job(
action,
{
"trigger": {
CONF_PLATFORM: "litejet",
CONF_NUMBER: number,
CONF_HELD_MORE_THAN: held_more_than,
CONF_HELD_LESS_THAN: held_less_than,
}
},
})
)
# held_more_than and held_less_than: trigger on released (if in time range)
# held_more_than: trigger after pressed with calculation
@ -73,9 +80,8 @@ def async_trigger(hass, config, action):
hass.add_job(call_action)
if held_more_than is not None and held_less_than is None:
cancel_pressed_more_than = track_point_in_utc_time(
hass,
pressed_more_than_satisfied,
dt_util.utcnow() + held_more_than)
hass, pressed_more_than_satisfied, dt_util.utcnow() + held_more_than
)
def released():
"""Handle the release of the LiteJet switch's button."""
@ -90,8 +96,8 @@ def async_trigger(hass, config, action):
if held_more_than is None or held_time > held_more_than:
hass.add_job(call_action)
hass.data['litejet_system'].on_switch_pressed(number, pressed)
hass.data['litejet_system'].on_switch_released(number, released)
hass.data["litejet_system"].on_switch_pressed(number, pressed)
hass.data["litejet_system"].on_switch_released(number, released)
def async_remove():
"""Remove all subscriptions used for this trigger."""

View file

@ -11,18 +11,20 @@ import voluptuous as vol
from homeassistant.core import callback
from homeassistant.components import mqtt
from homeassistant.const import (CONF_PLATFORM, CONF_PAYLOAD)
from homeassistant.const import CONF_PLATFORM, CONF_PAYLOAD
import homeassistant.helpers.config_validation as cv
DEPENDENCIES = ['mqtt']
DEPENDENCIES = ["mqtt"]
CONF_TOPIC = 'topic'
CONF_TOPIC = "topic"
TRIGGER_SCHEMA = vol.Schema({
vol.Required(CONF_PLATFORM): mqtt.DOMAIN,
vol.Required(CONF_TOPIC): mqtt.valid_subscribe_topic,
vol.Optional(CONF_PAYLOAD): cv.string,
})
TRIGGER_SCHEMA = vol.Schema(
{
vol.Required(CONF_PLATFORM): mqtt.DOMAIN,
vol.Required(CONF_TOPIC): mqtt.valid_subscribe_topic,
vol.Optional(CONF_PAYLOAD): cv.string,
}
)
@asyncio.coroutine
@ -36,21 +38,18 @@ def async_trigger(hass, config, action):
"""Listen for MQTT messages."""
if payload is None or payload == msg_payload:
data = {
'platform': 'mqtt',
'topic': msg_topic,
'payload': msg_payload,
'qos': qos,
"platform": "mqtt",
"topic": msg_topic,
"payload": msg_payload,
"qos": qos,
}
try:
data['payload_json'] = json.loads(msg_payload)
data["payload_json"] = json.loads(msg_payload)
except ValueError:
pass
hass.async_run_job(action, {
'trigger': data
})
hass.async_run_job(action, {"trigger": data})
remove = yield from mqtt.async_subscribe(
hass, topic, mqtt_automation_listener)
remove = yield from mqtt.async_subscribe(hass, topic, mqtt_automation_listener)
return remove

View file

@ -11,20 +11,29 @@ import voluptuous as vol
from homeassistant.core import callback
from homeassistant.const import (
CONF_VALUE_TEMPLATE, CONF_PLATFORM, CONF_ENTITY_ID,
CONF_BELOW, CONF_ABOVE, CONF_FOR)
from homeassistant.helpers.event import (
async_track_state_change, async_track_same_state)
CONF_VALUE_TEMPLATE,
CONF_PLATFORM,
CONF_ENTITY_ID,
CONF_BELOW,
CONF_ABOVE,
CONF_FOR,
)
from homeassistant.helpers.event import async_track_state_change, async_track_same_state
from homeassistant.helpers import condition, config_validation as cv
TRIGGER_SCHEMA = vol.All(vol.Schema({
vol.Required(CONF_PLATFORM): 'numeric_state',
vol.Required(CONF_ENTITY_ID): cv.entity_ids,
vol.Optional(CONF_BELOW): vol.Coerce(float),
vol.Optional(CONF_ABOVE): vol.Coerce(float),
vol.Optional(CONF_VALUE_TEMPLATE): cv.template,
vol.Optional(CONF_FOR): vol.All(cv.time_period, cv.positive_timedelta),
}), cv.has_at_least_one_key(CONF_BELOW, CONF_ABOVE))
TRIGGER_SCHEMA = vol.All(
vol.Schema(
{
vol.Required(CONF_PLATFORM): "numeric_state",
vol.Required(CONF_ENTITY_ID): cv.entity_ids,
vol.Optional(CONF_BELOW): vol.Coerce(float),
vol.Optional(CONF_ABOVE): vol.Coerce(float),
vol.Optional(CONF_VALUE_TEMPLATE): cv.template,
vol.Optional(CONF_FOR): vol.All(cv.time_period, cv.positive_timedelta),
}
),
cv.has_at_least_one_key(CONF_BELOW, CONF_ABOVE),
)
_LOGGER = logging.getLogger(__name__)
@ -50,32 +59,39 @@ def async_trigger(hass, config, action):
return False
variables = {
'trigger': {
'platform': 'numeric_state',
'entity_id': entity,
'below': below,
'above': above,
"trigger": {
"platform": "numeric_state",
"entity_id": entity,
"below": below,
"above": above,
}
}
return condition.async_numeric_state(
hass, to_s, below, above, value_template, variables)
hass, to_s, below, above, value_template, variables
)
@callback
def state_automation_listener(entity, from_s, to_s):
"""Listen for state changes and calls action."""
@callback
def call_action():
"""Call action with right context."""
hass.async_run_job(action({
'trigger': {
'platform': 'numeric_state',
'entity_id': entity,
'below': below,
'above': above,
'from_state': from_s,
'to_state': to_s,
}
}, context=to_s.context))
hass.async_run_job(
action(
{
"trigger": {
"platform": "numeric_state",
"entity_id": entity,
"below": below,
"above": above,
"from_state": from_s,
"to_state": to_s,
}
},
context=to_s.context,
)
)
matching = check_numeric_state(entity, from_s, to_s)
@ -86,13 +102,16 @@ def async_trigger(hass, config, action):
if time_delta:
unsub_track_same[entity] = async_track_same_state(
hass, time_delta, call_action, entity_ids=entity_id,
async_check_same_func=check_numeric_state)
hass,
time_delta,
call_action,
entity_ids=entity_id,
async_check_same_func=check_numeric_state,
)
else:
call_action()
unsub = async_track_state_change(
hass, entity_id, state_automation_listener)
unsub = async_track_state_change(hass, entity_id, state_automation_listener)
@callback
def async_remove():

View file

@ -9,22 +9,26 @@ import voluptuous as vol
from homeassistant.core import callback
from homeassistant.const import MATCH_ALL, CONF_PLATFORM, CONF_FOR
from homeassistant.helpers.event import (
async_track_state_change, async_track_same_state)
from homeassistant.helpers.event import async_track_state_change, async_track_same_state
import homeassistant.helpers.config_validation as cv
CONF_ENTITY_ID = 'entity_id'
CONF_FROM = 'from'
CONF_TO = 'to'
CONF_ENTITY_ID = "entity_id"
CONF_FROM = "from"
CONF_TO = "to"
TRIGGER_SCHEMA = vol.All(vol.Schema({
vol.Required(CONF_PLATFORM): 'state',
vol.Required(CONF_ENTITY_ID): cv.entity_ids,
# These are str on purpose. Want to catch YAML conversions
vol.Optional(CONF_FROM): str,
vol.Optional(CONF_TO): str,
vol.Optional(CONF_FOR): vol.All(cv.time_period, cv.positive_timedelta),
}), cv.key_dependency(CONF_FOR, CONF_TO))
TRIGGER_SCHEMA = vol.All(
vol.Schema(
{
vol.Required(CONF_PLATFORM): "state",
vol.Required(CONF_ENTITY_ID): cv.entity_ids,
# These are str on purpose. Want to catch YAML conversions
vol.Optional(CONF_FROM): str,
vol.Optional(CONF_TO): str,
vol.Optional(CONF_FOR): vol.All(cv.time_period, cv.positive_timedelta),
}
),
cv.key_dependency(CONF_FOR, CONF_TO),
)
@asyncio.coroutine
@ -34,28 +38,38 @@ def async_trigger(hass, config, action):
from_state = config.get(CONF_FROM, MATCH_ALL)
to_state = config.get(CONF_TO, MATCH_ALL)
time_delta = config.get(CONF_FOR)
match_all = (from_state == MATCH_ALL and to_state == MATCH_ALL)
match_all = from_state == MATCH_ALL and to_state == MATCH_ALL
unsub_track_same = {}
@callback
def state_automation_listener(entity, from_s, to_s):
"""Listen for state changes and calls action."""
@callback
def call_action():
"""Call action with right context."""
hass.async_run_job(action({
'trigger': {
'platform': 'state',
'entity_id': entity,
'from_state': from_s,
'to_state': to_s,
'for': time_delta,
}
}, context=to_s.context))
hass.async_run_job(
action(
{
"trigger": {
"platform": "state",
"entity_id": entity,
"from_state": from_s,
"to_state": to_s,
"for": time_delta,
}
},
context=to_s.context,
)
)
# Ignore changes to state attributes if from/to is in use
if (not match_all and from_s is not None and to_s is not None and
from_s.state == to_s.state):
if (
not match_all
and from_s is not None
and to_s is not None
and from_s.state == to_s.state
):
return
if not time_delta:
@ -63,12 +77,16 @@ def async_trigger(hass, config, action):
return
unsub_track_same[entity] = async_track_same_state(
hass, time_delta, call_action,
hass,
time_delta,
call_action,
lambda _, _2, to_state: to_state.state == to_s.state,
entity_ids=entity_id)
entity_ids=entity_id,
)
unsub = async_track_state_change(
hass, entity_id, state_automation_listener, from_state, to_state)
hass, entity_id, state_automation_listener, from_state, to_state
)
@callback
def async_remove():

View file

@ -12,17 +12,23 @@ import voluptuous as vol
from homeassistant.core import callback
from homeassistant.const import (
CONF_EVENT, CONF_OFFSET, CONF_PLATFORM, SUN_EVENT_SUNRISE)
CONF_EVENT,
CONF_OFFSET,
CONF_PLATFORM,
SUN_EVENT_SUNRISE,
)
from homeassistant.helpers.event import async_track_sunrise, async_track_sunset
import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__)
TRIGGER_SCHEMA = vol.Schema({
vol.Required(CONF_PLATFORM): 'sun',
vol.Required(CONF_EVENT): cv.sun_event,
vol.Required(CONF_OFFSET, default=timedelta(0)): cv.time_period,
})
TRIGGER_SCHEMA = vol.Schema(
{
vol.Required(CONF_PLATFORM): "sun",
vol.Required(CONF_EVENT): cv.sun_event,
vol.Required(CONF_OFFSET, default=timedelta(0)): cv.time_period,
}
)
@asyncio.coroutine
@ -34,13 +40,9 @@ def async_trigger(hass, config, action):
@callback
def call_action():
"""Call action with right context."""
hass.async_run_job(action, {
'trigger': {
'platform': 'sun',
'event': event,
'offset': offset,
},
})
hass.async_run_job(
action, {"trigger": {"platform": "sun", "event": event, "offset": offset}}
)
if event == SUN_EVENT_SUNRISE:
return async_track_sunrise(hass, call_action, offset)

View file

@ -17,10 +17,12 @@ import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__)
TRIGGER_SCHEMA = IF_ACTION_SCHEMA = vol.Schema({
vol.Required(CONF_PLATFORM): 'template',
vol.Required(CONF_VALUE_TEMPLATE): cv.template,
})
TRIGGER_SCHEMA = IF_ACTION_SCHEMA = vol.Schema(
{
vol.Required(CONF_PLATFORM): "template",
vol.Required(CONF_VALUE_TEMPLATE): cv.template,
}
)
@asyncio.coroutine
@ -32,13 +34,18 @@ def async_trigger(hass, config, action):
@callback
def template_listener(entity_id, from_s, to_s):
"""Listen for state changes and calls action."""
hass.async_run_job(action({
'trigger': {
'platform': 'template',
'entity_id': entity_id,
'from_state': from_s,
'to_state': to_s,
},
}, context=to_s.context))
hass.async_run_job(
action(
{
"trigger": {
"platform": "template",
"entity_id": entity_id,
"from_state": from_s,
"to_state": to_s,
}
},
context=to_s.context,
)
)
return async_track_template(hass, value_template, template_listener)

View file

@ -14,19 +14,24 @@ from homeassistant.const import CONF_AT, CONF_PLATFORM
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.event import async_track_time_change
CONF_HOURS = 'hours'
CONF_MINUTES = 'minutes'
CONF_SECONDS = 'seconds'
CONF_HOURS = "hours"
CONF_MINUTES = "minutes"
CONF_SECONDS = "seconds"
_LOGGER = logging.getLogger(__name__)
TRIGGER_SCHEMA = vol.All(vol.Schema({
vol.Required(CONF_PLATFORM): 'time',
CONF_AT: cv.time,
CONF_HOURS: vol.Any(vol.Coerce(int), vol.Coerce(str)),
CONF_MINUTES: vol.Any(vol.Coerce(int), vol.Coerce(str)),
CONF_SECONDS: vol.Any(vol.Coerce(int), vol.Coerce(str)),
}), cv.has_at_least_one_key(CONF_HOURS, CONF_MINUTES, CONF_SECONDS, CONF_AT))
TRIGGER_SCHEMA = vol.All(
vol.Schema(
{
vol.Required(CONF_PLATFORM): "time",
CONF_AT: cv.time,
CONF_HOURS: vol.Any(vol.Coerce(int), vol.Coerce(str)),
CONF_MINUTES: vol.Any(vol.Coerce(int), vol.Coerce(str)),
CONF_SECONDS: vol.Any(vol.Coerce(int), vol.Coerce(str)),
}
),
cv.has_at_least_one_key(CONF_HOURS, CONF_MINUTES, CONF_SECONDS, CONF_AT),
)
@asyncio.coroutine
@ -43,12 +48,8 @@ def async_trigger(hass, config, action):
@callback
def time_automation_listener(now):
"""Listen for time changes and calls action."""
hass.async_run_job(action, {
'trigger': {
'platform': 'time',
'now': now,
},
})
hass.async_run_job(action, {"trigger": {"platform": "time", "now": now}})
return async_track_time_change(hass, time_automation_listener,
hour=hours, minute=minutes, second=seconds)
return async_track_time_change(
hass, time_automation_listener, hour=hours, minute=minutes, second=seconds
)

View file

@ -9,22 +9,29 @@ import voluptuous as vol
from homeassistant.core import callback
from homeassistant.const import (
CONF_EVENT, CONF_ENTITY_ID, CONF_ZONE, MATCH_ALL, CONF_PLATFORM)
CONF_EVENT,
CONF_ENTITY_ID,
CONF_ZONE,
MATCH_ALL,
CONF_PLATFORM,
)
from homeassistant.helpers.event import async_track_state_change
from homeassistant.helpers import (
condition, config_validation as cv, location)
from homeassistant.helpers import condition, config_validation as cv, location
EVENT_ENTER = 'enter'
EVENT_LEAVE = 'leave'
EVENT_ENTER = "enter"
EVENT_LEAVE = "leave"
DEFAULT_EVENT = EVENT_ENTER
TRIGGER_SCHEMA = vol.Schema({
vol.Required(CONF_PLATFORM): 'zone',
vol.Required(CONF_ENTITY_ID): cv.entity_ids,
vol.Required(CONF_ZONE): cv.entity_id,
vol.Required(CONF_EVENT, default=DEFAULT_EVENT):
vol.Any(EVENT_ENTER, EVENT_LEAVE),
})
TRIGGER_SCHEMA = vol.Schema(
{
vol.Required(CONF_PLATFORM): "zone",
vol.Required(CONF_ENTITY_ID): cv.entity_ids,
vol.Required(CONF_ZONE): cv.entity_id,
vol.Required(CONF_EVENT, default=DEFAULT_EVENT): vol.Any(
EVENT_ENTER, EVENT_LEAVE
),
}
)
@asyncio.coroutine
@ -37,8 +44,11 @@ def async_trigger(hass, config, action):
@callback
def zone_automation_listener(entity, from_s, to_s):
"""Listen for state changes and calls action."""
if from_s and not location.has_location(from_s) or \
not location.has_location(to_s):
if (
from_s
and not location.has_location(from_s)
or not location.has_location(to_s)
):
return
zone_state = hass.states.get(zone_entity_id)
@ -49,18 +59,30 @@ def async_trigger(hass, config, action):
to_match = condition.zone(hass, zone_state, to_s)
# pylint: disable=too-many-boolean-expressions
if event == EVENT_ENTER and not from_match and to_match or \
event == EVENT_LEAVE and from_match and not to_match:
hass.async_run_job(action({
'trigger': {
'platform': 'zone',
'entity_id': entity,
'from_state': from_s,
'to_state': to_s,
'zone': zone_state,
'event': event,
},
}, context=to_s.context))
if (
event == EVENT_ENTER
and not from_match
and to_match
or event == EVENT_LEAVE
and from_match
and not to_match
):
hass.async_run_job(
action(
{
"trigger": {
"platform": "zone",
"entity_id": entity,
"from_state": from_s,
"to_state": to_s,
"zone": zone_state,
"event": event,
}
},
context=to_s.context,
)
)
return async_track_state_change(hass, entity_id, zone_automation_listener,
MATCH_ALL, MATCH_ALL)
return async_track_state_change(
hass, entity_id, zone_automation_listener, MATCH_ALL, MATCH_ALL
)

View file

@ -10,67 +10,76 @@ import voluptuous as vol
from homeassistant.components.discovery import SERVICE_AXIS
from homeassistant.const import (
ATTR_LOCATION, ATTR_TRIPPED, CONF_EVENT, CONF_HOST, CONF_INCLUDE,
CONF_NAME, CONF_PASSWORD, CONF_PORT, CONF_TRIGGER_TIME, CONF_USERNAME,
EVENT_HOMEASSISTANT_STOP)
ATTR_LOCATION,
ATTR_TRIPPED,
CONF_EVENT,
CONF_HOST,
CONF_INCLUDE,
CONF_NAME,
CONF_PASSWORD,
CONF_PORT,
CONF_TRIGGER_TIME,
CONF_USERNAME,
EVENT_HOMEASSISTANT_STOP,
)
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers import discovery
from homeassistant.helpers.dispatcher import dispatcher_send
from homeassistant.helpers.entity import Entity
from homeassistant.util.json import load_json, save_json
REQUIREMENTS = ['axis==14']
REQUIREMENTS = ["axis==14"]
_LOGGER = logging.getLogger(__name__)
DOMAIN = 'axis'
CONFIG_FILE = 'axis.conf'
DOMAIN = "axis"
CONFIG_FILE = "axis.conf"
AXIS_DEVICES = {}
EVENT_TYPES = ['motion', 'vmd3', 'pir', 'sound',
'daynight', 'tampering', 'input']
EVENT_TYPES = ["motion", "vmd3", "pir", "sound", "daynight", "tampering", "input"]
PLATFORMS = ['camera']
PLATFORMS = ["camera"]
AXIS_INCLUDE = EVENT_TYPES + PLATFORMS
AXIS_DEFAULT_HOST = '192.168.0.90'
AXIS_DEFAULT_USERNAME = 'root'
AXIS_DEFAULT_PASSWORD = 'pass'
AXIS_DEFAULT_HOST = "192.168.0.90"
AXIS_DEFAULT_USERNAME = "root"
AXIS_DEFAULT_PASSWORD = "pass"
DEVICE_SCHEMA = vol.Schema({
vol.Required(CONF_INCLUDE):
vol.All(cv.ensure_list, [vol.In(AXIS_INCLUDE)]),
vol.Optional(CONF_NAME): cv.string,
vol.Optional(CONF_HOST, default=AXIS_DEFAULT_HOST): cv.string,
vol.Optional(CONF_USERNAME, default=AXIS_DEFAULT_USERNAME): cv.string,
vol.Optional(CONF_PASSWORD, default=AXIS_DEFAULT_PASSWORD): cv.string,
vol.Optional(CONF_TRIGGER_TIME, default=0): cv.positive_int,
vol.Optional(CONF_PORT, default=80): cv.positive_int,
vol.Optional(ATTR_LOCATION, default=''): cv.string,
})
DEVICE_SCHEMA = vol.Schema(
{
vol.Required(CONF_INCLUDE): vol.All(cv.ensure_list, [vol.In(AXIS_INCLUDE)]),
vol.Optional(CONF_NAME): cv.string,
vol.Optional(CONF_HOST, default=AXIS_DEFAULT_HOST): cv.string,
vol.Optional(CONF_USERNAME, default=AXIS_DEFAULT_USERNAME): cv.string,
vol.Optional(CONF_PASSWORD, default=AXIS_DEFAULT_PASSWORD): cv.string,
vol.Optional(CONF_TRIGGER_TIME, default=0): cv.positive_int,
vol.Optional(CONF_PORT, default=80): cv.positive_int,
vol.Optional(ATTR_LOCATION, default=""): cv.string,
}
)
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
cv.slug: DEVICE_SCHEMA,
}),
}, extra=vol.ALLOW_EXTRA)
CONFIG_SCHEMA = vol.Schema(
{DOMAIN: vol.Schema({cv.slug: DEVICE_SCHEMA})}, extra=vol.ALLOW_EXTRA
)
SERVICE_VAPIX_CALL = 'vapix_call'
SERVICE_VAPIX_CALL_RESPONSE = 'vapix_call_response'
SERVICE_CGI = 'cgi'
SERVICE_ACTION = 'action'
SERVICE_PARAM = 'param'
SERVICE_DEFAULT_CGI = 'param.cgi'
SERVICE_DEFAULT_ACTION = 'update'
SERVICE_VAPIX_CALL = "vapix_call"
SERVICE_VAPIX_CALL_RESPONSE = "vapix_call_response"
SERVICE_CGI = "cgi"
SERVICE_ACTION = "action"
SERVICE_PARAM = "param"
SERVICE_DEFAULT_CGI = "param.cgi"
SERVICE_DEFAULT_ACTION = "update"
SERVICE_SCHEMA = vol.Schema({
vol.Required(CONF_NAME): cv.string,
vol.Required(SERVICE_PARAM): cv.string,
vol.Optional(SERVICE_CGI, default=SERVICE_DEFAULT_CGI): cv.string,
vol.Optional(SERVICE_ACTION, default=SERVICE_DEFAULT_ACTION): cv.string,
})
SERVICE_SCHEMA = vol.Schema(
{
vol.Required(CONF_NAME): cv.string,
vol.Required(SERVICE_PARAM): cv.string,
vol.Optional(SERVICE_CGI, default=SERVICE_DEFAULT_CGI): cv.string,
vol.Optional(SERVICE_ACTION, default=SERVICE_DEFAULT_ACTION): cv.string,
}
)
def request_configuration(hass, config, name, host, serialnumber):
@ -80,8 +89,7 @@ def request_configuration(hass, config, name, host, serialnumber):
def configuration_callback(callback_data):
"""Call when configuration is submitted."""
if CONF_INCLUDE not in callback_data:
configurator.notify_errors(
request_id, "Functionality mandatory.")
configurator.notify_errors(request_id, "Functionality mandatory.")
return False
callback_data[CONF_INCLUDE] = callback_data[CONF_INCLUDE].split()
@ -93,58 +101,58 @@ def request_configuration(hass, config, name, host, serialnumber):
try:
device_config = DEVICE_SCHEMA(callback_data)
except vol.Invalid:
configurator.notify_errors(
request_id, "Bad input, please check spelling.")
configurator.notify_errors(request_id, "Bad input, please check spelling.")
return False
if setup_device(hass, config, device_config):
del device_config['events']
del device_config['signal']
del device_config["events"]
del device_config["signal"]
config_file = load_json(hass.config.path(CONFIG_FILE))
config_file[serialnumber] = dict(device_config)
save_json(hass.config.path(CONFIG_FILE), config_file)
configurator.request_done(request_id)
else:
configurator.notify_errors(
request_id, "Failed to register, please try again.")
request_id, "Failed to register, please try again."
)
return False
title = '{} ({})'.format(name, host)
title = "{} ({})".format(name, host)
request_id = configurator.request_config(
title, configuration_callback,
description='Functionality: ' + str(AXIS_INCLUDE),
title,
configuration_callback,
description="Functionality: " + str(AXIS_INCLUDE),
entity_picture="/static/images/logo_axis.png",
link_name='Axis platform documentation',
link_url='https://home-assistant.io/components/axis/',
link_name="Axis platform documentation",
link_url="https://home-assistant.io/components/axis/",
submit_caption="Confirm",
fields=[
{'id': CONF_NAME,
'name': "Device name",
'type': 'text'},
{'id': CONF_USERNAME,
'name': "User name",
'type': 'text'},
{'id': CONF_PASSWORD,
'name': 'Password',
'type': 'password'},
{'id': CONF_INCLUDE,
'name': "Device functionality (space separated list)",
'type': 'text'},
{'id': ATTR_LOCATION,
'name': "Physical location of device (optional)",
'type': 'text'},
{'id': CONF_PORT,
'name': "HTTP port (default=80)",
'type': 'number'},
{'id': CONF_TRIGGER_TIME,
'name': "Sensor update interval (optional)",
'type': 'number'},
]
{"id": CONF_NAME, "name": "Device name", "type": "text"},
{"id": CONF_USERNAME, "name": "User name", "type": "text"},
{"id": CONF_PASSWORD, "name": "Password", "type": "password"},
{
"id": CONF_INCLUDE,
"name": "Device functionality (space separated list)",
"type": "text",
},
{
"id": ATTR_LOCATION,
"name": "Physical location of device (optional)",
"type": "text",
},
{"id": CONF_PORT, "name": "HTTP port (default=80)", "type": "number"},
{
"id": CONF_TRIGGER_TIME,
"name": "Sensor update interval (optional)",
"type": "number",
},
],
)
def setup(hass, config):
"""Set up for Axis devices."""
def _shutdown(call):
"""Stop the event stream on shutdown."""
for serialnumber, device in AXIS_DEVICES.items():
@ -156,8 +164,8 @@ def setup(hass, config):
def axis_device_discovered(service, discovery_info):
"""Call when axis devices has been found."""
host = discovery_info[CONF_HOST]
name = discovery_info['hostname']
serialnumber = discovery_info['properties']['macaddress']
name = discovery_info["hostname"]
serialnumber = discovery_info["properties"]["macaddress"]
if serialnumber not in AXIS_DEVICES:
config_file = load_json(hass.config.path(CONFIG_FILE))
@ -170,8 +178,7 @@ def setup(hass, config):
_LOGGER.error("Bad data from %s. %s", CONFIG_FILE, err)
return False
if not setup_device(hass, config, device_config):
_LOGGER.error(
"Couldn't set up %s", device_config[CONF_NAME])
_LOGGER.error("Couldn't set up %s", device_config[CONF_NAME])
else:
# New device, create configuration request for UI
request_configuration(hass, config, name, host, serialnumber)
@ -179,7 +186,7 @@ def setup(hass, config):
# Device already registered, but on a different IP
device = AXIS_DEVICES[serialnumber]
device.config.host = host
dispatcher_send(hass, DOMAIN + '_' + device.name + '_new_ip', host)
dispatcher_send(hass, DOMAIN + "_" + device.name + "_new_ip", host)
# Register discovery service
discovery.listen(hass, SERVICE_AXIS, axis_device_discovered)
@ -199,7 +206,8 @@ def setup(hass, config):
response = device.vapix.do_request(
call.data[SERVICE_CGI],
call.data[SERVICE_ACTION],
call.data[SERVICE_PARAM])
call.data[SERVICE_PARAM],
)
hass.bus.fire(SERVICE_VAPIX_CALL_RESPONSE, response)
return True
_LOGGER.info("Couldn't find device %s", call.data[CONF_NAME])
@ -207,7 +215,8 @@ def setup(hass, config):
# Register service with Home Assistant.
hass.services.register(
DOMAIN, SERVICE_VAPIX_CALL, vapix_service, schema=SERVICE_SCHEMA)
DOMAIN, SERVICE_VAPIX_CALL, vapix_service, schema=SERVICE_SCHEMA
)
return True
@ -217,21 +226,19 @@ def setup_device(hass, config, device_config):
def signal_callback(action, event):
"""Call to configure events when initialized on event stream."""
if action == 'add':
if action == "add":
event_config = {
CONF_EVENT: event,
CONF_NAME: device_config[CONF_NAME],
ATTR_LOCATION: device_config[ATTR_LOCATION],
CONF_TRIGGER_TIME: device_config[CONF_TRIGGER_TIME]
CONF_TRIGGER_TIME: device_config[CONF_TRIGGER_TIME],
}
component = event.event_platform
discovery.load_platform(
hass, component, DOMAIN, event_config, config)
discovery.load_platform(hass, component, DOMAIN, event_config, config)
event_types = list(filter(lambda x: x in device_config[CONF_INCLUDE],
EVENT_TYPES))
device_config['events'] = event_types
device_config['signal'] = signal_callback
event_types = list(filter(lambda x: x in device_config[CONF_INCLUDE], EVENT_TYPES))
device_config["events"] = event_types
device_config["signal"] = signal_callback
device = AxisDevice(hass.loop, **device_config)
device.name = device_config[CONF_NAME]
@ -241,16 +248,15 @@ def setup_device(hass, config, device_config):
return False
for component in device_config[CONF_INCLUDE]:
if component == 'camera':
if component == "camera":
camera_config = {
CONF_NAME: device_config[CONF_NAME],
CONF_HOST: device_config[CONF_HOST],
CONF_PORT: device_config[CONF_PORT],
CONF_USERNAME: device_config[CONF_USERNAME],
CONF_PASSWORD: device_config[CONF_PASSWORD]
CONF_PASSWORD: device_config[CONF_PASSWORD],
}
discovery.load_platform(
hass, component, DOMAIN, camera_config, config)
discovery.load_platform(hass, component, DOMAIN, camera_config, config)
AXIS_DEVICES[device.serial_number] = device
if event_types:
@ -264,9 +270,9 @@ class AxisDeviceEvent(Entity):
def __init__(self, event_config):
"""Initialize the event."""
self.axis_event = event_config[CONF_EVENT]
self._name = '{}_{}_{}'.format(
event_config[CONF_NAME], self.axis_event.event_type,
self.axis_event.id)
self._name = "{}_{}_{}".format(
event_config[CONF_NAME], self.axis_event.event_type, self.axis_event.id
)
self.location = event_config[ATTR_LOCATION]
self.axis_event.callback = self._update_callback
@ -295,7 +301,7 @@ class AxisDeviceEvent(Entity):
attr = {}
tripped = self.axis_event.is_tripped
attr[ATTR_TRIPPED] = 'True' if tripped else 'False'
attr[ATTR_TRIPPED] = "True" if tripped else "False"
attr[ATTR_LOCATION] = self.location

View file

@ -6,14 +6,13 @@ https://home-assistant.io/components/bbb_gpio/
"""
import logging
from homeassistant.const import (
EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP)
from homeassistant.const import EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP
REQUIREMENTS = ['Adafruit_BBIO==1.0.0']
REQUIREMENTS = ["Adafruit_BBIO==1.0.0"]
_LOGGER = logging.getLogger(__name__)
DOMAIN = 'bbb_gpio'
DOMAIN = "bbb_gpio"
def setup(hass, config):
@ -37,6 +36,7 @@ def setup_output(pin):
"""Set up a GPIO as output."""
# pylint: disable=import-error
from Adafruit_BBIO import GPIO
GPIO.setup(pin, GPIO.OUT)
@ -44,15 +44,15 @@ def setup_input(pin, pull_mode):
"""Set up a GPIO as input."""
# pylint: disable=import-error
from Adafruit_BBIO import GPIO
GPIO.setup(pin, GPIO.IN,
GPIO.PUD_DOWN if pull_mode == 'DOWN'
else GPIO.PUD_UP)
GPIO.setup(pin, GPIO.IN, GPIO.PUD_DOWN if pull_mode == "DOWN" else GPIO.PUD_UP)
def write_output(pin, value):
"""Write a value to a GPIO."""
# pylint: disable=import-error
from Adafruit_BBIO import GPIO
GPIO.output(pin, value)
@ -60,6 +60,7 @@ def read_input(pin):
"""Read a value from a GPIO."""
# pylint: disable=import-error
from Adafruit_BBIO import GPIO
return GPIO.input(pin) is GPIO.HIGH
@ -67,5 +68,5 @@ def edge_detect(pin, event_callback, bounce):
"""Add detection for RISING and FALLING events."""
# pylint: disable=import-error
from Adafruit_BBIO import GPIO
GPIO.add_event_detect(
pin, GPIO.BOTH, callback=event_callback, bouncetime=bounce)
GPIO.add_event_detect(pin, GPIO.BOTH, callback=event_callback, bouncetime=bounce)

View file

@ -12,37 +12,37 @@ import voluptuous as vol
from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.entity import Entity
from homeassistant.const import (STATE_ON, STATE_OFF)
from homeassistant.const import STATE_ON, STATE_OFF
from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa
DOMAIN = 'binary_sensor'
DOMAIN = "binary_sensor"
SCAN_INTERVAL = timedelta(seconds=30)
ENTITY_ID_FORMAT = DOMAIN + '.{}'
ENTITY_ID_FORMAT = DOMAIN + ".{}"
DEVICE_CLASSES = [
'battery', # On means low, Off means normal
'cold', # On means cold, Off means normal
'connectivity', # On means connected, Off means disconnected
'door', # On means open, Off means closed
'garage_door', # On means open, Off means closed
'gas', # On means gas detected, Off means no gas (clear)
'heat', # On means hot, Off means normal
'light', # On means light detected, Off means no light
'lock', # On means open (unlocked), Off means closed (locked)
'moisture', # On means wet, Off means dry
'motion', # On means motion detected, Off means no motion (clear)
'moving', # On means moving, Off means not moving (stopped)
'occupancy', # On means occupied, Off means not occupied (clear)
'opening', # On means open, Off means closed
'plug', # On means plugged in, Off means unplugged
'power', # On means power detected, Off means no power
'presence', # On means home, Off means away
'problem', # On means problem detected, Off means no problem (OK)
'safety', # On means unsafe, Off means safe
'smoke', # On means smoke detected, Off means no smoke (clear)
'sound', # On means sound detected, Off means no sound (clear)
'vibration', # On means vibration detected, Off means no vibration
'window', # On means open, Off means closed
"battery", # On means low, Off means normal
"cold", # On means cold, Off means normal
"connectivity", # On means connected, Off means disconnected
"door", # On means open, Off means closed
"garage_door", # On means open, Off means closed
"gas", # On means gas detected, Off means no gas (clear)
"heat", # On means hot, Off means normal
"light", # On means light detected, Off means no light
"lock", # On means open (unlocked), Off means closed (locked)
"moisture", # On means wet, Off means dry
"motion", # On means motion detected, Off means no motion (clear)
"moving", # On means moving, Off means not moving (stopped)
"occupancy", # On means occupied, Off means not occupied (clear)
"opening", # On means open, Off means closed
"plug", # On means plugged in, Off means unplugged
"power", # On means power detected, Off means no power
"presence", # On means home, Off means away
"problem", # On means problem detected, Off means no problem (OK)
"safety", # On means unsafe, Off means safe
"smoke", # On means smoke detected, Off means no smoke (clear)
"sound", # On means sound detected, Off means no sound (clear)
"vibration", # On means vibration detected, Off means no vibration
"window", # On means open, Off means closed
]
DEVICE_CLASSES_SCHEMA = vol.All(vol.Lower, vol.In(DEVICE_CLASSES))
@ -51,7 +51,8 @@ DEVICE_CLASSES_SCHEMA = vol.All(vol.Lower, vol.In(DEVICE_CLASSES))
async def async_setup(hass, config):
"""Track states and offer events for binary sensors."""
component = hass.data[DOMAIN] = EntityComponent(
logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL)
logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL
)
await component.async_setup(config)
return True

View file

@ -6,12 +6,15 @@ https://home-assistant.io/components/binary_sensor.abode/
"""
import logging
from homeassistant.components.abode import (AbodeDevice, AbodeAutomation,
DOMAIN as ABODE_DOMAIN)
from homeassistant.components.abode import (
AbodeDevice,
AbodeAutomation,
DOMAIN as ABODE_DOMAIN,
)
from homeassistant.components.binary_sensor import BinarySensorDevice
DEPENDENCIES = ['abode']
DEPENDENCIES = ["abode"]
_LOGGER = logging.getLogger(__name__)
@ -23,9 +26,13 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
data = hass.data[ABODE_DOMAIN]
device_types = [CONST.TYPE_CONNECTIVITY, CONST.TYPE_MOISTURE,
CONST.TYPE_MOTION, CONST.TYPE_OCCUPANCY,
CONST.TYPE_OPENING]
device_types = [
CONST.TYPE_CONNECTIVITY,
CONST.TYPE_MOISTURE,
CONST.TYPE_MOTION,
CONST.TYPE_OCCUPANCY,
CONST.TYPE_OPENING,
]
devices = []
for device in data.abode.get_devices(generic_type=device_types):
@ -34,13 +41,15 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
devices.append(AbodeBinarySensor(data, device))
for automation in data.abode.get_automations(
generic_type=CONST.TYPE_QUICK_ACTION):
for automation in data.abode.get_automations(generic_type=CONST.TYPE_QUICK_ACTION):
if data.is_automation_excluded(automation):
continue
devices.append(AbodeQuickActionBinarySensor(
data, automation, TIMELINE.AUTOMATION_EDIT_GROUP))
devices.append(
AbodeQuickActionBinarySensor(
data, automation, TIMELINE.AUTOMATION_EDIT_GROUP
)
)
data.devices.extend(devices)

View file

@ -11,20 +11,25 @@ import voluptuous as vol
from homeassistant.components.ads import CONF_ADS_VAR, DATA_ADS
from homeassistant.components.binary_sensor import (
DEVICE_CLASSES_SCHEMA, PLATFORM_SCHEMA, BinarySensorDevice)
DEVICE_CLASSES_SCHEMA,
PLATFORM_SCHEMA,
BinarySensorDevice,
)
from homeassistant.const import CONF_DEVICE_CLASS, CONF_NAME
import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__)
DEFAULT_NAME = 'ADS binary sensor'
DEPENDENCIES = ['ads']
DEFAULT_NAME = "ADS binary sensor"
DEPENDENCIES = ["ads"]
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_ADS_VAR): cv.string,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA,
})
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
{
vol.Required(CONF_ADS_VAR): cv.string,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA,
}
)
def setup_platform(hass, config, add_entities, discovery_info=None):
@ -46,22 +51,26 @@ class AdsBinarySensor(BinarySensorDevice):
"""Initialize ADS binary sensor."""
self._name = name
self._state = False
self._device_class = device_class or 'moving'
self._device_class = device_class or "moving"
self._ads_hub = ads_hub
self.ads_var = ads_var
@asyncio.coroutine
def async_added_to_hass(self):
"""Register device notification."""
def update(name, value):
"""Handle device notifications."""
_LOGGER.debug('Variable %s changed its value to %d', name, value)
_LOGGER.debug("Variable %s changed its value to %d", name, value)
self._state = value
self.schedule_update_ha_state()
self.hass.async_add_job(
self._ads_hub.add_device_notification,
self.ads_var, self._ads_hub.PLCTYPE_BOOL, update)
self.ads_var,
self._ads_hub.PLCTYPE_BOOL,
update,
)
@property
def name(self):

View file

@ -9,23 +9,31 @@ import logging
from homeassistant.components.binary_sensor import BinarySensorDevice
from homeassistant.components.alarmdecoder import (
ZONE_SCHEMA, CONF_ZONES, CONF_ZONE_NAME, CONF_ZONE_TYPE,
CONF_ZONE_RFID, SIGNAL_ZONE_FAULT, SIGNAL_ZONE_RESTORE,
SIGNAL_RFX_MESSAGE, SIGNAL_REL_MESSAGE, CONF_RELAY_ADDR,
CONF_RELAY_CHAN)
ZONE_SCHEMA,
CONF_ZONES,
CONF_ZONE_NAME,
CONF_ZONE_TYPE,
CONF_ZONE_RFID,
SIGNAL_ZONE_FAULT,
SIGNAL_ZONE_RESTORE,
SIGNAL_RFX_MESSAGE,
SIGNAL_REL_MESSAGE,
CONF_RELAY_ADDR,
CONF_RELAY_CHAN,
)
DEPENDENCIES = ['alarmdecoder']
DEPENDENCIES = ["alarmdecoder"]
_LOGGER = logging.getLogger(__name__)
ATTR_RF_BIT0 = 'rf_bit0'
ATTR_RF_LOW_BAT = 'rf_low_battery'
ATTR_RF_SUPERVISED = 'rf_supervised'
ATTR_RF_BIT3 = 'rf_bit3'
ATTR_RF_LOOP3 = 'rf_loop3'
ATTR_RF_LOOP2 = 'rf_loop2'
ATTR_RF_LOOP4 = 'rf_loop4'
ATTR_RF_LOOP1 = 'rf_loop1'
ATTR_RF_BIT0 = "rf_bit0"
ATTR_RF_LOW_BAT = "rf_low_battery"
ATTR_RF_SUPERVISED = "rf_supervised"
ATTR_RF_BIT3 = "rf_bit3"
ATTR_RF_LOOP3 = "rf_loop3"
ATTR_RF_LOOP2 = "rf_loop2"
ATTR_RF_LOOP4 = "rf_loop4"
ATTR_RF_LOOP1 = "rf_loop1"
def setup_platform(hass, config, add_entities, discovery_info=None):
@ -41,7 +49,8 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
relay_addr = device_config_data.get(CONF_RELAY_ADDR)
relay_chan = device_config_data.get(CONF_RELAY_CHAN)
device = AlarmDecoderBinarySensor(
zone_num, zone_name, zone_type, zone_rfid, relay_addr, relay_chan)
zone_num, zone_name, zone_type, zone_rfid, relay_addr, relay_chan
)
devices.append(device)
add_entities(devices)
@ -52,8 +61,9 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
class AlarmDecoderBinarySensor(BinarySensorDevice):
"""Representation of an AlarmDecoder binary sensor."""
def __init__(self, zone_number, zone_name, zone_type, zone_rfid,
relay_addr, relay_chan):
def __init__(
self, zone_number, zone_name, zone_type, zone_rfid, relay_addr, relay_chan
):
"""Initialize the binary_sensor."""
self._zone_number = zone_number
self._zone_type = zone_type
@ -68,16 +78,20 @@ class AlarmDecoderBinarySensor(BinarySensorDevice):
def async_added_to_hass(self):
"""Register callbacks."""
self.hass.helpers.dispatcher.async_dispatcher_connect(
SIGNAL_ZONE_FAULT, self._fault_callback)
SIGNAL_ZONE_FAULT, self._fault_callback
)
self.hass.helpers.dispatcher.async_dispatcher_connect(
SIGNAL_ZONE_RESTORE, self._restore_callback)
SIGNAL_ZONE_RESTORE, self._restore_callback
)
self.hass.helpers.dispatcher.async_dispatcher_connect(
SIGNAL_RFX_MESSAGE, self._rfx_message_callback)
SIGNAL_RFX_MESSAGE, self._rfx_message_callback
)
self.hass.helpers.dispatcher.async_dispatcher_connect(
SIGNAL_REL_MESSAGE, self._rel_message_callback)
SIGNAL_REL_MESSAGE, self._rel_message_callback
)
@property
def name(self):
@ -134,9 +148,9 @@ class AlarmDecoderBinarySensor(BinarySensorDevice):
def _rel_message_callback(self, message):
"""Update relay state."""
if (self._relay_addr == message.address and
self._relay_chan == message.channel):
_LOGGER.debug("Relay %d:%d value:%d", message.address,
message.channel, message.value)
if self._relay_addr == message.address and self._relay_chan == message.channel:
_LOGGER.debug(
"Relay %d:%d value:%d", message.address, message.channel, message.value
)
self._state = message.value
self.schedule_update_ha_state()

View file

@ -8,14 +8,18 @@ import asyncio
from homeassistant.components.binary_sensor import BinarySensorDevice
from homeassistant.components.android_ip_webcam import (
KEY_MAP, DATA_IP_WEBCAM, AndroidIPCamEntity, CONF_HOST, CONF_NAME)
KEY_MAP,
DATA_IP_WEBCAM,
AndroidIPCamEntity,
CONF_HOST,
CONF_NAME,
)
DEPENDENCIES = ['android_ip_webcam']
DEPENDENCIES = ["android_ip_webcam"]
@asyncio.coroutine
def async_setup_platform(hass, config, async_add_entities,
discovery_info=None):
def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
"""Set up the IP Webcam binary sensors."""
if discovery_info is None:
return
@ -24,8 +28,7 @@ def async_setup_platform(hass, config, async_add_entities,
name = discovery_info[CONF_NAME]
ipcam = hass.data[DATA_IP_WEBCAM][host]
async_add_entities(
[IPWebcamBinarySensor(name, host, ipcam, 'motion_active')], True)
async_add_entities([IPWebcamBinarySensor(name, host, ipcam, "motion_active")], True)
class IPWebcamBinarySensor(AndroidIPCamEntity, BinarySensorDevice):
@ -37,7 +40,7 @@ class IPWebcamBinarySensor(AndroidIPCamEntity, BinarySensorDevice):
self._sensor = sensor
self._mapped_name = KEY_MAP.get(self._sensor, self._sensor)
self._name = '{} {}'.format(name, self._mapped_name)
self._name = "{} {}".format(name, self._mapped_name)
self._state = None
self._unit = None
@ -60,4 +63,4 @@ class IPWebcamBinarySensor(AndroidIPCamEntity, BinarySensorDevice):
@property
def device_class(self):
"""Return the class of this device, from component DEVICE_CLASSES."""
return 'motion'
return "motion"

View file

@ -6,18 +6,17 @@ https://home-assistant.io/components/binary_sensor.apcupsd/
"""
import voluptuous as vol
from homeassistant.components.binary_sensor import (
BinarySensorDevice, PLATFORM_SCHEMA)
from homeassistant.components.binary_sensor import BinarySensorDevice, PLATFORM_SCHEMA
from homeassistant.const import CONF_NAME
import homeassistant.helpers.config_validation as cv
from homeassistant.components import apcupsd
DEFAULT_NAME = 'UPS Online Status'
DEFAULT_NAME = "UPS Online Status"
DEPENDENCIES = [apcupsd.DOMAIN]
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
})
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
{vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string}
)
def setup_platform(hass, config, add_entities, discovery_info=None):

View file

@ -11,9 +11,11 @@ import requests
import voluptuous as vol
from homeassistant.components.binary_sensor import (
BinarySensorDevice, PLATFORM_SCHEMA, DEVICE_CLASSES_SCHEMA)
from homeassistant.const import (
CONF_RESOURCE, CONF_PIN, CONF_NAME, CONF_DEVICE_CLASS)
BinarySensorDevice,
PLATFORM_SCHEMA,
DEVICE_CLASSES_SCHEMA,
)
from homeassistant.const import CONF_RESOURCE, CONF_PIN, CONF_NAME, CONF_DEVICE_CLASS
from homeassistant.util import Throttle
import homeassistant.helpers.config_validation as cv
@ -21,12 +23,14 @@ _LOGGER = logging.getLogger(__name__)
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=30)
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_RESOURCE): cv.url,
vol.Optional(CONF_NAME): cv.string,
vol.Required(CONF_PIN): cv.string,
vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA,
})
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
{
vol.Required(CONF_RESOURCE): cv.url,
vol.Optional(CONF_NAME): cv.string,
vol.Required(CONF_PIN): cv.string,
vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA,
}
)
def setup_platform(hass, config, add_entities, discovery_info=None):
@ -38,8 +42,9 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
try:
response = requests.get(resource, timeout=10).json()
except requests.exceptions.MissingSchema:
_LOGGER.error("Missing resource or schema in configuration. "
"Add http:// to your URL")
_LOGGER.error(
"Missing resource or schema in configuration. " "Add http:// to your URL"
)
return False
except requests.exceptions.ConnectionError:
_LOGGER.error("No route to device at %s", resource)
@ -47,9 +52,18 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
arest = ArestData(resource, pin)
add_entities([ArestBinarySensor(
arest, resource, config.get(CONF_NAME, response[CONF_NAME]),
device_class, pin)], True)
add_entities(
[
ArestBinarySensor(
arest,
resource,
config.get(CONF_NAME, response[CONF_NAME]),
device_class,
pin,
)
],
True,
)
class ArestBinarySensor(BinarySensorDevice):
@ -65,7 +79,8 @@ class ArestBinarySensor(BinarySensorDevice):
if self._pin is not None:
request = requests.get(
'{}/mode/{}/i'.format(self._resource, self._pin), timeout=10)
"{}/mode/{}/i".format(self._resource, self._pin), timeout=10
)
if request.status_code != 200:
_LOGGER.error("Can't set mode of %s", self._resource)
@ -77,7 +92,7 @@ class ArestBinarySensor(BinarySensorDevice):
@property
def is_on(self):
"""Return true if the binary sensor is on."""
return bool(self.arest.data.get('state'))
return bool(self.arest.data.get("state"))
@property
def device_class(self):
@ -102,8 +117,9 @@ class ArestData:
def update(self):
"""Get the latest data from aREST device."""
try:
response = requests.get('{}/digital/{}'.format(
self._resource, self._pin), timeout=10)
self.data = {'state': response.json()['return_value']}
response = requests.get(
"{}/digital/{}".format(self._resource, self._pin), timeout=10
)
self.data = {"state": response.json()["return_value"]}
except requests.exceptions.ConnectionError:
_LOGGER.error("No route to device '%s'", self._resource)

View file

@ -7,9 +7,9 @@ https://home-assistant.io/components/sensor.august/
from datetime import timedelta, datetime
from homeassistant.components.august import DATA_AUGUST
from homeassistant.components.binary_sensor import (BinarySensorDevice)
from homeassistant.components.binary_sensor import BinarySensorDevice
DEPENDENCIES = ['august']
DEPENDENCIES = ["august"]
SCAN_INTERVAL = timedelta(seconds=5)
@ -22,21 +22,21 @@ def _retrieve_online_state(data, doorbell):
def _retrieve_motion_state(data, doorbell):
from august.activity import ActivityType
return _activity_time_based_state(data, doorbell,
[ActivityType.DOORBELL_MOTION,
ActivityType.DOORBELL_DING])
return _activity_time_based_state(
data, doorbell, [ActivityType.DOORBELL_MOTION, ActivityType.DOORBELL_DING]
)
def _retrieve_ding_state(data, doorbell):
from august.activity import ActivityType
return _activity_time_based_state(data, doorbell,
[ActivityType.DOORBELL_DING])
return _activity_time_based_state(data, doorbell, [ActivityType.DOORBELL_DING])
def _activity_time_based_state(data, doorbell, activity_types):
"""Get the latest state of the sensor."""
latest = data.get_latest_device_activity(doorbell.device_id,
*activity_types)
latest = data.get_latest_device_activity(doorbell.device_id, *activity_types)
if latest is not None:
start = latest.activity_start_time
@ -47,9 +47,9 @@ def _activity_time_based_state(data, doorbell, activity_types):
# Sensor types: Name, device_class, state_provider
SENSOR_TYPES = {
'doorbell_ding': ['Ding', 'occupancy', _retrieve_ding_state],
'doorbell_motion': ['Motion', 'motion', _retrieve_motion_state],
'doorbell_online': ['Online', 'connectivity', _retrieve_online_state],
"doorbell_ding": ["Ding", "occupancy", _retrieve_ding_state],
"doorbell_motion": ["Motion", "motion", _retrieve_motion_state],
"doorbell_online": ["Online", "connectivity", _retrieve_online_state],
}
@ -88,8 +88,9 @@ class AugustBinarySensor(BinarySensorDevice):
@property
def name(self):
"""Return the name of the binary sensor."""
return "{} {}".format(self._doorbell.device_name,
SENSOR_TYPES[self._sensor_type][0])
return "{} {}".format(
self._doorbell.device_name, SENSOR_TYPES[self._sensor_type][0]
)
def update(self):
"""Get the latest state of the sensor."""

View file

@ -11,20 +11,18 @@ from aiohttp.hdrs import USER_AGENT
import requests
import voluptuous as vol
from homeassistant.components.binary_sensor import (
PLATFORM_SCHEMA, BinarySensorDevice)
from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorDevice
from homeassistant.const import CONF_NAME, ATTR_ATTRIBUTION
import homeassistant.helpers.config_validation as cv
from homeassistant.util import Throttle
_LOGGER = logging.getLogger(__name__)
CONF_ATTRIBUTION = "Data provided by the National Oceanic and Atmospheric" \
"Administration"
CONF_THRESHOLD = 'forecast_threshold'
CONF_ATTRIBUTION = "Data provided by the National Oceanic and Atmospheric" "Administration"
CONF_THRESHOLD = "forecast_threshold"
DEFAULT_DEVICE_CLASS = 'visible'
DEFAULT_NAME = 'Aurora Visibility'
DEFAULT_DEVICE_CLASS = "visible"
DEFAULT_NAME = "Aurora Visibility"
DEFAULT_THRESHOLD = 75
HA_USER_AGENT = "Home Assistant Aurora Tracker v.0.1.0"
@ -33,10 +31,12 @@ MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=5)
URL = "http://services.swpc.noaa.gov/text/aurora-nowcast-map.txt"
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_THRESHOLD, default=DEFAULT_THRESHOLD): cv.positive_int,
})
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
{
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_THRESHOLD, default=DEFAULT_THRESHOLD): cv.positive_int,
}
)
def setup_platform(hass, config, add_entities, discovery_info=None):
@ -49,12 +49,10 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
threshold = config.get(CONF_THRESHOLD)
try:
aurora_data = AuroraData(
hass.config.latitude, hass.config.longitude, threshold)
aurora_data = AuroraData(hass.config.latitude, hass.config.longitude, threshold)
aurora_data.update()
except requests.exceptions.HTTPError as error:
_LOGGER.error(
"Connection to aurora forecast service failed: %s", error)
_LOGGER.error("Connection to aurora forecast service failed: %s", error)
return False
add_entities([AuroraSensor(aurora_data, name)], True)
@ -71,7 +69,7 @@ class AuroraSensor(BinarySensorDevice):
@property
def name(self):
"""Return the name of the sensor."""
return '{}'.format(self._name)
return "{}".format(self._name)
@property
def is_on(self):
@ -89,8 +87,8 @@ class AuroraSensor(BinarySensorDevice):
attrs = {}
if self.aurora_data:
attrs['visibility_level'] = self.aurora_data.visibility_level
attrs['message'] = self.aurora_data.is_visible_text
attrs["visibility_level"] = self.aurora_data.visibility_level
attrs["message"] = self.aurora_data.is_visible_text
attrs[ATTR_ATTRIBUTION] = CONF_ATTRIBUTION
return attrs
@ -127,8 +125,7 @@ class AuroraData:
self.is_visible_text = "nothing's out"
except requests.exceptions.HTTPError as error:
_LOGGER.error(
"Connection to aurora forecast service failed: %s", error)
_LOGGER.error("Connection to aurora forecast service failed: %s", error)
return False
def get_aurora_forecast(self):
@ -141,9 +138,11 @@ class AuroraData:
]
# Convert lat and long for data points in table
converted_latitude = round((self.latitude / 180)
* self.number_of_latitude_intervals)
converted_longitude = round((self.longitude / 360)
* self.number_of_longitude_intervals)
converted_latitude = round(
(self.latitude / 180) * self.number_of_latitude_intervals
)
converted_longitude = round(
(self.longitude / 360) * self.number_of_longitude_intervals
)
return forecast_table[converted_latitude][converted_longitude]

View file

@ -13,7 +13,7 @@ from homeassistant.const import CONF_TRIGGER_TIME
from homeassistant.helpers.event import track_point_in_utc_time
from homeassistant.util.dt import utcnow
DEPENDENCIES = ['axis']
DEPENDENCIES = ["axis"]
_LOGGER = logging.getLogger(__name__)
@ -55,13 +55,14 @@ class AxisBinarySensor(AxisDeviceEvent, BinarySensorDevice):
# Set timer to wait until updating the state
def _delay_update(now):
"""Timer callback for sensor update."""
_LOGGER.debug("%s called delayed (%s sec) update",
self._name, self._delay)
_LOGGER.debug(
"%s called delayed (%s sec) update", self._name, self._delay
)
self.schedule_update_ha_state()
self._timer = None
self._timer = track_point_in_utc_time(
self.hass, _delay_update,
utcnow() + timedelta(seconds=self._delay))
self.hass, _delay_update, utcnow() + timedelta(seconds=self._delay)
)
else:
self.schedule_update_ha_state()

View file

@ -11,58 +11,73 @@ from collections import OrderedDict
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.components.binary_sensor import (
BinarySensorDevice, PLATFORM_SCHEMA)
from homeassistant.components.binary_sensor import BinarySensorDevice, PLATFORM_SCHEMA
from homeassistant.const import (
CONF_ABOVE, CONF_BELOW, CONF_DEVICE_CLASS, CONF_ENTITY_ID, CONF_NAME,
CONF_PLATFORM, CONF_STATE, STATE_UNKNOWN)
CONF_ABOVE,
CONF_BELOW,
CONF_DEVICE_CLASS,
CONF_ENTITY_ID,
CONF_NAME,
CONF_PLATFORM,
CONF_STATE,
STATE_UNKNOWN,
)
from homeassistant.core import callback
from homeassistant.helpers import condition
from homeassistant.helpers.event import async_track_state_change
_LOGGER = logging.getLogger(__name__)
ATTR_OBSERVATIONS = 'observations'
ATTR_PROBABILITY = 'probability'
ATTR_PROBABILITY_THRESHOLD = 'probability_threshold'
ATTR_OBSERVATIONS = "observations"
ATTR_PROBABILITY = "probability"
ATTR_PROBABILITY_THRESHOLD = "probability_threshold"
CONF_OBSERVATIONS = 'observations'
CONF_PRIOR = 'prior'
CONF_PROBABILITY_THRESHOLD = 'probability_threshold'
CONF_P_GIVEN_F = 'prob_given_false'
CONF_P_GIVEN_T = 'prob_given_true'
CONF_TO_STATE = 'to_state'
CONF_OBSERVATIONS = "observations"
CONF_PRIOR = "prior"
CONF_PROBABILITY_THRESHOLD = "probability_threshold"
CONF_P_GIVEN_F = "prob_given_false"
CONF_P_GIVEN_T = "prob_given_true"
CONF_TO_STATE = "to_state"
DEFAULT_NAME = "Bayesian Binary Sensor"
DEFAULT_PROBABILITY_THRESHOLD = 0.5
NUMERIC_STATE_SCHEMA = vol.Schema({
CONF_PLATFORM: 'numeric_state',
vol.Required(CONF_ENTITY_ID): cv.entity_id,
vol.Optional(CONF_ABOVE): vol.Coerce(float),
vol.Optional(CONF_BELOW): vol.Coerce(float),
vol.Required(CONF_P_GIVEN_T): vol.Coerce(float),
vol.Optional(CONF_P_GIVEN_F): vol.Coerce(float)
}, required=True)
NUMERIC_STATE_SCHEMA = vol.Schema(
{
CONF_PLATFORM: "numeric_state",
vol.Required(CONF_ENTITY_ID): cv.entity_id,
vol.Optional(CONF_ABOVE): vol.Coerce(float),
vol.Optional(CONF_BELOW): vol.Coerce(float),
vol.Required(CONF_P_GIVEN_T): vol.Coerce(float),
vol.Optional(CONF_P_GIVEN_F): vol.Coerce(float),
},
required=True,
)
STATE_SCHEMA = vol.Schema({
CONF_PLATFORM: CONF_STATE,
vol.Required(CONF_ENTITY_ID): cv.entity_id,
vol.Required(CONF_TO_STATE): cv.string,
vol.Required(CONF_P_GIVEN_T): vol.Coerce(float),
vol.Optional(CONF_P_GIVEN_F): vol.Coerce(float)
}, required=True)
STATE_SCHEMA = vol.Schema(
{
CONF_PLATFORM: CONF_STATE,
vol.Required(CONF_ENTITY_ID): cv.entity_id,
vol.Required(CONF_TO_STATE): cv.string,
vol.Required(CONF_P_GIVEN_T): vol.Coerce(float),
vol.Optional(CONF_P_GIVEN_F): vol.Coerce(float),
},
required=True,
)
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_DEVICE_CLASS): cv.string,
vol.Required(CONF_OBSERVATIONS):
vol.Schema(vol.All(cv.ensure_list,
[vol.Any(NUMERIC_STATE_SCHEMA, STATE_SCHEMA)])),
vol.Required(CONF_PRIOR): vol.Coerce(float),
vol.Optional(CONF_PROBABILITY_THRESHOLD,
default=DEFAULT_PROBABILITY_THRESHOLD): vol.Coerce(float),
})
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
{
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_DEVICE_CLASS): cv.string,
vol.Required(CONF_OBSERVATIONS): vol.Schema(
vol.All(cv.ensure_list, [vol.Any(NUMERIC_STATE_SCHEMA, STATE_SCHEMA)])
),
vol.Required(CONF_PRIOR): vol.Coerce(float),
vol.Optional(
CONF_PROBABILITY_THRESHOLD, default=DEFAULT_PROBABILITY_THRESHOLD
): vol.Coerce(float),
}
)
def update_probability(prior, prob_true, prob_false):
@ -75,8 +90,7 @@ def update_probability(prior, prob_true, prob_false):
@asyncio.coroutine
def async_setup_platform(hass, config, async_add_entities,
discovery_info=None):
def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
"""Set up the Bayesian Binary sensor."""
name = config.get(CONF_NAME)
observations = config.get(CONF_OBSERVATIONS)
@ -84,17 +98,20 @@ def async_setup_platform(hass, config, async_add_entities,
probability_threshold = config.get(CONF_PROBABILITY_THRESHOLD)
device_class = config.get(CONF_DEVICE_CLASS)
async_add_entities([
BayesianBinarySensor(
name, prior, observations, probability_threshold, device_class)
], True)
async_add_entities(
[
BayesianBinarySensor(
name, prior, observations, probability_threshold, device_class
)
],
True,
)
class BayesianBinarySensor(BinarySensorDevice):
"""Representation of a Bayesian sensor."""
def __init__(self, name, prior, observations, probability_threshold,
device_class):
def __init__(self, name, prior, observations, probability_threshold, device_class):
"""Initialize the Bayesian sensor."""
self._name = name
self._observations = observations
@ -106,25 +123,25 @@ class BayesianBinarySensor(BinarySensorDevice):
self.current_obs = OrderedDict({})
to_observe = set(obs['entity_id'] for obs in self._observations)
to_observe = set(obs["entity_id"] for obs in self._observations)
self.entity_obs = dict.fromkeys(to_observe, [])
for ind, obs in enumerate(self._observations):
obs['id'] = ind
self.entity_obs[obs['entity_id']].append(obs)
obs["id"] = ind
self.entity_obs[obs["entity_id"]].append(obs)
self.watchers = {
'numeric_state': self._process_numeric_state,
'state': self._process_state
"numeric_state": self._process_numeric_state,
"state": self._process_state,
}
@asyncio.coroutine
def async_added_to_hass(self):
"""Call when entity about to be added."""
@callback
def async_threshold_sensor_state_listener(entity, old_state,
new_state):
def async_threshold_sensor_state_listener(entity, old_state, new_state):
"""Handle sensor state changes."""
if new_state.state == STATE_UNKNOWN:
return
@ -132,34 +149,33 @@ class BayesianBinarySensor(BinarySensorDevice):
entity_obs_list = self.entity_obs[entity]
for entity_obs in entity_obs_list:
platform = entity_obs['platform']
platform = entity_obs["platform"]
self.watchers[platform](entity_obs)
prior = self.prior
for obs in self.current_obs.values():
prior = update_probability(
prior, obs['prob_true'], obs['prob_false'])
prior = update_probability(prior, obs["prob_true"], obs["prob_false"])
self.probability = prior
self.hass.async_add_job(self.async_update_ha_state, True)
entities = [obs['entity_id'] for obs in self._observations]
entities = [obs["entity_id"] for obs in self._observations]
async_track_state_change(
self.hass, entities, async_threshold_sensor_state_listener)
self.hass, entities, async_threshold_sensor_state_listener
)
def _update_current_obs(self, entity_observation, should_trigger):
"""Update current observation."""
obs_id = entity_observation['id']
obs_id = entity_observation["id"]
if should_trigger:
prob_true = entity_observation['prob_given_true']
prob_false = entity_observation.get(
'prob_given_false', 1 - prob_true)
prob_true = entity_observation["prob_given_true"]
prob_false = entity_observation.get("prob_given_false", 1 - prob_true)
self.current_obs[obs_id] = {
'prob_true': prob_true,
'prob_false': prob_false
"prob_true": prob_true,
"prob_false": prob_false,
}
else:
@ -167,21 +183,26 @@ class BayesianBinarySensor(BinarySensorDevice):
def _process_numeric_state(self, entity_observation):
"""Add entity to current_obs if numeric state conditions are met."""
entity = entity_observation['entity_id']
entity = entity_observation["entity_id"]
should_trigger = condition.async_numeric_state(
self.hass, entity,
entity_observation.get('below'),
entity_observation.get('above'), None, entity_observation)
self.hass,
entity,
entity_observation.get("below"),
entity_observation.get("above"),
None,
entity_observation,
)
self._update_current_obs(entity_observation, should_trigger)
def _process_state(self, entity_observation):
"""Add entity to current observations if state conditions are met."""
entity = entity_observation['entity_id']
entity = entity_observation["entity_id"]
should_trigger = condition.state(
self.hass, entity, entity_observation.get('to_state'))
self.hass, entity, entity_observation.get("to_state")
)
self._update_current_obs(entity_observation, should_trigger)

View file

@ -9,36 +9,35 @@ import logging
import voluptuous as vol
from homeassistant.components import bbb_gpio
from homeassistant.components.binary_sensor import (
BinarySensorDevice, PLATFORM_SCHEMA)
from homeassistant.const import (DEVICE_DEFAULT_NAME, CONF_NAME)
from homeassistant.components.binary_sensor import BinarySensorDevice, PLATFORM_SCHEMA
from homeassistant.const import DEVICE_DEFAULT_NAME, CONF_NAME
import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__)
DEPENDENCIES = ['bbb_gpio']
DEPENDENCIES = ["bbb_gpio"]
CONF_PINS = 'pins'
CONF_BOUNCETIME = 'bouncetime'
CONF_INVERT_LOGIC = 'invert_logic'
CONF_PULL_MODE = 'pull_mode'
CONF_PINS = "pins"
CONF_BOUNCETIME = "bouncetime"
CONF_INVERT_LOGIC = "invert_logic"
CONF_PULL_MODE = "pull_mode"
DEFAULT_BOUNCETIME = 50
DEFAULT_INVERT_LOGIC = False
DEFAULT_PULL_MODE = 'UP'
DEFAULT_PULL_MODE = "UP"
PIN_SCHEMA = vol.Schema({
vol.Required(CONF_NAME): cv.string,
vol.Optional(CONF_BOUNCETIME, default=DEFAULT_BOUNCETIME): cv.positive_int,
vol.Optional(CONF_INVERT_LOGIC, default=DEFAULT_INVERT_LOGIC): cv.boolean,
vol.Optional(CONF_PULL_MODE, default=DEFAULT_PULL_MODE):
vol.In(['UP', 'DOWN'])
})
PIN_SCHEMA = vol.Schema(
{
vol.Required(CONF_NAME): cv.string,
vol.Optional(CONF_BOUNCETIME, default=DEFAULT_BOUNCETIME): cv.positive_int,
vol.Optional(CONF_INVERT_LOGIC, default=DEFAULT_INVERT_LOGIC): cv.boolean,
vol.Optional(CONF_PULL_MODE, default=DEFAULT_PULL_MODE): vol.In(["UP", "DOWN"]),
}
)
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_PINS, default={}):
vol.Schema({cv.string: PIN_SCHEMA}),
})
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
{vol.Required(CONF_PINS, default={}): vol.Schema({cv.string: PIN_SCHEMA})}
)
def setup_platform(hass, config, add_entities, discovery_info=None):

View file

@ -7,7 +7,7 @@ https://home-assistant.io/components/binary_sensor.blink/
from homeassistant.components.blink import DOMAIN
from homeassistant.components.binary_sensor import BinarySensorDevice
DEPENDENCIES = ['blink']
DEPENDENCIES = ["blink"]
def setup_platform(hass, config, add_entities, discovery_info=None):
@ -28,7 +28,7 @@ class BlinkCameraMotionSensor(BinarySensorDevice):
def __init__(self, name, data):
"""Initialize the sensor."""
self._name = 'blink_' + name + '_motion_enabled'
self._name = "blink_" + name + "_motion_enabled"
self._camera_name = name
self.data = data
self._state = self.data.cameras[self._camera_name].armed
@ -54,7 +54,7 @@ class BlinkSystemSensor(BinarySensorDevice):
def __init__(self, data):
"""Initialize the sensor."""
self._name = 'blink armed status'
self._name = "blink armed status"
self.data = data
self._state = self.data.arm

View file

@ -8,24 +8,23 @@ import logging
import voluptuous as vol
from homeassistant.components.binary_sensor import (
BinarySensorDevice, PLATFORM_SCHEMA)
from homeassistant.components.binary_sensor import BinarySensorDevice, PLATFORM_SCHEMA
from homeassistant.const import CONF_MONITORED_CONDITIONS
import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__)
DEPENDENCIES = ['bloomsky']
DEPENDENCIES = ["bloomsky"]
SENSOR_TYPES = {
'Rain': 'moisture',
'Night': None,
}
SENSOR_TYPES = {"Rain": "moisture", "Night": None}
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_MONITORED_CONDITIONS, default=list(SENSOR_TYPES)):
vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]),
})
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
{
vol.Optional(CONF_MONITORED_CONDITIONS, default=list(SENSOR_TYPES)): vol.All(
cv.ensure_list, [vol.In(SENSOR_TYPES)]
)
}
)
def setup_platform(hass, config, add_entities, discovery_info=None):
@ -36,8 +35,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
for device in bloomsky.BLOOMSKY.devices.values():
for variable in sensors:
add_entities(
[BloomSkySensor(bloomsky.BLOOMSKY, device, variable)], True)
add_entities([BloomSkySensor(bloomsky.BLOOMSKY, device, variable)], True)
class BloomSkySensor(BinarySensorDevice):
@ -46,9 +44,9 @@ class BloomSkySensor(BinarySensorDevice):
def __init__(self, bs, device, sensor_name):
"""Initialize a BloomSky binary sensor."""
self._bloomsky = bs
self._device_id = device['DeviceID']
self._device_id = device["DeviceID"]
self._sensor_name = sensor_name
self._name = '{} {}'.format(device['DeviceName'], sensor_name)
self._name = "{} {}".format(device["DeviceName"], sensor_name)
self._state = None
@property
@ -70,5 +68,4 @@ class BloomSkySensor(BinarySensorDevice):
"""Request an update from the BloomSky API."""
self._bloomsky.refresh_devices()
self._state = \
self._bloomsky.devices[self._device_id]['Data'][self._sensor_name]
self._state = self._bloomsky.devices[self._device_id]["Data"][self._sensor_name]

View file

@ -10,22 +10,22 @@ import logging
from homeassistant.components.binary_sensor import BinarySensorDevice
from homeassistant.components.bmw_connected_drive import DOMAIN as BMW_DOMAIN
DEPENDENCIES = ['bmw_connected_drive']
DEPENDENCIES = ["bmw_connected_drive"]
_LOGGER = logging.getLogger(__name__)
SENSOR_TYPES = {
'lids': ['Doors', 'opening'],
'windows': ['Windows', 'opening'],
'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']
"lids": ["Doors", "opening"],
"windows": ["Windows", "opening"],
"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']
"charging_status": ["Charging status", "power"],
"connection_status": ["Connection status", "plug"],
}
SENSOR_TYPES_ELEC.update(SENSOR_TYPES)
@ -34,22 +34,23 @@ SENSOR_TYPES_ELEC.update(SENSOR_TYPES)
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the BMW sensors."""
accounts = hass.data[BMW_DOMAIN]
_LOGGER.debug('Found BMW accounts: %s',
', '.join([a.name for a in accounts]))
_LOGGER.debug("Found BMW accounts: %s", ", ".join([a.name for a in accounts]))
devices = []
for account in accounts:
for vehicle in account.account.vehicles:
if vehicle.has_hv_battery:
_LOGGER.debug('BMW with a high voltage 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])
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')
_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])
device = BMWConnectedDriveSensor(
account, vehicle, key, value[0], value[1]
)
devices.append(device)
add_entities(devices, True)
@ -57,14 +58,13 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
class BMWConnectedDriveSensor(BinarySensorDevice):
"""Representation of a BMW vehicle binary sensor."""
def __init__(self, account, vehicle, attribute: str, sensor_name,
device_class):
def __init__(self, account, vehicle, attribute: str, sensor_name, device_class):
"""Constructor."""
self._account = account
self._vehicle = vehicle
self._attribute = attribute
self._name = '{} {}'.format(self._vehicle.name, self._attribute)
self._unique_id = '{}-{}'.format(self._vehicle.vin, self._attribute)
self._name = "{} {}".format(self._vehicle.name, self._attribute)
self._unique_id = "{}-{}".format(self._vehicle.vin, self._attribute)
self._sensor_name = sensor_name
self._device_class = device_class
self._state = None
@ -101,39 +101,37 @@ class BMWConnectedDriveSensor(BinarySensorDevice):
def device_state_attributes(self):
"""Return the state attributes of the binary sensor."""
vehicle_state = self._vehicle.state
result = {
'car': self._vehicle.name
}
result = {"car": self._vehicle.name}
if self._attribute == 'lids':
if self._attribute == "lids":
for lid in vehicle_state.lids:
result[lid.name] = lid.state.value
elif self._attribute == 'windows':
elif self._attribute == "windows":
for window in vehicle_state.windows:
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':
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':
elif self._attribute == "check_control_messages":
check_control_messages = vehicle_state.check_control_messages
if not check_control_messages:
result['check_control_messages'] = 'OK'
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
result["check_control_messages"] = check_control_messages
elif self._attribute == "charging_status":
result["charging_status"] = vehicle_state.charging_status.value
# pylint: disable=protected-access
result['last_charging_end_result'] = \
vehicle_state._attributes['lastChargingEndResult']
if self._attribute == 'connection_status':
result["last_charging_end_result"] = vehicle_state._attributes[
"lastChargingEndResult"
]
if self._attribute == "connection_status":
# pylint: disable=protected-access
result['connection_status'] = \
vehicle_state._attributes['connectionStatus']
result["connection_status"] = vehicle_state._attributes["connectionStatus"]
return sorted(result.items())
@ -141,49 +139,52 @@ class BMWConnectedDriveSensor(BinarySensorDevice):
"""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
if self._attribute == 'lids':
if self._attribute == "lids":
_LOGGER.debug("Status of lid: %s", vehicle_state.all_lids_closed)
self._state = not vehicle_state.all_lids_closed
if self._attribute == 'windows':
if self._attribute == "windows":
self._state = not vehicle_state.all_windows_closed
# device class safety: On means unsafe, Off means safe
if self._attribute == 'door_lock_state':
if self._attribute == "door_lock_state":
# Possible values: LOCKED, SECURED, SELECTIVE_LOCKED, UNLOCKED
self._state = vehicle_state.door_lock_state not in \
[LockState.LOCKED, LockState.SECURED]
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':
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':
if self._attribute == "condition_based_services":
self._state = not vehicle_state.are_all_cbs_ok
if self._attribute == 'check_control_messages':
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]
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':
if self._attribute == "connection_status":
# pylint: disable=protected-access
self._state = (vehicle_state._attributes['connectionStatus'] ==
'CONNECTED')
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
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')
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)
result["{} distance".format(service_type)] = "{} km".format(
report.due_distance
)
return result
def update_callback(self):

View file

@ -11,33 +11,42 @@ import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.components.binary_sensor import (
BinarySensorDevice, DEVICE_CLASSES_SCHEMA, PLATFORM_SCHEMA)
BinarySensorDevice,
DEVICE_CLASSES_SCHEMA,
PLATFORM_SCHEMA,
)
from homeassistant.components.sensor.command_line import CommandSensorData
from homeassistant.const import (
CONF_PAYLOAD_OFF, CONF_PAYLOAD_ON, CONF_NAME, CONF_VALUE_TEMPLATE,
CONF_COMMAND, CONF_DEVICE_CLASS)
CONF_PAYLOAD_OFF,
CONF_PAYLOAD_ON,
CONF_NAME,
CONF_VALUE_TEMPLATE,
CONF_COMMAND,
CONF_DEVICE_CLASS,
)
_LOGGER = logging.getLogger(__name__)
DEFAULT_NAME = 'Binary Command Sensor'
DEFAULT_PAYLOAD_ON = 'ON'
DEFAULT_PAYLOAD_OFF = 'OFF'
DEFAULT_NAME = "Binary Command Sensor"
DEFAULT_PAYLOAD_ON = "ON"
DEFAULT_PAYLOAD_OFF = "OFF"
SCAN_INTERVAL = timedelta(seconds=60)
CONF_COMMAND_TIMEOUT = 'command_timeout'
CONF_COMMAND_TIMEOUT = "command_timeout"
DEFAULT_TIMEOUT = 15
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_COMMAND): cv.string,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_PAYLOAD_OFF, default=DEFAULT_PAYLOAD_OFF): cv.string,
vol.Optional(CONF_PAYLOAD_ON, default=DEFAULT_PAYLOAD_ON): cv.string,
vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA,
vol.Optional(CONF_VALUE_TEMPLATE): cv.template,
vol.Optional(
CONF_COMMAND_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int,
})
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
{
vol.Required(CONF_COMMAND): cv.string,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_PAYLOAD_OFF, default=DEFAULT_PAYLOAD_OFF): cv.string,
vol.Optional(CONF_PAYLOAD_ON, default=DEFAULT_PAYLOAD_ON): cv.string,
vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA,
vol.Optional(CONF_VALUE_TEMPLATE): cv.template,
vol.Optional(CONF_COMMAND_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int,
}
)
def setup_platform(hass, config, add_entities, discovery_info=None):
@ -53,16 +62,22 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
value_template.hass = hass
data = CommandSensorData(hass, command, command_timeout)
add_entities([CommandBinarySensor(
hass, data, name, device_class, payload_on, payload_off,
value_template)], True)
add_entities(
[
CommandBinarySensor(
hass, data, name, device_class, payload_on, payload_off, value_template
)
],
True,
)
class CommandBinarySensor(BinarySensorDevice):
"""Representation of a command line binary sensor."""
def __init__(self, hass, data, name, device_class, payload_on,
payload_off, value_template):
def __init__(
self, hass, data, name, device_class, payload_on, payload_off, value_template
):
"""Initialize the Command line binary sensor."""
self._hass = hass
self.data = data
@ -83,7 +98,7 @@ class CommandBinarySensor(BinarySensorDevice):
"""Return true if the binary sensor is on."""
return self._state
@ property
@property
def device_class(self):
"""Return the class of the binary sensor."""
return self._device_class
@ -94,8 +109,7 @@ class CommandBinarySensor(BinarySensorDevice):
value = self.data.value
if self._value_template is not None:
value = self._value_template.render_with_possible_json_value(
value, False)
value = self._value_template.render_with_possible_json_value(value, False)
if value == self._payload_on:
self._state = True
elif value == self._payload_off:

View file

@ -11,35 +11,39 @@ import requests
import voluptuous as vol
from homeassistant.components.binary_sensor import (
BinarySensorDevice, PLATFORM_SCHEMA, DEVICE_CLASSES)
from homeassistant.const import (CONF_HOST, CONF_PORT)
BinarySensorDevice,
PLATFORM_SCHEMA,
DEVICE_CLASSES,
)
from homeassistant.const import CONF_HOST, CONF_PORT
import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['concord232==0.15']
REQUIREMENTS = ["concord232==0.15"]
_LOGGER = logging.getLogger(__name__)
CONF_EXCLUDE_ZONES = 'exclude_zones'
CONF_ZONE_TYPES = 'zone_types'
CONF_EXCLUDE_ZONES = "exclude_zones"
CONF_ZONE_TYPES = "zone_types"
DEFAULT_HOST = 'localhost'
DEFAULT_NAME = 'Alarm'
DEFAULT_PORT = '5007'
DEFAULT_HOST = "localhost"
DEFAULT_NAME = "Alarm"
DEFAULT_PORT = "5007"
DEFAULT_SSL = False
SCAN_INTERVAL = datetime.timedelta(seconds=10)
ZONE_TYPES_SCHEMA = vol.Schema({
cv.positive_int: vol.In(DEVICE_CLASSES),
})
ZONE_TYPES_SCHEMA = vol.Schema({cv.positive_int: vol.In(DEVICE_CLASSES)})
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_EXCLUDE_ZONES, default=[]):
vol.All(cv.ensure_list, [cv.positive_int]),
vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string,
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
vol.Optional(CONF_ZONE_TYPES, default={}): ZONE_TYPES_SCHEMA,
})
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
{
vol.Optional(CONF_EXCLUDE_ZONES, default=[]): vol.All(
cv.ensure_list, [cv.positive_int]
),
vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string,
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
vol.Optional(CONF_ZONE_TYPES, default={}): ZONE_TYPES_SCHEMA,
}
)
def setup_platform(hass, config, add_entities, discovery_info=None):
@ -54,7 +58,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
try:
_LOGGER.debug("Initializing client")
client = concord232_client.Client('http://{}:{}'.format(host, port))
client = concord232_client.Client("http://{}:{}".format(host, port))
client.zones = client.list_zones()
client.last_zone_update = datetime.datetime.now()
@ -67,15 +71,17 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
# name mapping to different sensors in an unpredictable way. Sort
# the zones by zone number to prevent this.
client.zones.sort(key=lambda zone: zone['number'])
client.zones.sort(key=lambda zone: zone["number"])
for zone in client.zones:
_LOGGER.info("Loading Zone found: %s", zone['name'])
if zone['number'] not in exclude:
_LOGGER.info("Loading Zone found: %s", zone["name"])
if zone["number"] not in exclude:
sensors.append(
Concord232ZoneSensor(
hass, client, zone, zone_types.get(
zone['number'], get_opening_type(zone))
hass,
client,
zone,
zone_types.get(zone["number"], get_opening_type(zone)),
)
)
@ -84,15 +90,15 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
def get_opening_type(zone):
"""Return the result of the type guessing from name."""
if 'MOTION' in zone['name']:
return 'motion'
if 'KEY' in zone['name']:
return 'safety'
if 'SMOKE' in zone['name']:
return 'smoke'
if 'WATER' in zone['name']:
return 'water'
return 'opening'
if "MOTION" in zone["name"]:
return "motion"
if "KEY" in zone["name"]:
return "safety"
if "SMOKE" in zone["name"]:
return "smoke"
if "WATER" in zone["name"]:
return "water"
return "opening"
class Concord232ZoneSensor(BinarySensorDevice):
@ -103,7 +109,7 @@ class Concord232ZoneSensor(BinarySensorDevice):
self._hass = hass
self._client = client
self._zone = zone
self._number = zone['number']
self._number = zone["number"]
self._zone_type = zone_type
@property
@ -119,13 +125,13 @@ class Concord232ZoneSensor(BinarySensorDevice):
@property
def name(self):
"""Return the name of the binary sensor."""
return self._zone['name']
return self._zone["name"]
@property
def is_on(self):
"""Return true if the binary sensor is on."""
# True means "faulted" or "open" or "abnormal state"
return bool(self._zone['state'] != 'Normal')
return bool(self._zone["state"] != "Normal")
def update(self):
"""Get updated stats from API."""
@ -134,8 +140,9 @@ class Concord232ZoneSensor(BinarySensorDevice):
if last_update > datetime.timedelta(seconds=1):
self._client.zones = self._client.list_zones()
self._client.last_zone_update = datetime.datetime.now()
_LOGGER.debug("Updated from zone: %s", self._zone['name'])
_LOGGER.debug("Updated from zone: %s", self._zone["name"])
if hasattr(self._client, 'zones'):
self._zone = next((x for x in self._client.zones
if x['number'] == self._number), None)
if hasattr(self._client, "zones"):
self._zone = next(
(x for x in self._client.zones if x["number"] == self._number), None
)

View file

@ -6,38 +6,47 @@ https://home-assistant.io/components/binary_sensor.deconz/
"""
from homeassistant.components.binary_sensor import BinarySensorDevice
from homeassistant.components.deconz.const import (
ATTR_DARK, ATTR_ON, CONF_ALLOW_CLIP_SENSOR, DOMAIN as DATA_DECONZ,
DATA_DECONZ_ID, DATA_DECONZ_UNSUB, DECONZ_DOMAIN)
ATTR_DARK,
ATTR_ON,
CONF_ALLOW_CLIP_SENSOR,
DOMAIN as DATA_DECONZ,
DATA_DECONZ_ID,
DATA_DECONZ_UNSUB,
DECONZ_DOMAIN,
)
from homeassistant.const import ATTR_BATTERY_LEVEL
from homeassistant.core import callback
from homeassistant.helpers.device_registry import CONNECTION_ZIGBEE
from homeassistant.helpers.dispatcher import async_dispatcher_connect
DEPENDENCIES = ['deconz']
DEPENDENCIES = ["deconz"]
async def async_setup_platform(hass, config, async_add_entities,
discovery_info=None):
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
"""Old way of setting up deCONZ binary sensors."""
pass
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up the deCONZ binary sensor."""
@callback
def async_add_sensor(sensors):
"""Add binary sensor from deCONZ."""
from pydeconz.sensor import DECONZ_BINARY_SENSOR
entities = []
allow_clip_sensor = config_entry.data.get(CONF_ALLOW_CLIP_SENSOR, True)
for sensor in sensors:
if sensor.type in DECONZ_BINARY_SENSOR and \
not (not allow_clip_sensor and sensor.type.startswith('CLIP')):
if sensor.type in DECONZ_BINARY_SENSOR and not (
not allow_clip_sensor and sensor.type.startswith("CLIP")
):
entities.append(DeconzBinarySensor(sensor))
async_add_entities(entities, True)
hass.data[DATA_DECONZ_UNSUB].append(
async_dispatcher_connect(hass, 'deconz_new_sensor', async_add_sensor))
async_dispatcher_connect(hass, "deconz_new_sensor", async_add_sensor)
)
async_add_sensor(hass.data[DATA_DECONZ].sensors.values())
@ -66,10 +75,12 @@ class DeconzBinarySensor(BinarySensorDevice):
If reason is that state is updated,
or reachable has changed or battery has changed.
"""
if reason['state'] or \
'reachable' in reason['attr'] or \
'battery' in reason['attr'] or \
'on' in reason['attr']:
if (
reason["state"]
or "reachable" in reason["attr"]
or "battery" in reason["attr"]
or "on" in reason["attr"]
):
self.async_schedule_update_ha_state()
@property
@ -111,6 +122,7 @@ class DeconzBinarySensor(BinarySensorDevice):
def device_state_attributes(self):
"""Return the state attributes of the sensor."""
from pydeconz.sensor import PRESENCE
attr = {}
if self._sensor.battery:
attr[ATTR_BATTERY_LEVEL] = self._sensor.battery
@ -123,15 +135,14 @@ class DeconzBinarySensor(BinarySensorDevice):
@property
def device_info(self):
"""Return a device description for device registry."""
if (self._sensor.uniqueid is None or
self._sensor.uniqueid.count(':') != 7):
if self._sensor.uniqueid is None or self._sensor.uniqueid.count(":") != 7:
return None
serial = self._sensor.uniqueid.split('-', 1)[0]
serial = self._sensor.uniqueid.split("-", 1)[0]
return {
'connections': {(CONNECTION_ZIGBEE, serial)},
'identifiers': {(DECONZ_DOMAIN, serial)},
'manufacturer': self._sensor.manufacturer,
'model': self._sensor.modelid,
'name': self._sensor.name,
'sw_version': self._sensor.swversion,
"connections": {(CONNECTION_ZIGBEE, serial)},
"identifiers": {(DECONZ_DOMAIN, serial)},
"manufacturer": self._sensor.manufacturer,
"model": self._sensor.modelid,
"name": self._sensor.name,
"sw_version": self._sensor.swversion,
}

View file

@ -9,10 +9,12 @@ from homeassistant.components.binary_sensor import BinarySensorDevice
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the Demo binary sensor platform."""
add_entities([
DemoBinarySensor('Basement Floor Wet', False, 'moisture'),
DemoBinarySensor('Movement Backyard', True, 'motion'),
])
add_entities(
[
DemoBinarySensor("Basement Floor Wet", False, "moisture"),
DemoBinarySensor("Movement Backyard", True, "motion"),
]
)
class DemoBinarySensor(BinarySensorDevice):

View file

@ -9,23 +9,32 @@ import logging
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.components.binary_sensor import (
BinarySensorDevice, PLATFORM_SCHEMA)
from homeassistant.components.binary_sensor import BinarySensorDevice, PLATFORM_SCHEMA
from homeassistant.components.digital_ocean import (
CONF_DROPLETS, ATTR_CREATED_AT, ATTR_DROPLET_ID, ATTR_DROPLET_NAME,
ATTR_FEATURES, ATTR_IPV4_ADDRESS, ATTR_IPV6_ADDRESS, ATTR_MEMORY,
ATTR_REGION, ATTR_VCPUS, CONF_ATTRIBUTION, DATA_DIGITAL_OCEAN)
CONF_DROPLETS,
ATTR_CREATED_AT,
ATTR_DROPLET_ID,
ATTR_DROPLET_NAME,
ATTR_FEATURES,
ATTR_IPV4_ADDRESS,
ATTR_IPV6_ADDRESS,
ATTR_MEMORY,
ATTR_REGION,
ATTR_VCPUS,
CONF_ATTRIBUTION,
DATA_DIGITAL_OCEAN,
)
from homeassistant.const import ATTR_ATTRIBUTION
_LOGGER = logging.getLogger(__name__)
DEFAULT_NAME = 'Droplet'
DEFAULT_DEVICE_CLASS = 'moving'
DEPENDENCIES = ['digital_ocean']
DEFAULT_NAME = "Droplet"
DEFAULT_DEVICE_CLASS = "moving"
DEPENDENCIES = ["digital_ocean"]
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_DROPLETS): vol.All(cv.ensure_list, [cv.string]),
})
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
{vol.Required(CONF_DROPLETS): vol.All(cv.ensure_list, [cv.string])}
)
def setup_platform(hass, config, add_entities, discovery_info=None):
@ -65,7 +74,7 @@ class DigitalOceanBinarySensor(BinarySensorDevice):
@property
def is_on(self):
"""Return true if the binary sensor is on."""
return self.data.status == 'active'
return self.data.status == "active"
@property
def device_class(self):
@ -84,7 +93,7 @@ class DigitalOceanBinarySensor(BinarySensorDevice):
ATTR_IPV4_ADDRESS: self.data.ip_address,
ATTR_IPV6_ADDRESS: self.data.ip_v6_address,
ATTR_MEMORY: self.data.memory,
ATTR_REGION: self.data.region['name'],
ATTR_REGION: self.data.region["name"],
ATTR_VCPUS: self.data.vcpus,
}

View file

@ -7,9 +7,9 @@ https://home-assistant.io/components/binary_sensor.ecobee/
from homeassistant.components import ecobee
from homeassistant.components.binary_sensor import BinarySensorDevice
DEPENDENCIES = ['ecobee']
DEPENDENCIES = ["ecobee"]
ECOBEE_CONFIG_FILE = 'ecobee.conf'
ECOBEE_CONFIG_FILE = "ecobee.conf"
def setup_platform(hass, config, add_entities, discovery_info=None):
@ -20,11 +20,11 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
dev = list()
for index in range(len(data.ecobee.thermostats)):
for sensor in data.ecobee.get_remote_sensors(index):
for item in sensor['capability']:
if item['type'] != 'occupancy':
for item in sensor["capability"]:
if item["type"] != "occupancy":
continue
dev.append(EcobeeBinarySensor(sensor['name'], index))
dev.append(EcobeeBinarySensor(sensor["name"], index))
add_entities(dev, True)
@ -34,11 +34,11 @@ class EcobeeBinarySensor(BinarySensorDevice):
def __init__(self, sensor_name, sensor_index):
"""Initialize the sensor."""
self._name = sensor_name + ' Occupancy'
self._name = sensor_name + " Occupancy"
self.sensor_name = sensor_name
self.index = sensor_index
self._state = None
self._device_class = 'occupancy'
self._device_class = "occupancy"
@property
def name(self):
@ -48,7 +48,7 @@ class EcobeeBinarySensor(BinarySensorDevice):
@property
def is_on(self):
"""Return the status of the sensor."""
return self._state == 'true'
return self._state == "true"
@property
def device_class(self):
@ -60,7 +60,6 @@ class EcobeeBinarySensor(BinarySensorDevice):
data = ecobee.NETWORK
data.update()
for sensor in data.ecobee.get_remote_sensors(self.index):
for item in sensor['capability']:
if (item['type'] == 'occupancy' and
self.sensor_name == sensor['name']):
self._state = item['value']
for item in sensor["capability"]:
if item["type"] == "occupancy" and self.sensor_name == sensor["name"]:
self._state = item["value"]

View file

@ -9,21 +9,21 @@ import logging
from homeassistant.components.binary_sensor import BinarySensorDevice
from homeassistant.const import STATE_ON, STATE_OFF
from homeassistant.components.egardia import (
EGARDIA_DEVICE, ATTR_DISCOVER_DEVICES)
from homeassistant.components.egardia import EGARDIA_DEVICE, ATTR_DISCOVER_DEVICES
_LOGGER = logging.getLogger(__name__)
DEPENDENCIES = ['egardia']
EGARDIA_TYPE_TO_DEVICE_CLASS = {'IR Sensor': 'motion',
'Door Contact': 'opening',
'IR': 'motion'}
DEPENDENCIES = ["egardia"]
EGARDIA_TYPE_TO_DEVICE_CLASS = {
"IR Sensor": "motion",
"Door Contact": "opening",
"IR": "motion",
}
@asyncio.coroutine
def async_setup_platform(hass, config, async_add_entities,
discovery_info=None):
def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
"""Initialize the platform."""
if (discovery_info is None or
discovery_info[ATTR_DISCOVER_DEVICES] is None):
if discovery_info is None or discovery_info[ATTR_DISCOVER_DEVICES] is None:
return
disc_info = discovery_info[ATTR_DISCOVER_DEVICES]
@ -31,14 +31,17 @@ def async_setup_platform(hass, config, async_add_entities,
async_add_entities(
(
EgardiaBinarySensor(
sensor_id=disc_info[sensor]['id'],
name=disc_info[sensor]['name'],
sensor_id=disc_info[sensor]["id"],
name=disc_info[sensor]["name"],
egardia_system=hass.data[EGARDIA_DEVICE],
device_class=EGARDIA_TYPE_TO_DEVICE_CLASS.get(
disc_info[sensor]['type'], None)
disc_info[sensor]["type"], None
),
)
for sensor in disc_info
), True)
),
True,
)
class EgardiaBinarySensor(BinarySensorDevice):

View file

@ -8,20 +8,23 @@ import logging
from homeassistant.components.binary_sensor import BinarySensorDevice
from homeassistant.components.eight_sleep import (
DATA_EIGHT, EightSleepHeatEntity, CONF_BINARY_SENSORS, NAME_MAP)
DATA_EIGHT,
EightSleepHeatEntity,
CONF_BINARY_SENSORS,
NAME_MAP,
)
_LOGGER = logging.getLogger(__name__)
DEPENDENCIES = ['eight_sleep']
DEPENDENCIES = ["eight_sleep"]
async def async_setup_platform(hass, config, async_add_entities,
discovery_info=None):
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
"""Set up the eight sleep binary sensor."""
if discovery_info is None:
return
name = 'Eight'
name = "Eight"
sensors = discovery_info[CONF_BINARY_SENSORS]
eight = hass.data[DATA_EIGHT]
@ -42,15 +45,19 @@ class EightHeatSensor(EightSleepHeatEntity, BinarySensorDevice):
self._sensor = sensor
self._mapped_name = NAME_MAP.get(self._sensor, self._sensor)
self._name = '{} {}'.format(name, self._mapped_name)
self._name = "{} {}".format(name, self._mapped_name)
self._state = None
self._side = self._sensor.split('_')[0]
self._side = self._sensor.split("_")[0]
self._userid = self._eight.fetch_userid(self._side)
self._usrobj = self._eight.users[self._userid]
_LOGGER.debug("Presence Sensor: %s, Side: %s, User: %s",
self._sensor, self._side, self._userid)
_LOGGER.debug(
"Presence Sensor: %s, Side: %s, User: %s",
self._sensor,
self._side,
self._userid,
)
@property
def name(self):

View file

@ -9,22 +9,26 @@ import logging
import voluptuous as vol
from homeassistant.components.binary_sensor import (
BinarySensorDevice, PLATFORM_SCHEMA, DEVICE_CLASSES_SCHEMA)
BinarySensorDevice,
PLATFORM_SCHEMA,
DEVICE_CLASSES_SCHEMA,
)
from homeassistant.components import enocean
from homeassistant.const import (
CONF_NAME, CONF_ID, CONF_DEVICE_CLASS)
from homeassistant.const import CONF_NAME, CONF_ID, CONF_DEVICE_CLASS
import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__)
DEPENDENCIES = ['enocean']
DEFAULT_NAME = 'EnOcean binary sensor'
DEPENDENCIES = ["enocean"]
DEFAULT_NAME = "EnOcean binary sensor"
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_ID): vol.All(cv.ensure_list, [vol.Coerce(int)]),
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA,
})
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
{
vol.Required(CONF_ID): vol.All(cv.ensure_list, [vol.Coerce(int)]),
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA,
}
)
def setup_platform(hass, config, add_entities, discovery_info=None):
@ -42,7 +46,7 @@ class EnOceanBinarySensor(enocean.EnOceanDevice, BinarySensorDevice):
def __init__(self, dev_id, devname, device_class):
"""Initialize the EnOcean binary sensor."""
enocean.EnOceanDevice.__init__(self)
self.stype = 'listener'
self.stype = "listener"
self.dev_id = dev_id
self.which = -1
self.onoff = -1
@ -84,7 +88,12 @@ class EnOceanBinarySensor(enocean.EnOceanDevice, BinarySensorDevice):
elif value2 == 0x15:
self.which = 10
self.onoff = 1
self.hass.bus.fire('button_pressed', {'id': self.dev_id,
'pushed': value,
'which': self.which,
'onoff': self.onoff})
self.hass.bus.fire(
"button_pressed",
{
"id": self.dev_id,
"pushed": value,
"which": self.which,
"onoff": self.onoff,
},
)

View file

@ -12,21 +12,25 @@ from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.components.binary_sensor import BinarySensorDevice
from homeassistant.components.envisalink import (
DATA_EVL, ZONE_SCHEMA, CONF_ZONENAME, CONF_ZONETYPE, EnvisalinkDevice,
SIGNAL_ZONE_UPDATE)
DATA_EVL,
ZONE_SCHEMA,
CONF_ZONENAME,
CONF_ZONETYPE,
EnvisalinkDevice,
SIGNAL_ZONE_UPDATE,
)
from homeassistant.const import ATTR_LAST_TRIP_TIME
from homeassistant.util import dt as dt_util
_LOGGER = logging.getLogger(__name__)
DEPENDENCIES = ['envisalink']
DEPENDENCIES = ["envisalink"]
@asyncio.coroutine
def async_setup_platform(hass, config, async_add_entities,
discovery_info=None):
def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
"""Set up the Envisalink binary sensor devices."""
configured_zones = discovery_info['zones']
configured_zones = discovery_info["zones"]
devices = []
for zone_num in configured_zones:
@ -36,8 +40,8 @@ def async_setup_platform(hass, config, async_add_entities,
zone_num,
device_config_data[CONF_ZONENAME],
device_config_data[CONF_ZONETYPE],
hass.data[DATA_EVL].alarm_state['zone'][zone_num],
hass.data[DATA_EVL]
hass.data[DATA_EVL].alarm_state["zone"][zone_num],
hass.data[DATA_EVL],
)
devices.append(device)
@ -47,20 +51,18 @@ def async_setup_platform(hass, config, async_add_entities,
class EnvisalinkBinarySensor(EnvisalinkDevice, BinarySensorDevice):
"""Representation of an Envisalink binary sensor."""
def __init__(self, hass, zone_number, zone_name, zone_type, info,
controller):
def __init__(self, hass, zone_number, zone_name, zone_type, info, controller):
"""Initialize the binary_sensor."""
self._zone_type = zone_type
self._zone_number = zone_number
_LOGGER.debug('Setting up zone: %s', zone_name)
_LOGGER.debug("Setting up zone: %s", zone_name)
super().__init__(zone_name, info, controller)
@asyncio.coroutine
def async_added_to_hass(self):
"""Register callbacks."""
async_dispatcher_connect(
self.hass, SIGNAL_ZONE_UPDATE, self._update_callback)
async_dispatcher_connect(self.hass, SIGNAL_ZONE_UPDATE, self._update_callback)
@property
def device_state_attributes(self):
@ -76,7 +78,7 @@ class EnvisalinkBinarySensor(EnvisalinkDevice, BinarySensorDevice):
# interval, so we subtract it from the current second-accurate time
# unless it is already at the maximum value, in which case we set it
# to None since we can't determine the actual value.
seconds_ago = self._info['last_fault']
seconds_ago = self._info["last_fault"]
if seconds_ago < 65536 * 5:
now = dt_util.now().replace(microsecond=0)
delta = datetime.timedelta(seconds=seconds_ago)
@ -90,7 +92,7 @@ class EnvisalinkBinarySensor(EnvisalinkDevice, BinarySensorDevice):
@property
def is_on(self):
"""Return true if sensor is on."""
return self._info['status']['open']
return self._info["status"]["open"]
@property
def device_class(self):

View file

@ -11,44 +11,52 @@ import voluptuous as vol
from homeassistant.core import callback
import homeassistant.helpers.config_validation as cv
from homeassistant.components.binary_sensor import (
BinarySensorDevice, PLATFORM_SCHEMA)
from homeassistant.components.binary_sensor import BinarySensorDevice, PLATFORM_SCHEMA
from homeassistant.components.ffmpeg import (
FFmpegBase, DATA_FFMPEG, CONF_INPUT, CONF_EXTRA_ARGUMENTS,
CONF_INITIAL_STATE)
FFmpegBase,
DATA_FFMPEG,
CONF_INPUT,
CONF_EXTRA_ARGUMENTS,
CONF_INITIAL_STATE,
)
from homeassistant.const import CONF_NAME
DEPENDENCIES = ['ffmpeg']
DEPENDENCIES = ["ffmpeg"]
_LOGGER = logging.getLogger(__name__)
CONF_RESET = 'reset'
CONF_CHANGES = 'changes'
CONF_REPEAT = 'repeat'
CONF_REPEAT_TIME = 'repeat_time'
CONF_RESET = "reset"
CONF_CHANGES = "changes"
CONF_REPEAT = "repeat"
CONF_REPEAT_TIME = "repeat_time"
DEFAULT_NAME = 'FFmpeg Motion'
DEFAULT_NAME = "FFmpeg Motion"
DEFAULT_INIT_STATE = True
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_INPUT): cv.string,
vol.Optional(CONF_INITIAL_STATE, default=DEFAULT_INIT_STATE): cv.boolean,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_EXTRA_ARGUMENTS): cv.string,
vol.Optional(CONF_RESET, default=10):
vol.All(vol.Coerce(int), vol.Range(min=1)),
vol.Optional(CONF_CHANGES, default=10):
vol.All(vol.Coerce(float), vol.Range(min=0, max=99)),
vol.Inclusive(CONF_REPEAT, 'repeat'):
vol.All(vol.Coerce(int), vol.Range(min=1)),
vol.Inclusive(CONF_REPEAT_TIME, 'repeat'):
vol.All(vol.Coerce(int), vol.Range(min=1)),
})
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
{
vol.Required(CONF_INPUT): cv.string,
vol.Optional(CONF_INITIAL_STATE, default=DEFAULT_INIT_STATE): cv.boolean,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_EXTRA_ARGUMENTS): cv.string,
vol.Optional(CONF_RESET, default=10): vol.All(
vol.Coerce(int), vol.Range(min=1)
),
vol.Optional(CONF_CHANGES, default=10): vol.All(
vol.Coerce(float), vol.Range(min=0, max=99)
),
vol.Inclusive(CONF_REPEAT, "repeat"): vol.All(
vol.Coerce(int), vol.Range(min=1)
),
vol.Inclusive(CONF_REPEAT_TIME, "repeat"): vol.All(
vol.Coerce(int), vol.Range(min=1)
),
}
)
@asyncio.coroutine
def async_setup_platform(hass, config, async_add_entities,
discovery_info=None):
def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
"""Set up the FFmpeg binary motion sensor."""
manager = hass.data[DATA_FFMPEG]
@ -95,8 +103,7 @@ class FFmpegMotion(FFmpegBinarySensor):
from haffmpeg import SensorMotion
super().__init__(config)
self.ffmpeg = SensorMotion(
manager.binary, hass.loop, self._async_callback)
self.ffmpeg = SensorMotion(manager.binary, hass.loop, self._async_callback)
@asyncio.coroutine
def _async_start_ffmpeg(self, entity_ids):
@ -124,4 +131,4 @@ class FFmpegMotion(FFmpegBinarySensor):
@property
def device_class(self):
"""Return the class of this sensor, from DEVICE_CLASSES."""
return 'motion'
return "motion"

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