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: try:
import uvloop import uvloop
asyncio.set_event_loop_policy(uvloop.EventLoopPolicy()) asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
except ImportError: except ImportError:
pass pass
@ -33,28 +34,40 @@ def attempt_use_uvloop() -> None:
def validate_python() -> None: def validate_python() -> None:
"""Validate that the right Python version is running.""" """Validate that the right Python version is running."""
if sys.version_info[:3] < REQUIRED_PYTHON_VER: if sys.version_info[:3] < REQUIRED_PYTHON_VER:
print("Home Assistant requires at least Python {}.{}.{}".format( print(
*REQUIRED_PYTHON_VER)) "Home Assistant requires at least Python {}.{}.{}".format(
*REQUIRED_PYTHON_VER
)
)
sys.exit(1) sys.exit(1)
def ensure_config_path(config_dir: str) -> None: def ensure_config_path(config_dir: str) -> None:
"""Validate the configuration directory.""" """Validate the configuration directory."""
import homeassistant.config as config_util 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 # Test if configuration directory exists
if not os.path.isdir(config_dir): if not os.path.isdir(config_dir):
if config_dir != config_util.get_default_config_dir(): if config_dir != config_util.get_default_config_dir():
print(('Fatal Error: Specified configuration directory does ' print(
'not exist {} ').format(config_dir)) (
"Fatal Error: Specified configuration directory does "
"not exist {} "
).format(config_dir)
)
sys.exit(1) sys.exit(1)
try: try:
os.mkdir(config_dir) os.mkdir(config_dir)
except OSError: except OSError:
print(('Fatal Error: Unable to create default configuration ' print(
'directory {} ').format(config_dir)) (
"Fatal Error: Unable to create default configuration "
"directory {} "
).format(config_dir)
)
sys.exit(1) sys.exit(1)
# Test if library directory exists # Test if library directory exists
@ -62,18 +75,22 @@ def ensure_config_path(config_dir: str) -> None:
try: try:
os.mkdir(lib_dir) os.mkdir(lib_dir)
except OSError: except OSError:
print(('Fatal Error: Unable to create library ' print(
'directory {} ').format(lib_dir)) ("Fatal Error: Unable to create library " "directory {} ").format(
lib_dir
)
)
sys.exit(1) sys.exit(1)
def ensure_config_file(config_dir: str) -> str: def ensure_config_file(config_dir: str) -> str:
"""Ensure configuration file exists.""" """Ensure configuration file exists."""
import homeassistant.config as config_util import homeassistant.config as config_util
config_path = config_util.ensure_config_exists(config_dir) config_path = config_util.ensure_config_exists(config_dir)
if config_path is None: if config_path is None:
print('Error getting configuration path') print("Error getting configuration path")
sys.exit(1) sys.exit(1)
return config_path return config_path
@ -82,71 +99,72 @@ def ensure_config_file(config_dir: str) -> str:
def get_arguments() -> argparse.Namespace: def get_arguments() -> argparse.Namespace:
"""Get parsed passed in arguments.""" """Get parsed passed in arguments."""
import homeassistant.config as config_util import homeassistant.config as config_util
parser = argparse.ArgumentParser( parser = argparse.ArgumentParser(
description="Home Assistant: Observe, Control, Automate.") description="Home Assistant: Observe, Control, Automate."
parser.add_argument('--version', action='version', version=__version__) )
parser.add_argument("--version", action="version", version=__version__)
parser.add_argument( parser.add_argument(
'-c', '--config', "-c",
metavar='path_to_config_dir', "--config",
metavar="path_to_config_dir",
default=config_util.get_default_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( parser.add_argument(
'--demo-mode', "--demo-mode", action="store_true", help="Start Home Assistant in demo mode"
action='store_true', )
help='Start Home Assistant in demo mode')
parser.add_argument( parser.add_argument(
'--debug', "--debug", action="store_true", help="Start Home Assistant in debug mode"
action='store_true', )
help='Start Home Assistant in debug mode')
parser.add_argument( parser.add_argument(
'--open-ui', "--open-ui", action="store_true", help="Open the webinterface in a browser"
action='store_true', )
help='Open the webinterface in a browser')
parser.add_argument( parser.add_argument(
'--skip-pip', "--skip-pip",
action='store_true', action="store_true",
help='Skips pip install of required packages on startup') help="Skips pip install of required packages on startup",
)
parser.add_argument( parser.add_argument(
'-v', '--verbose', "-v", "--verbose", action="store_true", help="Enable verbose logging to file."
action='store_true', )
help="Enable verbose logging to file.")
parser.add_argument( parser.add_argument(
'--pid-file', "--pid-file",
metavar='path_to_pid_file', metavar="path_to_pid_file",
default=None, 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( parser.add_argument(
'--log-rotate-days', "--log-rotate-days",
type=int, type=int,
default=None, 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( parser.add_argument(
'--log-file', "--log-file",
type=str, type=str,
default=None, default=None,
help='Log file to write to. If not set, CONFIG/home-assistant.log ' help="Log file to write to. If not set, CONFIG/home-assistant.log " "is used",
'is used') )
parser.add_argument( parser.add_argument(
'--log-no-color', "--log-no-color", action="store_true", help="Disable color logs"
action='store_true', )
help="Disable color logs")
parser.add_argument( parser.add_argument(
'--runner', "--runner",
action='store_true', action="store_true",
help='On restart exit with code {}'.format(RESTART_EXIT_CODE)) help="On restart exit with code {}".format(RESTART_EXIT_CODE),
)
parser.add_argument( parser.add_argument(
'--script', "--script", nargs=argparse.REMAINDER, help="Run one of the embedded scripts"
nargs=argparse.REMAINDER, )
help='Run one of the embedded scripts')
if os.name == "posix": if os.name == "posix":
parser.add_argument( parser.add_argument(
'--daemon', "--daemon", action="store_true", help="Run Home Assistant as daemon"
action='store_true', )
help='Run Home Assistant as daemon')
arguments = parser.parse_args() arguments = parser.parse_args()
if os.name != "posix" or arguments.debug or arguments.runner: if os.name != "posix" or arguments.debug or arguments.runner:
setattr(arguments, 'daemon', False) setattr(arguments, "daemon", False)
return arguments return arguments
@ -167,8 +185,8 @@ def daemonize() -> None:
sys.exit(0) sys.exit(0)
# redirect standard file descriptors to devnull # redirect standard file descriptors to devnull
infd = open(os.devnull, 'r') infd = open(os.devnull, "r")
outfd = open(os.devnull, 'a+') outfd = open(os.devnull, "a+")
sys.stdout.flush() sys.stdout.flush()
sys.stderr.flush() sys.stderr.flush()
os.dup2(infd.fileno(), sys.stdin.fileno()) 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 that Home Assistant is not already running."""
# Check pid file # Check pid file
try: try:
with open(pid_file, 'r') as file: with open(pid_file, "r") as file:
pid = int(file.readline()) pid = int(file.readline())
except IOError: except IOError:
# PID File does not exist # PID File does not exist
@ -195,7 +213,7 @@ def check_pid(pid_file: str) -> None:
except OSError: except OSError:
# PID does not exist # PID does not exist
return return
print('Fatal Error: HomeAssistant is already running.') print("Fatal Error: HomeAssistant is already running.")
sys.exit(1) sys.exit(1)
@ -203,10 +221,10 @@ def write_pid(pid_file: str) -> None:
"""Create a PID File.""" """Create a PID File."""
pid = os.getpid() pid = os.getpid()
try: try:
with open(pid_file, 'w') as file: with open(pid_file, "w") as file:
file.write(str(pid)) file.write(str(pid))
except IOError: 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) sys.exit(1)
@ -230,23 +248,21 @@ def closefds_osx(min_fd: int, max_fd: int) -> None:
def cmdline() -> List[str]: def cmdline() -> List[str]:
"""Collect path and arguments to re-execute the current hass instance.""" """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]) modulepath = os.path.dirname(sys.argv[0])
os.environ['PYTHONPATH'] = os.path.dirname(modulepath) os.environ["PYTHONPATH"] = os.path.dirname(modulepath)
return [sys.executable] + [arg for arg in sys.argv if return [sys.executable] + [arg for arg in sys.argv if arg != "--daemon"]
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, def setup_and_run_hass(config_dir: str, args: argparse.Namespace) -> int:
args: argparse.Namespace) -> int:
"""Set up HASS and run.""" """Set up HASS and run."""
from homeassistant import bootstrap from homeassistant import bootstrap
# Run a simple daemon runner process on Windows to handle restarts # Run a simple daemon runner process on Windows to handle restarts
if os.name == 'nt' and '--runner' not in sys.argv: if os.name == "nt" and "--runner" not in sys.argv:
nt_args = cmdline() + ['--runner'] nt_args = cmdline() + ["--runner"]
while True: while True:
try: try:
subprocess.check_call(nt_args) subprocess.check_call(nt_args)
@ -256,21 +272,27 @@ def setup_and_run_hass(config_dir: str,
sys.exit(exc.returncode) sys.exit(exc.returncode)
if args.demo_mode: if args.demo_mode:
config = { config = {"frontend": {}, "demo": {}} # type: Dict[str, Any]
'frontend': {},
'demo': {}
} # type: Dict[str, Any]
hass = bootstrap.from_config_dict( hass = bootstrap.from_config_dict(
config, config_dir=config_dir, verbose=args.verbose, config,
skip_pip=args.skip_pip, log_rotate_days=args.log_rotate_days, config_dir=config_dir,
log_file=args.log_file, log_no_color=args.log_no_color) 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: else:
config_file = ensure_config_file(config_dir) config_file = ensure_config_file(config_dir)
print('Config directory:', config_dir) print("Config directory:", config_dir)
hass = bootstrap.from_config_file( hass = bootstrap.from_config_file(
config_file, verbose=args.verbose, skip_pip=args.skip_pip, config_file,
log_rotate_days=args.log_rotate_days, log_file=args.log_file, verbose=args.verbose,
log_no_color=args.log_no_color) 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: if hass is None:
return -1 return -1
@ -283,12 +305,14 @@ def setup_and_run_hass(config_dir: str,
"""Open the web interface in a browser.""" """Open the web interface in a browser."""
if hass.config.api is not None: # type: ignore if hass.config.api is not None: # type: ignore
import webbrowser import webbrowser
webbrowser.open(hass.config.api.base_url) # type: ignore webbrowser.open(hass.config.api.base_url) # type: ignore
run_callback_threadsafe( run_callback_threadsafe(
hass.loop, hass.loop,
hass.bus.async_listen_once, hass.bus.async_listen_once,
EVENT_HOMEASSISTANT_START, open_browser EVENT_HOMEASSISTANT_START,
open_browser,
) )
return hass.start() return hass.start()
@ -298,17 +322,17 @@ def try_to_restart() -> None:
"""Attempt to clean up state and start a new Home Assistant instance.""" """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 # Things should be mostly shut down already at this point, now just try
# to clean up things that may have been left behind. # 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 # 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 # thread left (which is us). Nothing we really do with it, but it might be
# useful when debugging shutdown/restart issues. # useful when debugging shutdown/restart issues.
try: try:
nthreads = sum(thread.is_alive() and not thread.daemon nthreads = sum(
for thread in threading.enumerate()) thread.is_alive() and not thread.daemon for thread in threading.enumerate()
)
if nthreads > 1: if nthreads > 1:
sys.stderr.write( sys.stderr.write("Found {} non-daemonic threads.\n".format(nthreads))
"Found {} non-daemonic threads.\n".format(nthreads))
# Somehow we sometimes seem to trigger an assertion in the python threading # 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 # module. It seems we find threads that have no associated OS level thread
@ -322,7 +346,7 @@ def try_to_restart() -> None:
except ValueError: except ValueError:
max_fd = 256 max_fd = 256
if platform.system() == 'Darwin': if platform.system() == "Darwin":
closefds_osx(3, max_fd) closefds_osx(3, max_fd)
else: else:
os.closerange(3, max_fd) os.closerange(3, max_fd)
@ -341,7 +365,7 @@ def main() -> int:
validate_python() validate_python()
monkey_patch_needed = sys.version_info[:3] < (3, 6, 3) 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): if sys.version_info[:2] >= (3, 6):
monkey_patch.disable_c_asyncio() monkey_patch.disable_c_asyncio()
monkey_patch.patch_weakref_tasks() monkey_patch.patch_weakref_tasks()
@ -352,6 +376,7 @@ def main() -> int:
if args.script is not None: if args.script is not None:
from homeassistant import scripts from homeassistant import scripts
return scripts.run(args.script) return scripts.run(args.script)
config_dir = os.path.join(os.getcwd(), args.config) 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( async def auth_manager_from_config(
hass: HomeAssistant, hass: HomeAssistant,
provider_configs: List[Dict[str, Any]], provider_configs: List[Dict[str, Any]],
module_configs: List[Dict[str, Any]]) -> 'AuthManager': module_configs: List[Dict[str, Any]],
) -> "AuthManager":
"""Initialize an auth manager from config. """Initialize an auth manager from config.
CORE_CONFIG_SCHEMA will make sure do duplicated auth providers or 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) store = auth_store.AuthStore(hass)
if provider_configs: if provider_configs:
providers = await asyncio.gather( 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: else:
providers = () providers = ()
# So returned auth providers are in same order as config # So returned auth providers are in same order as config
@ -46,8 +50,8 @@ async def auth_manager_from_config(
if module_configs: if module_configs:
modules = await asyncio.gather( modules = await asyncio.gather(
*[auth_mfa_module_from_config(hass, config) *[auth_mfa_module_from_config(hass, config) for config in module_configs]
for config in module_configs]) )
else: else:
modules = () modules = ()
# So returned auth modules are in same order as config # So returned auth modules are in same order as config
@ -62,17 +66,21 @@ async def auth_manager_from_config(
class AuthManager: class AuthManager:
"""Manage the authentication for Home Assistant.""" """Manage the authentication for Home Assistant."""
def __init__(self, hass: HomeAssistant, store: auth_store.AuthStore, def __init__(
providers: _ProviderDict, mfa_modules: _MfaModuleDict) \ self,
-> None: hass: HomeAssistant,
store: auth_store.AuthStore,
providers: _ProviderDict,
mfa_modules: _MfaModuleDict,
) -> None:
"""Initialize the auth manager.""" """Initialize the auth manager."""
self.hass = hass self.hass = hass
self._store = store self._store = store
self._providers = providers self._providers = providers
self._mfa_modules = mfa_modules self._mfa_modules = mfa_modules
self.login_flow = data_entry_flow.FlowManager( self.login_flow = data_entry_flow.FlowManager(
hass, self._async_create_login_flow, hass, self._async_create_login_flow, self._async_finish_login_flow
self._async_finish_login_flow) )
@property @property
def active(self) -> bool: def active(self) -> bool:
@ -87,7 +95,7 @@ class AuthManager:
Should be removed when we removed legacy_api_password auth providers. Should be removed when we removed legacy_api_password auth providers.
""" """
for provider_type, _ in self._providers: for provider_type, _ in self._providers:
if provider_type == 'legacy_api_password': if provider_type == "legacy_api_password":
return True return True
return False return False
@ -101,8 +109,7 @@ class AuthManager:
"""Return a list of available auth modules.""" """Return a list of available auth modules."""
return list(self._mfa_modules.values()) return list(self._mfa_modules.values())
def get_auth_mfa_module(self, module_id: str) \ def get_auth_mfa_module(self, module_id: str) -> Optional[MultiFactorAuthModule]:
-> Optional[MultiFactorAuthModule]:
"""Return an multi-factor auth module, None if not found.""" """Return an multi-factor auth module, None if not found."""
return self._mfa_modules.get(module_id) return self._mfa_modules.get(module_id)
@ -115,7 +122,8 @@ class AuthManager:
return await self._store.async_get_user(user_id) return await self._store.async_get_user(user_id)
async def async_get_user_by_credentials( 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.""" """Get a user by credential, return None if not found."""
for user in await self.async_get_users(): for user in await self.async_get_users():
for creds in user.credentials: for creds in user.credentials:
@ -127,49 +135,43 @@ class AuthManager:
async def async_create_system_user(self, name: str) -> models.User: async def async_create_system_user(self, name: str) -> models.User:
"""Create a system user.""" """Create a system user."""
return await self._store.async_create_user( return await self._store.async_create_user(
name=name, name=name, system_generated=True, is_active=True
system_generated=True,
is_active=True,
) )
async def async_create_user(self, name: str) -> models.User: async def async_create_user(self, name: str) -> models.User:
"""Create a user.""" """Create a user."""
kwargs = { kwargs = {"name": name, "is_active": True} # type: Dict[str, Any]
'name': name,
'is_active': True,
} # type: Dict[str, Any]
if await self._user_should_be_owner(): if await self._user_should_be_owner():
kwargs['is_owner'] = True kwargs["is_owner"] = True
return await self._store.async_create_user(**kwargs) return await self._store.async_create_user(**kwargs)
async def async_get_or_create_user(self, credentials: models.Credentials) \ async def async_get_or_create_user(
-> models.User: self, credentials: models.Credentials
) -> models.User:
"""Get or create a user.""" """Get or create a user."""
if not credentials.is_new: if not credentials.is_new:
user = await self.async_get_user_by_credentials(credentials) user = await self.async_get_user_by_credentials(credentials)
if user is None: if user is None:
raise ValueError('Unable to find the user.') raise ValueError("Unable to find the user.")
else: else:
return user return user
auth_provider = self._async_get_auth_provider(credentials) auth_provider = self._async_get_auth_provider(credentials)
if auth_provider is None: 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( info = await auth_provider.async_user_meta_for_credentials(credentials)
credentials)
return await self._store.async_create_user( return await self._store.async_create_user(
credentials=credentials, credentials=credentials, name=info.name, is_active=info.is_active
name=info.name,
is_active=info.is_active,
) )
async def async_link_user(self, user: models.User, async def async_link_user(
credentials: models.Credentials) -> None: self, user: models.User, credentials: models.Credentials
) -> None:
"""Link credentials to an existing user.""" """Link credentials to an existing user."""
await self._store.async_link_user(user, credentials) await self._store.async_link_user(user, credentials)
@ -192,47 +194,50 @@ class AuthManager:
async def async_deactivate_user(self, user: models.User) -> None: async def async_deactivate_user(self, user: models.User) -> None:
"""Deactivate a user.""" """Deactivate a user."""
if user.is_owner: 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) await self._store.async_deactivate_user(user)
async def async_remove_credentials( async def async_remove_credentials(self, credentials: models.Credentials) -> None:
self, credentials: models.Credentials) -> None:
"""Remove credentials.""" """Remove credentials."""
provider = self._async_get_auth_provider(credentials) provider = self._async_get_auth_provider(credentials)
if (provider is not None and if provider is not None and hasattr(provider, "async_will_remove_credentials"):
hasattr(provider, 'async_will_remove_credentials')):
# https://github.com/python/mypy/issues/1424 # https://github.com/python/mypy/issues/1424
await provider.async_will_remove_credentials( # type: ignore await provider.async_will_remove_credentials(credentials) # type: ignore
credentials)
await self._store.async_remove_credentials(credentials) await self._store.async_remove_credentials(credentials)
async def async_enable_user_mfa(self, user: models.User, async def async_enable_user_mfa(
mfa_module_id: str, data: Any) -> None: self, user: models.User, mfa_module_id: str, data: Any
) -> None:
"""Enable a multi-factor auth module for user.""" """Enable a multi-factor auth module for user."""
if user.system_generated: if user.system_generated:
raise ValueError('System generated users cannot enable ' raise ValueError(
'multi-factor auth module.') "System generated users cannot enable " "multi-factor auth module."
)
module = self.get_auth_mfa_module(mfa_module_id) module = self.get_auth_mfa_module(mfa_module_id)
if module is None: if module is None:
raise ValueError('Unable find multi-factor auth module: {}' raise ValueError(
.format(mfa_module_id)) "Unable find multi-factor auth module: {}".format(mfa_module_id)
)
await module.async_setup_user(user.id, data) await module.async_setup_user(user.id, data)
async def async_disable_user_mfa(self, user: models.User, async def async_disable_user_mfa(
mfa_module_id: str) -> None: self, user: models.User, mfa_module_id: str
) -> None:
"""Disable a multi-factor auth module for user.""" """Disable a multi-factor auth module for user."""
if user.system_generated: if user.system_generated:
raise ValueError('System generated users cannot disable ' raise ValueError(
'multi-factor auth module.') "System generated users cannot disable " "multi-factor auth module."
)
module = self.get_auth_mfa_module(mfa_module_id) module = self.get_auth_mfa_module(mfa_module_id)
if module is None: if module is None:
raise ValueError('Unable find multi-factor auth module: {}' raise ValueError(
.format(mfa_module_id)) "Unable find multi-factor auth module: {}".format(mfa_module_id)
)
await module.async_depose_user(user.id) await module.async_depose_user(user.id)
@ -245,20 +250,23 @@ class AuthManager:
return modules return modules
async def async_create_refresh_token( async def async_create_refresh_token(
self, user: models.User, client_id: Optional[str] = None, self,
client_name: Optional[str] = None, user: models.User,
client_icon: Optional[str] = None, client_id: Optional[str] = None,
token_type: Optional[str] = None, client_name: Optional[str] = None,
access_token_expiration: timedelta = ACCESS_TOKEN_EXPIRATION) \ client_icon: Optional[str] = None,
-> models.RefreshToken: token_type: Optional[str] = None,
access_token_expiration: timedelta = ACCESS_TOKEN_EXPIRATION,
) -> models.RefreshToken:
"""Create a new refresh token for a user.""" """Create a new refresh token for a user."""
if not user.is_active: 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: if user.system_generated and client_id is not None:
raise ValueError( raise ValueError(
'System generated users cannot have refresh tokens connected ' "System generated users cannot have refresh tokens connected "
'to a client.') "to a client."
)
if token_type is None: if token_type is None:
if user.system_generated: if user.system_generated:
@ -268,62 +276,77 @@ class AuthManager:
if user.system_generated != (token_type == models.TOKEN_TYPE_SYSTEM): if user.system_generated != (token_type == models.TOKEN_TYPE_SYSTEM):
raise ValueError( raise ValueError(
'System generated users can only have system type ' "System generated users can only have system type " "refresh tokens"
'refresh tokens') )
if token_type == models.TOKEN_TYPE_NORMAL and client_id is None: 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 if (
client_name is None): token_type == models.TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN
raise ValueError('Client_name is required for long-lived access ' and client_name is None
'token') ):
raise ValueError("Client_name is required for long-lived access " "token")
if token_type == models.TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN: if token_type == models.TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN:
for token in user.refresh_tokens.values(): for token in user.refresh_tokens.values():
if (token.client_name == client_name and token.token_type == if (
models.TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN): token.client_name == client_name
and token.token_type == models.TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN
):
# Each client_name can only have one # Each client_name can only have one
# long_lived_access_token type of refresh token # 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( return await self._store.async_create_refresh_token(
user, client_id, client_name, client_icon, user,
token_type, access_token_expiration) client_id,
client_name,
client_icon,
token_type,
access_token_expiration,
)
async def async_get_refresh_token( 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.""" """Get refresh token by id."""
return await self._store.async_get_refresh_token(token_id) return await self._store.async_get_refresh_token(token_id)
async def async_get_refresh_token_by_token( 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.""" """Get refresh token by token."""
return await self._store.async_get_refresh_token_by_token(token) return await self._store.async_get_refresh_token_by_token(token)
async def async_remove_refresh_token(self, async def async_remove_refresh_token(
refresh_token: models.RefreshToken) \ self, refresh_token: models.RefreshToken
-> None: ) -> None:
"""Delete a refresh token.""" """Delete a refresh token."""
await self._store.async_remove_refresh_token(refresh_token) await self._store.async_remove_refresh_token(refresh_token)
@callback @callback
def async_create_access_token(self, def async_create_access_token(
refresh_token: models.RefreshToken, self, refresh_token: models.RefreshToken, remote_ip: Optional[str] = None
remote_ip: Optional[str] = None) -> str: ) -> str:
"""Create a new access token.""" """Create a new access token."""
self._store.async_log_refresh_token_usage(refresh_token, remote_ip) self._store.async_log_refresh_token_usage(refresh_token, remote_ip)
# pylint: disable=no-self-use # pylint: disable=no-self-use
now = dt_util.utcnow() now = dt_util.utcnow()
return jwt.encode({ return jwt.encode(
'iss': refresh_token.id, {
'iat': now, "iss": refresh_token.id,
'exp': now + refresh_token.access_token_expiration, "iat": now,
}, refresh_token.jwt_key, algorithm='HS256').decode() "exp": now + refresh_token.access_token_expiration,
},
refresh_token.jwt_key,
algorithm="HS256",
).decode()
async def async_validate_access_token( 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.""" """Return refresh token if an access token is valid."""
try: try:
unverif_claims = jwt.decode(token, verify=False) unverif_claims = jwt.decode(token, verify=False)
@ -331,23 +354,18 @@ class AuthManager:
return None return None
refresh_token = await self.async_get_refresh_token( 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: if refresh_token is None:
jwt_key = '' jwt_key = ""
issuer = '' issuer = ""
else: else:
jwt_key = refresh_token.jwt_key jwt_key = refresh_token.jwt_key
issuer = refresh_token.id issuer = refresh_token.id
try: try:
jwt.decode( jwt.decode(token, jwt_key, leeway=10, issuer=issuer, algorithms=["HS256"])
token,
jwt_key,
leeway=10,
issuer=issuer,
algorithms=['HS256']
)
except jwt.InvalidTokenError: except jwt.InvalidTokenError:
return None return None
@ -357,31 +375,32 @@ class AuthManager:
return refresh_token return refresh_token
async def _async_create_login_flow( async def _async_create_login_flow(
self, handler: _ProviderKey, *, context: Optional[Dict], self, handler: _ProviderKey, *, context: Optional[Dict], data: Optional[Any]
data: Optional[Any]) -> data_entry_flow.FlowHandler: ) -> data_entry_flow.FlowHandler:
"""Create a login flow.""" """Create a login flow."""
auth_provider = self._providers[handler] auth_provider = self._providers[handler]
return await auth_provider.async_login_flow(context) return await auth_provider.async_login_flow(context)
async def _async_finish_login_flow( async def _async_finish_login_flow(
self, flow: LoginFlow, result: Dict[str, Any]) \ self, flow: LoginFlow, result: Dict[str, Any]
-> Dict[str, Any]: ) -> Dict[str, Any]:
"""Return a user as result of login flow.""" """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 return result
# we got final result # we got final result
if isinstance(result['data'], models.User): if isinstance(result["data"], models.User):
result['result'] = result['data'] result["result"] = result["data"]
return result return result
auth_provider = self._providers[result['handler']] auth_provider = self._providers[result["handler"]]
credentials = await auth_provider.async_get_or_create_credentials( 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'): if flow.context is not None and flow.context.get("credential_only"):
result['result'] = credentials result["result"] = credentials
return result return result
# multi-factor module cannot enabled for new credential # multi-factor module cannot enabled for new credential
@ -396,15 +415,18 @@ class AuthManager:
flow.available_mfa_modules = modules flow.available_mfa_modules = modules
return await flow.async_step_select_mfa_module() 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 return result
@callback @callback
def _async_get_auth_provider( 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.""" """Get auth provider from a set of credentials."""
auth_provider_key = (credentials.auth_provider_type, auth_provider_key = (
credentials.auth_provider_id) credentials.auth_provider_type,
credentials.auth_provider_id,
)
return self._providers.get(auth_provider_key) return self._providers.get(auth_provider_key)
async def _user_should_be_owner(self) -> bool: 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 from . import models
STORAGE_VERSION = 1 STORAGE_VERSION = 1
STORAGE_KEY = 'auth' STORAGE_KEY = "auth"
class AuthStore: class AuthStore:
@ -47,27 +47,28 @@ class AuthStore:
return self._users.get(user_id) return self._users.get(user_id)
async def async_create_user( async def async_create_user(
self, name: Optional[str], is_owner: Optional[bool] = None, self,
is_active: Optional[bool] = None, name: Optional[str],
system_generated: Optional[bool] = None, is_owner: Optional[bool] = None,
credentials: Optional[models.Credentials] = None) -> models.User: is_active: Optional[bool] = None,
system_generated: Optional[bool] = None,
credentials: Optional[models.Credentials] = None,
) -> models.User:
"""Create a new user.""" """Create a new user."""
if self._users is None: if self._users is None:
await self._async_load() await self._async_load()
assert self._users is not None assert self._users is not None
kwargs = { kwargs = {"name": name} # type: Dict[str, Any]
'name': name
} # type: Dict[str, Any]
if is_owner is not None: if is_owner is not None:
kwargs['is_owner'] = is_owner kwargs["is_owner"] = is_owner
if is_active is not None: if is_active is not None:
kwargs['is_active'] = is_active kwargs["is_active"] = is_active
if system_generated is not None: if system_generated is not None:
kwargs['system_generated'] = system_generated kwargs["system_generated"] = system_generated
new_user = models.User(**kwargs) new_user = models.User(**kwargs)
@ -81,8 +82,9 @@ class AuthStore:
await self.async_link_user(new_user, credentials) await self.async_link_user(new_user, credentials)
return new_user return new_user
async def async_link_user(self, user: models.User, async def async_link_user(
credentials: models.Credentials) -> None: self, user: models.User, credentials: models.Credentials
) -> None:
"""Add credentials to an existing user.""" """Add credentials to an existing user."""
user.credentials.append(credentials) user.credentials.append(credentials)
self._async_schedule_save() self._async_schedule_save()
@ -107,8 +109,7 @@ class AuthStore:
user.is_active = False user.is_active = False
self._async_schedule_save() self._async_schedule_save()
async def async_remove_credentials( async def async_remove_credentials(self, credentials: models.Credentials) -> None:
self, credentials: models.Credentials) -> None:
"""Remove credentials.""" """Remove credentials."""
if self._users is None: if self._users is None:
await self._async_load() await self._async_load()
@ -129,23 +130,25 @@ class AuthStore:
self._async_schedule_save() self._async_schedule_save()
async def async_create_refresh_token( async def async_create_refresh_token(
self, user: models.User, client_id: Optional[str] = None, self,
client_name: Optional[str] = None, user: models.User,
client_icon: Optional[str] = None, client_id: Optional[str] = None,
token_type: str = models.TOKEN_TYPE_NORMAL, client_name: Optional[str] = None,
access_token_expiration: timedelta = ACCESS_TOKEN_EXPIRATION) \ client_icon: Optional[str] = None,
-> models.RefreshToken: token_type: str = models.TOKEN_TYPE_NORMAL,
access_token_expiration: timedelta = ACCESS_TOKEN_EXPIRATION,
) -> models.RefreshToken:
"""Create a new token for a user.""" """Create a new token for a user."""
kwargs = { kwargs = {
'user': user, "user": user,
'client_id': client_id, "client_id": client_id,
'token_type': token_type, "token_type": token_type,
'access_token_expiration': access_token_expiration "access_token_expiration": access_token_expiration,
} # type: Dict[str, Any] } # type: Dict[str, Any]
if client_name: if client_name:
kwargs['client_name'] = client_name kwargs["client_name"] = client_name
if client_icon: if client_icon:
kwargs['client_icon'] = client_icon kwargs["client_icon"] = client_icon
refresh_token = models.RefreshToken(**kwargs) refresh_token = models.RefreshToken(**kwargs)
user.refresh_tokens[refresh_token.id] = refresh_token user.refresh_tokens[refresh_token.id] = refresh_token
@ -154,7 +157,8 @@ class AuthStore:
return refresh_token return refresh_token
async def async_remove_refresh_token( async def async_remove_refresh_token(
self, refresh_token: models.RefreshToken) -> None: self, refresh_token: models.RefreshToken
) -> None:
"""Remove a refresh token.""" """Remove a refresh token."""
if self._users is None: if self._users is None:
await self._async_load() await self._async_load()
@ -166,7 +170,8 @@ class AuthStore:
break break
async def async_get_refresh_token( 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.""" """Get refresh token by id."""
if self._users is None: if self._users is None:
await self._async_load() await self._async_load()
@ -180,7 +185,8 @@ class AuthStore:
return None return None
async def async_get_refresh_token_by_token( 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.""" """Get refresh token by token."""
if self._users is None: if self._users is None:
await self._async_load() await self._async_load()
@ -197,8 +203,8 @@ class AuthStore:
@callback @callback
def async_log_refresh_token_usage( def async_log_refresh_token_usage(
self, refresh_token: models.RefreshToken, self, refresh_token: models.RefreshToken, remote_ip: Optional[str] = None
remote_ip: Optional[str] = None) -> None: ) -> None:
"""Update refresh token last used information.""" """Update refresh token last used information."""
refresh_token.last_used_at = dt_util.utcnow() refresh_token.last_used_at = dt_util.utcnow()
refresh_token.last_used_ip = remote_ip refresh_token.last_used_ip = remote_ip
@ -219,61 +225,66 @@ class AuthStore:
self._users = users self._users = users
return return
for user_dict in data['users']: for user_dict in data["users"]:
users[user_dict['id']] = models.User(**user_dict) users[user_dict["id"]] = models.User(**user_dict)
for cred_dict in data['credentials']: for cred_dict in data["credentials"]:
users[cred_dict['user_id']].credentials.append(models.Credentials( users[cred_dict["user_id"]].credentials.append(
id=cred_dict['id'], models.Credentials(
is_new=False, id=cred_dict["id"],
auth_provider_type=cred_dict['auth_provider_type'], is_new=False,
auth_provider_id=cred_dict['auth_provider_id'], auth_provider_type=cred_dict["auth_provider_type"],
data=cred_dict['data'], 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) # 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 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: if created_at is None:
getLogger(__name__).error( getLogger(__name__).error(
'Ignoring refresh token %(id)s with invalid created_at ' "Ignoring refresh token %(id)s with invalid created_at "
'%(created_at)s for user_id %(user_id)s', rt_dict) "%(created_at)s for user_id %(user_id)s",
rt_dict,
)
continue continue
token_type = rt_dict.get('token_type') token_type = rt_dict.get("token_type")
if token_type is None: 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 token_type = models.TOKEN_TYPE_SYSTEM
else: else:
token_type = models.TOKEN_TYPE_NORMAL token_type = models.TOKEN_TYPE_NORMAL
# old refresh_token don't have last_used_at (pre-0.78) # 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: if last_used_at_str:
last_used_at = dt_util.parse_datetime(last_used_at_str) last_used_at = dt_util.parse_datetime(last_used_at_str)
else: else:
last_used_at = None last_used_at = None
token = models.RefreshToken( token = models.RefreshToken(
id=rt_dict['id'], id=rt_dict["id"],
user=users[rt_dict['user_id']], user=users[rt_dict["user_id"]],
client_id=rt_dict['client_id'], client_id=rt_dict["client_id"],
# use dict.get to keep backward compatibility # use dict.get to keep backward compatibility
client_name=rt_dict.get('client_name'), client_name=rt_dict.get("client_name"),
client_icon=rt_dict.get('client_icon'), client_icon=rt_dict.get("client_icon"),
token_type=token_type, token_type=token_type,
created_at=created_at, created_at=created_at,
access_token_expiration=timedelta( access_token_expiration=timedelta(
seconds=rt_dict['access_token_expiration']), seconds=rt_dict["access_token_expiration"]
token=rt_dict['token'], ),
jwt_key=rt_dict['jwt_key'], token=rt_dict["token"],
jwt_key=rt_dict["jwt_key"],
last_used_at=last_used_at, 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 self._users = users
@ -292,22 +303,22 @@ class AuthStore:
users = [ users = [
{ {
'id': user.id, "id": user.id,
'is_owner': user.is_owner, "is_owner": user.is_owner,
'is_active': user.is_active, "is_active": user.is_active,
'name': user.name, "name": user.name,
'system_generated': user.system_generated, "system_generated": user.system_generated,
} }
for user in self._users.values() for user in self._users.values()
] ]
credentials = [ credentials = [
{ {
'id': credential.id, "id": credential.id,
'user_id': user.id, "user_id": user.id,
'auth_provider_type': credential.auth_provider_type, "auth_provider_type": credential.auth_provider_type,
'auth_provider_id': credential.auth_provider_id, "auth_provider_id": credential.auth_provider_id,
'data': credential.data, "data": credential.data,
} }
for user in self._users.values() for user in self._users.values()
for credential in user.credentials for credential in user.credentials
@ -315,28 +326,27 @@ class AuthStore:
refresh_tokens = [ refresh_tokens = [
{ {
'id': refresh_token.id, "id": refresh_token.id,
'user_id': user.id, "user_id": user.id,
'client_id': refresh_token.client_id, "client_id": refresh_token.client_id,
'client_name': refresh_token.client_name, "client_name": refresh_token.client_name,
'client_icon': refresh_token.client_icon, "client_icon": refresh_token.client_icon,
'token_type': refresh_token.token_type, "token_type": refresh_token.token_type,
'created_at': refresh_token.created_at.isoformat(), "created_at": refresh_token.created_at.isoformat(),
'access_token_expiration': "access_token_expiration": refresh_token.access_token_expiration.total_seconds(),
refresh_token.access_token_expiration.total_seconds(), "token": refresh_token.token,
'token': refresh_token.token, "jwt_key": refresh_token.jwt_key,
'jwt_key': refresh_token.jwt_key, "last_used_at": refresh_token.last_used_at.isoformat()
'last_used_at': if refresh_token.last_used_at
refresh_token.last_used_at.isoformat() else None,
if refresh_token.last_used_at else None, "last_used_ip": refresh_token.last_used_ip,
'last_used_ip': refresh_token.last_used_ip,
} }
for user in self._users.values() for user in self._users.values()
for refresh_token in user.refresh_tokens.values() for refresh_token in user.refresh_tokens.values()
] ]
return { return {
'users': users, "users": users,
'credentials': credentials, "credentials": credentials,
'refresh_tokens': refresh_tokens, "refresh_tokens": refresh_tokens,
} }

View file

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

View file

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

View file

@ -8,23 +8,26 @@ import voluptuous as vol
from homeassistant.auth.models import User from homeassistant.auth.models import User
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from . import MultiFactorAuthModule, MULTI_FACTOR_AUTH_MODULES, \ from . import (
MULTI_FACTOR_AUTH_MODULE_SCHEMA, SetupFlow 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({ CONFIG_SCHEMA = MULTI_FACTOR_AUTH_MODULE_SCHEMA.extend({}, extra=vol.PREVENT_EXTRA)
}, extra=vol.PREVENT_EXTRA)
STORAGE_VERSION = 1 STORAGE_VERSION = 1
STORAGE_KEY = 'auth_module.totp' STORAGE_KEY = "auth_module.totp"
STORAGE_USERS = 'users' STORAGE_USERS = "users"
STORAGE_USER_ID = 'user_id' STORAGE_USER_ID = "user_id"
STORAGE_OTA_SECRET = 'ota_secret' STORAGE_OTA_SECRET = "ota_secret"
INPUT_FIELD_CODE = 'code' INPUT_FIELD_CODE = "code"
DUMMY_SECRET = 'FPPTH34D4E3MI2HG' DUMMY_SECRET = "FPPTH34D4E3MI2HG"
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -37,10 +40,15 @@ def _generate_qr_code(data: str) -> str:
with BytesIO() as buffer: with BytesIO() as buffer:
qr_code.svg(file=buffer, scale=4) qr_code.svg(file=buffer, scale=4)
return '{}'.format( return "{}".format(
buffer.getvalue().decode("ascii").replace('\n', '') buffer.getvalue()
.replace('<?xml version="1.0" encoding="UTF-8"?>' .decode("ascii")
'<svg xmlns="http://www.w3.org/2000/svg"', '<svg') .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() ota_secret = pyotp.random_base32()
url = pyotp.totp.TOTP(ota_secret).provisioning_uri( url = pyotp.totp.TOTP(ota_secret).provisioning_uri(
username, issuer_name="Home Assistant") username, issuer_name="Home Assistant"
)
image = _generate_qr_code(url) image = _generate_qr_code(url)
return ota_secret, url, image return ota_secret, url, image
@MULTI_FACTOR_AUTH_MODULES.register('totp') @MULTI_FACTOR_AUTH_MODULES.register("totp")
class TotpAuthModule(MultiFactorAuthModule): class TotpAuthModule(MultiFactorAuthModule):
"""Auth module validate time-based one time password.""" """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: def __init__(self, hass: HomeAssistant, config: Dict[str, Any]) -> None:
"""Initialize the user data store.""" """Initialize the user data store."""
super().__init__(hass, config) super().__init__(hass, config)
self._users = None # type: Optional[Dict[str, str]] self._users = None # type: Optional[Dict[str, str]]
self._user_store = hass.helpers.storage.Store( self._user_store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY)
STORAGE_VERSION, STORAGE_KEY)
@property @property
def input_schema(self) -> vol.Schema: def input_schema(self) -> vol.Schema:
@ -86,14 +94,13 @@ class TotpAuthModule(MultiFactorAuthModule):
"""Save data.""" """Save data."""
await self._user_store.async_save({STORAGE_USERS: self._users}) await self._user_store.async_save({STORAGE_USERS: self._users})
def _add_ota_secret(self, user_id: str, def _add_ota_secret(self, user_id: str, secret: Optional[str] = None) -> str:
secret: Optional[str] = None) -> str:
"""Create a ota_secret for user.""" """Create a ota_secret for user."""
import pyotp import pyotp
ota_secret = secret or pyotp.random_base32() # type: str 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 return ota_secret
async def async_setup_flow(self, user_id: str) -> SetupFlow: async def async_setup_flow(self, user_id: str) -> SetupFlow:
@ -101,7 +108,7 @@ class TotpAuthModule(MultiFactorAuthModule):
Mfa module should extend SetupFlow 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) return TotpSetupFlow(self, self.input_schema, user)
async def async_setup_user(self, user_id: str, setup_data: Any) -> str: async def async_setup_user(self, user_id: str, setup_data: Any) -> str:
@ -110,7 +117,8 @@ class TotpAuthModule(MultiFactorAuthModule):
await self._async_load() await self._async_load()
result = await self.hass.async_add_executor_job( 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() await self._async_save()
return result return result
@ -120,7 +128,7 @@ class TotpAuthModule(MultiFactorAuthModule):
if self._users is None: if self._users is None:
await self._async_load() 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() await self._async_save()
async def async_is_user_setup(self, user_id: str) -> bool: async def async_is_user_setup(self, user_id: str) -> bool:
@ -128,10 +136,9 @@ class TotpAuthModule(MultiFactorAuthModule):
if self._users is None: if self._users is None:
await self._async_load() await self._async_load()
return user_id in self._users # type: ignore return user_id in self._users # type: ignore
async def async_validation( async def async_validation(self, user_id: str, user_input: Dict[str, Any]) -> bool:
self, user_id: str, user_input: Dict[str, Any]) -> bool:
"""Return True if validation passed.""" """Return True if validation passed."""
if self._users is None: if self._users is None:
await self._async_load() await self._async_load()
@ -139,7 +146,8 @@ class TotpAuthModule(MultiFactorAuthModule):
# user_input has been validate in caller # user_input has been validate in caller
# set INPUT_FIELD_CODE as vol.Required is not user friendly # set INPUT_FIELD_CODE as vol.Required is not user friendly
return await self.hass.async_add_executor_job( 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: def _validate_2fa(self, user_id: str, code: str) -> bool:
"""Validate two factor authentication code.""" """Validate two factor authentication code."""
@ -158,9 +166,9 @@ class TotpAuthModule(MultiFactorAuthModule):
class TotpSetupFlow(SetupFlow): class TotpSetupFlow(SetupFlow):
"""Handler for the setup flow.""" """Handler for the setup flow."""
def __init__(self, auth_module: TotpAuthModule, def __init__(
setup_schema: vol.Schema, self, auth_module: TotpAuthModule, setup_schema: vol.Schema, user: User
user: User) -> None: ) -> None:
"""Initialize the setup flow.""" """Initialize the setup flow."""
super().__init__(auth_module, setup_schema, user.id) super().__init__(auth_module, setup_schema, user.id)
# to fix typing complaint # to fix typing complaint
@ -171,8 +179,8 @@ class TotpSetupFlow(SetupFlow):
self._image = None # type Optional[str] self._image = None # type Optional[str]
async def async_step_init( async def async_step_init(
self, user_input: Optional[Dict[str, str]] = None) \ self, user_input: Optional[Dict[str, str]] = None
-> Dict[str, Any]: ) -> Dict[str, Any]:
"""Handle the first step of setup flow. """Handle the first step of setup flow.
Return self.async_show_form(step_id='init') if user_input == None. Return self.async_show_form(step_id='init') if user_input == None.
@ -184,30 +192,31 @@ class TotpSetupFlow(SetupFlow):
if user_input: if user_input:
verified = await self.hass.async_add_executor_job( # type: ignore 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: if verified:
result = await self._auth_module.async_setup_user( 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( return self.async_create_entry(
title=self._auth_module.name, title=self._auth_module.name, data={"result": result}
data={'result': result}
) )
errors['base'] = 'invalid_code' errors["base"] = "invalid_code"
else: else:
hass = self._auth_module.hass hass = self._auth_module.hass
self._ota_secret, self._url, self._image = \ self._ota_secret, self._url, self._image = await hass.async_add_executor_job( # type: ignore
await hass.async_add_executor_job( # type: ignore _generate_secret_and_qr_code, str(self._user.name)
_generate_secret_and_qr_code, str(self._user.name)) )
return self.async_show_form( return self.async_show_form(
step_id='init', step_id="init",
data_schema=self._setup_schema, data_schema=self._setup_schema,
description_placeholders={ description_placeholders={
'code': self._ota_secret, "code": self._ota_secret,
'url': self._url, "url": self._url,
'qr_code': self._image "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 from .util import generate_secret
TOKEN_TYPE_NORMAL = 'normal' TOKEN_TYPE_NORMAL = "normal"
TOKEN_TYPE_SYSTEM = 'system' TOKEN_TYPE_SYSTEM = "system"
TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN = 'long_lived_access_token' TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN = "long_lived_access_token"
@attr.s(slots=True) @attr.s(slots=True)
@ -44,16 +44,17 @@ class RefreshToken:
access_token_expiration = attr.ib(type=timedelta) access_token_expiration = attr.ib(type=timedelta)
client_name = attr.ib(type=Optional[str], default=None) client_name = attr.ib(type=Optional[str], default=None)
client_icon = 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, token_type = attr.ib(
validator=attr.validators.in_(( type=str,
TOKEN_TYPE_NORMAL, TOKEN_TYPE_SYSTEM, default=TOKEN_TYPE_NORMAL,
TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN))) 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)) id = attr.ib(type=str, default=attr.Factory(lambda: uuid.uuid4().hex))
created_at = attr.ib(type=datetime, default=attr.Factory(dt_util.utcnow)) created_at = attr.ib(type=datetime, default=attr.Factory(dt_util.utcnow))
token = attr.ib(type=str, token = attr.ib(type=str, default=attr.Factory(lambda: generate_secret(64)))
default=attr.Factory(lambda: generate_secret(64))) jwt_key = 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_at = attr.ib(type=Optional[datetime], default=None)
last_used_ip = attr.ib(type=Optional[str], 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) is_new = attr.ib(type=bool, default=True)
UserMeta = NamedTuple("UserMeta", UserMeta = NamedTuple("UserMeta", [("name", Optional[str]), ("is_active", bool)])
[('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 from ..mfa_modules import SESSION_EXPIRATION
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
DATA_REQS = 'auth_prov_reqs_processed' DATA_REQS = "auth_prov_reqs_processed"
AUTH_PROVIDERS = Registry() AUTH_PROVIDERS = Registry()
AUTH_PROVIDER_SCHEMA = vol.Schema({ AUTH_PROVIDER_SCHEMA = vol.Schema(
vol.Required(CONF_TYPE): str, {
vol.Optional(CONF_NAME): str, vol.Required(CONF_TYPE): str,
# Specify ID if you have two auth providers for same type. vol.Optional(CONF_NAME): str,
vol.Optional(CONF_ID): str, # Specify ID if you have two auth providers for same type.
}, extra=vol.ALLOW_EXTRA) vol.Optional(CONF_ID): str,
},
extra=vol.ALLOW_EXTRA,
)
class AuthProvider: class AuthProvider:
"""Provider of user authentication.""" """Provider of user authentication."""
DEFAULT_TITLE = 'Unnamed auth provider' DEFAULT_TITLE = "Unnamed auth provider"
def __init__(self, hass: HomeAssistant, store: AuthStore, def __init__(
config: Dict[str, Any]) -> None: self, hass: HomeAssistant, store: AuthStore, config: Dict[str, Any]
) -> None:
"""Initialize an auth provider.""" """Initialize an auth provider."""
self.hass = hass self.hass = hass
self.store = store self.store = store
@ -73,22 +77,22 @@ class AuthProvider:
credentials credentials
for user in users for user in users
for credentials in user.credentials for credentials in user.credentials
if (credentials.auth_provider_type == self.type and if (
credentials.auth_provider_id == self.id) credentials.auth_provider_type == self.type
and credentials.auth_provider_id == self.id
)
] ]
@callback @callback
def async_create_credentials(self, data: Dict[str, str]) -> Credentials: def async_create_credentials(self, data: Dict[str, str]) -> Credentials:
"""Create credentials.""" """Create credentials."""
return Credentials( return Credentials(
auth_provider_type=self.type, auth_provider_type=self.type, auth_provider_id=self.id, data=data
auth_provider_id=self.id,
data=data,
) )
# Implement by extending class # 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. """Return the data flow for logging in with auth provider.
Auth provider should extend LoginFlow and return an instance. Auth provider should extend LoginFlow and return an instance.
@ -96,12 +100,14 @@ class AuthProvider:
raise NotImplementedError raise NotImplementedError
async def async_get_or_create_credentials( 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.""" """Get credentials based on the flow result."""
raise NotImplementedError raise NotImplementedError
async def async_user_meta_for_credentials( async def async_user_meta_for_credentials(
self, credentials: Credentials) -> UserMeta: self, credentials: Credentials
) -> UserMeta:
"""Return extra user metadata for credentials. """Return extra user metadata for credentials.
Will be used to populate info when creating a new user. Will be used to populate info when creating a new user.
@ -110,8 +116,8 @@ class AuthProvider:
async def auth_provider_from_config( async def auth_provider_from_config(
hass: HomeAssistant, store: AuthStore, hass: HomeAssistant, store: AuthStore, config: Dict[str, Any]
config: Dict[str, Any]) -> AuthProvider: ) -> AuthProvider:
"""Initialize an auth provider from a config.""" """Initialize an auth provider from a config."""
provider_name = config[CONF_TYPE] provider_name = config[CONF_TYPE]
module = await load_auth_provider_module(hass, provider_name) module = await load_auth_provider_module(hass, provider_name)
@ -119,25 +125,31 @@ async def auth_provider_from_config(
try: try:
config = module.CONFIG_SCHEMA(config) # type: ignore config = module.CONFIG_SCHEMA(config) # type: ignore
except vol.Invalid as err: except vol.Invalid as err:
_LOGGER.error('Invalid configuration for auth provider %s: %s', _LOGGER.error(
provider_name, humanize_error(config, err)) "Invalid configuration for auth provider %s: %s",
provider_name,
humanize_error(config, err),
)
raise raise
return AUTH_PROVIDERS[provider_name](hass, store, config) # type: ignore return AUTH_PROVIDERS[provider_name](hass, store, config) # type: ignore
async def load_auth_provider_module( async def load_auth_provider_module(
hass: HomeAssistant, provider: str) -> types.ModuleType: hass: HomeAssistant, provider: str
) -> types.ModuleType:
"""Load an auth provider.""" """Load an auth provider."""
try: try:
module = importlib.import_module( module = importlib.import_module(
'homeassistant.auth.providers.{}'.format(provider)) "homeassistant.auth.providers.{}".format(provider)
)
except ImportError as err: except ImportError as err:
_LOGGER.error('Unable to load auth provider %s: %s', provider, err) _LOGGER.error("Unable to load auth provider %s: %s", provider, err)
raise HomeAssistantError('Unable to load auth provider {}: {}'.format( raise HomeAssistantError(
provider, err)) "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 return module
processed = hass.data.get(DATA_REQS) processed = hass.data.get(DATA_REQS)
@ -150,12 +162,13 @@ async def load_auth_provider_module(
# https://github.com/python/mypy/issues/1424 # https://github.com/python/mypy/issues/1424
reqs = module.REQUIREMENTS # type: ignore reqs = module.REQUIREMENTS # type: ignore
req_success = await requirements.async_process_requirements( req_success = await requirements.async_process_requirements(
hass, 'auth provider {}'.format(provider), reqs) hass, "auth provider {}".format(provider), reqs
)
if not req_success: if not req_success:
raise HomeAssistantError( raise HomeAssistantError(
'Unable to process requirements of auth provider {}'.format( "Unable to process requirements of auth provider {}".format(provider)
provider)) )
processed.add(provider) processed.add(provider)
return module return module
@ -174,8 +187,8 @@ class LoginFlow(data_entry_flow.FlowHandler):
self.user = None # type: Optional[User] self.user = None # type: Optional[User]
async def async_step_init( async def async_step_init(
self, user_input: Optional[Dict[str, str]] = None) \ self, user_input: Optional[Dict[str, str]] = None
-> Dict[str, Any]: ) -> Dict[str, Any]:
"""Handle the first step of login flow. """Handle the first step of login flow.
Return self.async_show_form(step_id='init') if user_input == None. Return self.async_show_form(step_id='init') if user_input == None.
@ -184,38 +197,37 @@ class LoginFlow(data_entry_flow.FlowHandler):
raise NotImplementedError raise NotImplementedError
async def async_step_select_mfa_module( async def async_step_select_mfa_module(
self, user_input: Optional[Dict[str, str]] = None) \ self, user_input: Optional[Dict[str, str]] = None
-> Dict[str, Any]: ) -> Dict[str, Any]:
"""Handle the step of select mfa module.""" """Handle the step of select mfa module."""
errors = {} errors = {}
if user_input is not None: 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: if auth_module in self.available_mfa_modules:
self._auth_module_id = auth_module self._auth_module_id = auth_module
return await self.async_step_mfa() return await self.async_step_mfa()
errors['base'] = 'invalid_auth_module' errors["base"] = "invalid_auth_module"
if len(self.available_mfa_modules) == 1: if len(self.available_mfa_modules) == 1:
self._auth_module_id = list(self.available_mfa_modules.keys())[0] self._auth_module_id = list(self.available_mfa_modules.keys())[0]
return await self.async_step_mfa() return await self.async_step_mfa()
return self.async_show_form( return self.async_show_form(
step_id='select_mfa_module', step_id="select_mfa_module",
data_schema=vol.Schema({ data_schema=vol.Schema(
'multi_factor_auth_module': vol.In(self.available_mfa_modules) {"multi_factor_auth_module": vol.In(self.available_mfa_modules)}
}), ),
errors=errors, errors=errors,
) )
async def async_step_mfa( async def async_step_mfa(
self, user_input: Optional[Dict[str, str]] = None) \ self, user_input: Optional[Dict[str, str]] = None
-> Dict[str, Any]: ) -> Dict[str, Any]:
"""Handle the step of mfa validation.""" """Handle the step of mfa validation."""
errors = {} errors = {}
auth_module = self._auth_manager.get_auth_mfa_module( auth_module = self._auth_manager.get_auth_mfa_module(self._auth_module_id)
self._auth_module_id)
if auth_module is None: if auth_module is None:
# Given an invalid input to async_step_select_mfa_module # Given an invalid input to async_step_select_mfa_module
# will show invalid_auth_module error # will show invalid_auth_module error
@ -224,25 +236,24 @@ class LoginFlow(data_entry_flow.FlowHandler):
if user_input is not None: if user_input is not None:
expires = self.created_at + SESSION_EXPIRATION expires = self.created_at + SESSION_EXPIRATION
if dt_util.utcnow() > expires: if dt_util.utcnow() > expires:
return self.async_abort( return self.async_abort(reason="login_expired")
reason='login_expired'
)
result = await auth_module.async_validation( result = await auth_module.async_validation(
self.user.id, user_input) # type: ignore self.user.id, user_input
) # type: ignore
if not result: if not result:
errors['base'] = 'invalid_code' errors["base"] = "invalid_code"
if not errors: if not errors:
return await self.async_finish(self.user) return await self.async_finish(self.user)
description_placeholders = { description_placeholders = {
'mfa_module_name': auth_module.name, "mfa_module_name": auth_module.name,
'mfa_module_id': auth_module.id "mfa_module_id": auth_module.id,
} # type: Dict[str, str] } # type: Dict[str, str]
return self.async_show_form( return self.async_show_form(
step_id='mfa', step_id="mfa",
data_schema=auth_module.input_schema, data_schema=auth_module.input_schema,
description_placeholders=description_placeholders, description_placeholders=description_placeholders,
errors=errors, errors=errors,
@ -250,7 +261,4 @@ class LoginFlow(data_entry_flow.FlowHandler):
async def async_finish(self, flow_result: Any) -> Dict: async def async_finish(self, flow_result: Any) -> Dict:
"""Handle the pass of login flow.""" """Handle the pass of login flow."""
return self.async_create_entry( return self.async_create_entry(title=self._auth_provider.name, data=flow_result)
title=self._auth_provider.name,
data=flow_result
)

View file

@ -20,14 +20,13 @@ from ..util import generate_secret
STORAGE_VERSION = 1 STORAGE_VERSION = 1
STORAGE_KEY = 'auth_provider.homeassistant' STORAGE_KEY = "auth_provider.homeassistant"
def _disallow_id(conf: Dict[str, Any]) -> Dict[str, Any]: def _disallow_id(conf: Dict[str, Any]) -> Dict[str, Any]:
"""Disallow ID in config.""" """Disallow ID in config."""
if CONF_ID in conf: if CONF_ID in conf:
raise vol.Invalid( raise vol.Invalid("ID is not allowed for the homeassistant auth provider.")
'ID is not allowed for the homeassistant auth provider.')
return conf return conf
@ -60,68 +59,62 @@ class Data:
data = await self._store.async_load() data = await self._store.async_load()
if data is None: if data is None:
data = { data = {"salt": generate_secret(), "users": []}
'salt': generate_secret(),
'users': []
}
self._data = data self._data = data
@property @property
def users(self) -> List[Dict[str, str]]: def users(self) -> List[Dict[str, str]]:
"""Return users.""" """Return users."""
return self._data['users'] # type: ignore return self._data["users"] # type: ignore
def validate_login(self, username: str, password: str) -> None: def validate_login(self, username: str, password: str) -> None:
"""Validate a username and password. """Validate a username and password.
Raises InvalidAuth if auth invalid. Raises InvalidAuth if auth invalid.
""" """
dummy = b'$2b$12$CiuFGszHx9eNHxPuQcwBWez4CwDTOcLTX5CbOpV6gef2nYuXkY7BO' dummy = b"$2b$12$CiuFGszHx9eNHxPuQcwBWez4CwDTOcLTX5CbOpV6gef2nYuXkY7BO"
found = None found = None
# Compare all users to avoid timing attacks. # Compare all users to avoid timing attacks.
for user in self.users: for user in self.users:
if username == user['username']: if username == user["username"]:
found = user found = user
if found is None: if found is None:
# check a hash to make timing the same as if user was found # check a hash to make timing the same as if user was found
bcrypt.checkpw(b'foo', bcrypt.checkpw(b"foo", dummy)
dummy)
raise InvalidAuth raise InvalidAuth
user_hash = base64.b64decode(found['password']) user_hash = base64.b64decode(found["password"])
# if the hash is not a bcrypt hash... # if the hash is not a bcrypt hash...
# provide a transparant upgrade for old pbkdf2 hash format # provide a transparant upgrade for old pbkdf2 hash format
if not (user_hash.startswith(b'$2a$') if not (
or user_hash.startswith(b'$2b$') user_hash.startswith(b"$2a$")
or user_hash.startswith(b'$2x$') or user_hash.startswith(b"$2b$")
or user_hash.startswith(b'$2y$')): or user_hash.startswith(b"$2x$")
or user_hash.startswith(b"$2y$")
):
# IMPORTANT! validate the login, bail if invalid # IMPORTANT! validate the login, bail if invalid
hashed = self.legacy_hash_password(password) hashed = self.legacy_hash_password(password)
if not hmac.compare_digest(hashed, user_hash): if not hmac.compare_digest(hashed, user_hash):
raise InvalidAuth raise InvalidAuth
# then re-hash the valid password with bcrypt # then re-hash the valid password with bcrypt
self.change_password(found['username'], password) self.change_password(found["username"], password)
run_coroutine_threadsafe( run_coroutine_threadsafe(self.async_save(), self.hass.loop).result()
self.async_save(), self.hass.loop user_hash = base64.b64decode(found["password"])
).result()
user_hash = base64.b64decode(found['password'])
# bcrypt.checkpw is timing-safe # bcrypt.checkpw is timing-safe
if not bcrypt.checkpw(password.encode(), if not bcrypt.checkpw(password.encode(), user_hash):
user_hash):
raise InvalidAuth raise InvalidAuth
def legacy_hash_password(self, password: str, def legacy_hash_password(self, password: str, for_storage: bool = False) -> bytes:
for_storage: bool = False) -> bytes:
"""LEGACY password encoding.""" """LEGACY password encoding."""
# We're no longer storing salts in data, but if one exists we # We're no longer storing salts in data, but if one exists we
# should be able to retrieve it. # should be able to retrieve it.
salt = self._data['salt'].encode() # type: ignore salt = self._data["salt"].encode() # type: ignore
hashed = hashlib.pbkdf2_hmac('sha512', password.encode(), salt, 100000) hashed = hashlib.pbkdf2_hmac("sha512", password.encode(), salt, 100000)
if for_storage: if for_storage:
hashed = base64.b64encode(hashed) hashed = base64.b64encode(hashed)
return hashed return hashed
@ -129,28 +122,30 @@ class Data:
# pylint: disable=no-self-use # pylint: disable=no-self-use
def hash_password(self, password: str, for_storage: bool = False) -> bytes: def hash_password(self, password: str, for_storage: bool = False) -> bytes:
"""Encode a password.""" """Encode a password."""
hashed = bcrypt.hashpw(password.encode(), bcrypt.gensalt(rounds=12)) \ hashed = bcrypt.hashpw(password.encode(), bcrypt.gensalt(rounds=12))
# type: bytes # type: bytes
if for_storage: if for_storage:
hashed = base64.b64encode(hashed) hashed = base64.b64encode(hashed)
return hashed return hashed
def add_auth(self, username: str, password: str) -> None: def add_auth(self, username: str, password: str) -> None:
"""Add a new authenticated user/pass.""" """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 raise InvalidUser
self.users.append({ self.users.append(
'username': username, {
'password': self.hash_password(password, True).decode(), "username": username,
}) "password": self.hash_password(password, True).decode(),
}
)
@callback @callback
def async_remove_auth(self, username: str) -> None: def async_remove_auth(self, username: str) -> None:
"""Remove authentication.""" """Remove authentication."""
index = None index = None
for i, user in enumerate(self.users): for i, user in enumerate(self.users):
if user['username'] == username: if user["username"] == username:
index = i index = i
break break
@ -165,9 +160,8 @@ class Data:
Raises InvalidUser if user cannot be found. Raises InvalidUser if user cannot be found.
""" """
for user in self.users: for user in self.users:
if user['username'] == username: if user["username"] == username:
user['password'] = self.hash_password( user["password"] = self.hash_password(new_password, True).decode()
new_password, True).decode()
break break
else: else:
raise InvalidUser raise InvalidUser
@ -177,11 +171,11 @@ class Data:
await self._store.async_save(self._data) await self._store.async_save(self._data)
@AUTH_PROVIDERS.register('homeassistant') @AUTH_PROVIDERS.register("homeassistant")
class HassAuthProvider(AuthProvider): class HassAuthProvider(AuthProvider):
"""Auth provider based on a local storage of users in HASS config dir.""" """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 data = None
@ -193,8 +187,7 @@ class HassAuthProvider(AuthProvider):
self.data = Data(self.hass) self.data = Data(self.hass)
await self.data.async_load() await self.data.async_load()
async def async_login_flow( async def async_login_flow(self, context: Optional[Dict]) -> LoginFlow:
self, context: Optional[Dict]) -> LoginFlow:
"""Return a flow to login.""" """Return a flow to login."""
return HassLoginFlow(self) return HassLoginFlow(self)
@ -205,36 +198,36 @@ class HassAuthProvider(AuthProvider):
assert self.data is not None assert self.data is not None
await self.hass.async_add_executor_job( 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( 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.""" """Get credentials based on the flow result."""
username = flow_result['username'] username = flow_result["username"]
for credential in await self.async_credentials(): for credential in await self.async_credentials():
if credential.data['username'] == username: if credential.data["username"] == username:
return credential return credential
# Create new credentials. # Create new credentials.
return self.async_create_credentials({ return self.async_create_credentials({"username": username})
'username': username
})
async def async_user_meta_for_credentials( async def async_user_meta_for_credentials(
self, credentials: Credentials) -> UserMeta: self, credentials: Credentials
) -> UserMeta:
"""Get extra info for this credential.""" """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( async def async_will_remove_credentials(self, credentials: Credentials) -> None:
self, credentials: Credentials) -> None:
"""When credentials get removed, also remove the auth.""" """When credentials get removed, also remove the auth."""
if self.data is None: if self.data is None:
await self.async_initialize() await self.async_initialize()
assert self.data is not None assert self.data is not None
try: try:
self.data.async_remove_auth(credentials.data['username']) self.data.async_remove_auth(credentials.data["username"])
await self.data.async_save() await self.data.async_save()
except InvalidUser: except InvalidUser:
# Can happen if somehow we didn't clean up a credential # Can happen if somehow we didn't clean up a credential
@ -245,29 +238,27 @@ class HassLoginFlow(LoginFlow):
"""Handler for the login flow.""" """Handler for the login flow."""
async def async_step_init( async def async_step_init(
self, user_input: Optional[Dict[str, str]] = None) \ self, user_input: Optional[Dict[str, str]] = None
-> Dict[str, Any]: ) -> Dict[str, Any]:
"""Handle the step of the form.""" """Handle the step of the form."""
errors = {} errors = {}
if user_input is not None: if user_input is not None:
try: try:
await cast(HassAuthProvider, self._auth_provider)\ await cast(HassAuthProvider, self._auth_provider).async_validate_login(
.async_validate_login(user_input['username'], user_input["username"], user_input["password"]
user_input['password']) )
except InvalidAuth: except InvalidAuth:
errors['base'] = 'invalid_auth' errors["base"] = "invalid_auth"
if not errors: if not errors:
user_input.pop('password') user_input.pop("password")
return await self.async_finish(user_input) return await self.async_finish(user_input)
schema = OrderedDict() # type: Dict[str, type] schema = OrderedDict() # type: Dict[str, type]
schema['username'] = str schema["username"] = str
schema['password'] = str schema["password"] = str
return self.async_show_form( return self.async_show_form(
step_id='init', step_id="init", data_schema=vol.Schema(schema), errors=errors
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 from ..models import Credentials, UserMeta
USER_SCHEMA = vol.Schema({ USER_SCHEMA = vol.Schema(
vol.Required('username'): str, {
vol.Required('password'): str, vol.Required("username"): str,
vol.Optional('name'): str, vol.Required("password"): str,
}) vol.Optional("name"): str,
}
)
CONFIG_SCHEMA = AUTH_PROVIDER_SCHEMA.extend({ CONFIG_SCHEMA = AUTH_PROVIDER_SCHEMA.extend(
vol.Required('users'): [USER_SCHEMA] {vol.Required("users"): [USER_SCHEMA]}, extra=vol.PREVENT_EXTRA
}, extra=vol.PREVENT_EXTRA) )
class InvalidAuthError(HomeAssistantError): class InvalidAuthError(HomeAssistantError):
"""Raised when submitting invalid authentication.""" """Raised when submitting invalid authentication."""
@AUTH_PROVIDERS.register('insecure_example') @AUTH_PROVIDERS.register("insecure_example")
class ExampleAuthProvider(AuthProvider): class ExampleAuthProvider(AuthProvider):
"""Example auth provider based on hardcoded usernames and passwords.""" """Example auth provider based on hardcoded usernames and passwords."""
@ -42,47 +44,48 @@ class ExampleAuthProvider(AuthProvider):
user = None user = None
# Compare all users to avoid timing attacks. # Compare all users to avoid timing attacks.
for usr in self.config['users']: for usr in self.config["users"]:
if hmac.compare_digest(username.encode('utf-8'), if hmac.compare_digest(
usr['username'].encode('utf-8')): username.encode("utf-8"), usr["username"].encode("utf-8")
):
user = usr user = usr
if user is None: if user is None:
# Do one more compare to make timing the same as if user was found. # Do one more compare to make timing the same as if user was found.
hmac.compare_digest(password.encode('utf-8'), hmac.compare_digest(password.encode("utf-8"), password.encode("utf-8"))
password.encode('utf-8'))
raise InvalidAuthError raise InvalidAuthError
if not hmac.compare_digest(user['password'].encode('utf-8'), if not hmac.compare_digest(
password.encode('utf-8')): user["password"].encode("utf-8"), password.encode("utf-8")
):
raise InvalidAuthError raise InvalidAuthError
async def async_get_or_create_credentials( 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.""" """Get credentials based on the flow result."""
username = flow_result['username'] username = flow_result["username"]
for credential in await self.async_credentials(): for credential in await self.async_credentials():
if credential.data['username'] == username: if credential.data["username"] == username:
return credential return credential
# Create new credentials. # Create new credentials.
return self.async_create_credentials({ return self.async_create_credentials({"username": username})
'username': username
})
async def async_user_meta_for_credentials( async def async_user_meta_for_credentials(
self, credentials: Credentials) -> UserMeta: self, credentials: Credentials
) -> UserMeta:
"""Return extra user metadata for credentials. """Return extra user metadata for credentials.
Will be used to populate info when creating a new user. Will be used to populate info when creating a new user.
""" """
username = credentials.data['username'] username = credentials.data["username"]
name = None name = None
for user in self.config['users']: for user in self.config["users"]:
if user['username'] == username: if user["username"] == username:
name = user.get('name') name = user.get("name")
break break
return UserMeta(name=name, is_active=True) return UserMeta(name=name, is_active=True)
@ -92,29 +95,27 @@ class ExampleLoginFlow(LoginFlow):
"""Handler for the login flow.""" """Handler for the login flow."""
async def async_step_init( async def async_step_init(
self, user_input: Optional[Dict[str, str]] = None) \ self, user_input: Optional[Dict[str, str]] = None
-> Dict[str, Any]: ) -> Dict[str, Any]:
"""Handle the step of the form.""" """Handle the step of the form."""
errors = {} errors = {}
if user_input is not None: if user_input is not None:
try: try:
cast(ExampleAuthProvider, self._auth_provider)\ cast(ExampleAuthProvider, self._auth_provider).async_validate_login(
.async_validate_login(user_input['username'], user_input["username"], user_input["password"]
user_input['password']) )
except InvalidAuthError: except InvalidAuthError:
errors['base'] = 'invalid_auth' errors["base"] = "invalid_auth"
if not errors: if not errors:
user_input.pop('password') user_input.pop("password")
return await self.async_finish(user_input) return await self.async_finish(user_input)
schema = OrderedDict() # type: Dict[str, type] schema = OrderedDict() # type: Dict[str, type]
schema['username'] = str schema["username"] = str
schema['password'] = str schema["password"] = str
return self.async_show_form( return self.async_show_form(
step_id='init', step_id="init", data_schema=vol.Schema(schema), errors=errors
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 from ..models import Credentials, UserMeta
USER_SCHEMA = vol.Schema({ USER_SCHEMA = vol.Schema({vol.Required("username"): str})
vol.Required('username'): str,
})
CONFIG_SCHEMA = AUTH_PROVIDER_SCHEMA.extend({ CONFIG_SCHEMA = AUTH_PROVIDER_SCHEMA.extend({}, extra=vol.PREVENT_EXTRA)
}, extra=vol.PREVENT_EXTRA)
LEGACY_USER_NAME = 'Legacy API password user' LEGACY_USER_NAME = "Legacy API password user"
class InvalidAuthError(HomeAssistantError): class InvalidAuthError(HomeAssistantError):
"""Raised when submitting invalid authentication.""" """Raised when submitting invalid authentication."""
@AUTH_PROVIDERS.register('legacy_api_password') @AUTH_PROVIDERS.register("legacy_api_password")
class LegacyApiPasswordAuthProvider(AuthProvider): class LegacyApiPasswordAuthProvider(AuthProvider):
"""Example auth provider based on hardcoded usernames and passwords.""" """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: async def async_login_flow(self, context: Optional[Dict]) -> LoginFlow:
"""Return a flow to login.""" """Return a flow to login."""
@ -44,14 +41,16 @@ class LegacyApiPasswordAuthProvider(AuthProvider):
@callback @callback
def async_validate_login(self, password: str) -> None: def async_validate_login(self, password: str) -> None:
"""Validate a username and password.""" """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'), if not hmac.compare_digest(
password.encode('utf-8')): hass_http.api_password.encode("utf-8"), password.encode("utf-8")
):
raise InvalidAuthError raise InvalidAuthError
async def async_get_or_create_credentials( 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.""" """Return credentials for this login."""
credentials = await self.async_credentials() credentials = await self.async_credentials()
if credentials: if credentials:
@ -60,7 +59,8 @@ class LegacyApiPasswordAuthProvider(AuthProvider):
return self.async_create_credentials({}) return self.async_create_credentials({})
async def async_user_meta_for_credentials( async def async_user_meta_for_credentials(
self, credentials: Credentials) -> UserMeta: self, credentials: Credentials
) -> UserMeta:
""" """
Return info for the user. Return info for the user.
@ -73,29 +73,26 @@ class LegacyLoginFlow(LoginFlow):
"""Handler for the login flow.""" """Handler for the login flow."""
async def async_step_init( async def async_step_init(
self, user_input: Optional[Dict[str, str]] = None) \ self, user_input: Optional[Dict[str, str]] = None
-> Dict[str, Any]: ) -> Dict[str, Any]:
"""Handle the step of the form.""" """Handle the step of the form."""
errors = {} 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: if hass_http is None or not hass_http.api_password:
return self.async_abort( return self.async_abort(reason="no_api_password_set")
reason='no_api_password_set'
)
if user_input is not None: if user_input is not None:
try: try:
cast(LegacyApiPasswordAuthProvider, self._auth_provider)\ cast(
.async_validate_login(user_input['password']) LegacyApiPasswordAuthProvider, self._auth_provider
).async_validate_login(user_input["password"])
except InvalidAuthError: except InvalidAuthError:
errors['base'] = 'invalid_auth' errors["base"] = "invalid_auth"
if not errors: if not errors:
return await self.async_finish({}) return await self.async_finish({})
return self.async_show_form( return self.async_show_form(
step_id='init', step_id="init", data_schema=vol.Schema({"password": str}), errors=errors
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 . import AuthProvider, AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS, LoginFlow
from ..models import Credentials, UserMeta from ..models import Credentials, UserMeta
CONFIG_SCHEMA = AUTH_PROVIDER_SCHEMA.extend({ CONFIG_SCHEMA = AUTH_PROVIDER_SCHEMA.extend({}, extra=vol.PREVENT_EXTRA)
}, extra=vol.PREVENT_EXTRA)
class InvalidAuthError(HomeAssistantError): class InvalidAuthError(HomeAssistantError):
@ -26,14 +25,14 @@ class InvalidUserError(HomeAssistantError):
"""Raised when try to login as invalid user.""" """Raised when try to login as invalid user."""
@AUTH_PROVIDERS.register('trusted_networks') @AUTH_PROVIDERS.register("trusted_networks")
class TrustedNetworksAuthProvider(AuthProvider): class TrustedNetworksAuthProvider(AuthProvider):
"""Trusted Networks auth provider. """Trusted Networks auth provider.
Allow passwordless access from trusted network. Allow passwordless access from trusted network.
""" """
DEFAULT_TITLE = 'Trusted Networks' DEFAULT_TITLE = "Trusted Networks"
@property @property
def support_mfa(self) -> bool: def support_mfa(self) -> bool:
@ -44,27 +43,29 @@ class TrustedNetworksAuthProvider(AuthProvider):
"""Return a flow to login.""" """Return a flow to login."""
assert context is not None assert context is not None
users = await self.store.async_get_users() users = await self.store.async_get_users()
available_users = {user.id: user.name available_users = {
for user in users user.id: user.name
if not user.system_generated and user.is_active} for user in users
if not user.system_generated and user.is_active
}
return TrustedNetworksLoginFlow( 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( 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.""" """Get credentials based on the flow result."""
user_id = flow_result['user'] user_id = flow_result["user"]
users = await self.store.async_get_users() users = await self.store.async_get_users()
for user in users: for user in users:
if (not user.system_generated and if not user.system_generated and user.is_active and user.id == user_id:
user.is_active and
user.id == user_id):
for credential in await self.async_credentials(): for credential in await self.async_credentials():
if credential.data['user_id'] == user_id: if credential.data["user_id"] == user_id:
return credential 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) await self.store.async_link_user(user, cred)
return cred return cred
@ -72,7 +73,8 @@ class TrustedNetworksAuthProvider(AuthProvider):
raise InvalidUserError raise InvalidUserError
async def async_user_meta_for_credentials( async def async_user_meta_for_credentials(
self, credentials: Credentials) -> UserMeta: self, credentials: Credentials
) -> UserMeta:
"""Return extra user metadata for credentials. """Return extra user metadata for credentials.
Trusted network auth provider should never create new user. Trusted network auth provider should never create new user.
@ -86,44 +88,48 @@ class TrustedNetworksAuthProvider(AuthProvider):
Raise InvalidAuthError if not. Raise InvalidAuthError if not.
Raise InvalidAuthError if trusted_networks is not configured. 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: 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 if not any(
in hass_http.trusted_networks): ip_address in trusted_network
raise InvalidAuthError('Not in trusted_networks') for trusted_network in hass_http.trusted_networks
):
raise InvalidAuthError("Not in trusted_networks")
class TrustedNetworksLoginFlow(LoginFlow): class TrustedNetworksLoginFlow(LoginFlow):
"""Handler for the login flow.""" """Handler for the login flow."""
def __init__(self, auth_provider: TrustedNetworksAuthProvider, def __init__(
ip_address: str, available_users: Dict[str, Optional[str]]) \ self,
-> None: auth_provider: TrustedNetworksAuthProvider,
ip_address: str,
available_users: Dict[str, Optional[str]],
) -> None:
"""Initialize the login flow.""" """Initialize the login flow."""
super().__init__(auth_provider) super().__init__(auth_provider)
self._available_users = available_users self._available_users = available_users
self._ip_address = ip_address self._ip_address = ip_address
async def async_step_init( async def async_step_init(
self, user_input: Optional[Dict[str, str]] = None) \ self, user_input: Optional[Dict[str, str]] = None
-> Dict[str, Any]: ) -> Dict[str, Any]:
"""Handle the step of the form.""" """Handle the step of the form."""
try: try:
cast(TrustedNetworksAuthProvider, self._auth_provider)\ cast(
.async_validate_access(self._ip_address) TrustedNetworksAuthProvider, self._auth_provider
).async_validate_access(self._ip_address)
except InvalidAuthError: except InvalidAuthError:
return self.async_abort( return self.async_abort(reason="not_whitelisted")
reason='not_whitelisted'
)
if user_input is not None: if user_input is not None:
return await self.async_finish(user_input) return await self.async_finish(user_input)
return self.async_show_form( return self.async_show_form(
step_id='init', step_id="init",
data_schema=vol.Schema({'user': vol.In(self._available_users)}), 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. 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 import voluptuous as vol
from homeassistant import ( 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.components import persistent_notification
from homeassistant.const import EVENT_HOMEASSISTANT_CLOSE from homeassistant.const import EVENT_HOMEASSISTANT_CLOSE
from homeassistant.setup import async_setup_component from homeassistant.setup import async_setup_component
@ -22,25 +26,34 @@ from homeassistant.helpers.signal import async_register_signal_handling
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
ERROR_LOG_FILENAME = 'home-assistant.log' ERROR_LOG_FILENAME = "home-assistant.log"
# hass.data key for logging information. # hass.data key for logging information.
DATA_LOGGING = 'logging' DATA_LOGGING = "logging"
FIRST_INIT_COMPONENT = {'system_log', 'recorder', 'mqtt', 'mqtt_eventstream', FIRST_INIT_COMPONENT = {
'logger', 'introduction', 'frontend', 'history'} "system_log",
"recorder",
"mqtt",
"mqtt_eventstream",
"logger",
"introduction",
"frontend",
"history",
}
def from_config_dict(config: Dict[str, Any], def from_config_dict(
hass: Optional[core.HomeAssistant] = None, config: Dict[str, Any],
config_dir: Optional[str] = None, hass: Optional[core.HomeAssistant] = None,
enable_log: bool = True, config_dir: Optional[str] = None,
verbose: bool = False, enable_log: bool = True,
skip_pip: bool = False, verbose: bool = False,
log_rotate_days: Any = None, skip_pip: bool = False,
log_file: Any = None, log_rotate_days: Any = None,
log_no_color: bool = False) \ log_file: Any = None,
-> Optional[core.HomeAssistant]: log_no_color: bool = False,
) -> Optional[core.HomeAssistant]:
"""Try to configure Home Assistant from a configuration dictionary. """Try to configure Home Assistant from a configuration dictionary.
Dynamically loads required components and its dependencies. 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) config_dir = os.path.abspath(config_dir)
hass.config.config_dir = config_dir hass.config.config_dir = config_dir
if not is_virtual_env(): if not is_virtual_env():
hass.loop.run_until_complete( hass.loop.run_until_complete(async_mount_local_lib_path(config_dir))
async_mount_local_lib_path(config_dir))
# run task # run task
hass = hass.loop.run_until_complete( hass = hass.loop.run_until_complete(
async_from_config_dict( async_from_config_dict(
config, hass, config_dir, enable_log, verbose, skip_pip, config,
log_rotate_days, log_file, log_no_color) hass,
config_dir,
enable_log,
verbose,
skip_pip,
log_rotate_days,
log_file,
log_no_color,
)
) )
return hass return hass
async def async_from_config_dict(config: Dict[str, Any], async def async_from_config_dict(
hass: core.HomeAssistant, config: Dict[str, Any],
config_dir: Optional[str] = None, hass: core.HomeAssistant,
enable_log: bool = True, config_dir: Optional[str] = None,
verbose: bool = False, enable_log: bool = True,
skip_pip: bool = False, verbose: bool = False,
log_rotate_days: Any = None, skip_pip: bool = False,
log_file: Any = None, log_rotate_days: Any = None,
log_no_color: bool = False) \ log_file: Any = None,
-> Optional[core.HomeAssistant]: log_no_color: bool = False,
) -> Optional[core.HomeAssistant]:
"""Try to configure Home Assistant from a configuration dictionary. """Try to configure Home Assistant from a configuration dictionary.
Dynamically loads required components and its dependencies. Dynamically loads required components and its dependencies.
@ -81,40 +102,41 @@ async def async_from_config_dict(config: Dict[str, Any],
start = time() start = time()
if enable_log: if enable_log:
async_enable_logging(hass, verbose, log_rotate_days, log_file, async_enable_logging(hass, verbose, log_rotate_days, log_file, log_no_color)
log_no_color)
core_config = config.get(core.DOMAIN, {}) core_config = config.get(core.DOMAIN, {})
has_api_password = bool((config.get('http') or {}).get('api_password')) has_api_password = bool((config.get("http") or {}).get("api_password"))
has_trusted_networks = bool((config.get('http') or {}) has_trusted_networks = bool((config.get("http") or {}).get("trusted_networks"))
.get('trusted_networks'))
try: try:
await conf_util.async_process_ha_core_config( 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: except vol.Invalid as config_err:
conf_util.async_log_exception( conf_util.async_log_exception(config_err, "homeassistant", core_config, hass)
config_err, 'homeassistant', core_config, hass)
return None return None
except HomeAssistantError: except HomeAssistantError:
_LOGGER.error("Home Assistant core failed to initialize. " _LOGGER.error(
"Further initialization aborted") "Home Assistant core failed to initialize. "
"Further initialization aborted"
)
return None return None
await hass.async_add_executor_job( await hass.async_add_executor_job(conf_util.process_ha_config_upgrade, hass)
conf_util.process_ha_config_upgrade, hass)
hass.config.skip_pip = skip_pip hass.config.skip_pip = skip_pip
if skip_pip: if skip_pip:
_LOGGER.warning("Skipping pip installation of required modules. " _LOGGER.warning(
"This may cause issues") "Skipping pip installation of required modules. " "This may cause issues"
)
# Make a copy because we are mutating it. # Make a copy because we are mutating it.
config = OrderedDict(config) config = OrderedDict(config)
# Merge packages # Merge packages
conf_util.merge_packages_config( 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 # Ensure we have no None values after merge
for key, value in config.items(): 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() await hass.config_entries.async_load()
# Filter out the repeating and common config section [homeassistant] # Filter out the repeating and common config section [homeassistant]
components = set(key.split(' ')[0] for key in config.keys() components = set(key.split(" ")[0] for key in config.keys() if key != core.DOMAIN)
if key != core.DOMAIN)
components.update(hass.config_entries.async_domains()) components.update(hass.config_entries.async_domains())
# setup components # setup components
res = await core_components.async_setup(hass, config) res = await core_components.async_setup(hass, config)
if not res: if not res:
_LOGGER.error("Home Assistant core failed to initialize. " _LOGGER.error(
"Further initialization aborted") "Home Assistant core failed to initialize. "
"Further initialization aborted"
)
return hass return hass
await persistent_notification.async_setup(hass, config) 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() await hass.async_block_till_done()
stop = time() 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) async_register_signal_handling(hass)
return hass return hass
def from_config_file(config_path: str, def from_config_file(
hass: Optional[core.HomeAssistant] = None, config_path: str,
verbose: bool = False, hass: Optional[core.HomeAssistant] = None,
skip_pip: bool = True, verbose: bool = False,
log_rotate_days: Any = None, skip_pip: bool = True,
log_file: Any = None, log_rotate_days: Any = None,
log_no_color: bool = False)\ log_file: Any = None,
-> Optional[core.HomeAssistant]: log_no_color: bool = False,
) -> Optional[core.HomeAssistant]:
"""Read the configuration file and try to start all the functionality. """Read the configuration file and try to start all the functionality.
Will add functionality to 'hass' parameter if given, Will add functionality to 'hass' parameter if given,
@ -182,21 +206,28 @@ def from_config_file(config_path: str,
# run task # run task
hass = hass.loop.run_until_complete( hass = hass.loop.run_until_complete(
async_from_config_file( async_from_config_file(
config_path, hass, verbose, skip_pip, config_path,
log_rotate_days, log_file, log_no_color) hass,
verbose,
skip_pip,
log_rotate_days,
log_file,
log_no_color,
)
) )
return hass return hass
async def async_from_config_file(config_path: str, async def async_from_config_file(
hass: core.HomeAssistant, config_path: str,
verbose: bool = False, hass: core.HomeAssistant,
skip_pip: bool = True, verbose: bool = False,
log_rotate_days: Any = None, skip_pip: bool = True,
log_file: Any = None, log_rotate_days: Any = None,
log_no_color: bool = False)\ log_file: Any = None,
-> Optional[core.HomeAssistant]: log_no_color: bool = False,
) -> Optional[core.HomeAssistant]:
"""Read the configuration file and try to start all the functionality. """Read the configuration file and try to start all the functionality.
Will add functionality to 'hass' parameter. Will add functionality to 'hass' parameter.
@ -209,12 +240,12 @@ async def async_from_config_file(config_path: str,
if not is_virtual_env(): if not is_virtual_env():
await async_mount_local_lib_path(config_dir) await async_mount_local_lib_path(config_dir)
async_enable_logging(hass, verbose, log_rotate_days, log_file, async_enable_logging(hass, verbose, log_rotate_days, log_file, log_no_color)
log_no_color)
try: try:
config_dict = await hass.async_add_executor_job( 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: except HomeAssistantError as err:
_LOGGER.error("Error loading %s: %s", config_path, err) _LOGGER.error("Error loading %s: %s", config_path, err)
return None return None
@ -222,43 +253,48 @@ async def async_from_config_file(config_path: str,
clear_secret_cache() clear_secret_cache()
return await async_from_config_dict( 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 @core.callback
def async_enable_logging(hass: core.HomeAssistant, def async_enable_logging(
verbose: bool = False, hass: core.HomeAssistant,
log_rotate_days: Optional[int] = None, verbose: bool = False,
log_file: Optional[str] = None, log_rotate_days: Optional[int] = None,
log_no_color: bool = False) -> None: log_file: Optional[str] = None,
log_no_color: bool = False,
) -> None:
"""Set up the logging. """Set up the logging.
This method must be run in the event loop. This method must be run in the event loop.
""" """
fmt = ("%(asctime)s %(levelname)s (%(threadName)s) " fmt = "%(asctime)s %(levelname)s (%(threadName)s) " "[%(name)s] %(message)s"
"[%(name)s] %(message)s") datefmt = "%Y-%m-%d %H:%M:%S"
datefmt = '%Y-%m-%d %H:%M:%S'
if not log_no_color: if not log_no_color:
try: try:
from colorlog import ColoredFormatter from colorlog import ColoredFormatter
# basicConfig must be called after importing colorlog in order to # basicConfig must be called after importing colorlog in order to
# ensure that the handlers it sets up wraps the correct streams. # ensure that the handlers it sets up wraps the correct streams.
logging.basicConfig(level=logging.INFO) logging.basicConfig(level=logging.INFO)
colorfmt = "%(log_color)s{}%(reset)s".format(fmt) colorfmt = "%(log_color)s{}%(reset)s".format(fmt)
logging.getLogger().handlers[0].setFormatter(ColoredFormatter( logging.getLogger().handlers[0].setFormatter(
colorfmt, ColoredFormatter(
datefmt=datefmt, colorfmt,
reset=True, datefmt=datefmt,
log_colors={ reset=True,
'DEBUG': 'cyan', log_colors={
'INFO': 'green', "DEBUG": "cyan",
'WARNING': 'yellow', "INFO": "green",
'ERROR': 'red', "WARNING": "yellow",
'CRITICAL': 'red', "ERROR": "red",
} "CRITICAL": "red",
)) },
)
)
except ImportError: except ImportError:
pass pass
@ -267,9 +303,9 @@ def async_enable_logging(hass: core.HomeAssistant,
logging.basicConfig(format=fmt, datefmt=datefmt, level=logging.INFO) logging.basicConfig(format=fmt, datefmt=datefmt, level=logging.INFO)
# Suppress overly verbose logs from libraries that aren't helpful # Suppress overly verbose logs from libraries that aren't helpful
logging.getLogger('requests').setLevel(logging.WARNING) logging.getLogger("requests").setLevel(logging.WARNING)
logging.getLogger('urllib3').setLevel(logging.WARNING) logging.getLogger("urllib3").setLevel(logging.WARNING)
logging.getLogger('aiohttp.access').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 # Log errors to a file if we have write access to file or config dir
if log_file is None: 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 # Check if we can write to the error log if it exists or that
# we can create files in the containing directory if not. # we can create files in the containing directory if not.
if (err_path_exists and os.access(err_log_path, os.W_OK)) or \ 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)): not err_path_exists and os.access(err_dir, os.W_OK)
):
if log_rotate_days: if log_rotate_days:
err_handler = logging.handlers.TimedRotatingFileHandler( err_handler = logging.handlers.TimedRotatingFileHandler(
err_log_path, when='midnight', err_log_path, when="midnight", backupCount=log_rotate_days
backupCount=log_rotate_days) # type: logging.FileHandler ) # type: logging.FileHandler
else: else:
err_handler = logging.FileHandler( err_handler = logging.FileHandler(err_log_path, mode="w", delay=True)
err_log_path, mode='w', delay=True)
err_handler.setLevel(logging.INFO if verbose else logging.WARNING) err_handler.setLevel(logging.INFO if verbose else logging.WARNING)
err_handler.setFormatter(logging.Formatter(fmt, datefmt=datefmt)) 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: async def async_stop_async_handler(_: Any) -> None:
"""Cleanup async handler.""" """Cleanup async handler."""
logging.getLogger('').removeHandler(async_handler) # type: ignore logging.getLogger("").removeHandler(async_handler) # type: ignore
await async_handler.async_close(blocking=True) await async_handler.async_close(blocking=True)
hass.bus.async_listen_once( hass.bus.async_listen_once(EVENT_HOMEASSISTANT_CLOSE, async_stop_async_handler)
EVENT_HOMEASSISTANT_CLOSE, async_stop_async_handler)
logger = logging.getLogger('') logger = logging.getLogger("")
logger.addHandler(async_handler) # type: ignore logger.addHandler(async_handler) # type: ignore
logger.setLevel(logging.INFO) logger.setLevel(logging.INFO)
# Save the log file location for access by other components. # Save the log file location for access by other components.
hass.data[DATA_LOGGING] = err_log_path hass.data[DATA_LOGGING] = err_log_path
else: else:
_LOGGER.error( _LOGGER.error("Unable to set up error log %s (access denied)", err_log_path)
"Unable to set up error log %s (access denied)", err_log_path)
async def async_mount_local_lib_path(config_dir: str) -> str: 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. 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) lib_dir = await async_get_user_site(deps_dir)
if lib_dir not in sys.path: if lib_dir not in sys.path:
sys.path.insert(0, lib_dir) 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.service import extract_entity_ids
from homeassistant.helpers import intent from homeassistant.helpers import intent
from homeassistant.const import ( from homeassistant.const import (
ATTR_ENTITY_ID, SERVICE_TURN_ON, SERVICE_TURN_OFF, SERVICE_TOGGLE, ATTR_ENTITY_ID,
SERVICE_HOMEASSISTANT_STOP, SERVICE_HOMEASSISTANT_RESTART, SERVICE_TURN_ON,
RESTART_EXIT_CODE) SERVICE_TURN_OFF,
SERVICE_TOGGLE,
SERVICE_HOMEASSISTANT_STOP,
SERVICE_HOMEASSISTANT_RESTART,
RESTART_EXIT_CODE,
)
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
SERVICE_RELOAD_CORE_CONFIG = 'reload_core_config' SERVICE_RELOAD_CORE_CONFIG = "reload_core_config"
SERVICE_CHECK_CONFIG = 'check_config' SERVICE_CHECK_CONFIG = "check_config"
def is_on(hass, entity_id=None): def is_on(hass, entity_id=None):
@ -45,11 +50,10 @@ def is_on(hass, entity_id=None):
component = getattr(hass.components, domain) component = getattr(hass.components, domain)
except ImportError: except ImportError:
_LOGGER.error('Failed to call %s.is_on: component not found', _LOGGER.error("Failed to call %s.is_on: component not found", domain)
domain)
continue continue
if not hasattr(component, 'is_on'): if not hasattr(component, "is_on"):
_LOGGER.warning("Component %s has no is_on method.", domain) _LOGGER.warning("Component %s has no is_on method.", domain)
continue continue
@ -112,6 +116,7 @@ def async_reload_core_config(hass):
@asyncio.coroutine @asyncio.coroutine
def async_setup(hass: ha.HomeAssistant, config: dict) -> Awaitable[bool]: def async_setup(hass: ha.HomeAssistant, config: dict) -> Awaitable[bool]:
"""Set up general services related to Home Assistant.""" """Set up general services related to Home Assistant."""
@asyncio.coroutine @asyncio.coroutine
def async_handle_turn_service(service): def async_handle_turn_service(service):
"""Handle calls to homeassistant.turn_on/off.""" """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 # Generic turn on/off method requires entity id
if not entity_ids: if not entity_ids:
_LOGGER.error( _LOGGER.error(
"homeassistant/%s cannot be called without entity_id", "homeassistant/%s cannot be called without entity_id", service.service
service.service) )
return return
# Group entity_ids by domain. groupby requires sorted data. # Group entity_ids by domain. groupby requires sorted data.
by_domain = it.groupby(sorted(entity_ids), by_domain = it.groupby(
lambda item: ha.split_entity_id(item)[0]) sorted(entity_ids), lambda item: ha.split_entity_id(item)[0]
)
tasks = [] 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. # ent_ids is a generator, convert it to a list.
data[ATTR_ENTITY_ID] = list(ent_ids) data[ATTR_ENTITY_ID] = list(ent_ids)
tasks.append(hass.services.async_call( tasks.append(
domain, service.service, data, blocking)) hass.services.async_call(domain, service.service, data, blocking)
)
yield from asyncio.wait(tasks, loop=hass.loop) yield from asyncio.wait(tasks, loop=hass.loop)
hass.services.async_register( hass.services.async_register(ha.DOMAIN, SERVICE_TURN_OFF, async_handle_turn_service)
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( hass.services.async_register(ha.DOMAIN, SERVICE_TOGGLE, async_handle_turn_service)
ha.DOMAIN, SERVICE_TURN_ON, async_handle_turn_service) hass.helpers.intent.async_register(
hass.services.async_register( intent.ServiceIntentHandler(
ha.DOMAIN, SERVICE_TOGGLE, async_handle_turn_service) intent.INTENT_TURN_ON, ha.DOMAIN, SERVICE_TURN_ON, "Turned {} on"
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( hass.helpers.intent.async_register(
intent.INTENT_TURN_OFF, ha.DOMAIN, SERVICE_TURN_OFF, intent.ServiceIntentHandler(
"Turned {} off")) 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.helpers.intent.async_register(
intent.ServiceIntentHandler(
intent.INTENT_TOGGLE, ha.DOMAIN, SERVICE_TOGGLE, "Toggled {}"
)
)
@asyncio.coroutine @asyncio.coroutine
def async_handle_core_service(call): def async_handle_core_service(call):
@ -180,18 +192,23 @@ def async_setup(hass: ha.HomeAssistant, config: dict) -> Awaitable[bool]:
_LOGGER.error(errors) _LOGGER.error(errors)
hass.components.persistent_notification.async_create( hass.components.persistent_notification.async_create(
"Config error. See dev-info panel for details.", "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 return
if call.service == SERVICE_HOMEASSISTANT_RESTART: if call.service == SERVICE_HOMEASSISTANT_RESTART:
hass.async_create_task(hass.async_stop(RESTART_EXIT_CODE)) hass.async_create_task(hass.async_stop(RESTART_EXIT_CODE))
hass.services.async_register( 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( 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( hass.services.async_register(
ha.DOMAIN, SERVICE_CHECK_CONFIG, async_handle_core_service) ha.DOMAIN, SERVICE_CHECK_CONFIG, async_handle_core_service
)
@asyncio.coroutine @asyncio.coroutine
def async_handle_reload_config(call): def async_handle_reload_config(call):
@ -203,9 +220,11 @@ def async_setup(hass: ha.HomeAssistant, config: dict) -> Awaitable[bool]:
return return
yield from conf_util.async_process_ha_core_config( 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( 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 return True

View file

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

View file

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

View file

@ -11,25 +11,31 @@ import logging
import voluptuous as vol import voluptuous as vol
from homeassistant.const import ( from homeassistant.const import (
ATTR_CODE, ATTR_CODE_FORMAT, ATTR_ENTITY_ID, SERVICE_ALARM_TRIGGER, ATTR_CODE,
SERVICE_ALARM_DISARM, SERVICE_ALARM_ARM_HOME, SERVICE_ALARM_ARM_AWAY, ATTR_CODE_FORMAT,
SERVICE_ALARM_ARM_NIGHT, SERVICE_ALARM_ARM_CUSTOM_BYPASS) 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.loader import bind_hass
from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.entity_component import EntityComponent
DOMAIN = 'alarm_control_panel' DOMAIN = "alarm_control_panel"
SCAN_INTERVAL = timedelta(seconds=30) 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({ ALARM_SERVICE_SCHEMA = vol.Schema(
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, {vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, vol.Optional(ATTR_CODE): cv.string}
vol.Optional(ATTR_CODE): cv.string, )
})
@bind_hass @bind_hass
@ -108,33 +114,30 @@ def alarm_arm_custom_bypass(hass, code=None, entity_id=None):
def async_setup(hass, config): def async_setup(hass, config):
"""Track states and offer events for sensors.""" """Track states and offer events for sensors."""
component = hass.data[DOMAIN] = EntityComponent( 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) yield from component.async_setup(config)
component.async_register_entity_service( component.async_register_entity_service(
SERVICE_ALARM_DISARM, ALARM_SERVICE_SCHEMA, SERVICE_ALARM_DISARM, ALARM_SERVICE_SCHEMA, "async_alarm_disarm"
'async_alarm_disarm'
) )
component.async_register_entity_service( component.async_register_entity_service(
SERVICE_ALARM_ARM_HOME, ALARM_SERVICE_SCHEMA, SERVICE_ALARM_ARM_HOME, ALARM_SERVICE_SCHEMA, "async_alarm_arm_home"
'async_alarm_arm_home'
) )
component.async_register_entity_service( component.async_register_entity_service(
SERVICE_ALARM_ARM_AWAY, ALARM_SERVICE_SCHEMA, SERVICE_ALARM_ARM_AWAY, ALARM_SERVICE_SCHEMA, "async_alarm_arm_away"
'async_alarm_arm_away'
) )
component.async_register_entity_service( component.async_register_entity_service(
SERVICE_ALARM_ARM_NIGHT, ALARM_SERVICE_SCHEMA, SERVICE_ALARM_ARM_NIGHT, ALARM_SERVICE_SCHEMA, "async_alarm_arm_night"
'async_alarm_arm_night'
) )
component.async_register_entity_service( component.async_register_entity_service(
SERVICE_ALARM_ARM_CUSTOM_BYPASS, ALARM_SERVICE_SCHEMA, SERVICE_ALARM_ARM_CUSTOM_BYPASS,
'async_alarm_arm_custom_bypass' ALARM_SERVICE_SCHEMA,
"async_alarm_arm_custom_bypass",
) )
component.async_register_entity_service( component.async_register_entity_service(
SERVICE_ALARM_TRIGGER, ALARM_SERVICE_SCHEMA, SERVICE_ALARM_TRIGGER, ALARM_SERVICE_SCHEMA, "async_alarm_trigger"
'async_alarm_trigger'
) )
return True return True
@ -228,14 +231,13 @@ class AlarmControlPanel(Entity):
This method must be run in the event loop and returns a coroutine. This method must be run in the event loop and returns a coroutine.
""" """
return self.hass.async_add_executor_job( return self.hass.async_add_executor_job(self.alarm_arm_custom_bypass, code)
self.alarm_arm_custom_bypass, code)
@property @property
def state_attributes(self): def state_attributes(self):
"""Return the state attributes.""" """Return the state attributes."""
state_attr = { state_attr = {
ATTR_CODE_FORMAT: self.code_format, ATTR_CODE_FORMAT: self.code_format,
ATTR_CHANGED_BY: self.changed_by ATTR_CHANGED_BY: self.changed_by,
} }
return state_attr 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.abode import DOMAIN as ABODE_DOMAIN
from homeassistant.components.alarm_control_panel import AlarmControlPanel from homeassistant.components.alarm_control_panel import AlarmControlPanel
from homeassistant.const import ( from homeassistant.const import (
ATTR_ATTRIBUTION, STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, ATTR_ATTRIBUTION,
STATE_ALARM_DISARMED) STATE_ALARM_ARMED_AWAY,
STATE_ALARM_ARMED_HOME,
STATE_ALARM_DISARMED,
)
DEPENDENCIES = ['abode'] DEPENDENCIES = ["abode"]
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
ICON = 'mdi:security' ICON = "mdi:security"
def setup_platform(hass, config, add_entities, discovery_info=None): def setup_platform(hass, config, add_entities, discovery_info=None):
@ -79,7 +82,7 @@ class AbodeAlarm(AbodeDevice, AlarmControlPanel):
"""Return the state attributes.""" """Return the state attributes."""
return { return {
ATTR_ATTRIBUTION: CONF_ATTRIBUTION, ATTR_ATTRIBUTION: CONF_ATTRIBUTION,
'device_id': self._device.device_id, "device_id": self._device.device_id,
'battery_backup': self._device.battery, "battery_backup": self._device.battery,
'cellular_backup': self._device.is_cellular, "cellular_backup": self._device.is_cellular,
} }

View file

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

View file

@ -13,28 +13,36 @@ import voluptuous as vol
import homeassistant.components.alarm_control_panel as alarm import homeassistant.components.alarm_control_panel as alarm
from homeassistant.components.alarm_control_panel import PLATFORM_SCHEMA from homeassistant.components.alarm_control_panel import PLATFORM_SCHEMA
from homeassistant.const import ( from homeassistant.const import (
CONF_CODE, CONF_NAME, CONF_PASSWORD, CONF_USERNAME, STATE_ALARM_ARMED_AWAY, CONF_CODE,
STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED, STATE_UNKNOWN) 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 from homeassistant.helpers.aiohttp_client import async_get_clientsession
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['pyalarmdotcom==0.3.2'] REQUIREMENTS = ["pyalarmdotcom==0.3.2"]
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
DEFAULT_NAME = 'Alarm.com' DEFAULT_NAME = "Alarm.com"
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
vol.Required(CONF_PASSWORD): cv.string, {
vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_PASSWORD): cv.string,
vol.Optional(CONF_CODE): cv.positive_int, vol.Required(CONF_USERNAME): cv.string,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_CODE): cv.positive_int,
}) vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
}
)
@asyncio.coroutine @asyncio.coroutine
def async_setup_platform(hass, config, async_add_entities, def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
discovery_info=None):
"""Set up a Alarm.com control panel.""" """Set up a Alarm.com control panel."""
name = config.get(CONF_NAME) name = config.get(CONF_NAME)
code = config.get(CONF_CODE) code = config.get(CONF_CODE)
@ -52,7 +60,8 @@ class AlarmDotCom(alarm.AlarmControlPanel):
def __init__(self, hass, name, code, username, password): def __init__(self, hass, name, code, username, password):
"""Initialize the Alarm.com status.""" """Initialize the Alarm.com status."""
from pyalarmdotcom import Alarmdotcom from pyalarmdotcom import Alarmdotcom
_LOGGER.debug('Setting up Alarm.com...')
_LOGGER.debug("Setting up Alarm.com...")
self._hass = hass self._hass = hass
self._name = name self._name = name
self._code = str(code) if code else None self._code = str(code) if code else None
@ -60,8 +69,7 @@ class AlarmDotCom(alarm.AlarmControlPanel):
self._password = password self._password = password
self._websession = async_get_clientsession(self._hass) self._websession = async_get_clientsession(self._hass)
self._state = STATE_UNKNOWN self._state = STATE_UNKNOWN
self._alarm = Alarmdotcom( self._alarm = Alarmdotcom(username, password, self._websession, hass.loop)
username, password, self._websession, hass.loop)
@asyncio.coroutine @asyncio.coroutine
def async_login(self): def async_login(self):
@ -84,27 +92,25 @@ class AlarmDotCom(alarm.AlarmControlPanel):
"""Return one or more digits/characters.""" """Return one or more digits/characters."""
if self._code is None: if self._code is None:
return None return None
if isinstance(self._code, str) and re.search('^\\d+$', self._code): if isinstance(self._code, str) and re.search("^\\d+$", self._code):
return 'Number' return "Number"
return 'Any' return "Any"
@property @property
def state(self): def state(self):
"""Return the state of the device.""" """Return the state of the device."""
if self._alarm.state.lower() == 'disarmed': if self._alarm.state.lower() == "disarmed":
return STATE_ALARM_DISARMED return STATE_ALARM_DISARMED
if self._alarm.state.lower() == 'armed stay': if self._alarm.state.lower() == "armed stay":
return STATE_ALARM_ARMED_HOME 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_ALARM_ARMED_AWAY
return STATE_UNKNOWN return STATE_UNKNOWN
@property @property
def device_state_attributes(self): def device_state_attributes(self):
"""Return the state attributes.""" """Return the state attributes."""
return { return {"sensor_status": self._alarm.sensor_status}
'sensor_status': self._alarm.sensor_status
}
@asyncio.coroutine @asyncio.coroutine
def async_alarm_disarm(self, code=None): 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.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.components.alarm_control_panel import ( from homeassistant.components.alarm_control_panel import (
AlarmControlPanel, PLATFORM_SCHEMA) AlarmControlPanel,
PLATFORM_SCHEMA,
)
from homeassistant.components.arlo import ( from homeassistant.components.arlo import (
DATA_ARLO, CONF_ATTRIBUTION, SIGNAL_UPDATE_ARLO) DATA_ARLO,
CONF_ATTRIBUTION,
SIGNAL_UPDATE_ARLO,
)
from homeassistant.const import ( from homeassistant.const import (
ATTR_ATTRIBUTION, STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, ATTR_ATTRIBUTION,
STATE_ALARM_DISARMED) STATE_ALARM_ARMED_AWAY,
STATE_ALARM_ARMED_HOME,
STATE_ALARM_DISARMED,
)
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
ARMED = 'armed' ARMED = "armed"
CONF_HOME_MODE_NAME = 'home_mode_name' CONF_HOME_MODE_NAME = "home_mode_name"
CONF_AWAY_MODE_NAME = 'away_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({ 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, 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): 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) away_mode_name = config.get(CONF_AWAY_MODE_NAME)
base_stations = [] base_stations = []
for base_station in arlo.base_stations: for base_station in arlo.base_stations:
base_stations.append(ArloBaseStation(base_station, home_mode_name, base_stations.append(
away_mode_name)) ArloBaseStation(base_station, home_mode_name, away_mode_name)
)
add_entities(base_stations, True) add_entities(base_stations, True)
@ -71,8 +82,7 @@ class ArloBaseStation(AlarmControlPanel):
async def async_added_to_hass(self): async def async_added_to_hass(self):
"""Register callbacks.""" """Register callbacks."""
async_dispatcher_connect( async_dispatcher_connect(self.hass, SIGNAL_UPDATE_ARLO, self._update_callback)
self.hass, SIGNAL_UPDATE_ARLO, self._update_callback)
@callback @callback
def _update_callback(self): def _update_callback(self):
@ -115,7 +125,7 @@ class ArloBaseStation(AlarmControlPanel):
"""Return the state attributes.""" """Return the state attributes."""
return { return {
ATTR_ATTRIBUTION: CONF_ATTRIBUTION, 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): 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.alarm_control_panel import AlarmControlPanel
from homeassistant.components.canary import DATA_CANARY from homeassistant.components.canary import DATA_CANARY
from homeassistant.const import STATE_ALARM_DISARMED, STATE_ALARM_ARMED_AWAY, \ from homeassistant.const import (
STATE_ALARM_ARMED_NIGHT, STATE_ALARM_ARMED_HOME STATE_ALARM_DISARMED,
STATE_ALARM_ARMED_AWAY,
STATE_ALARM_ARMED_NIGHT,
STATE_ALARM_ARMED_HOME,
)
DEPENDENCIES = ['canary'] DEPENDENCIES = ["canary"]
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -44,8 +48,11 @@ class CanaryAlarm(AlarmControlPanel):
@property @property
def state(self): def state(self):
"""Return the state of the device.""" """Return the state of the device."""
from canary.api import LOCATION_MODE_AWAY, LOCATION_MODE_HOME, \ from canary.api import (
LOCATION_MODE_NIGHT LOCATION_MODE_AWAY,
LOCATION_MODE_HOME,
LOCATION_MODE_NIGHT,
)
location = self._data.get_location(self._location_id) location = self._data.get_location(self._location_id)
@ -65,27 +72,27 @@ class CanaryAlarm(AlarmControlPanel):
def device_state_attributes(self): def device_state_attributes(self):
"""Return the state attributes.""" """Return the state attributes."""
location = self._data.get_location(self._location_id) location = self._data.get_location(self._location_id)
return { return {"private": location.is_private}
'private': location.is_private
}
def alarm_disarm(self, code=None): def alarm_disarm(self, code=None):
"""Send disarm command.""" """Send disarm command."""
location = self._data.get_location(self._location_id) location = self._data.get_location(self._location_id)
self._data.set_location_mode(self._location_id, location.mode.name, self._data.set_location_mode(self._location_id, location.mode.name, True)
True)
def alarm_arm_home(self, code=None): def alarm_arm_home(self, code=None):
"""Send arm home command.""" """Send arm home command."""
from canary.api import LOCATION_MODE_HOME from canary.api import LOCATION_MODE_HOME
self._data.set_location_mode(self._location_id, LOCATION_MODE_HOME) self._data.set_location_mode(self._location_id, LOCATION_MODE_HOME)
def alarm_arm_away(self, code=None): def alarm_arm_away(self, code=None):
"""Send arm away command.""" """Send arm away command."""
from canary.api import LOCATION_MODE_AWAY from canary.api import LOCATION_MODE_AWAY
self._data.set_location_mode(self._location_id, LOCATION_MODE_AWAY) self._data.set_location_mode(self._location_id, LOCATION_MODE_AWAY)
def alarm_arm_night(self, code=None): def alarm_arm_night(self, code=None):
"""Send arm night command.""" """Send arm night command."""
from canary.api import LOCATION_MODE_NIGHT from canary.api import LOCATION_MODE_NIGHT
self._data.set_location_mode(self._location_id, 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 import homeassistant.components.alarm_control_panel as alarm
from homeassistant.components.alarm_control_panel import PLATFORM_SCHEMA from homeassistant.components.alarm_control_panel import PLATFORM_SCHEMA
from homeassistant.const import ( from homeassistant.const import (
CONF_HOST, CONF_NAME, CONF_PORT, STATE_ALARM_ARMED_AWAY, CONF_HOST,
STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED, STATE_UNKNOWN) CONF_NAME,
CONF_PORT,
STATE_ALARM_ARMED_AWAY,
STATE_ALARM_ARMED_HOME,
STATE_ALARM_DISARMED,
STATE_UNKNOWN,
)
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['concord232==0.15'] REQUIREMENTS = ["concord232==0.15"]
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
DEFAULT_HOST = 'localhost' DEFAULT_HOST = "localhost"
DEFAULT_NAME = 'CONCORD232' DEFAULT_NAME = "CONCORD232"
DEFAULT_PORT = 5007 DEFAULT_PORT = 5007
SCAN_INTERVAL = timedelta(seconds=10) SCAN_INTERVAL = timedelta(seconds=10)
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ 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_HOST, default=DEFAULT_HOST): cv.string,
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, 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): 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) host = config.get(CONF_HOST)
port = config.get(CONF_PORT) port = config.get(CONF_PORT)
url = 'http://{}:{}'.format(host, port) url = "http://{}:{}".format(host, port)
try: try:
add_entities([Concord232Alarm(hass, url, name)]) add_entities([Concord232Alarm(hass, url, name)])
@ -80,7 +88,7 @@ class Concord232Alarm(alarm.AlarmControlPanel):
@property @property
def code_format(self): def code_format(self):
"""Return the characters if code is defined.""" """Return the characters if code is defined."""
return 'Number' return "Number"
@property @property
def state(self): def state(self):
@ -92,16 +100,18 @@ class Concord232Alarm(alarm.AlarmControlPanel):
try: try:
part = self._alarm.list_partitions()[0] part = self._alarm.list_partitions()[0]
except requests.exceptions.ConnectionError as ex: except requests.exceptions.ConnectionError as ex:
_LOGGER.error("Unable to connect to %(host)s: %(reason)s", _LOGGER.error(
dict(host=self._url, reason=ex)) "Unable to connect to %(host)s: %(reason)s",
dict(host=self._url, reason=ex),
)
newstate = STATE_UNKNOWN newstate = STATE_UNKNOWN
except IndexError: except IndexError:
_LOGGER.error("Concord232 reports no partitions") _LOGGER.error("Concord232 reports no partitions")
newstate = STATE_UNKNOWN newstate = STATE_UNKNOWN
if part['arming_level'] == 'Off': if part["arming_level"] == "Off":
newstate = STATE_ALARM_DISARMED newstate = STATE_ALARM_DISARMED
elif 'Home' in part['arming_level']: elif "Home" in part["arming_level"]:
newstate = STATE_ALARM_ARMED_HOME newstate = STATE_ALARM_ARMED_HOME
else: else:
newstate = STATE_ALARM_ARMED_AWAY newstate = STATE_ALARM_ARMED_AWAY
@ -117,8 +127,8 @@ class Concord232Alarm(alarm.AlarmControlPanel):
def alarm_arm_home(self, code=None): def alarm_arm_home(self, code=None):
"""Send arm home command.""" """Send arm home command."""
self._alarm.arm('stay') self._alarm.arm("stay")
def alarm_arm_away(self, code=None): def alarm_arm_away(self, code=None):
"""Send arm away command.""" """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 import datetime
from homeassistant.components.alarm_control_panel import manual from homeassistant.components.alarm_control_panel import manual
from homeassistant.const import ( from homeassistant.const import (
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_CUSTOM_BYPASS, STATE_ALARM_ARMED_AWAY,
STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT, STATE_ALARM_ARMED_CUSTOM_BYPASS,
STATE_ALARM_DISARMED, STATE_ALARM_TRIGGERED, CONF_DELAY_TIME, STATE_ALARM_ARMED_HOME,
CONF_PENDING_TIME, CONF_TRIGGER_TIME) 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): def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the Demo alarm control panel platform.""" """Set up the Demo alarm control panel platform."""
add_entities([ add_entities(
manual.ManualAlarm(hass, 'Alarm', '1234', None, False, { [
STATE_ALARM_ARMED_AWAY: { manual.ManualAlarm(
CONF_DELAY_TIME: datetime.timedelta(seconds=0), hass,
CONF_PENDING_TIME: datetime.timedelta(seconds=5), "Alarm",
CONF_TRIGGER_TIME: datetime.timedelta(seconds=10), "1234",
}, None,
STATE_ALARM_ARMED_HOME: { False,
CONF_DELAY_TIME: datetime.timedelta(seconds=0), {
CONF_PENDING_TIME: datetime.timedelta(seconds=5), STATE_ALARM_ARMED_AWAY: {
CONF_TRIGGER_TIME: datetime.timedelta(seconds=10), CONF_DELAY_TIME: datetime.timedelta(seconds=0),
}, CONF_PENDING_TIME: datetime.timedelta(seconds=5),
STATE_ALARM_ARMED_NIGHT: { CONF_TRIGGER_TIME: datetime.timedelta(seconds=10),
CONF_DELAY_TIME: datetime.timedelta(seconds=0), },
CONF_PENDING_TIME: datetime.timedelta(seconds=5), STATE_ALARM_ARMED_HOME: {
CONF_TRIGGER_TIME: datetime.timedelta(seconds=10), CONF_DELAY_TIME: datetime.timedelta(seconds=0),
}, CONF_PENDING_TIME: datetime.timedelta(seconds=5),
STATE_ALARM_DISARMED: { CONF_TRIGGER_TIME: datetime.timedelta(seconds=10),
CONF_DELAY_TIME: datetime.timedelta(seconds=0), },
CONF_TRIGGER_TIME: datetime.timedelta(seconds=10), STATE_ALARM_ARMED_NIGHT: {
}, CONF_DELAY_TIME: datetime.timedelta(seconds=0),
STATE_ALARM_ARMED_CUSTOM_BYPASS: { CONF_PENDING_TIME: datetime.timedelta(seconds=5),
CONF_DELAY_TIME: datetime.timedelta(seconds=0), CONF_TRIGGER_TIME: datetime.timedelta(seconds=10),
CONF_PENDING_TIME: datetime.timedelta(seconds=5), },
CONF_TRIGGER_TIME: datetime.timedelta(seconds=10), STATE_ALARM_DISARMED: {
}, CONF_DELAY_TIME: datetime.timedelta(seconds=0),
STATE_ALARM_TRIGGERED: { CONF_TRIGGER_TIME: datetime.timedelta(seconds=10),
CONF_PENDING_TIME: datetime.timedelta(seconds=5), },
}, 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 import homeassistant.components.alarm_control_panel as alarm
from homeassistant.const import ( from homeassistant.const import (
STATE_ALARM_DISARMED, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED,
STATE_ALARM_ARMED_AWAY, STATE_ALARM_TRIGGERED, STATE_ALARM_ARMED_HOME,
STATE_ALARM_ARMED_NIGHT) STATE_ALARM_ARMED_AWAY,
STATE_ALARM_TRIGGERED,
STATE_ALARM_ARMED_NIGHT,
)
from homeassistant.components.egardia import ( from homeassistant.components.egardia import (
EGARDIA_DEVICE, EGARDIA_SERVER, EGARDIA_DEVICE,
REPORT_SERVER_CODES_IGNORE, CONF_REPORT_SERVER_CODES, EGARDIA_SERVER,
CONF_REPORT_SERVER_ENABLED, CONF_REPORT_SERVER_PORT REPORT_SERVER_CODES_IGNORE,
) CONF_REPORT_SERVER_CODES,
DEPENDENCIES = ['egardia'] CONF_REPORT_SERVER_ENABLED,
CONF_REPORT_SERVER_PORT,
)
DEPENDENCIES = ["egardia"]
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
STATES = { STATES = {
'ARM': STATE_ALARM_ARMED_AWAY, "ARM": STATE_ALARM_ARMED_AWAY,
'DAY HOME': STATE_ALARM_ARMED_HOME, "DAY HOME": STATE_ALARM_ARMED_HOME,
'DISARM': STATE_ALARM_DISARMED, "DISARM": STATE_ALARM_DISARMED,
'ARMHOME': STATE_ALARM_ARMED_HOME, "ARMHOME": STATE_ALARM_ARMED_HOME,
'HOME': STATE_ALARM_ARMED_HOME, "HOME": STATE_ALARM_ARMED_HOME,
'NIGHT HOME': STATE_ALARM_ARMED_NIGHT, "NIGHT HOME": STATE_ALARM_ARMED_NIGHT,
'TRIGGERED': STATE_ALARM_TRIGGERED "TRIGGERED": STATE_ALARM_TRIGGERED,
} }
@ -39,11 +46,12 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
if discovery_info is None: if discovery_info is None:
return return
device = EgardiaAlarm( device = EgardiaAlarm(
discovery_info['name'], discovery_info["name"],
hass.data[EGARDIA_DEVICE], hass.data[EGARDIA_DEVICE],
discovery_info[CONF_REPORT_SERVER_ENABLED], discovery_info[CONF_REPORT_SERVER_ENABLED],
discovery_info.get(CONF_REPORT_SERVER_CODES), discovery_info.get(CONF_REPORT_SERVER_CODES),
discovery_info[CONF_REPORT_SERVER_PORT]) discovery_info[CONF_REPORT_SERVER_PORT],
)
# add egardia alarm device # add egardia alarm device
add_entities([device], True) add_entities([device], True)
@ -51,8 +59,9 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
class EgardiaAlarm(alarm.AlarmControlPanel): class EgardiaAlarm(alarm.AlarmControlPanel):
"""Representation of a Egardia alarm.""" """Representation of a Egardia alarm."""
def __init__(self, name, egardiasystem, def __init__(
rs_enabled=False, rs_codes=None, rs_port=52010): self, name, egardiasystem, rs_enabled=False, rs_codes=None, rs_port=52010
):
"""Initialize the Egardia alarm.""" """Initialize the Egardia alarm."""
self._name = name self._name = name
self._egardiasystem = egardiasystem self._egardiasystem = egardiasystem
@ -66,8 +75,7 @@ class EgardiaAlarm(alarm.AlarmControlPanel):
"""Add Egardiaserver callback if enabled.""" """Add Egardiaserver callback if enabled."""
if self._rs_enabled: if self._rs_enabled:
_LOGGER.debug("Registering callback to Egardiaserver") _LOGGER.debug("Registering callback to Egardiaserver")
self.hass.data[EGARDIA_SERVER].register_callback( self.hass.data[EGARDIA_SERVER].register_callback(self.handle_status_event)
self.handle_status_event)
@property @property
def name(self): def name(self):
@ -88,7 +96,7 @@ class EgardiaAlarm(alarm.AlarmControlPanel):
def handle_status_event(self, event): def handle_status_event(self, event):
"""Handle the Egardia system status event.""" """Handle the Egardia system status event."""
statuscode = event.get('status') statuscode = event.get("status")
if statuscode is not None: if statuscode is not None:
status = self.lookupstatusfromcode(statuscode) status = self.lookupstatusfromcode(statuscode)
self.parsestatus(status) self.parsestatus(status)
@ -96,10 +104,15 @@ class EgardiaAlarm(alarm.AlarmControlPanel):
def lookupstatusfromcode(self, statuscode): def lookupstatusfromcode(self, statuscode):
"""Look at the rs_codes and returns the status from the code.""" """Look at the rs_codes and returns the status from the code."""
status = next(( status = next(
status_group.upper() for status_group, codes (
in self._rs_codes.items() for code in codes status_group.upper()
if statuscode == code), 'UNKNOWN') for status_group, codes in self._rs_codes.items()
for code in codes
if statuscode == code
),
"UNKNOWN",
)
return status return status
def parsestatus(self, status): def parsestatus(self, status):
@ -124,21 +137,29 @@ class EgardiaAlarm(alarm.AlarmControlPanel):
try: try:
self._egardiasystem.alarm_disarm() self._egardiasystem.alarm_disarm()
except requests.exceptions.RequestException as err: except requests.exceptions.RequestException as err:
_LOGGER.error("Egardia device exception occurred when " _LOGGER.error(
"sending disarm command: %s", err) "Egardia device exception occurred when " "sending disarm command: %s",
err,
)
def alarm_arm_home(self, code=None): def alarm_arm_home(self, code=None):
"""Send arm home command.""" """Send arm home command."""
try: try:
self._egardiasystem.alarm_arm_home() self._egardiasystem.alarm_arm_home()
except requests.exceptions.RequestException as err: except requests.exceptions.RequestException as err:
_LOGGER.error("Egardia device exception occurred when " _LOGGER.error(
"sending arm home command: %s", err) "Egardia device exception occurred when "
"sending arm home command: %s",
err,
)
def alarm_arm_away(self, code=None): def alarm_arm_away(self, code=None):
"""Send arm away command.""" """Send arm away command."""
try: try:
self._egardiasystem.alarm_arm_away() self._egardiasystem.alarm_arm_away()
except requests.exceptions.RequestException as err: except requests.exceptions.RequestException as err:
_LOGGER.error("Egardia device exception occurred when " _LOGGER.error(
"sending arm away command: %s", err) "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.components.alarm_control_panel as alarm
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.components.envisalink import ( from homeassistant.components.envisalink import (
DATA_EVL, EnvisalinkDevice, PARTITION_SCHEMA, CONF_CODE, CONF_PANIC, DATA_EVL,
CONF_PARTITIONNAME, SIGNAL_KEYPAD_UPDATE, SIGNAL_PARTITION_UPDATE) EnvisalinkDevice,
PARTITION_SCHEMA,
CONF_CODE,
CONF_PANIC,
CONF_PARTITIONNAME,
SIGNAL_KEYPAD_UPDATE,
SIGNAL_PARTITION_UPDATE,
)
from homeassistant.const import ( from homeassistant.const import (
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED, STATE_ALARM_ARMED_AWAY,
STATE_UNKNOWN, STATE_ALARM_TRIGGERED, STATE_ALARM_PENDING, ATTR_ENTITY_ID) STATE_ALARM_ARMED_HOME,
STATE_ALARM_DISARMED,
STATE_UNKNOWN,
STATE_ALARM_TRIGGERED,
STATE_ALARM_PENDING,
ATTR_ENTITY_ID,
)
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
DEPENDENCIES = ['envisalink'] DEPENDENCIES = ["envisalink"]
SERVICE_ALARM_KEYPRESS = 'envisalink_alarm_keypress' SERVICE_ALARM_KEYPRESS = "envisalink_alarm_keypress"
ATTR_KEYPRESS = 'keypress' ATTR_KEYPRESS = "keypress"
ALARM_KEYPRESS_SCHEMA = vol.Schema({ ALARM_KEYPRESS_SCHEMA = vol.Schema(
vol.Required(ATTR_ENTITY_ID): cv.entity_ids, {
vol.Required(ATTR_KEYPRESS): cv.string vol.Required(ATTR_ENTITY_ID): cv.entity_ids,
}) vol.Required(ATTR_KEYPRESS): cv.string,
}
)
@asyncio.coroutine @asyncio.coroutine
def async_setup_platform(hass, config, async_add_entities, def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
discovery_info=None):
"""Perform the setup for Envisalink alarm panels.""" """Perform the setup for Envisalink alarm panels."""
configured_partitions = discovery_info['partitions'] configured_partitions = discovery_info["partitions"]
code = discovery_info[CONF_CODE] code = discovery_info[CONF_CODE]
panic_type = discovery_info[CONF_PANIC] panic_type = discovery_info[CONF_PANIC]
@ -49,8 +63,8 @@ def async_setup_platform(hass, config, async_add_entities,
device_config_data[CONF_PARTITIONNAME], device_config_data[CONF_PARTITIONNAME],
code, code,
panic_type, panic_type,
hass.data[DATA_EVL].alarm_state['partition'][part_num], hass.data[DATA_EVL].alarm_state["partition"][part_num],
hass.data[DATA_EVL] hass.data[DATA_EVL],
) )
devices.append(device) devices.append(device)
@ -62,15 +76,19 @@ def async_setup_platform(hass, config, async_add_entities,
entity_ids = service.data.get(ATTR_ENTITY_ID) entity_ids = service.data.get(ATTR_ENTITY_ID)
keypress = service.data.get(ATTR_KEYPRESS) keypress = service.data.get(ATTR_KEYPRESS)
target_devices = [device for device in devices target_devices = [
if device.entity_id in entity_ids] device for device in devices if device.entity_id in entity_ids
]
for device in target_devices: for device in target_devices:
device.async_alarm_keypress(keypress) device.async_alarm_keypress(keypress)
hass.services.async_register( hass.services.async_register(
alarm.DOMAIN, SERVICE_ALARM_KEYPRESS, alarm_keypress_handler, alarm.DOMAIN,
schema=ALARM_KEYPRESS_SCHEMA) SERVICE_ALARM_KEYPRESS,
alarm_keypress_handler,
schema=ALARM_KEYPRESS_SCHEMA,
)
return True return True
@ -78,8 +96,9 @@ def async_setup_platform(hass, config, async_add_entities,
class EnvisalinkAlarm(EnvisalinkDevice, alarm.AlarmControlPanel): class EnvisalinkAlarm(EnvisalinkDevice, alarm.AlarmControlPanel):
"""Representation of an Envisalink-based alarm panel.""" """Representation of an Envisalink-based alarm panel."""
def __init__(self, hass, partition_number, alarm_name, code, panic_type, def __init__(
info, controller): self, hass, partition_number, alarm_name, code, panic_type, info, controller
):
"""Initialize the alarm panel.""" """Initialize the alarm panel."""
self._partition_number = partition_number self._partition_number = partition_number
self._code = code self._code = code
@ -91,10 +110,10 @@ class EnvisalinkAlarm(EnvisalinkDevice, alarm.AlarmControlPanel):
@asyncio.coroutine @asyncio.coroutine
def async_added_to_hass(self): def async_added_to_hass(self):
"""Register callbacks.""" """Register callbacks."""
async_dispatcher_connect(self.hass, SIGNAL_KEYPAD_UPDATE, self._update_callback)
async_dispatcher_connect( async_dispatcher_connect(
self.hass, SIGNAL_KEYPAD_UPDATE, self._update_callback) self.hass, SIGNAL_PARTITION_UPDATE, self._update_callback
async_dispatcher_connect( )
self.hass, SIGNAL_PARTITION_UPDATE, self._update_callback)
@callback @callback
def _update_callback(self, partition): 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.""" """Regex for code format or None if no code is required."""
if self._code: if self._code:
return None return None
return 'Number' return "Number"
@property @property
def state(self): def state(self):
"""Return the state of the device.""" """Return the state of the device."""
state = STATE_UNKNOWN state = STATE_UNKNOWN
if self._info['status']['alarm']: if self._info["status"]["alarm"]:
state = STATE_ALARM_TRIGGERED state = STATE_ALARM_TRIGGERED
elif self._info['status']['armed_away']: elif self._info["status"]["armed_away"]:
state = STATE_ALARM_ARMED_AWAY state = STATE_ALARM_ARMED_AWAY
elif self._info['status']['armed_stay']: elif self._info["status"]["armed_stay"]:
state = STATE_ALARM_ARMED_HOME state = STATE_ALARM_ARMED_HOME
elif self._info['status']['exit_delay']: elif self._info["status"]["exit_delay"]:
state = STATE_ALARM_PENDING state = STATE_ALARM_PENDING
elif self._info['status']['entry_delay']: elif self._info["status"]["entry_delay"]:
state = STATE_ALARM_PENDING state = STATE_ALARM_PENDING
elif self._info['status']['alpha']: elif self._info["status"]["alpha"]:
state = STATE_ALARM_DISARMED state = STATE_ALARM_DISARMED
return state return state
@ -132,31 +151,35 @@ class EnvisalinkAlarm(EnvisalinkDevice, alarm.AlarmControlPanel):
def async_alarm_disarm(self, code=None): def async_alarm_disarm(self, code=None):
"""Send disarm command.""" """Send disarm command."""
if code: if code:
self.hass.data[DATA_EVL].disarm_partition( self.hass.data[DATA_EVL].disarm_partition(str(code), self._partition_number)
str(code), self._partition_number)
else: else:
self.hass.data[DATA_EVL].disarm_partition( self.hass.data[DATA_EVL].disarm_partition(
str(self._code), self._partition_number) str(self._code), self._partition_number
)
@asyncio.coroutine @asyncio.coroutine
def async_alarm_arm_home(self, code=None): def async_alarm_arm_home(self, code=None):
"""Send arm home command.""" """Send arm home command."""
if code: if code:
self.hass.data[DATA_EVL].arm_stay_partition( self.hass.data[DATA_EVL].arm_stay_partition(
str(code), self._partition_number) str(code), self._partition_number
)
else: else:
self.hass.data[DATA_EVL].arm_stay_partition( self.hass.data[DATA_EVL].arm_stay_partition(
str(self._code), self._partition_number) str(self._code), self._partition_number
)
@asyncio.coroutine @asyncio.coroutine
def async_alarm_arm_away(self, code=None): def async_alarm_arm_away(self, code=None):
"""Send arm away command.""" """Send arm away command."""
if code: if code:
self.hass.data[DATA_EVL].arm_away_partition( self.hass.data[DATA_EVL].arm_away_partition(
str(code), self._partition_number) str(code), self._partition_number
)
else: else:
self.hass.data[DATA_EVL].arm_away_partition( self.hass.data[DATA_EVL].arm_away_partition(
str(self._code), self._partition_number) str(self._code), self._partition_number
)
@asyncio.coroutine @asyncio.coroutine
def async_alarm_trigger(self, code=None): def async_alarm_trigger(self, code=None):
@ -168,4 +191,5 @@ class EnvisalinkAlarm(EnvisalinkDevice, alarm.AlarmControlPanel):
"""Send custom keypress.""" """Send custom keypress."""
if keypress: if keypress:
self.hass.data[DATA_EVL].keypresses_to_partition( 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.alarm_control_panel import AlarmControlPanel
from homeassistant.components.homematicip_cloud import ( from homeassistant.components.homematicip_cloud import (
HMIPC_HAPID, HomematicipGenericDevice) HMIPC_HAPID,
HomematicipGenericDevice,
)
from homeassistant.components.homematicip_cloud import DOMAIN as HMIPC_DOMAIN from homeassistant.components.homematicip_cloud import DOMAIN as HMIPC_DOMAIN
from homeassistant.const import ( from homeassistant.const import (
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED, STATE_ALARM_ARMED_AWAY,
STATE_ALARM_TRIGGERED) STATE_ALARM_ARMED_HOME,
STATE_ALARM_DISARMED,
STATE_ALARM_TRIGGERED,
)
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
DEPENDENCIES = ['homematicip_cloud'] DEPENDENCIES = ["homematicip_cloud"]
HMIP_ZONE_AWAY = 'EXTERNAL' HMIP_ZONE_AWAY = "EXTERNAL"
HMIP_ZONE_HOME = 'INTERNAL' HMIP_ZONE_HOME = "INTERNAL"
async def async_setup_platform( async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
hass, config, async_add_entities, discovery_info=None):
"""Set up the HomematicIP Cloud alarm control devices.""" """Set up the HomematicIP Cloud alarm control devices."""
pass pass
@ -48,8 +52,8 @@ class HomematicipSecurityZone(HomematicipGenericDevice, AlarmControlPanel):
def __init__(self, home, device): def __init__(self, home, device):
"""Initialize the security zone group.""" """Initialize the security zone group."""
device.modelType = 'Group-SecurityZone' device.modelType = "Group-SecurityZone"
device.windowState = '' device.windowState = ""
super().__init__(home, device) super().__init__(home, device)
@property @property
@ -58,8 +62,11 @@ class HomematicipSecurityZone(HomematicipGenericDevice, AlarmControlPanel):
from homematicip.base.enums import WindowState from homematicip.base.enums import WindowState
if self._device.active: if self._device.active:
if (self._device.sabotage or self._device.motionDetected or if (
self._device.windowState == WindowState.OPEN): self._device.sabotage
or self._device.motionDetected
or self._device.windowState == WindowState.OPEN
):
return STATE_ALARM_TRIGGERED return STATE_ALARM_TRIGGERED
active = self._home.get_security_zones_activation() 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 import homeassistant.components.alarm_control_panel as alarm
from homeassistant.components.alarm_control_panel import PLATFORM_SCHEMA from homeassistant.components.alarm_control_panel import PLATFORM_SCHEMA
from homeassistant.const import ( from homeassistant.const import (
CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_USERNAME, STATE_ALARM_ARMED_AWAY, CONF_HOST,
STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED) CONF_NAME,
CONF_PASSWORD,
CONF_USERNAME,
STATE_ALARM_ARMED_AWAY,
STATE_ALARM_ARMED_HOME,
STATE_ALARM_DISARMED,
)
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['pyialarm==0.2'] REQUIREMENTS = ["pyialarm==0.2"]
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
DEFAULT_NAME = 'iAlarm' DEFAULT_NAME = "iAlarm"
def no_application_protocol(value): def no_application_protocol(value):
"""Validate that value is without the application protocol.""" """Validate that value is without the application protocol."""
protocol_separator = "://" protocol_separator = "://"
if not value or protocol_separator in value: if not value or protocol_separator in value:
raise vol.Invalid( raise vol.Invalid("Invalid host, {} is not allowed".format(protocol_separator))
'Invalid host, {} is not allowed'.format(protocol_separator))
return value return value
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ 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_HOST): vol.All(cv.string, no_application_protocol),
vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_PASSWORD): cv.string,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): 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): 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) password = config.get(CONF_PASSWORD)
host = config.get(CONF_HOST) host = config.get(CONF_HOST)
url = 'http://{}'.format(host) url = "http://{}".format(host)
ialarm = IAlarmPanel(name, username, password, url) ialarm = IAlarmPanel(name, username, password, url)
add_entities([ialarm], True) add_entities([ialarm], True)
@ -79,7 +86,7 @@ class IAlarmPanel(alarm.AlarmControlPanel):
def update(self): def update(self):
"""Return the state of the device.""" """Return the state of the device."""
status = self._client.get_status() status = self._client.get_status()
_LOGGER.debug('iAlarm status: %s', status) _LOGGER.debug("iAlarm status: %s", status)
if status: if status:
status = int(status) status = int(status)

View file

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

View file

@ -13,37 +13,54 @@ import voluptuous as vol
import homeassistant.components.alarm_control_panel as alarm import homeassistant.components.alarm_control_panel as alarm
from homeassistant.const import ( from homeassistant.const import (
CONF_CODE, CONF_DELAY_TIME, CONF_DISARM_AFTER_TRIGGER, CONF_NAME, CONF_CODE,
CONF_PENDING_TIME, CONF_PLATFORM, CONF_TRIGGER_TIME, CONF_DELAY_TIME,
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_CUSTOM_BYPASS, CONF_DISARM_AFTER_TRIGGER,
STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT, STATE_ALARM_DISARMED, CONF_NAME,
STATE_ALARM_PENDING, STATE_ALARM_TRIGGERED) 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 import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.event import track_point_in_time from homeassistant.helpers.event import track_point_in_time
import homeassistant.util.dt as dt_util import homeassistant.util.dt as dt_util
_LOGGER = logging.getLogger(__name__) _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_DELAY_TIME = datetime.timedelta(seconds=0)
DEFAULT_PENDING_TIME = datetime.timedelta(seconds=60) DEFAULT_PENDING_TIME = datetime.timedelta(seconds=60)
DEFAULT_TRIGGER_TIME = datetime.timedelta(seconds=120) DEFAULT_TRIGGER_TIME = datetime.timedelta(seconds=120)
DEFAULT_DISARM_AFTER_TRIGGER = False DEFAULT_DISARM_AFTER_TRIGGER = False
SUPPORTED_STATES = [STATE_ALARM_DISARMED, STATE_ALARM_ARMED_AWAY, SUPPORTED_STATES = [
STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT, STATE_ALARM_DISARMED,
STATE_ALARM_ARMED_CUSTOM_BYPASS, STATE_ALARM_TRIGGERED] 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 SUPPORTED_PRETRIGGER_STATES = [
if state != STATE_ALARM_TRIGGERED] state for state in SUPPORTED_STATES if state != STATE_ALARM_TRIGGERED
]
SUPPORTED_PENDING_STATES = [state for state in SUPPORTED_STATES SUPPORTED_PENDING_STATES = [
if state != STATE_ALARM_DISARMED] state for state in SUPPORTED_STATES if state != STATE_ALARM_DISARMED
]
ATTR_PRE_PENDING_STATE = 'pre_pending_state' ATTR_PRE_PENDING_STATE = "pre_pending_state"
ATTR_POST_PENDING_STATE = 'post_pending_state' ATTR_POST_PENDING_STATE = "post_pending_state"
def _state_validator(config): def _state_validator(config):
@ -66,53 +83,75 @@ def _state_schema(state):
schema = {} schema = {}
if state in SUPPORTED_PRETRIGGER_STATES: if state in SUPPORTED_PRETRIGGER_STATES:
schema[vol.Optional(CONF_DELAY_TIME)] = vol.All( 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( 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: if state in SUPPORTED_PENDING_STATES:
schema[vol.Optional(CONF_PENDING_TIME)] = vol.All( schema[vol.Optional(CONF_PENDING_TIME)] = vol.All(
cv.time_period, cv.positive_timedelta) cv.time_period, cv.positive_timedelta
)
return vol.Schema(schema) return vol.Schema(schema)
PLATFORM_SCHEMA = vol.Schema(vol.All({ PLATFORM_SCHEMA = vol.Schema(
vol.Required(CONF_PLATFORM): 'manual', vol.All(
vol.Optional(CONF_NAME, default=DEFAULT_ALARM_NAME): cv.string, {
vol.Exclusive(CONF_CODE, 'code validation'): cv.string, vol.Required(CONF_PLATFORM): "manual",
vol.Exclusive(CONF_CODE_TEMPLATE, 'code validation'): cv.template, vol.Optional(CONF_NAME, default=DEFAULT_ALARM_NAME): cv.string,
vol.Optional(CONF_DELAY_TIME, default=DEFAULT_DELAY_TIME): vol.Exclusive(CONF_CODE, "code validation"): cv.string,
vol.All(cv.time_period, cv.positive_timedelta), vol.Exclusive(CONF_CODE_TEMPLATE, "code validation"): cv.template,
vol.Optional(CONF_PENDING_TIME, default=DEFAULT_PENDING_TIME): vol.Optional(CONF_DELAY_TIME, default=DEFAULT_DELAY_TIME): vol.All(
vol.All(cv.time_period, cv.positive_timedelta), 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_PENDING_TIME, default=DEFAULT_PENDING_TIME): vol.All(
vol.Optional(CONF_DISARM_AFTER_TRIGGER, cv.time_period, cv.positive_timedelta
default=DEFAULT_DISARM_AFTER_TRIGGER): cv.boolean, ),
vol.Optional(STATE_ALARM_ARMED_AWAY, default={}): vol.Optional(CONF_TRIGGER_TIME, default=DEFAULT_TRIGGER_TIME): vol.All(
_state_schema(STATE_ALARM_ARMED_AWAY), cv.time_period, cv.positive_timedelta
vol.Optional(STATE_ALARM_ARMED_HOME, default={}): ),
_state_schema(STATE_ALARM_ARMED_HOME), vol.Optional(
vol.Optional(STATE_ALARM_ARMED_NIGHT, default={}): CONF_DISARM_AFTER_TRIGGER, default=DEFAULT_DISARM_AFTER_TRIGGER
_state_schema(STATE_ALARM_ARMED_NIGHT), ): cv.boolean,
vol.Optional(STATE_ALARM_ARMED_CUSTOM_BYPASS, default={}): vol.Optional(STATE_ALARM_ARMED_AWAY, default={}): _state_schema(
_state_schema(STATE_ALARM_ARMED_CUSTOM_BYPASS), STATE_ALARM_ARMED_AWAY
vol.Optional(STATE_ALARM_DISARMED, default={}): ),
_state_schema(STATE_ALARM_DISARMED), vol.Optional(STATE_ALARM_ARMED_HOME, default={}): _state_schema(
vol.Optional(STATE_ALARM_TRIGGERED, default={}): STATE_ALARM_ARMED_HOME
_state_schema(STATE_ALARM_TRIGGERED), ),
}, _state_validator)) 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): def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the manual alarm platform.""" """Set up the manual alarm platform."""
add_entities([ManualAlarm( add_entities(
hass, [
config[CONF_NAME], ManualAlarm(
config.get(CONF_CODE), hass,
config.get(CONF_CODE_TEMPLATE), config[CONF_NAME],
config.get(CONF_DISARM_AFTER_TRIGGER, DEFAULT_DISARM_AFTER_TRIGGER), config.get(CONF_CODE),
config config.get(CONF_CODE_TEMPLATE),
)]) config.get(CONF_DISARM_AFTER_TRIGGER, DEFAULT_DISARM_AFTER_TRIGGER),
config,
)
]
)
class ManualAlarm(alarm.AlarmControlPanel): class ManualAlarm(alarm.AlarmControlPanel):
@ -127,8 +166,7 @@ class ManualAlarm(alarm.AlarmControlPanel):
A trigger_time of zero disables the alarm_trigger service. A trigger_time of zero disables the alarm_trigger service.
""" """
def __init__(self, hass, name, code, code_template, def __init__(self, hass, name, code, code_template, disarm_after_trigger, config):
disarm_after_trigger, config):
"""Init the manual alarm panel.""" """Init the manual alarm panel."""
self._state = STATE_ALARM_DISARMED self._state = STATE_ALARM_DISARMED
self._hass = hass self._hass = hass
@ -144,13 +182,16 @@ class ManualAlarm(alarm.AlarmControlPanel):
self._delay_time_by_state = { self._delay_time_by_state = {
state: config[state][CONF_DELAY_TIME] state: config[state][CONF_DELAY_TIME]
for state in SUPPORTED_PRETRIGGER_STATES} for state in SUPPORTED_PRETRIGGER_STATES
}
self._trigger_time_by_state = { self._trigger_time_by_state = {
state: config[state][CONF_TRIGGER_TIME] state: config[state][CONF_TRIGGER_TIME]
for state in SUPPORTED_PRETRIGGER_STATES} for state in SUPPORTED_PRETRIGGER_STATES
}
self._pending_time_by_state = { self._pending_time_by_state = {
state: config[state][CONF_PENDING_TIME] state: config[state][CONF_PENDING_TIME]
for state in SUPPORTED_PENDING_STATES} for state in SUPPORTED_PENDING_STATES
}
@property @property
def should_poll(self): def should_poll(self):
@ -169,15 +210,17 @@ class ManualAlarm(alarm.AlarmControlPanel):
if self._within_pending_time(self._state): if self._within_pending_time(self._state):
return STATE_ALARM_PENDING return STATE_ALARM_PENDING
trigger_time = self._trigger_time_by_state[self._previous_state] trigger_time = self._trigger_time_by_state[self._previous_state]
if (self._state_ts + self._pending_time(self._state) + if (
trigger_time) < dt_util.utcnow(): self._state_ts + self._pending_time(self._state) + trigger_time
) < dt_util.utcnow():
if self._disarm_after_trigger: if self._disarm_after_trigger:
return STATE_ALARM_DISARMED return STATE_ALARM_DISARMED
self._state = self._previous_state self._state = self._previous_state
return self._state return self._state
if self._state in SUPPORTED_PENDING_STATES and \ if self._state in SUPPORTED_PENDING_STATES and self._within_pending_time(
self._within_pending_time(self._state): self._state
):
return STATE_ALARM_PENDING return STATE_ALARM_PENDING
return self._state return self._state
@ -205,9 +248,9 @@ class ManualAlarm(alarm.AlarmControlPanel):
"""Return one or more digits/characters.""" """Return one or more digits/characters."""
if self._code is None: if self._code is None:
return None return None
if isinstance(self._code, str) and re.search('^\\d+$', self._code): if isinstance(self._code, str) and re.search("^\\d+$", self._code):
return 'Number' return "Number"
return 'Any' return "Any"
def alarm_disarm(self, code=None): def alarm_disarm(self, code=None):
"""Send disarm command.""" """Send disarm command."""
@ -270,17 +313,19 @@ class ManualAlarm(alarm.AlarmControlPanel):
pending_time = self._pending_time(state) pending_time = self._pending_time(state)
if state == STATE_ALARM_TRIGGERED: if state == STATE_ALARM_TRIGGERED:
track_point_in_time( track_point_in_time(
self._hass, self.async_update_ha_state, self._hass, self.async_update_ha_state, self._state_ts + pending_time
self._state_ts + pending_time) )
trigger_time = self._trigger_time_by_state[self._previous_state] trigger_time = self._trigger_time_by_state[self._previous_state]
track_point_in_time( track_point_in_time(
self._hass, self.async_update_ha_state, self._hass,
self._state_ts + pending_time + trigger_time) self.async_update_ha_state,
self._state_ts + pending_time + trigger_time,
)
elif state in SUPPORTED_PENDING_STATES and pending_time: elif state in SUPPORTED_PENDING_STATES and pending_time:
track_point_in_time( track_point_in_time(
self._hass, self.async_update_ha_state, self._hass, self.async_update_ha_state, self._state_ts + pending_time
self._state_ts + pending_time) )
def _validate_code(self, code, state): def _validate_code(self, code, state):
"""Validate given code.""" """Validate given code."""
@ -289,8 +334,7 @@ class ManualAlarm(alarm.AlarmControlPanel):
if isinstance(self._code, str): if isinstance(self._code, str):
alarm_code = self._code alarm_code = self._code
else: else:
alarm_code = self._code.render(from_state=self._state, alarm_code = self._code.render(from_state=self._state, to_state=state)
to_state=state)
check = not alarm_code or code == alarm_code check = not alarm_code or code == alarm_code
if not check: if not check:
_LOGGER.warning("Invalid code given for %s", state) _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.components.alarm_control_panel as alarm
import homeassistant.util.dt as dt_util import homeassistant.util.dt as dt_util
from homeassistant.const import ( from homeassistant.const import (
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT, STATE_ALARM_ARMED_AWAY,
STATE_ALARM_DISARMED, STATE_ALARM_PENDING, STATE_ALARM_TRIGGERED, STATE_ALARM_ARMED_HOME,
CONF_PLATFORM, CONF_NAME, CONF_CODE, CONF_DELAY_TIME, CONF_PENDING_TIME, STATE_ALARM_ARMED_NIGHT,
CONF_TRIGGER_TIME, CONF_DISARM_AFTER_TRIGGER) 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.components import mqtt
from homeassistant.helpers.event import async_track_state_change 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__) _LOGGER = logging.getLogger(__name__)
CONF_CODE_TEMPLATE = 'code_template' CONF_CODE_TEMPLATE = "code_template"
CONF_PAYLOAD_DISARM = 'payload_disarm' CONF_PAYLOAD_DISARM = "payload_disarm"
CONF_PAYLOAD_ARM_HOME = 'payload_arm_home' CONF_PAYLOAD_ARM_HOME = "payload_arm_home"
CONF_PAYLOAD_ARM_AWAY = 'payload_arm_away' CONF_PAYLOAD_ARM_AWAY = "payload_arm_away"
CONF_PAYLOAD_ARM_NIGHT = 'payload_arm_night' 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_DELAY_TIME = datetime.timedelta(seconds=0)
DEFAULT_PENDING_TIME = datetime.timedelta(seconds=60) DEFAULT_PENDING_TIME = datetime.timedelta(seconds=60)
DEFAULT_TRIGGER_TIME = datetime.timedelta(seconds=120) DEFAULT_TRIGGER_TIME = datetime.timedelta(seconds=120)
DEFAULT_DISARM_AFTER_TRIGGER = False DEFAULT_DISARM_AFTER_TRIGGER = False
DEFAULT_ARM_AWAY = 'ARM_AWAY' DEFAULT_ARM_AWAY = "ARM_AWAY"
DEFAULT_ARM_HOME = 'ARM_HOME' DEFAULT_ARM_HOME = "ARM_HOME"
DEFAULT_ARM_NIGHT = 'ARM_NIGHT' DEFAULT_ARM_NIGHT = "ARM_NIGHT"
DEFAULT_DISARM = 'DISARM' DEFAULT_DISARM = "DISARM"
SUPPORTED_STATES = [STATE_ALARM_DISARMED, STATE_ALARM_ARMED_AWAY, SUPPORTED_STATES = [
STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT, STATE_ALARM_DISARMED,
STATE_ALARM_TRIGGERED] STATE_ALARM_ARMED_AWAY,
STATE_ALARM_ARMED_HOME,
STATE_ALARM_ARMED_NIGHT,
STATE_ALARM_TRIGGERED,
]
SUPPORTED_PRETRIGGER_STATES = [state for state in SUPPORTED_STATES SUPPORTED_PRETRIGGER_STATES = [
if state != STATE_ALARM_TRIGGERED] state for state in SUPPORTED_STATES if state != STATE_ALARM_TRIGGERED
]
SUPPORTED_PENDING_STATES = [state for state in SUPPORTED_STATES SUPPORTED_PENDING_STATES = [
if state != STATE_ALARM_DISARMED] state for state in SUPPORTED_STATES if state != STATE_ALARM_DISARMED
]
ATTR_PRE_PENDING_STATE = 'pre_pending_state' ATTR_PRE_PENDING_STATE = "pre_pending_state"
ATTR_POST_PENDING_STATE = 'post_pending_state' ATTR_POST_PENDING_STATE = "post_pending_state"
def _state_validator(config): def _state_validator(config):
@ -80,65 +96,95 @@ def _state_schema(state):
schema = {} schema = {}
if state in SUPPORTED_PRETRIGGER_STATES: if state in SUPPORTED_PRETRIGGER_STATES:
schema[vol.Optional(CONF_DELAY_TIME)] = vol.All( 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( 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: if state in SUPPORTED_PENDING_STATES:
schema[vol.Optional(CONF_PENDING_TIME)] = vol.All( schema[vol.Optional(CONF_PENDING_TIME)] = vol.All(
cv.time_period, cv.positive_timedelta) cv.time_period, cv.positive_timedelta
)
return vol.Schema(schema) return vol.Schema(schema)
DEPENDENCIES = ['mqtt'] DEPENDENCIES = ["mqtt"]
PLATFORM_SCHEMA = vol.Schema(vol.All(mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend({ PLATFORM_SCHEMA = vol.Schema(
vol.Required(CONF_PLATFORM): 'manual_mqtt', vol.All(
vol.Optional(CONF_NAME, default=DEFAULT_ALARM_NAME): cv.string, mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend(
vol.Exclusive(CONF_CODE, 'code validation'): cv.string, {
vol.Exclusive(CONF_CODE_TEMPLATE, 'code validation'): cv.template, vol.Required(CONF_PLATFORM): "manual_mqtt",
vol.Optional(CONF_DELAY_TIME, default=DEFAULT_DELAY_TIME): vol.Optional(CONF_NAME, default=DEFAULT_ALARM_NAME): cv.string,
vol.All(cv.time_period, cv.positive_timedelta), vol.Exclusive(CONF_CODE, "code validation"): cv.string,
vol.Optional(CONF_PENDING_TIME, default=DEFAULT_PENDING_TIME): vol.Exclusive(CONF_CODE_TEMPLATE, "code validation"): cv.template,
vol.All(cv.time_period, cv.positive_timedelta), vol.Optional(CONF_DELAY_TIME, default=DEFAULT_DELAY_TIME): vol.All(
vol.Optional(CONF_TRIGGER_TIME, default=DEFAULT_TRIGGER_TIME): cv.time_period, cv.positive_timedelta
vol.All(cv.time_period, cv.positive_timedelta), ),
vol.Optional(CONF_DISARM_AFTER_TRIGGER, vol.Optional(CONF_PENDING_TIME, default=DEFAULT_PENDING_TIME): vol.All(
default=DEFAULT_DISARM_AFTER_TRIGGER): cv.boolean, cv.time_period, cv.positive_timedelta
vol.Optional(STATE_ALARM_ARMED_AWAY, default={}): ),
_state_schema(STATE_ALARM_ARMED_AWAY), vol.Optional(CONF_TRIGGER_TIME, default=DEFAULT_TRIGGER_TIME): vol.All(
vol.Optional(STATE_ALARM_ARMED_HOME, default={}): cv.time_period, cv.positive_timedelta
_state_schema(STATE_ALARM_ARMED_HOME), ),
vol.Optional(STATE_ALARM_ARMED_NIGHT, default={}): vol.Optional(
_state_schema(STATE_ALARM_ARMED_NIGHT), CONF_DISARM_AFTER_TRIGGER, default=DEFAULT_DISARM_AFTER_TRIGGER
vol.Optional(STATE_ALARM_DISARMED, default={}): ): cv.boolean,
_state_schema(STATE_ALARM_DISARMED), vol.Optional(STATE_ALARM_ARMED_AWAY, default={}): _state_schema(
vol.Optional(STATE_ALARM_TRIGGERED, default={}): STATE_ALARM_ARMED_AWAY
_state_schema(STATE_ALARM_TRIGGERED), ),
vol.Required(mqtt.CONF_COMMAND_TOPIC): mqtt.valid_publish_topic, vol.Optional(STATE_ALARM_ARMED_HOME, default={}): _state_schema(
vol.Required(mqtt.CONF_STATE_TOPIC): mqtt.valid_subscribe_topic, STATE_ALARM_ARMED_HOME
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(STATE_ALARM_ARMED_NIGHT, default={}): _state_schema(
vol.Optional(CONF_PAYLOAD_ARM_NIGHT, default=DEFAULT_ARM_NIGHT): cv.string, STATE_ALARM_ARMED_NIGHT
vol.Optional(CONF_PAYLOAD_DISARM, default=DEFAULT_DISARM): cv.string, ),
}), _state_validator)) 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): def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the manual MQTT alarm platform.""" """Set up the manual MQTT alarm platform."""
add_entities([ManualMQTTAlarm( add_entities(
hass, [
config[CONF_NAME], ManualMQTTAlarm(
config.get(CONF_CODE), hass,
config.get(CONF_CODE_TEMPLATE), config[CONF_NAME],
config.get(CONF_DISARM_AFTER_TRIGGER, DEFAULT_DISARM_AFTER_TRIGGER), config.get(CONF_CODE),
config.get(mqtt.CONF_STATE_TOPIC), config.get(CONF_CODE_TEMPLATE),
config.get(mqtt.CONF_COMMAND_TOPIC), config.get(CONF_DISARM_AFTER_TRIGGER, DEFAULT_DISARM_AFTER_TRIGGER),
config.get(mqtt.CONF_QOS), config.get(mqtt.CONF_STATE_TOPIC),
config.get(CONF_PAYLOAD_DISARM), config.get(mqtt.CONF_COMMAND_TOPIC),
config.get(CONF_PAYLOAD_ARM_HOME), config.get(mqtt.CONF_QOS),
config.get(CONF_PAYLOAD_ARM_AWAY), config.get(CONF_PAYLOAD_DISARM),
config.get(CONF_PAYLOAD_ARM_NIGHT), config.get(CONF_PAYLOAD_ARM_HOME),
config)]) config.get(CONF_PAYLOAD_ARM_AWAY),
config.get(CONF_PAYLOAD_ARM_NIGHT),
config,
)
]
)
class ManualMQTTAlarm(alarm.AlarmControlPanel): class ManualMQTTAlarm(alarm.AlarmControlPanel):
@ -153,10 +199,22 @@ class ManualMQTTAlarm(alarm.AlarmControlPanel):
A trigger_time of zero disables the alarm_trigger service. A trigger_time of zero disables the alarm_trigger service.
""" """
def __init__(self, hass, name, code, code_template, disarm_after_trigger, def __init__(
state_topic, command_topic, qos, payload_disarm, self,
payload_arm_home, payload_arm_away, payload_arm_night, hass,
config): 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.""" """Init the manual MQTT alarm panel."""
self._state = STATE_ALARM_DISARMED self._state = STATE_ALARM_DISARMED
self._hass = hass self._hass = hass
@ -172,13 +230,16 @@ class ManualMQTTAlarm(alarm.AlarmControlPanel):
self._delay_time_by_state = { self._delay_time_by_state = {
state: config[state][CONF_DELAY_TIME] state: config[state][CONF_DELAY_TIME]
for state in SUPPORTED_PRETRIGGER_STATES} for state in SUPPORTED_PRETRIGGER_STATES
}
self._trigger_time_by_state = { self._trigger_time_by_state = {
state: config[state][CONF_TRIGGER_TIME] state: config[state][CONF_TRIGGER_TIME]
for state in SUPPORTED_PRETRIGGER_STATES} for state in SUPPORTED_PRETRIGGER_STATES
}
self._pending_time_by_state = { self._pending_time_by_state = {
state: config[state][CONF_PENDING_TIME] state: config[state][CONF_PENDING_TIME]
for state in SUPPORTED_PENDING_STATES} for state in SUPPORTED_PENDING_STATES
}
self._state_topic = state_topic self._state_topic = state_topic
self._command_topic = command_topic self._command_topic = command_topic
@ -205,15 +266,17 @@ class ManualMQTTAlarm(alarm.AlarmControlPanel):
if self._within_pending_time(self._state): if self._within_pending_time(self._state):
return STATE_ALARM_PENDING return STATE_ALARM_PENDING
trigger_time = self._trigger_time_by_state[self._previous_state] trigger_time = self._trigger_time_by_state[self._previous_state]
if (self._state_ts + self._pending_time(self._state) + if (
trigger_time) < dt_util.utcnow(): self._state_ts + self._pending_time(self._state) + trigger_time
) < dt_util.utcnow():
if self._disarm_after_trigger: if self._disarm_after_trigger:
return STATE_ALARM_DISARMED return STATE_ALARM_DISARMED
self._state = self._previous_state self._state = self._previous_state
return self._state return self._state
if self._state in SUPPORTED_PENDING_STATES and \ if self._state in SUPPORTED_PENDING_STATES and self._within_pending_time(
self._within_pending_time(self._state): self._state
):
return STATE_ALARM_PENDING return STATE_ALARM_PENDING
return self._state return self._state
@ -241,9 +304,9 @@ class ManualMQTTAlarm(alarm.AlarmControlPanel):
"""Return one or more digits/characters.""" """Return one or more digits/characters."""
if self._code is None: if self._code is None:
return None return None
if isinstance(self._code, str) and re.search('^\\d+$', self._code): if isinstance(self._code, str) and re.search("^\\d+$", self._code):
return 'Number' return "Number"
return 'Any' return "Any"
def alarm_disarm(self, code=None): def alarm_disarm(self, code=None):
"""Send disarm command.""" """Send disarm command."""
@ -299,17 +362,19 @@ class ManualMQTTAlarm(alarm.AlarmControlPanel):
pending_time = self._pending_time(state) pending_time = self._pending_time(state)
if state == STATE_ALARM_TRIGGERED: if state == STATE_ALARM_TRIGGERED:
track_point_in_time( track_point_in_time(
self._hass, self.async_update_ha_state, self._hass, self.async_update_ha_state, self._state_ts + pending_time
self._state_ts + pending_time) )
trigger_time = self._trigger_time_by_state[self._previous_state] trigger_time = self._trigger_time_by_state[self._previous_state]
track_point_in_time( track_point_in_time(
self._hass, self.async_update_ha_state, self._hass,
self._state_ts + pending_time + trigger_time) self.async_update_ha_state,
self._state_ts + pending_time + trigger_time,
)
elif state in SUPPORTED_PENDING_STATES and pending_time: elif state in SUPPORTED_PENDING_STATES and pending_time:
track_point_in_time( track_point_in_time(
self._hass, self.async_update_ha_state, self._hass, self.async_update_ha_state, self._state_ts + pending_time
self._state_ts + pending_time) )
def _validate_code(self, code, state): def _validate_code(self, code, state):
"""Validate given code.""" """Validate given code."""
@ -318,8 +383,7 @@ class ManualMQTTAlarm(alarm.AlarmControlPanel):
if isinstance(self._code, str): if isinstance(self._code, str):
alarm_code = self._code alarm_code = self._code
else: else:
alarm_code = self._code.render(from_state=self._state, alarm_code = self._code.render(from_state=self._state, to_state=state)
to_state=state)
check = not alarm_code or code == alarm_code check = not alarm_code or code == alarm_code
if not check: if not check:
_LOGGER.warning("Invalid code given for %s", state) _LOGGER.warning("Invalid code given for %s", state)
@ -361,10 +425,12 @@ class ManualMQTTAlarm(alarm.AlarmControlPanel):
return return
return mqtt.async_subscribe( return mqtt.async_subscribe(
self.hass, self._command_topic, message_received, self._qos) self.hass, self._command_topic, message_received, self._qos
)
@asyncio.coroutine @asyncio.coroutine
def _async_state_changed_listener(self, entity_id, old_state, new_state): def _async_state_changed_listener(self, entity_id, old_state, new_state):
"""Publish state change to MQTT.""" """Publish state change to MQTT."""
mqtt.async_publish( 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 import homeassistant.components.alarm_control_panel as alarm
from homeassistant.components import mqtt from homeassistant.components import mqtt
from homeassistant.const import ( from homeassistant.const import (
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED, STATE_ALARM_ARMED_AWAY,
STATE_ALARM_PENDING, STATE_ALARM_TRIGGERED, STATE_UNKNOWN, STATE_ALARM_ARMED_HOME,
CONF_NAME, CONF_CODE) STATE_ALARM_DISARMED,
STATE_ALARM_PENDING,
STATE_ALARM_TRIGGERED,
STATE_UNKNOWN,
CONF_NAME,
CONF_CODE,
)
from homeassistant.components.mqtt import ( from homeassistant.components.mqtt import (
CONF_AVAILABILITY_TOPIC, CONF_STATE_TOPIC, CONF_COMMAND_TOPIC, CONF_AVAILABILITY_TOPIC,
CONF_PAYLOAD_AVAILABLE, CONF_PAYLOAD_NOT_AVAILABLE, CONF_QOS, CONF_STATE_TOPIC,
CONF_RETAIN, MqttAvailability) CONF_COMMAND_TOPIC,
CONF_PAYLOAD_AVAILABLE,
CONF_PAYLOAD_NOT_AVAILABLE,
CONF_QOS,
CONF_RETAIN,
MqttAvailability,
)
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
CONF_PAYLOAD_DISARM = 'payload_disarm' CONF_PAYLOAD_DISARM = "payload_disarm"
CONF_PAYLOAD_ARM_HOME = 'payload_arm_home' CONF_PAYLOAD_ARM_HOME = "payload_arm_home"
CONF_PAYLOAD_ARM_AWAY = 'payload_arm_away' CONF_PAYLOAD_ARM_AWAY = "payload_arm_away"
DEFAULT_ARM_AWAY = 'ARM_AWAY' DEFAULT_ARM_AWAY = "ARM_AWAY"
DEFAULT_ARM_HOME = 'ARM_HOME' DEFAULT_ARM_HOME = "ARM_HOME"
DEFAULT_DISARM = 'DISARM' DEFAULT_DISARM = "DISARM"
DEFAULT_NAME = 'MQTT Alarm' DEFAULT_NAME = "MQTT Alarm"
DEPENDENCIES = ['mqtt'] DEPENDENCIES = ["mqtt"]
PLATFORM_SCHEMA = mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend({ 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.Required(CONF_COMMAND_TOPIC): mqtt.valid_publish_topic,
vol.Optional(CONF_CODE): cv.string, vol.Required(CONF_STATE_TOPIC): mqtt.valid_subscribe_topic,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_CODE): cv.string,
vol.Optional(CONF_PAYLOAD_ARM_AWAY, default=DEFAULT_ARM_AWAY): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_PAYLOAD_ARM_HOME, default=DEFAULT_ARM_HOME): cv.string, vol.Optional(CONF_PAYLOAD_ARM_AWAY, default=DEFAULT_ARM_AWAY): cv.string,
vol.Optional(CONF_PAYLOAD_DISARM, default=DEFAULT_DISARM): cv.string, vol.Optional(CONF_PAYLOAD_ARM_HOME, default=DEFAULT_ARM_HOME): cv.string,
}).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema) vol.Optional(CONF_PAYLOAD_DISARM, default=DEFAULT_DISARM): cv.string,
}
).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema)
@asyncio.coroutine @asyncio.coroutine
def async_setup_platform(hass, config, async_add_entities, def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
discovery_info=None):
"""Set up the MQTT Alarm Control Panel platform.""" """Set up the MQTT Alarm Control Panel platform."""
if discovery_info is not None: if discovery_info is not None:
config = PLATFORM_SCHEMA(discovery_info) config = PLATFORM_SCHEMA(discovery_info)
async_add_entities([MqttAlarm( async_add_entities(
config.get(CONF_NAME), [
config.get(CONF_STATE_TOPIC), MqttAlarm(
config.get(CONF_COMMAND_TOPIC), config.get(CONF_NAME),
config.get(CONF_QOS), config.get(CONF_STATE_TOPIC),
config.get(CONF_RETAIN), config.get(CONF_COMMAND_TOPIC),
config.get(CONF_PAYLOAD_DISARM), config.get(CONF_QOS),
config.get(CONF_PAYLOAD_ARM_HOME), config.get(CONF_RETAIN),
config.get(CONF_PAYLOAD_ARM_AWAY), config.get(CONF_PAYLOAD_DISARM),
config.get(CONF_CODE), config.get(CONF_PAYLOAD_ARM_HOME),
config.get(CONF_AVAILABILITY_TOPIC), config.get(CONF_PAYLOAD_ARM_AWAY),
config.get(CONF_PAYLOAD_AVAILABLE), config.get(CONF_CODE),
config.get(CONF_PAYLOAD_NOT_AVAILABLE))]) config.get(CONF_AVAILABILITY_TOPIC),
config.get(CONF_PAYLOAD_AVAILABLE),
config.get(CONF_PAYLOAD_NOT_AVAILABLE),
)
]
)
class MqttAlarm(MqttAvailability, alarm.AlarmControlPanel): class MqttAlarm(MqttAvailability, alarm.AlarmControlPanel):
"""Representation of a MQTT alarm status.""" """Representation of a MQTT alarm status."""
def __init__(self, name, state_topic, command_topic, qos, retain, def __init__(
payload_disarm, payload_arm_home, payload_arm_away, code, self,
availability_topic, payload_available, payload_not_available): 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.""" """Init the MQTT Alarm Control Panel."""
super().__init__(availability_topic, qos, payload_available, super().__init__(
payload_not_available) availability_topic, qos, payload_available, payload_not_available
)
self._state = STATE_UNKNOWN self._state = STATE_UNKNOWN
self._name = name self._name = name
self._state_topic = state_topic self._state_topic = state_topic
@ -96,16 +127,21 @@ class MqttAlarm(MqttAvailability, alarm.AlarmControlPanel):
@callback @callback
def message_received(topic, payload, qos): def message_received(topic, payload, qos):
"""Run when new MQTT message has been received.""" """Run when new MQTT message has been received."""
if payload not in (STATE_ALARM_DISARMED, STATE_ALARM_ARMED_HOME, if payload not in (
STATE_ALARM_ARMED_AWAY, STATE_ALARM_PENDING, STATE_ALARM_DISARMED,
STATE_ALARM_TRIGGERED): STATE_ALARM_ARMED_HOME,
STATE_ALARM_ARMED_AWAY,
STATE_ALARM_PENDING,
STATE_ALARM_TRIGGERED,
):
_LOGGER.warning("Received unexpected payload: %s", payload) _LOGGER.warning("Received unexpected payload: %s", payload)
return return
self._state = payload self._state = payload
self.async_schedule_update_ha_state() self.async_schedule_update_ha_state()
yield from mqtt.async_subscribe( yield from mqtt.async_subscribe(
self.hass, self._state_topic, message_received, self._qos) self.hass, self._state_topic, message_received, self._qos
)
@property @property
def should_poll(self): def should_poll(self):
@ -127,9 +163,9 @@ class MqttAlarm(MqttAvailability, alarm.AlarmControlPanel):
"""Return one or more digits/characters.""" """Return one or more digits/characters."""
if self._code is None: if self._code is None:
return None return None
if isinstance(self._code, str) and re.search('^\\d+$', self._code): if isinstance(self._code, str) and re.search("^\\d+$", self._code):
return 'Number' return "Number"
return 'Any' return "Any"
@asyncio.coroutine @asyncio.coroutine
def async_alarm_disarm(self, code=None): def async_alarm_disarm(self, code=None):
@ -137,11 +173,15 @@ class MqttAlarm(MqttAvailability, alarm.AlarmControlPanel):
This method is a coroutine. This method is a coroutine.
""" """
if not self._validate_code(code, 'disarming'): if not self._validate_code(code, "disarming"):
return return
mqtt.async_publish( mqtt.async_publish(
self.hass, self._command_topic, self._payload_disarm, self._qos, self.hass,
self._retain) self._command_topic,
self._payload_disarm,
self._qos,
self._retain,
)
@asyncio.coroutine @asyncio.coroutine
def async_alarm_arm_home(self, code=None): def async_alarm_arm_home(self, code=None):
@ -149,11 +189,15 @@ class MqttAlarm(MqttAvailability, alarm.AlarmControlPanel):
This method is a coroutine. This method is a coroutine.
""" """
if not self._validate_code(code, 'arming home'): if not self._validate_code(code, "arming home"):
return return
mqtt.async_publish( mqtt.async_publish(
self.hass, self._command_topic, self._payload_arm_home, self._qos, self.hass,
self._retain) self._command_topic,
self._payload_arm_home,
self._qos,
self._retain,
)
@asyncio.coroutine @asyncio.coroutine
def async_alarm_arm_away(self, code=None): def async_alarm_arm_away(self, code=None):
@ -161,15 +205,19 @@ class MqttAlarm(MqttAvailability, alarm.AlarmControlPanel):
This method is a coroutine. This method is a coroutine.
""" """
if not self._validate_code(code, 'arming away'): if not self._validate_code(code, "arming away"):
return return
mqtt.async_publish( mqtt.async_publish(
self.hass, self._command_topic, self._payload_arm_away, self._qos, self.hass,
self._retain) self._command_topic,
self._payload_arm_away,
self._qos,
self._retain,
)
def _validate_code(self, code, state): def _validate_code(self, code, state):
"""Validate given code.""" """Validate given code."""
check = self._code is None or code == self._code check = self._code is None or code == self._code
if not check: if not check:
_LOGGER.warning('Wrong code entered for %s', state) _LOGGER.warning("Wrong code entered for %s", state)
return check return check

View file

@ -12,23 +12,31 @@ import voluptuous as vol
import homeassistant.components.alarm_control_panel as alarm import homeassistant.components.alarm_control_panel as alarm
from homeassistant.components.alarm_control_panel import PLATFORM_SCHEMA from homeassistant.components.alarm_control_panel import PLATFORM_SCHEMA
from homeassistant.const import ( from homeassistant.const import (
CONF_HOST, CONF_NAME, CONF_PORT, STATE_ALARM_ARMED_AWAY, CONF_HOST,
STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED, STATE_UNKNOWN) CONF_NAME,
CONF_PORT,
STATE_ALARM_ARMED_AWAY,
STATE_ALARM_ARMED_HOME,
STATE_ALARM_DISARMED,
STATE_UNKNOWN,
)
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['pynx584==0.4'] REQUIREMENTS = ["pynx584==0.4"]
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
DEFAULT_HOST = 'localhost' DEFAULT_HOST = "localhost"
DEFAULT_NAME = 'NX584' DEFAULT_NAME = "NX584"
DEFAULT_PORT = 5007 DEFAULT_PORT = 5007
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ 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_HOST, default=DEFAULT_HOST): cv.string,
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, 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): 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) host = config.get(CONF_HOST)
port = config.get(CONF_PORT) port = config.get(CONF_PORT)
url = 'http://{}:{}'.format(host, port) url = "http://{}:{}".format(host, port)
try: try:
add_entities([NX584Alarm(hass, url, name)]) add_entities([NX584Alarm(hass, url, name)])
@ -52,6 +60,7 @@ class NX584Alarm(alarm.AlarmControlPanel):
def __init__(self, hass, url, name): def __init__(self, hass, url, name):
"""Init the nx584 alarm panel.""" """Init the nx584 alarm panel."""
from nx584 import client from nx584 import client
self._hass = hass self._hass = hass
self._name = name self._name = name
self._url = url self._url = url
@ -70,7 +79,7 @@ class NX584Alarm(alarm.AlarmControlPanel):
@property @property
def code_format(self): def code_format(self):
"""Return one or more digits/characters.""" """Return one or more digits/characters."""
return 'Number' return "Number"
@property @property
def state(self): def state(self):
@ -83,8 +92,10 @@ class NX584Alarm(alarm.AlarmControlPanel):
part = self._alarm.list_partitions()[0] part = self._alarm.list_partitions()[0]
zones = self._alarm.list_zones() zones = self._alarm.list_zones()
except requests.exceptions.ConnectionError as ex: except requests.exceptions.ConnectionError as ex:
_LOGGER.error("Unable to connect to %(host)s: %(reason)s", _LOGGER.error(
dict(host=self._url, reason=ex)) "Unable to connect to %(host)s: %(reason)s",
dict(host=self._url, reason=ex),
)
self._state = STATE_UNKNOWN self._state = STATE_UNKNOWN
zones = [] zones = []
except IndexError: except IndexError:
@ -94,13 +105,15 @@ class NX584Alarm(alarm.AlarmControlPanel):
bypassed = False bypassed = False
for zone in zones: for zone in zones:
if zone['bypassed']: if zone["bypassed"]:
_LOGGER.debug("Zone %(zone)s is bypassed, assuming HOME", _LOGGER.debug(
dict(zone=zone['number'])) "Zone %(zone)s is bypassed, assuming HOME",
dict(zone=zone["number"]),
)
bypassed = True bypassed = True
break break
if not part['armed']: if not part["armed"]:
self._state = STATE_ALARM_DISARMED self._state = STATE_ALARM_DISARMED
elif bypassed: elif bypassed:
self._state = STATE_ALARM_ARMED_HOME self._state = STATE_ALARM_ARMED_HOME
@ -113,8 +126,8 @@ class NX584Alarm(alarm.AlarmControlPanel):
def alarm_arm_home(self, code=None): def alarm_arm_home(self, code=None):
"""Send arm home command.""" """Send arm home command."""
self._alarm.arm('stay') self._alarm.arm("stay")
def alarm_arm_away(self, code=None): def alarm_arm_away(self, code=None):
"""Send arm away command.""" """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 import homeassistant.components.alarm_control_panel as alarm
from homeassistant.components.satel_integra import ( 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.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.dispatcher import async_dispatcher_connect
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
DEPENDENCIES = ['satel_integra'] DEPENDENCIES = ["satel_integra"]
@asyncio.coroutine @asyncio.coroutine
def async_setup_platform(hass, config, async_add_entities, def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
discovery_info=None):
"""Set up for Satel Integra alarm panels.""" """Set up for Satel Integra alarm panels."""
if not discovery_info: if not discovery_info:
return return
device = SatelIntegraAlarmPanel( device = SatelIntegraAlarmPanel(
"Alarm Panel", discovery_info.get(CONF_ARM_HOME_MODE)) "Alarm Panel", discovery_info.get(CONF_ARM_HOME_MODE)
)
async_add_entities([device]) async_add_entities([device])
@ -43,7 +46,8 @@ class SatelIntegraAlarmPanel(alarm.AlarmControlPanel):
def async_added_to_hass(self): def async_added_to_hass(self):
"""Register callbacks.""" """Register callbacks."""
async_dispatcher_connect( async_dispatcher_connect(
self.hass, SIGNAL_PANEL_MESSAGE, self._message_callback) self.hass, SIGNAL_PANEL_MESSAGE, self._message_callback
)
@callback @callback
def _message_callback(self, message): def _message_callback(self, message):
@ -67,7 +71,7 @@ class SatelIntegraAlarmPanel(alarm.AlarmControlPanel):
@property @property
def code_format(self): def code_format(self):
"""Return the regex for code format or None if no code is required.""" """Return the regex for code format or None if no code is required."""
return 'Number' return "Number"
@property @property
def state(self): def state(self):
@ -90,5 +94,4 @@ class SatelIntegraAlarmPanel(alarm.AlarmControlPanel):
def async_alarm_arm_home(self, code=None): def async_alarm_arm_home(self, code=None):
"""Send arm home command.""" """Send arm home command."""
if code: if code:
yield from self.hass.data[DATA_SATEL].arm( yield from self.hass.data[DATA_SATEL].arm(code, self._arm_home_mode)
code, self._arm_home_mode)

View file

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

View file

@ -9,17 +9,24 @@ import logging
import homeassistant.components.alarm_control_panel as alarm import homeassistant.components.alarm_control_panel as alarm
from homeassistant.components.spc import ( 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 ( from homeassistant.const import (
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED, STATE_ALARM_ARMED_AWAY,
STATE_UNKNOWN) STATE_ALARM_ARMED_HOME,
STATE_ALARM_DISARMED,
STATE_UNKNOWN,
)
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
SPC_AREA_MODE_TO_STATE = { SPC_AREA_MODE_TO_STATE = {
'0': STATE_ALARM_DISARMED, "0": STATE_ALARM_DISARMED,
'1': STATE_ALARM_ARMED_HOME, "1": STATE_ALARM_ARMED_HOME,
'3': STATE_ALARM_ARMED_AWAY, "3": STATE_ALARM_ARMED_AWAY,
} }
@ -29,16 +36,13 @@ def _get_alarm_state(spc_mode):
@asyncio.coroutine @asyncio.coroutine
def async_setup_platform(hass, config, async_add_entities, def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
discovery_info=None):
"""Set up the SPC alarm control panel platform.""" """Set up the SPC alarm control panel platform."""
if (discovery_info is None or if discovery_info is None or discovery_info[ATTR_DISCOVER_AREAS] is None:
discovery_info[ATTR_DISCOVER_AREAS] is None):
return return
api = hass.data[DATA_API] api = hass.data[DATA_API]
devices = [SpcAlarm(api, area) devices = [SpcAlarm(api, area) for area in discovery_info[ATTR_DISCOVER_AREAS]]
for area in discovery_info[ATTR_DISCOVER_AREAS]]
async_add_entities(devices) async_add_entities(devices)
@ -48,26 +52,25 @@ class SpcAlarm(alarm.AlarmControlPanel):
def __init__(self, api, area): def __init__(self, api, area):
"""Initialize the SPC alarm panel.""" """Initialize the SPC alarm panel."""
self._area_id = area['id'] self._area_id = area["id"]
self._name = area['name'] self._name = area["name"]
self._state = _get_alarm_state(area['mode']) self._state = _get_alarm_state(area["mode"])
if self._state == STATE_ALARM_DISARMED: 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: else:
self._changed_by = area.get('last_set_user_name', 'unknown') self._changed_by = area.get("last_set_user_name", "unknown")
self._api = api self._api = api
@asyncio.coroutine @asyncio.coroutine
def async_added_to_hass(self): def async_added_to_hass(self):
"""Call for adding new entities.""" """Call for adding new entities."""
self.hass.data[DATA_REGISTRY].register_alarm_device( self.hass.data[DATA_REGISTRY].register_alarm_device(self._area_id, self)
self._area_id, self)
@asyncio.coroutine @asyncio.coroutine
def async_update_from_spc(self, state, extra): def async_update_from_spc(self, state, extra):
"""Update the alarm panel with a new state.""" """Update the alarm panel with a new state."""
self._state = 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() self.async_schedule_update_ha_state()
@property @property
@ -94,16 +97,19 @@ class SpcAlarm(alarm.AlarmControlPanel):
def async_alarm_disarm(self, code=None): def async_alarm_disarm(self, code=None):
"""Send disarm command.""" """Send disarm command."""
yield from self._api.send_area_command( yield from self._api.send_area_command(
self._area_id, SpcWebGateway.AREA_COMMAND_UNSET) self._area_id, SpcWebGateway.AREA_COMMAND_UNSET
)
@asyncio.coroutine @asyncio.coroutine
def async_alarm_arm_home(self, code=None): def async_alarm_arm_home(self, code=None):
"""Send arm home command.""" """Send arm home command."""
yield from self._api.send_area_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 @asyncio.coroutine
def async_alarm_arm_away(self, code=None): def async_alarm_arm_away(self, code=None):
"""Send arm away command.""" """Send arm away command."""
yield from self._api.send_area_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 import homeassistant.components.alarm_control_panel as alarm
from homeassistant.components.alarm_control_panel import PLATFORM_SCHEMA from homeassistant.components.alarm_control_panel import PLATFORM_SCHEMA
from homeassistant.const import ( from homeassistant.const import (
CONF_PASSWORD, CONF_USERNAME, STATE_ALARM_ARMED_AWAY, CONF_PASSWORD,
STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT, STATE_ALARM_DISARMED, CONF_USERNAME,
STATE_ALARM_ARMING, STATE_ALARM_DISARMING, STATE_UNKNOWN, CONF_NAME, STATE_ALARM_ARMED_AWAY,
STATE_ALARM_ARMED_CUSTOM_BYPASS) 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__) _LOGGER = logging.getLogger(__name__)
DEFAULT_NAME = 'Total Connect' DEFAULT_NAME = "Total Connect"
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
vol.Required(CONF_PASSWORD): cv.string, {
vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_PASSWORD): cv.string,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): 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): def setup_platform(hass, config, add_entities, discovery_info=None):
@ -53,8 +63,7 @@ class TotalConnect(alarm.AlarmControlPanel):
self._username = username self._username = username
self._password = password self._password = password
self._state = STATE_UNKNOWN self._state = STATE_UNKNOWN
self._client = TotalConnectClient.TotalConnectClient( self._client = TotalConnectClient.TotalConnectClient(username, password)
username, password)
@property @property
def name(self): 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 CONF_ALARM, CONF_CODE_DIGITS
from homeassistant.components.verisure import HUB as hub from homeassistant.components.verisure import HUB as hub
from homeassistant.const import ( from homeassistant.const import (
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED, STATE_ALARM_ARMED_AWAY,
STATE_UNKNOWN) STATE_ALARM_ARMED_HOME,
STATE_ALARM_DISARMED,
STATE_UNKNOWN,
)
_LOGGER = logging.getLogger(__name__) _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): def set_arm_state(state, code=None):
"""Send set arm state command.""" """Send set arm state command."""
transaction_id = hub.session.set_arm_state(code, state)[ transaction_id = hub.session.set_arm_state(code, state)[
'armStateChangeTransactionId'] "armStateChangeTransactionId"
_LOGGER.info('verisure set arm state %s', state) ]
_LOGGER.info("verisure set arm state %s", state)
transaction = {} transaction = {}
while 'result' not in transaction: while "result" not in transaction:
sleep(0.5) sleep(0.5)
transaction = hub.session.get_arm_state_transaction(transaction_id) transaction = hub.session.get_arm_state_transaction(transaction_id)
# pylint: disable=unexpected-keyword-arg # pylint: disable=unexpected-keyword-arg
@ -51,7 +55,7 @@ class VerisureAlarm(alarm.AlarmControlPanel):
@property @property
def name(self): def name(self):
"""Return the name of the device.""" """Return the name of the device."""
return '{} alarm'.format(hub.session.installations[0]['alias']) return "{} alarm".format(hub.session.installations[0]["alias"])
@property @property
def state(self): def state(self):
@ -61,7 +65,7 @@ class VerisureAlarm(alarm.AlarmControlPanel):
@property @property
def code_format(self): def code_format(self):
"""Return one or more digits/characters.""" """Return one or more digits/characters."""
return 'Number' return "Number"
@property @property
def changed_by(self): def changed_by(self):
@ -72,24 +76,24 @@ class VerisureAlarm(alarm.AlarmControlPanel):
"""Update alarm status.""" """Update alarm status."""
hub.update_overview() hub.update_overview()
status = hub.get_first("$.armState.statusType") status = hub.get_first("$.armState.statusType")
if status == 'DISARMED': if status == "DISARMED":
self._state = STATE_ALARM_DISARMED self._state = STATE_ALARM_DISARMED
elif status == 'ARMED_HOME': elif status == "ARMED_HOME":
self._state = STATE_ALARM_ARMED_HOME self._state = STATE_ALARM_ARMED_HOME
elif status == 'ARMED_AWAY': elif status == "ARMED_AWAY":
self._state = STATE_ALARM_ARMED_AWAY self._state = STATE_ALARM_ARMED_AWAY
elif status != 'PENDING': elif status != "PENDING":
_LOGGER.error('Unknown alarm state %s', status) _LOGGER.error("Unknown alarm state %s", status)
self._changed_by = hub.get_first("$.armState.name") self._changed_by = hub.get_first("$.armState.name")
def alarm_disarm(self, code=None): def alarm_disarm(self, code=None):
"""Send disarm command.""" """Send disarm command."""
set_arm_state('DISARMED', code) set_arm_state("DISARMED", code)
def alarm_arm_home(self, code=None): def alarm_arm_home(self, code=None):
"""Send arm home command.""" """Send arm home command."""
set_arm_state('ARMED_HOME', code) set_arm_state("ARMED_HOME", code)
def alarm_arm_away(self, code=None): def alarm_arm_away(self, code=None):
"""Send arm away command.""" """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 import homeassistant.components.alarm_control_panel as alarm
from homeassistant.components.wink import DOMAIN, WinkDevice from homeassistant.components.wink import DOMAIN, WinkDevice
from homeassistant.const import ( from homeassistant.const import (
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED, STATE_ALARM_ARMED_AWAY,
STATE_UNKNOWN) STATE_ALARM_ARMED_HOME,
STATE_ALARM_DISARMED,
STATE_UNKNOWN,
)
_LOGGER = logging.getLogger(__name__) _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): 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() camera.capability()
except AttributeError: except AttributeError:
_id = camera.object_id() + camera.name() _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)]) add_entities([WinkCameraDevice(camera, hass)])
@ -41,7 +44,7 @@ class WinkCameraDevice(WinkDevice, alarm.AlarmControlPanel):
@asyncio.coroutine @asyncio.coroutine
def async_added_to_hass(self): def async_added_to_hass(self):
"""Call when entity is added to hass.""" """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 @property
def state(self): def state(self):
@ -72,6 +75,4 @@ class WinkCameraDevice(WinkDevice, alarm.AlarmControlPanel):
@property @property
def device_state_attributes(self): def device_state_attributes(self):
"""Return the state attributes.""" """Return the state attributes."""
return { return {"private": self.wink.private()}
'private': self.wink.private()
}

View file

@ -9,28 +9,37 @@ import logging
import voluptuous as vol import voluptuous as vol
from homeassistant.components.alarm_control_panel import ( from homeassistant.components.alarm_control_panel import (
AlarmControlPanel, PLATFORM_SCHEMA) AlarmControlPanel,
PLATFORM_SCHEMA,
)
from homeassistant.const import ( from homeassistant.const import (
CONF_PASSWORD, CONF_USERNAME, CONF_NAME, CONF_PASSWORD,
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED) CONF_USERNAME,
CONF_NAME,
STATE_ALARM_ARMED_AWAY,
STATE_ALARM_ARMED_HOME,
STATE_ALARM_DISARMED,
)
import homeassistant.helpers.config_validation as cv 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__) _LOGGER = logging.getLogger(__name__)
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
vol.Required(CONF_USERNAME): cv.string, {
vol.Required(CONF_PASSWORD): cv.string, vol.Required(CONF_USERNAME): cv.string,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Required(CONF_PASSWORD): cv.string,
vol.Optional(CONF_AREA_ID, default=DEFAULT_AREA_ID): 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): 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] password = config[CONF_PASSWORD]
area_id = config[CONF_AREA_ID] area_id = config[CONF_AREA_ID]
from yalesmartalarmclient.client import ( from yalesmartalarmclient.client import YaleSmartAlarmClient, AuthenticationError
YaleSmartAlarmClient, AuthenticationError)
try: try:
client = YaleSmartAlarmClient(username, password, area_id) client = YaleSmartAlarmClient(username, password, area_id)
except AuthenticationError: except AuthenticationError:
@ -60,13 +69,16 @@ class YaleAlarmDevice(AlarmControlPanel):
self._client = client self._client = client
self._state = None self._state = None
from yalesmartalarmclient.client import (YALE_STATE_DISARM, from yalesmartalarmclient.client import (
YALE_STATE_ARM_PARTIAL, YALE_STATE_DISARM,
YALE_STATE_ARM_FULL) YALE_STATE_ARM_PARTIAL,
YALE_STATE_ARM_FULL,
)
self._state_map = { self._state_map = {
YALE_STATE_DISARM: STATE_ALARM_DISARMED, YALE_STATE_DISARM: STATE_ALARM_DISARMED,
YALE_STATE_ARM_PARTIAL: STATE_ALARM_ARMED_HOME, 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 @property

View file

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

View file

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

View file

@ -1,23 +1,23 @@
"""Constants for the Alexa integration.""" """Constants for the Alexa integration."""
DOMAIN = 'alexa' DOMAIN = "alexa"
# Flash briefing constants # Flash briefing constants
CONF_UID = 'uid' CONF_UID = "uid"
CONF_TITLE = 'title' CONF_TITLE = "title"
CONF_AUDIO = 'audio' CONF_AUDIO = "audio"
CONF_TEXT = 'text' CONF_TEXT = "text"
CONF_DISPLAY_URL = 'display_url' CONF_DISPLAY_URL = "display_url"
CONF_FILTER = 'filter' CONF_FILTER = "filter"
CONF_ENTITY_CONFIG = 'entity_config' CONF_ENTITY_CONFIG = "entity_config"
ATTR_UID = 'uid' ATTR_UID = "uid"
ATTR_UPDATE_DATE = 'updateDate' ATTR_UPDATE_DATE = "updateDate"
ATTR_TITLE_TEXT = 'titleText' ATTR_TITLE_TEXT = "titleText"
ATTR_STREAM_URL = 'streamUrl' ATTR_STREAM_URL = "streamUrl"
ATTR_MAIN_TEXT = 'mainText' ATTR_MAIN_TEXT = "mainText"
ATTR_REDIRECTION_URL = 'redirectionURL' 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 homeassistant.helpers import template
from .const import ( from .const import (
ATTR_MAIN_TEXT, ATTR_REDIRECTION_URL, ATTR_STREAM_URL, ATTR_TITLE_TEXT, ATTR_MAIN_TEXT,
ATTR_UID, ATTR_UPDATE_DATE, CONF_AUDIO, CONF_DISPLAY_URL, CONF_TEXT, ATTR_REDIRECTION_URL,
CONF_TITLE, CONF_UID, DATE_FORMAT) 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__) _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 @callback
def async_setup(hass, flash_briefing_config): def async_setup(hass, flash_briefing_config):
"""Activate Alexa component.""" """Activate Alexa component."""
hass.http.register_view( hass.http.register_view(AlexaFlashBriefingView(hass, flash_briefing_config))
AlexaFlashBriefingView(hass, flash_briefing_config))
class AlexaFlashBriefingView(http.HomeAssistantView): class AlexaFlashBriefingView(http.HomeAssistantView):
"""Handle Alexa Flash Briefing skill requests.""" """Handle Alexa Flash Briefing skill requests."""
url = FLASH_BRIEFINGS_API_ENDPOINT url = FLASH_BRIEFINGS_API_ENDPOINT
name = 'api:alexa:flash_briefings' name = "api:alexa:flash_briefings"
def __init__(self, hass, flash_briefings): def __init__(self, hass, flash_briefings):
"""Initialize Alexa view.""" """Initialize Alexa view."""
@ -45,13 +54,12 @@ class AlexaFlashBriefingView(http.HomeAssistantView):
@callback @callback
def get(self, request, briefing_id): def get(self, request, briefing_id):
"""Handle Alexa Flash Briefing request.""" """Handle Alexa Flash Briefing request."""
_LOGGER.debug("Received Alexa flash briefing request for: %s", _LOGGER.debug("Received Alexa flash briefing request for: %s", briefing_id)
briefing_id)
if self.flash_briefings.get(briefing_id) is None: if self.flash_briefings.get(briefing_id) is None:
err = "No configured Alexa flash briefing was found for: %s" err = "No configured Alexa flash briefing was found for: %s"
_LOGGER.error(err, briefing_id) _LOGGER.error(err, briefing_id)
return b'', 404 return b"", 404
briefing = [] briefing = []
@ -81,10 +89,8 @@ class AlexaFlashBriefingView(http.HomeAssistantView):
output[ATTR_STREAM_URL] = item.get(CONF_AUDIO) output[ATTR_STREAM_URL] = item.get(CONF_AUDIO)
if item.get(CONF_DISPLAY_URL) is not None: if item.get(CONF_DISPLAY_URL) is not None:
if isinstance(item.get(CONF_DISPLAY_URL), if isinstance(item.get(CONF_DISPLAY_URL), template.Template):
template.Template): output[ATTR_REDIRECTION_URL] = item[CONF_DISPLAY_URL].async_render()
output[ATTR_REDIRECTION_URL] = \
item[CONF_DISPLAY_URL].async_render()
else: else:
output[ATTR_REDIRECTION_URL] = item.get(CONF_DISPLAY_URL) output[ATTR_REDIRECTION_URL] = item.get(CONF_DISPLAY_URL)

View file

@ -20,27 +20,24 @@ _LOGGER = logging.getLogger(__name__)
HANDLERS = Registry() HANDLERS = Registry()
INTENTS_API_ENDPOINT = '/api/alexa' INTENTS_API_ENDPOINT = "/api/alexa"
class SpeechType(enum.Enum): class SpeechType(enum.Enum):
"""The Alexa speech types.""" """The Alexa speech types."""
plaintext = 'PlainText' plaintext = "PlainText"
ssml = 'SSML' ssml = "SSML"
SPEECH_MAPPINGS = { SPEECH_MAPPINGS = {"plain": SpeechType.plaintext, "ssml": SpeechType.ssml}
'plain': SpeechType.plaintext,
'ssml': SpeechType.ssml,
}
class CardType(enum.Enum): class CardType(enum.Enum):
"""The Alexa card types.""" """The Alexa card types."""
simple = 'Simple' simple = "Simple"
link_account = 'LinkAccount' link_account = "LinkAccount"
@callback @callback
@ -57,45 +54,51 @@ class AlexaIntentsView(http.HomeAssistantView):
"""Handle Alexa requests.""" """Handle Alexa requests."""
url = INTENTS_API_ENDPOINT url = INTENTS_API_ENDPOINT
name = 'api:alexa' name = "api:alexa"
@asyncio.coroutine @asyncio.coroutine
def post(self, request): def post(self, request):
"""Handle Alexa.""" """Handle Alexa."""
hass = request.app['hass'] hass = request.app["hass"]
message = yield from request.json() message = yield from request.json()
_LOGGER.debug("Received Alexa request: %s", message) _LOGGER.debug("Received Alexa request: %s", message)
try: try:
response = yield from async_handle_message(hass, message) 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: except UnknownRequest as err:
_LOGGER.warning(str(err)) _LOGGER.warning(str(err))
return self.json(intent_error_response( return self.json(intent_error_response(hass, message, str(err)))
hass, message, str(err)))
except intent.UnknownIntent as err: except intent.UnknownIntent as err:
_LOGGER.warning(str(err)) _LOGGER.warning(str(err))
return self.json(intent_error_response( return self.json(
hass, message, intent_error_response(
"This intent is not yet configured within Home Assistant.")) hass,
message,
"This intent is not yet configured within Home Assistant.",
)
)
except intent.InvalidSlotInfo as err: except intent.InvalidSlotInfo as err:
_LOGGER.error("Received invalid slot data from Alexa: %s", err) _LOGGER.error("Received invalid slot data from Alexa: %s", err)
return self.json(intent_error_response( return self.json(
hass, message, intent_error_response(
"Invalid slot information received for this intent.")) hass, message, "Invalid slot information received for this intent."
)
)
except intent.IntentError as err: except intent.IntentError as err:
_LOGGER.exception(str(err)) _LOGGER.exception(str(err))
return self.json(intent_error_response( return self.json(
hass, message, "Error handling intent.")) intent_error_response(hass, message, "Error handling intent.")
)
def intent_error_response(hass, message, error): def intent_error_response(hass, message, error):
"""Return an Alexa response that will speak the error message.""" """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 = AlexaResponse(hass, alexa_intent_info)
alexa_response.add_speech(SpeechType.plaintext, error) alexa_response.add_speech(SpeechType.plaintext, error)
return alexa_response.as_dict() return alexa_response.as_dict()
@ -112,26 +115,26 @@ def async_handle_message(hass, message):
- intent.IntentError - intent.IntentError
""" """
req = message.get('request') req = message.get("request")
req_type = req['type'] req_type = req["type"]
handler = HANDLERS.get(req_type) handler = HANDLERS.get(req_type)
if not handler: 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)) return (yield from handler(hass, message))
@HANDLERS.register('SessionEndedRequest') @HANDLERS.register("SessionEndedRequest")
@asyncio.coroutine @asyncio.coroutine
def async_handle_session_end(hass, message): def async_handle_session_end(hass, message):
"""Handle a session end request.""" """Handle a session end request."""
return None return None
@HANDLERS.register('IntentRequest') @HANDLERS.register("IntentRequest")
@HANDLERS.register('LaunchRequest') @HANDLERS.register("LaunchRequest")
@asyncio.coroutine @asyncio.coroutine
def async_handle_intent(hass, message): def async_handle_intent(hass, message):
"""Handle an intent request. """Handle an intent request.
@ -142,33 +145,37 @@ def async_handle_intent(hass, message):
- intent.IntentError - intent.IntentError
""" """
req = message.get('request') req = message.get("request")
alexa_intent_info = req.get('intent') alexa_intent_info = req.get("intent")
alexa_response = AlexaResponse(hass, alexa_intent_info) alexa_response = AlexaResponse(hass, alexa_intent_info)
if req['type'] == 'LaunchRequest': if req["type"] == "LaunchRequest":
intent_name = message.get('session', {}) \ intent_name = (
.get('application', {}) \ message.get("session", {}).get("application", {}).get("applicationId")
.get('applicationId') )
else: else:
intent_name = alexa_intent_info['name'] intent_name = alexa_intent_info["name"]
intent_response = yield from intent.async_handle( intent_response = yield from intent.async_handle(
hass, DOMAIN, intent_name, hass,
{key: {'value': value} for key, value DOMAIN,
in alexa_response.variables.items()}) intent_name,
{key: {"value": value} for key, value in alexa_response.variables.items()},
)
for intent_speech, alexa_speech in SPEECH_MAPPINGS.items(): for intent_speech, alexa_speech in SPEECH_MAPPINGS.items():
if intent_speech in intent_response.speech: if intent_speech in intent_response.speech:
alexa_response.add_speech( alexa_response.add_speech(
alexa_speech, alexa_speech, intent_response.speech[intent_speech]["speech"]
intent_response.speech[intent_speech]['speech']) )
break break
if 'simple' in intent_response.card: if "simple" in intent_response.card:
alexa_response.add_card( alexa_response.add_card(
CardType.simple, intent_response.card['simple']['title'], CardType.simple,
intent_response.card['simple']['content']) intent_response.card["simple"]["title"],
intent_response.card["simple"]["content"],
)
return alexa_response.as_dict() 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 # 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: # reference to the request object structure, see the Alexa docs:
# https://tinyurl.com/ybvm7jhs # https://tinyurl.com/ybvm7jhs
resolved_value = request['value'] resolved_value = request["value"]
if ('resolutions' in request and if (
'resolutionsPerAuthority' in request['resolutions'] and "resolutions" in request
len(request['resolutions']['resolutionsPerAuthority']) >= 1): and "resolutionsPerAuthority" in request["resolutions"]
and len(request["resolutions"]["resolutionsPerAuthority"]) >= 1
):
# Extract all of the possible values from each authority with a # Extract all of the possible values from each authority with a
# successful match # successful match
possible_values = [] possible_values = []
for entry in request['resolutions']['resolutionsPerAuthority']: for entry in request["resolutions"]["resolutionsPerAuthority"]:
if entry['status']['code'] != SYN_RESOLUTION_MATCH: if entry["status"]["code"] != SYN_RESOLUTION_MATCH:
continue continue
possible_values.extend([item['value']['name'] possible_values.extend([item["value"]["name"] for item in entry["values"]])
for item
in entry['values']])
# If there is only one match use the resolved value, otherwise the # If there is only one match use the resolved value, otherwise the
# resolution cannot be determined, so use the spoken slot value # 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] resolved_value = possible_values[0]
else: else:
_LOGGER.debug( _LOGGER.debug(
'Found multiple synonym resolutions for slot value: {%s: %s}', "Found multiple synonym resolutions for slot value: {%s: %s}",
key, key,
request['value'] request["value"],
) )
return resolved_value return resolved_value
@ -225,12 +232,12 @@ class AlexaResponse:
# Intent is None if request was a LaunchRequest or SessionEndedRequest # Intent is None if request was a LaunchRequest or SessionEndedRequest
if intent_info is not None: 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 # Only include slots with values
if 'value' not in value: if "value" not in value:
continue continue
_key = key.replace('.', '_') _key = key.replace(".", "_")
self.variables[_key] = resolve_slot_synonyms(key, value) self.variables[_key] = resolve_slot_synonyms(key, value)
@ -238,9 +245,7 @@ class AlexaResponse:
"""Add a card to the response.""" """Add a card to the response."""
assert self.card is None assert self.card is None
card = { card = {"type": card_type.value}
"type": card_type.value
}
if card_type == CardType.link_account: if card_type == CardType.link_account:
self.card = card self.card = card
@ -254,43 +259,36 @@ class AlexaResponse:
"""Add speech to the response.""" """Add speech to the response."""
assert self.speech is None 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 = { self.speech = {"type": speech_type.value, key: text}
'type': speech_type.value,
key: text
}
def add_reprompt(self, speech_type, text): def add_reprompt(self, speech_type, text):
"""Add reprompt if user does not answer.""" """Add reprompt if user does not answer."""
assert self.reprompt is None 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 = { self.reprompt = {
'type': speech_type.value, "type": speech_type.value,
key: text.async_render(self.variables) key: text.async_render(self.variables),
} }
def as_dict(self): def as_dict(self):
"""Return response in an Alexa valid dict.""" """Return response in an Alexa valid dict."""
response = { response = {"shouldEndSession": self.should_end_session}
'shouldEndSession': self.should_end_session
}
if self.card is not None: if self.card is not None:
response['card'] = self.card response["card"] = self.card
if self.speech is not None: if self.speech is not None:
response['outputSpeech'] = self.speech response["outputSpeech"] = self.speech
if self.reprompt is not None: if self.reprompt is not None:
response['reprompt'] = { response["reprompt"] = {"outputSpeech": self.reprompt}
'outputSpeech': self.reprompt
}
return { return {
'version': '1.0', "version": "1.0",
'sessionAttributes': self.session_attributes, "sessionAttributes": self.session_attributes,
'response': response, "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 requests.exceptions import ConnectionError as ConnectError
from homeassistant.const import ( from homeassistant.const import (
CONF_NAME, CONF_HOST, CONF_PORT, CONF_USERNAME, CONF_PASSWORD, CONF_NAME,
CONF_SENSORS, CONF_SWITCHES, CONF_SCAN_INTERVAL, HTTP_BASIC_AUTHENTICATION) CONF_HOST,
CONF_PORT,
CONF_USERNAME,
CONF_PASSWORD,
CONF_SENSORS,
CONF_SWITCHES,
CONF_SCAN_INTERVAL,
HTTP_BASIC_AUTHENTICATION,
)
from homeassistant.helpers import discovery from homeassistant.helpers import discovery
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['amcrest==1.2.3'] REQUIREMENTS = ["amcrest==1.2.3"]
DEPENDENCIES = ['ffmpeg'] DEPENDENCIES = ["ffmpeg"]
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
CONF_AUTHENTICATION = 'authentication' CONF_AUTHENTICATION = "authentication"
CONF_RESOLUTION = 'resolution' CONF_RESOLUTION = "resolution"
CONF_STREAM_SOURCE = 'stream_source' CONF_STREAM_SOURCE = "stream_source"
CONF_FFMPEG_ARGUMENTS = 'ffmpeg_arguments' CONF_FFMPEG_ARGUMENTS = "ffmpeg_arguments"
DEFAULT_NAME = 'Amcrest Camera' DEFAULT_NAME = "Amcrest Camera"
DEFAULT_PORT = 80 DEFAULT_PORT = 80
DEFAULT_RESOLUTION = 'high' DEFAULT_RESOLUTION = "high"
DEFAULT_STREAM_SOURCE = 'snapshot' DEFAULT_STREAM_SOURCE = "snapshot"
TIMEOUT = 10 TIMEOUT = 10
DATA_AMCREST = 'amcrest' DATA_AMCREST = "amcrest"
DOMAIN = 'amcrest' DOMAIN = "amcrest"
NOTIFICATION_ID = 'amcrest_notification' NOTIFICATION_ID = "amcrest_notification"
NOTIFICATION_TITLE = 'Amcrest Camera Setup' NOTIFICATION_TITLE = "Amcrest Camera Setup"
RESOLUTION_LIST = { RESOLUTION_LIST = {"high": 0, "low": 1}
'high': 0,
'low': 1,
}
SCAN_INTERVAL = timedelta(seconds=10) SCAN_INTERVAL = timedelta(seconds=10)
AUTHENTICATION_LIST = { AUTHENTICATION_LIST = {"basic": "basic"}
'basic': 'basic'
}
STREAM_SOURCE_LIST = { STREAM_SOURCE_LIST = {"mjpeg": 0, "snapshot": 1, "rtsp": 2}
'mjpeg': 0,
'snapshot': 1,
'rtsp': 2,
}
# Sensor types are defined like: Name, units, icon # Sensor types are defined like: Name, units, icon
SENSORS = { SENSORS = {
'motion_detector': ['Motion Detected', None, 'mdi:run'], "motion_detector": ["Motion Detected", None, "mdi:run"],
'sdcard': ['SD Used', '%', 'mdi:sd'], "sdcard": ["SD Used", "%", "mdi:sd"],
'ptz_preset': ['PTZ Preset', None, 'mdi:camera-iris'], "ptz_preset": ["PTZ Preset", None, "mdi:camera-iris"],
} }
# Switch types are defined like: Name, icon # Switch types are defined like: Name, icon
SWITCHES = { SWITCHES = {
'motion_detection': ['Motion Detection', 'mdi:run-fast'], "motion_detection": ["Motion Detection", "mdi:run-fast"],
'motion_recording': ['Motion Recording', 'mdi:record-rec'] "motion_recording": ["Motion Recording", "mdi:record-rec"],
} }
CONFIG_SCHEMA = vol.Schema({ CONFIG_SCHEMA = vol.Schema(
DOMAIN: vol.All(cv.ensure_list, [vol.Schema({ {
vol.Required(CONF_HOST): cv.string, DOMAIN: vol.All(
vol.Required(CONF_USERNAME): cv.string, cv.ensure_list,
vol.Required(CONF_PASSWORD): cv.string, [
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Schema(
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, {
vol.Optional(CONF_AUTHENTICATION, default=HTTP_BASIC_AUTHENTICATION): vol.Required(CONF_HOST): cv.string,
vol.All(vol.In(AUTHENTICATION_LIST)), vol.Required(CONF_USERNAME): cv.string,
vol.Optional(CONF_RESOLUTION, default=DEFAULT_RESOLUTION): vol.Required(CONF_PASSWORD): cv.string,
vol.All(vol.In(RESOLUTION_LIST)), vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_STREAM_SOURCE, default=DEFAULT_STREAM_SOURCE): vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
vol.All(vol.In(STREAM_SOURCE_LIST)), vol.Optional(
vol.Optional(CONF_FFMPEG_ARGUMENTS): cv.string, CONF_AUTHENTICATION, default=HTTP_BASIC_AUTHENTICATION
vol.Optional(CONF_SCAN_INTERVAL, default=SCAN_INTERVAL): ): vol.All(vol.In(AUTHENTICATION_LIST)),
cv.time_period, vol.Optional(
vol.Optional(CONF_SENSORS): CONF_RESOLUTION, default=DEFAULT_RESOLUTION
vol.All(cv.ensure_list, [vol.In(SENSORS)]), ): vol.All(vol.In(RESOLUTION_LIST)),
vol.Optional(CONF_SWITCHES): vol.Optional(
vol.All(cv.ensure_list, [vol.In(SWITCHES)]), CONF_STREAM_SOURCE, default=DEFAULT_STREAM_SOURCE
})]) ): vol.All(vol.In(STREAM_SOURCE_LIST)),
}, extra=vol.ALLOW_EXTRA) 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): def setup(hass, config):
@ -103,21 +118,24 @@ def setup(hass, config):
for device in amcrest_cams: for device in amcrest_cams:
try: try:
camera = AmcrestCamera(device.get(CONF_HOST), camera = AmcrestCamera(
device.get(CONF_PORT), device.get(CONF_HOST),
device.get(CONF_USERNAME), device.get(CONF_PORT),
device.get(CONF_PASSWORD)).camera device.get(CONF_USERNAME),
device.get(CONF_PASSWORD),
).camera
# pylint: disable=pointless-statement # pylint: disable=pointless-statement
camera.current_time camera.current_time
except (ConnectError, ConnectTimeout, HTTPError) as ex: except (ConnectError, ConnectTimeout, HTTPError) as ex:
_LOGGER.error("Unable to connect to Amcrest camera: %s", str(ex)) _LOGGER.error("Unable to connect to Amcrest camera: %s", str(ex))
hass.components.persistent_notification.create( hass.components.persistent_notification.create(
'Error: {}<br />' "Error: {}<br />"
'You will need to restart hass after fixing.' "You will need to restart hass after fixing."
''.format(ex), "".format(ex),
title=NOTIFICATION_TITLE, title=NOTIFICATION_TITLE,
notification_id=NOTIFICATION_ID) notification_id=NOTIFICATION_ID,
)
continue continue
ffmpeg_arguments = device.get(CONF_FFMPEG_ARGUMENTS) ffmpeg_arguments = device.get(CONF_FFMPEG_ARGUMENTS)
@ -139,27 +157,24 @@ def setup(hass, config):
authentication = None authentication = None
hass.data[DATA_AMCREST][name] = AmcrestDevice( hass.data[DATA_AMCREST][name] = AmcrestDevice(
camera, name, authentication, ffmpeg_arguments, stream_source, camera, name, authentication, ffmpeg_arguments, stream_source, resolution
resolution) )
discovery.load_platform( discovery.load_platform(hass, "camera", DOMAIN, {CONF_NAME: name}, config)
hass, 'camera', DOMAIN, {
CONF_NAME: name,
}, config)
if sensors: if sensors:
discovery.load_platform( discovery.load_platform(
hass, 'sensor', DOMAIN, { hass, "sensor", DOMAIN, {CONF_NAME: name, CONF_SENSORS: sensors}, config
CONF_NAME: name, )
CONF_SENSORS: sensors,
}, config)
if switches: if switches:
discovery.load_platform( discovery.load_platform(
hass, 'switch', DOMAIN, { hass,
CONF_NAME: name, "switch",
CONF_SWITCHES: switches DOMAIN,
}, config) {CONF_NAME: name, CONF_SWITCHES: switches},
config,
)
return True return True
@ -167,8 +182,9 @@ def setup(hass, config):
class AmcrestDevice: class AmcrestDevice:
"""Representation of a base Amcrest discovery device.""" """Representation of a base Amcrest discovery device."""
def __init__(self, camera, name, authentication, ffmpeg_arguments, def __init__(
stream_source, resolution): self, camera, name, authentication, ffmpeg_arguments, stream_source, resolution
):
"""Initialize the entity.""" """Initialize the entity."""
self.device = camera self.device = camera
self.name = name self.name = name

View file

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

View file

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

View file

@ -14,11 +14,25 @@ import async_timeout
from homeassistant.bootstrap import DATA_LOGGING from homeassistant.bootstrap import DATA_LOGGING
from homeassistant.components.http import HomeAssistantView from homeassistant.components.http import HomeAssistantView
from homeassistant.const import ( from homeassistant.const import (
EVENT_HOMEASSISTANT_STOP, EVENT_TIME_CHANGED, HTTP_BAD_REQUEST, EVENT_HOMEASSISTANT_STOP,
HTTP_CREATED, HTTP_NOT_FOUND, MATCH_ALL, URL_API, URL_API_COMPONENTS, EVENT_TIME_CHANGED,
URL_API_CONFIG, URL_API_DISCOVERY_INFO, URL_API_ERROR_LOG, URL_API_EVENTS, HTTP_BAD_REQUEST,
URL_API_SERVICES, URL_API_STATES, URL_API_STATES_ENTITY, URL_API_STREAM, HTTP_CREATED,
URL_API_TEMPLATE, __version__) 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 import homeassistant.core as ha
from homeassistant.exceptions import TemplateError from homeassistant.exceptions import TemplateError
from homeassistant.helpers import template from homeassistant.helpers import template
@ -28,15 +42,15 @@ from homeassistant.helpers.json import JSONEncoder
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
ATTR_BASE_URL = 'base_url' ATTR_BASE_URL = "base_url"
ATTR_LOCATION_NAME = 'location_name' ATTR_LOCATION_NAME = "location_name"
ATTR_REQUIRES_API_PASSWORD = 'requires_api_password' ATTR_REQUIRES_API_PASSWORD = "requires_api_password"
ATTR_VERSION = 'version' ATTR_VERSION = "version"
DOMAIN = 'api' DOMAIN = "api"
DEPENDENCIES = ['http'] DEPENDENCIES = ["http"]
STREAM_PING_PAYLOAD = 'ping' STREAM_PING_PAYLOAD = "ping"
STREAM_PING_INTERVAL = 50 # seconds STREAM_PING_INTERVAL = 50 # seconds
@ -65,7 +79,7 @@ class APIStatusView(HomeAssistantView):
"""View to handle Status requests.""" """View to handle Status requests."""
url = URL_API url = URL_API
name = 'api:status' name = "api:status"
@ha.callback @ha.callback
def get(self, request): def get(self, request):
@ -77,17 +91,17 @@ class APIEventStream(HomeAssistantView):
"""View to handle EventStream requests.""" """View to handle EventStream requests."""
url = URL_API_STREAM url = URL_API_STREAM
name = 'api:stream' name = "api:stream"
async def get(self, request): async def get(self, request):
"""Provide a streaming interface for the event bus.""" """Provide a streaming interface for the event bus."""
hass = request.app['hass'] hass = request.app["hass"]
stop_obj = object() stop_obj = object()
to_write = asyncio.Queue(loop=hass.loop) to_write = asyncio.Queue(loop=hass.loop)
restrict = request.query.get('restrict') restrict = request.query.get("restrict")
if restrict: if restrict:
restrict = restrict.split(',') + [EVENT_HOMEASSISTANT_STOP] restrict = restrict.split(",") + [EVENT_HOMEASSISTANT_STOP]
async def forward_events(event): async def forward_events(event):
"""Forward events to the open request.""" """Forward events to the open request."""
@ -107,7 +121,7 @@ class APIEventStream(HomeAssistantView):
await to_write.put(data) await to_write.put(data)
response = web.StreamResponse() response = web.StreamResponse()
response.content_type = 'text/event-stream' response.content_type = "text/event-stream"
await response.prepare(request) await response.prepare(request)
unsub_stream = hass.bus.async_listen(MATCH_ALL, forward_events) unsub_stream = hass.bus.async_listen(MATCH_ALL, forward_events)
@ -120,17 +134,15 @@ class APIEventStream(HomeAssistantView):
while True: while True:
try: try:
with async_timeout.timeout(STREAM_PING_INTERVAL, with async_timeout.timeout(STREAM_PING_INTERVAL, loop=hass.loop):
loop=hass.loop):
payload = await to_write.get() payload = await to_write.get()
if payload is stop_obj: if payload is stop_obj:
break break
msg = "data: {}\n\n".format(payload) msg = "data: {}\n\n".format(payload)
_LOGGER.debug( _LOGGER.debug("STREAM %s WRITING %s", id(stop_obj), msg.strip())
"STREAM %s WRITING %s", id(stop_obj), msg.strip()) await response.write(msg.encode("UTF-8"))
await response.write(msg.encode('UTF-8'))
except asyncio.TimeoutError: except asyncio.TimeoutError:
await to_write.put(STREAM_PING_PAYLOAD) await to_write.put(STREAM_PING_PAYLOAD)
@ -146,12 +158,12 @@ class APIConfigView(HomeAssistantView):
"""View to handle Configuration requests.""" """View to handle Configuration requests."""
url = URL_API_CONFIG url = URL_API_CONFIG
name = 'api:config' name = "api:config"
@ha.callback @ha.callback
def get(self, request): def get(self, request):
"""Get current configuration.""" """Get current configuration."""
return self.json(request.app['hass'].config.as_dict()) return self.json(request.app["hass"].config.as_dict())
class APIDiscoveryView(HomeAssistantView): class APIDiscoveryView(HomeAssistantView):
@ -159,19 +171,21 @@ class APIDiscoveryView(HomeAssistantView):
requires_auth = False requires_auth = False
url = URL_API_DISCOVERY_INFO url = URL_API_DISCOVERY_INFO
name = 'api:discovery' name = "api:discovery"
@ha.callback @ha.callback
def get(self, request): def get(self, request):
"""Get discovery information.""" """Get discovery information."""
hass = request.app['hass'] hass = request.app["hass"]
needs_auth = hass.config.api.api_password is not None needs_auth = hass.config.api.api_password is not None
return self.json({ return self.json(
ATTR_BASE_URL: hass.config.api.base_url, {
ATTR_LOCATION_NAME: hass.config.location_name, ATTR_BASE_URL: hass.config.api.base_url,
ATTR_REQUIRES_API_PASSWORD: needs_auth, ATTR_LOCATION_NAME: hass.config.location_name,
ATTR_VERSION: __version__, ATTR_REQUIRES_API_PASSWORD: needs_auth,
}) ATTR_VERSION: __version__,
}
)
class APIStatesView(HomeAssistantView): class APIStatesView(HomeAssistantView):
@ -183,58 +197,58 @@ class APIStatesView(HomeAssistantView):
@ha.callback @ha.callback
def get(self, request): def get(self, request):
"""Get current states.""" """Get current states."""
return self.json(request.app['hass'].states.async_all()) return self.json(request.app["hass"].states.async_all())
class APIEntityStateView(HomeAssistantView): class APIEntityStateView(HomeAssistantView):
"""View to handle EntityState requests.""" """View to handle EntityState requests."""
url = '/api/states/{entity_id}' url = "/api/states/{entity_id}"
name = 'api:entity-state' name = "api:entity-state"
@ha.callback @ha.callback
def get(self, request, entity_id): def get(self, request, entity_id):
"""Retrieve state of entity.""" """Retrieve state of entity."""
state = request.app['hass'].states.get(entity_id) state = request.app["hass"].states.get(entity_id)
if state: if state:
return self.json(state) return self.json(state)
return self.json_message("Entity not found.", HTTP_NOT_FOUND) return self.json_message("Entity not found.", HTTP_NOT_FOUND)
async def post(self, request, entity_id): async def post(self, request, entity_id):
"""Update state of entity.""" """Update state of entity."""
hass = request.app['hass'] hass = request.app["hass"]
try: try:
data = await request.json() data = await request.json()
except ValueError: except ValueError:
return self.json_message( return self.json_message("Invalid JSON specified.", HTTP_BAD_REQUEST)
"Invalid JSON specified.", HTTP_BAD_REQUEST)
new_state = data.get('state') new_state = data.get("state")
if new_state is None: if new_state is None:
return self.json_message("No state specified.", HTTP_BAD_REQUEST) return self.json_message("No state specified.", HTTP_BAD_REQUEST)
attributes = data.get('attributes') attributes = data.get("attributes")
force_update = data.get('force_update', False) force_update = data.get("force_update", False)
is_new_state = hass.states.get(entity_id) is None is_new_state = hass.states.get(entity_id) is None
# Write state # Write state
hass.states.async_set(entity_id, new_state, attributes, force_update, hass.states.async_set(
self.context(request)) entity_id, new_state, attributes, force_update, self.context(request)
)
# Read the state back for our response # Read the state back for our response
status_code = HTTP_CREATED if is_new_state else 200 status_code = HTTP_CREATED if is_new_state else 200
resp = self.json(hass.states.get(entity_id), status_code) 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 return resp
@ha.callback @ha.callback
def delete(self, request, entity_id): def delete(self, request, entity_id):
"""Remove entity.""" """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 removed.")
return self.json_message("Entity not found.", HTTP_NOT_FOUND) return self.json_message("Entity not found.", HTTP_NOT_FOUND)
@ -243,19 +257,19 @@ class APIEventListenersView(HomeAssistantView):
"""View to handle EventListeners requests.""" """View to handle EventListeners requests."""
url = URL_API_EVENTS url = URL_API_EVENTS
name = 'api:event-listeners' name = "api:event-listeners"
@ha.callback @ha.callback
def get(self, request): def get(self, request):
"""Get event listeners.""" """Get event listeners."""
return self.json(async_events_json(request.app['hass'])) return self.json(async_events_json(request.app["hass"]))
class APIEventView(HomeAssistantView): class APIEventView(HomeAssistantView):
"""View to handle Event requests.""" """View to handle Event requests."""
url = '/api/events/{event_type}' url = "/api/events/{event_type}"
name = 'api:event' name = "api:event"
async def post(self, request, event_type): async def post(self, request, event_type):
"""Fire events.""" """Fire events."""
@ -264,24 +278,26 @@ class APIEventView(HomeAssistantView):
event_data = json.loads(body) if body else None event_data = json.loads(body) if body else None
except ValueError: except ValueError:
return self.json_message( 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): if event_data is not None and not isinstance(event_data, dict):
return self.json_message( 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 # Special case handling for event STATE_CHANGED
# We will try to convert state dicts back to State objects # We will try to convert state dicts back to State objects
if event_type == ha.EVENT_STATE_CHANGED and event_data: 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)) state = ha.State.from_dict(event_data.get(key))
if state: if state:
event_data[key] = state event_data[key] = state
request.app['hass'].bus.async_fire( request.app["hass"].bus.async_fire(
event_type, event_data, ha.EventOrigin.remote, event_type, event_data, ha.EventOrigin.remote, self.context(request)
self.context(request)) )
return self.json_message("Event {} fired.".format(event_type)) return self.json_message("Event {} fired.".format(event_type))
@ -290,36 +306,36 @@ class APIServicesView(HomeAssistantView):
"""View to handle Services requests.""" """View to handle Services requests."""
url = URL_API_SERVICES url = URL_API_SERVICES
name = 'api:services' name = "api:services"
async def get(self, request): async def get(self, request):
"""Get registered services.""" """Get registered services."""
services = await async_services_json(request.app['hass']) services = await async_services_json(request.app["hass"])
return self.json(services) return self.json(services)
class APIDomainServicesView(HomeAssistantView): class APIDomainServicesView(HomeAssistantView):
"""View to handle DomainServices requests.""" """View to handle DomainServices requests."""
url = '/api/services/{domain}/{service}' url = "/api/services/{domain}/{service}"
name = 'api:domain-services' name = "api:domain-services"
async def post(self, request, domain, service): async def post(self, request, domain, service):
"""Call a service. """Call a service.
Returns a list of changed states. Returns a list of changed states.
""" """
hass = request.app['hass'] hass = request.app["hass"]
body = await request.text() body = await request.text()
try: try:
data = json.loads(body) if body else None data = json.loads(body) if body else None
except ValueError: except ValueError:
return self.json_message( return self.json_message("Data should be valid JSON.", HTTP_BAD_REQUEST)
"Data should be valid JSON.", HTTP_BAD_REQUEST)
with AsyncTrackStates(hass) as changed_states: with AsyncTrackStates(hass) as changed_states:
await hass.services.async_call( await hass.services.async_call(
domain, service, data, True, self.context(request)) domain, service, data, True, self.context(request)
)
return self.json(changed_states) return self.json(changed_states)
@ -328,50 +344,52 @@ class APIComponentsView(HomeAssistantView):
"""View to handle Components requests.""" """View to handle Components requests."""
url = URL_API_COMPONENTS url = URL_API_COMPONENTS
name = 'api:components' name = "api:components"
@ha.callback @ha.callback
def get(self, request): def get(self, request):
"""Get current loaded components.""" """Get current loaded components."""
return self.json(request.app['hass'].config.components) return self.json(request.app["hass"].config.components)
class APITemplateView(HomeAssistantView): class APITemplateView(HomeAssistantView):
"""View to handle Template requests.""" """View to handle Template requests."""
url = URL_API_TEMPLATE url = URL_API_TEMPLATE
name = 'api:template' name = "api:template"
async def post(self, request): async def post(self, request):
"""Render a template.""" """Render a template."""
try: try:
data = await request.json() data = await request.json()
tpl = template.Template(data['template'], request.app['hass']) tpl = template.Template(data["template"], request.app["hass"])
return tpl.async_render(data.get('variables')) return tpl.async_render(data.get("variables"))
except (ValueError, TemplateError) as ex: except (ValueError, TemplateError) as ex:
return self.json_message( return self.json_message(
"Error rendering template: {}".format(ex), HTTP_BAD_REQUEST) "Error rendering template: {}".format(ex), HTTP_BAD_REQUEST
)
class APIErrorLog(HomeAssistantView): class APIErrorLog(HomeAssistantView):
"""View to fetch the API error log.""" """View to fetch the API error log."""
url = URL_API_ERROR_LOG url = URL_API_ERROR_LOG
name = 'api:error_log' name = "api:error_log"
async def get(self, request): async def get(self, request):
"""Retrieve API error log.""" """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): async def async_services_json(hass):
"""Generate services data to JSONify.""" """Generate services data to JSONify."""
descriptions = await async_get_all_descriptions(hass) descriptions = await async_get_all_descriptions(hass)
return [{'domain': key, 'services': value} return [{"domain": key, "services": value} for key, value in descriptions.items()]
for key, value in descriptions.items()]
def async_events_json(hass): def async_events_json(hass):
"""Generate event data to JSONify.""" """Generate event data to JSONify."""
return [{'event': key, 'listener_count': value} return [
for key, value in hass.bus.async_listeners().items()] {"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 from homeassistant.helpers.aiohttp_client import async_get_clientsession
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['pyatv==0.3.10'] REQUIREMENTS = ["pyatv==0.3.10"]
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
DOMAIN = 'apple_tv' DOMAIN = "apple_tv"
SERVICE_SCAN = 'apple_tv_scan' SERVICE_SCAN = "apple_tv_scan"
SERVICE_AUTHENTICATE = 'apple_tv_authenticate' SERVICE_AUTHENTICATE = "apple_tv_authenticate"
ATTR_ATV = 'atv' ATTR_ATV = "atv"
ATTR_POWER = 'power' ATTR_POWER = "power"
CONF_LOGIN_ID = 'login_id' CONF_LOGIN_ID = "login_id"
CONF_START_OFF = 'start_off' CONF_START_OFF = "start_off"
CONF_CREDENTIALS = 'credentials' CONF_CREDENTIALS = "credentials"
DEFAULT_NAME = 'Apple TV' DEFAULT_NAME = "Apple TV"
DATA_APPLE_TV = 'data_apple_tv' DATA_APPLE_TV = "data_apple_tv"
DATA_ENTITIES = 'data_apple_tv_entities' 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_ID = "apple_tv_auth_notification"
NOTIFICATION_AUTH_TITLE = 'Apple TV Authentication' NOTIFICATION_AUTH_TITLE = "Apple TV Authentication"
NOTIFICATION_SCAN_ID = 'apple_tv_scan_notification' NOTIFICATION_SCAN_ID = "apple_tv_scan_notification"
NOTIFICATION_SCAN_TITLE = 'Apple TV Scan' 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 # 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] return value if isinstance(value, list) else [value]
CONFIG_SCHEMA = vol.Schema({ CONFIG_SCHEMA = vol.Schema(
DOMAIN: vol.All(ensure_list, [vol.Schema({ {
vol.Required(CONF_HOST): cv.string, DOMAIN: vol.All(
vol.Required(CONF_LOGIN_ID): cv.string, ensure_list,
vol.Optional(CONF_CREDENTIALS): cv.string, [
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Schema(
vol.Optional(CONF_START_OFF, default=False): cv.boolean, {
})]) vol.Required(CONF_HOST): cv.string,
}, extra=vol.ALLOW_EXTRA) 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 # Currently no attributes but it might change later
APPLE_TV_SCAN_SCHEMA = vol.Schema({}) APPLE_TV_SCAN_SCHEMA = vol.Schema({})
APPLE_TV_AUTHENTICATE_SCHEMA = vol.Schema({ APPLE_TV_AUTHENTICATE_SCHEMA = vol.Schema({ATTR_ENTITY_ID: cv.entity_ids})
ATTR_ENTITY_ID: cv.entity_ids,
})
def request_configuration(hass, config, atv, credentials): def request_configuration(hass, config, atv, credentials):
@ -81,30 +89,34 @@ def request_configuration(hass, config, atv, credentials):
def configuration_callback(callback_data): def configuration_callback(callback_data):
"""Handle the submitted configuration.""" """Handle the submitted configuration."""
from pyatv import exceptions from pyatv import exceptions
pin = callback_data.get('pin')
pin = callback_data.get("pin")
try: try:
yield from atv.airplay.finish_authentication(pin) yield from atv.airplay.finish_authentication(pin)
hass.components.persistent_notification.async_create( hass.components.persistent_notification.async_create(
'Authentication succeeded!<br /><br />Add the following ' "Authentication succeeded!<br /><br />Add the following "
'to credentials: in your apple_tv configuration:<br /><br />' "to credentials: in your apple_tv configuration:<br /><br />"
'{0}'.format(credentials), "{0}".format(credentials),
title=NOTIFICATION_AUTH_TITLE, title=NOTIFICATION_AUTH_TITLE,
notification_id=NOTIFICATION_AUTH_ID) notification_id=NOTIFICATION_AUTH_ID,
)
except exceptions.DeviceAuthenticationError as ex: except exceptions.DeviceAuthenticationError as ex:
hass.components.persistent_notification.async_create( hass.components.persistent_notification.async_create(
'Authentication failed! Did you enter correct PIN?<br /><br />' "Authentication failed! Did you enter correct PIN?<br /><br />"
'Details: {0}'.format(ex), "Details: {0}".format(ex),
title=NOTIFICATION_AUTH_TITLE, title=NOTIFICATION_AUTH_TITLE,
notification_id=NOTIFICATION_AUTH_ID) notification_id=NOTIFICATION_AUTH_ID,
)
hass.async_add_job(configurator.request_done, instance) hass.async_add_job(configurator.request_done, instance)
instance = configurator.request_config( instance = configurator.request_config(
'Apple TV Authentication', configuration_callback, "Apple TV Authentication",
description='Please enter PIN code shown on screen.', configuration_callback,
submit_caption='Confirm', description="Please enter PIN code shown on screen.",
fields=[{'id': 'pin', 'name': 'PIN Code', 'type': 'password'}] 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): def scan_for_apple_tvs(hass):
"""Scan for devices and present a notification of the ones found.""" """Scan for devices and present a notification of the ones found."""
import pyatv import pyatv
atvs = yield from pyatv.scan_for_apple_tvs(hass.loop, timeout=3) atvs = yield from pyatv.scan_for_apple_tvs(hass.loop, timeout=3)
devices = [] devices = []
for atv in atvs: for atv in atvs:
login_id = atv.login_id login_id = atv.login_id
if login_id is None: if login_id is None:
login_id = 'Home Sharing disabled' login_id = "Home Sharing disabled"
devices.append('Name: {0}<br />Host: {1}<br />Login ID: {2}'.format( devices.append(
atv.name, atv.address, login_id)) "Name: {0}<br />Host: {1}<br />Login ID: {2}".format(
atv.name, atv.address, login_id
)
)
if not devices: if not devices:
devices = ['No device(s) found'] devices = ["No device(s) found"]
hass.components.persistent_notification.async_create( hass.components.persistent_notification.async_create(
'The following devices were found:<br /><br />' + "The following devices were found:<br /><br />" + "<br /><br />".join(devices),
'<br /><br />'.join(devices),
title=NOTIFICATION_SCAN_TITLE, title=NOTIFICATION_SCAN_TITLE,
notification_id=NOTIFICATION_SCAN_ID) notification_id=NOTIFICATION_SCAN_ID,
)
@asyncio.coroutine @asyncio.coroutine
@ -148,8 +164,11 @@ def async_setup(hass, config):
return return
if entity_ids: if entity_ids:
devices = [device for device in hass.data[DATA_ENTITIES] devices = [
if device.entity_id in entity_ids] device
for device in hass.data[DATA_ENTITIES]
if device.entity_id in entity_ids
]
else: else:
devices = hass.data[DATA_ENTITIES] devices = hass.data[DATA_ENTITIES]
@ -160,20 +179,22 @@ def async_setup(hass, config):
atv = device.atv atv = device.atv
credentials = yield from atv.airplay.generate_credentials() credentials = yield from atv.airplay.generate_credentials()
yield from atv.airplay.load_credentials(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() yield from atv.airplay.start_authentication()
hass.async_add_job(request_configuration, hass.async_add_job(request_configuration, hass, config, atv, credentials)
hass, config, atv, credentials)
@asyncio.coroutine @asyncio.coroutine
def atv_discovered(service, info): def atv_discovered(service, info):
"""Set up an Apple TV that was auto discovered.""" """Set up an Apple TV that was auto discovered."""
yield from _setup_atv(hass, { yield from _setup_atv(
CONF_NAME: info['name'], hass,
CONF_HOST: info['host'], {
CONF_LOGIN_ID: info['properties']['hG'], CONF_NAME: info["name"],
CONF_START_OFF: False CONF_HOST: info["host"],
}) CONF_LOGIN_ID: info["properties"]["hG"],
CONF_START_OFF: False,
},
)
discovery.async_listen(hass, SERVICE_APPLE_TV, atv_discovered) 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) yield from asyncio.wait(tasks, loop=hass.loop)
hass.services.async_register( hass.services.async_register(
DOMAIN, SERVICE_SCAN, async_service_handler, DOMAIN, SERVICE_SCAN, async_service_handler, schema=APPLE_TV_SCAN_SCHEMA
schema=APPLE_TV_SCAN_SCHEMA) )
hass.services.async_register( hass.services.async_register(
DOMAIN, SERVICE_AUTHENTICATE, async_service_handler, DOMAIN,
schema=APPLE_TV_AUTHENTICATE_SCHEMA) SERVICE_AUTHENTICATE,
async_service_handler,
schema=APPLE_TV_AUTHENTICATE_SCHEMA,
)
return True return True
@ -196,6 +220,7 @@ def async_setup(hass, config):
def _setup_atv(hass, atv_config): def _setup_atv(hass, atv_config):
"""Set up an Apple TV.""" """Set up an Apple TV."""
import pyatv import pyatv
name = atv_config.get(CONF_NAME) name = atv_config.get(CONF_NAME)
host = atv_config.get(CONF_HOST) host = atv_config.get(CONF_HOST)
login_id = atv_config.get(CONF_LOGIN_ID) 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) yield from atv.airplay.load_credentials(credentials)
power = AppleTVPowerManager(hass, atv, start_off) power = AppleTVPowerManager(hass, atv, start_off)
hass.data[DATA_APPLE_TV][host] = { hass.data[DATA_APPLE_TV][host] = {ATTR_ATV: atv, ATTR_POWER: power}
ATTR_ATV: atv,
ATTR_POWER: power
}
hass.async_create_task(discovery.async_load_platform( hass.async_create_task(
hass, 'media_player', DOMAIN, atv_config)) discovery.async_load_platform(hass, "media_player", DOMAIN, atv_config)
)
hass.async_create_task(discovery.async_load_platform( hass.async_create_task(
hass, 'remote', DOMAIN, atv_config)) discovery.async_load_platform(hass, "remote", DOMAIN, atv_config)
)
class AppleTVPowerManager: class AppleTVPowerManager:

View file

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

View file

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

View file

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

View file

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

View file

@ -126,8 +126,11 @@ from datetime import timedelta
from aiohttp import web from aiohttp import web
import voluptuous as vol import voluptuous as vol
from homeassistant.auth.models import User, Credentials, \ from homeassistant.auth.models import (
TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN User,
Credentials,
TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN,
)
from homeassistant.components import websocket_api from homeassistant.components import websocket_api
from homeassistant.components.http import KEY_REAL_IP from homeassistant.components.http import KEY_REAL_IP
from homeassistant.components.http.ban import log_invalid_auth from homeassistant.components.http.ban import log_invalid_auth
@ -140,38 +143,39 @@ from . import indieauth
from . import login_flow from . import login_flow
from . import mfa_setup_flow from . import mfa_setup_flow
DOMAIN = 'auth' DOMAIN = "auth"
DEPENDENCIES = ['http'] DEPENDENCIES = ["http"]
WS_TYPE_CURRENT_USER = 'auth/current_user' WS_TYPE_CURRENT_USER = "auth/current_user"
SCHEMA_WS_CURRENT_USER = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({ SCHEMA_WS_CURRENT_USER = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend(
vol.Required('type'): WS_TYPE_CURRENT_USER, {vol.Required("type"): WS_TYPE_CURRENT_USER}
}) )
WS_TYPE_LONG_LIVED_ACCESS_TOKEN = 'auth/long_lived_access_token' WS_TYPE_LONG_LIVED_ACCESS_TOKEN = "auth/long_lived_access_token"
SCHEMA_WS_LONG_LIVED_ACCESS_TOKEN = \ SCHEMA_WS_LONG_LIVED_ACCESS_TOKEN = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend(
websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({ {
vol.Required('type'): WS_TYPE_LONG_LIVED_ACCESS_TOKEN, vol.Required("type"): WS_TYPE_LONG_LIVED_ACCESS_TOKEN,
vol.Required('lifespan'): int, # days vol.Required("lifespan"): int, # days
vol.Required('client_name'): str, vol.Required("client_name"): str,
vol.Optional('client_icon'): str, vol.Optional("client_icon"): str,
}) }
)
WS_TYPE_REFRESH_TOKENS = 'auth/refresh_tokens' WS_TYPE_REFRESH_TOKENS = "auth/refresh_tokens"
SCHEMA_WS_REFRESH_TOKENS = \ SCHEMA_WS_REFRESH_TOKENS = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend(
websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({ {vol.Required("type"): WS_TYPE_REFRESH_TOKENS}
vol.Required('type'): WS_TYPE_REFRESH_TOKENS, )
})
WS_TYPE_DELETE_REFRESH_TOKEN = 'auth/delete_refresh_token' WS_TYPE_DELETE_REFRESH_TOKEN = "auth/delete_refresh_token"
SCHEMA_WS_DELETE_REFRESH_TOKEN = \ SCHEMA_WS_DELETE_REFRESH_TOKEN = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend(
websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({ {
vol.Required('type'): WS_TYPE_DELETE_REFRESH_TOKEN, vol.Required("type"): WS_TYPE_DELETE_REFRESH_TOKEN,
vol.Required('refresh_token_id'): str, vol.Required("refresh_token_id"): str,
}) }
)
RESULT_TYPE_CREDENTIALS = 'credentials' RESULT_TYPE_CREDENTIALS = "credentials"
RESULT_TYPE_USER = 'user' RESULT_TYPE_USER = "user"
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -184,23 +188,20 @@ async def async_setup(hass, config):
hass.http.register_view(LinkUserView(retrieve_result)) hass.http.register_view(LinkUserView(retrieve_result))
hass.components.websocket_api.async_register_command( hass.components.websocket_api.async_register_command(
WS_TYPE_CURRENT_USER, websocket_current_user, WS_TYPE_CURRENT_USER, websocket_current_user, SCHEMA_WS_CURRENT_USER
SCHEMA_WS_CURRENT_USER
) )
hass.components.websocket_api.async_register_command( hass.components.websocket_api.async_register_command(
WS_TYPE_LONG_LIVED_ACCESS_TOKEN, WS_TYPE_LONG_LIVED_ACCESS_TOKEN,
websocket_create_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( hass.components.websocket_api.async_register_command(
WS_TYPE_REFRESH_TOKENS, WS_TYPE_REFRESH_TOKENS, websocket_refresh_tokens, SCHEMA_WS_REFRESH_TOKENS
websocket_refresh_tokens,
SCHEMA_WS_REFRESH_TOKENS
) )
hass.components.websocket_api.async_register_command( hass.components.websocket_api.async_register_command(
WS_TYPE_DELETE_REFRESH_TOKEN, WS_TYPE_DELETE_REFRESH_TOKEN,
websocket_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) await login_flow.async_setup(hass, store_result)
@ -212,8 +213,8 @@ async def async_setup(hass, config):
class TokenView(HomeAssistantView): class TokenView(HomeAssistantView):
"""View to issue or revoke tokens.""" """View to issue or revoke tokens."""
url = '/auth/token' url = "/auth/token"
name = 'api:auth:token' name = "api:auth:token"
requires_auth = False requires_auth = False
cors_allowed = True cors_allowed = True
@ -224,29 +225,29 @@ class TokenView(HomeAssistantView):
@log_invalid_auth @log_invalid_auth
async def post(self, request): async def post(self, request):
"""Grant a token.""" """Grant a token."""
hass = request.app['hass'] hass = request.app["hass"]
data = await request.post() data = await request.post()
grant_type = data.get('grant_type') grant_type = data.get("grant_type")
# IndieAuth 6.3.5 # IndieAuth 6.3.5
# The revocation endpoint is the same as the token endpoint. # The revocation endpoint is the same as the token endpoint.
# The revocation request includes an additional parameter, # The revocation request includes an additional parameter,
# action=revoke. # action=revoke.
if data.get('action') == 'revoke': if data.get("action") == "revoke":
return await self._async_handle_revoke_token(hass, data) 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( 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( return await self._async_handle_refresh_token(
hass, data, str(request[KEY_REAL_IP])) hass, data, str(request[KEY_REAL_IP])
)
return self.json({ return self.json({"error": "unsupported_grant_type"}, status_code=400)
'error': 'unsupported_grant_type',
}, status_code=400)
async def _async_handle_revoke_token(self, hass, data): async def _async_handle_revoke_token(self, hass, data):
"""Handle revoke token request.""" """Handle revoke token request."""
@ -254,7 +255,7 @@ class TokenView(HomeAssistantView):
# 2.2 The authorization server responds with HTTP status code 200 # 2.2 The authorization server responds with HTTP status code 200
# if the token has been revoked successfully or if the client # if the token has been revoked successfully or if the client
# submitted an invalid token. # submitted an invalid token.
token = data.get('token') token = data.get("token")
if token is None: if token is None:
return web.Response(status=200) return web.Response(status=200)
@ -269,117 +270,112 @@ class TokenView(HomeAssistantView):
async def _async_handle_auth_code(self, hass, data, remote_addr): async def _async_handle_auth_code(self, hass, data, remote_addr):
"""Handle authorization code request.""" """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): if client_id is None or not indieauth.verify_client_id(client_id):
return self.json({ return self.json(
'error': 'invalid_request', {"error": "invalid_request", "error_description": "Invalid client id"},
'error_description': 'Invalid client id', status_code=400,
}, status_code=400) )
code = data.get('code') code = data.get("code")
if code is None: if code is None:
return self.json({ return self.json(
'error': 'invalid_request', {"error": "invalid_request", "error_description": "Invalid code"},
'error_description': 'Invalid code', status_code=400,
}, status_code=400) )
user = self._retrieve_user(client_id, RESULT_TYPE_USER, code) user = self._retrieve_user(client_id, RESULT_TYPE_USER, code)
if user is None or not isinstance(user, User): if user is None or not isinstance(user, User):
return self.json({ return self.json(
'error': 'invalid_request', {"error": "invalid_request", "error_description": "Invalid code"},
'error_description': 'Invalid code', status_code=400,
}, status_code=400) )
# refresh user # refresh user
user = await hass.auth.async_get_user(user.id) user = await hass.auth.async_get_user(user.id)
if not user.is_active: if not user.is_active:
return self.json({ return self.json(
'error': 'access_denied', {"error": "access_denied", "error_description": "User is not active"},
'error_description': 'User is not active', status_code=403,
}, status_code=403) )
refresh_token = await hass.auth.async_create_refresh_token(user, refresh_token = await hass.auth.async_create_refresh_token(user, client_id)
client_id) 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({ return self.json(
'access_token': access_token, {
'token_type': 'Bearer', "access_token": access_token,
'refresh_token': refresh_token.token, "token_type": "Bearer",
'expires_in': "refresh_token": refresh_token.token,
int(refresh_token.access_token_expiration.total_seconds()), "expires_in": int(
}) refresh_token.access_token_expiration.total_seconds()
),
}
)
async def _async_handle_refresh_token(self, hass, data, remote_addr): async def _async_handle_refresh_token(self, hass, data, remote_addr):
"""Handle authorization code request.""" """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): if client_id is not None and not indieauth.verify_client_id(client_id):
return self.json({ return self.json(
'error': 'invalid_request', {"error": "invalid_request", "error_description": "Invalid client id"},
'error_description': 'Invalid client id', status_code=400,
}, status_code=400) )
token = data.get('refresh_token') token = data.get("refresh_token")
if token is None: if token is None:
return self.json({ return self.json({"error": "invalid_request"}, status_code=400)
'error': 'invalid_request',
}, status_code=400)
refresh_token = await hass.auth.async_get_refresh_token_by_token(token) refresh_token = await hass.auth.async_get_refresh_token_by_token(token)
if refresh_token is None: if refresh_token is None:
return self.json({ return self.json({"error": "invalid_grant"}, status_code=400)
'error': 'invalid_grant',
}, status_code=400)
if refresh_token.client_id != client_id: if refresh_token.client_id != client_id:
return self.json({ return self.json({"error": "invalid_request"}, status_code=400)
'error': 'invalid_request',
}, status_code=400)
access_token = hass.auth.async_create_access_token( access_token = hass.auth.async_create_access_token(refresh_token, remote_addr)
refresh_token, remote_addr)
return self.json({ return self.json(
'access_token': access_token, {
'token_type': 'Bearer', "access_token": access_token,
'expires_in': "token_type": "Bearer",
int(refresh_token.access_token_expiration.total_seconds()), "expires_in": int(
}) refresh_token.access_token_expiration.total_seconds()
),
}
)
class LinkUserView(HomeAssistantView): class LinkUserView(HomeAssistantView):
"""View to link existing users to new credentials.""" """View to link existing users to new credentials."""
url = '/auth/link_user' url = "/auth/link_user"
name = 'api:auth:link_user' name = "api:auth:link_user"
def __init__(self, retrieve_credentials): def __init__(self, retrieve_credentials):
"""Initialize the link user view.""" """Initialize the link user view."""
self._retrieve_credentials = retrieve_credentials self._retrieve_credentials = retrieve_credentials
@RequestDataValidator(vol.Schema({ @RequestDataValidator(vol.Schema({"code": str, "client_id": str}))
'code': str,
'client_id': str,
}))
async def post(self, request, data): async def post(self, request, data):
"""Link a user.""" """Link a user."""
hass = request.app['hass'] hass = request.app["hass"]
user = request['hass_user'] user = request["hass_user"]
credentials = self._retrieve_credentials( credentials = self._retrieve_credentials(
data['client_id'], RESULT_TYPE_CREDENTIALS, data['code']) data["client_id"], RESULT_TYPE_CREDENTIALS, data["code"]
)
if credentials is None: 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) await hass.auth.async_link_user(user, credentials)
return self.json_message('User linked') return self.json_message("User linked")
@callback @callback
@ -395,11 +391,14 @@ def _create_auth_code_store():
elif isinstance(result, Credentials): elif isinstance(result, Credentials):
result_type = RESULT_TYPE_CREDENTIALS result_type = RESULT_TYPE_CREDENTIALS
else: 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 code = uuid.uuid4().hex
temp_results[(client_id, result_type, code)] = \ temp_results[(client_id, result_type, code)] = (
(dt_util.utcnow(), result_type, result) dt_util.utcnow(),
result_type,
result,
)
return code return code
@callback @callback
@ -427,26 +426,39 @@ def _create_auth_code_store():
@websocket_api.ws_require_user() @websocket_api.ws_require_user()
@callback @callback
def websocket_current_user( def websocket_current_user(
hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg): hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg
):
"""Return the current user.""" """Return the current user."""
async def async_get_current_user(user): async def async_get_current_user(user):
"""Get current user.""" """Get current user."""
enabled_modules = await hass.auth.async_get_enabled_mfa(user) enabled_modules = await hass.auth.async_get_enabled_mfa(user)
connection.send_message_outside( connection.send_message_outside(
websocket_api.result_message(msg['id'], { websocket_api.result_message(
'id': user.id, msg["id"],
'name': user.name, {
'is_owner': user.is_owner, "id": user.id,
'credentials': [{'auth_provider_type': c.auth_provider_type, "name": user.name,
'auth_provider_id': c.auth_provider_id} "is_owner": user.is_owner,
for c in user.credentials], "credentials": [
'mfa_modules': [{ {
'id': module.id, "auth_provider_type": c.auth_provider_type,
'name': module.name, "auth_provider_id": c.auth_provider_id,
'enabled': module.id in enabled_modules, }
} for module in hass.auth.auth_mfa_modules], 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)) hass.async_create_task(async_get_current_user(connection.user))
@ -454,63 +466,77 @@ def websocket_current_user(
@websocket_api.ws_require_user() @websocket_api.ws_require_user()
@callback @callback
def websocket_create_long_lived_access_token( 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.""" """Create or a long-lived access token."""
async def async_create_long_lived_access_token(user): async def async_create_long_lived_access_token(user):
"""Create or a long-lived access token.""" """Create or a long-lived access token."""
refresh_token = await hass.auth.async_create_refresh_token( refresh_token = await hass.auth.async_create_refresh_token(
user, user,
client_name=msg['client_name'], client_name=msg["client_name"],
client_icon=msg.get('client_icon'), client_icon=msg.get("client_icon"),
token_type=TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN, 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( access_token = hass.auth.async_create_access_token(refresh_token)
refresh_token)
connection.send_message_outside( connection.send_message_outside(
websocket_api.result_message(msg['id'], access_token)) websocket_api.result_message(msg["id"], access_token)
)
hass.async_create_task( hass.async_create_task(async_create_long_lived_access_token(connection.user))
async_create_long_lived_access_token(connection.user))
@websocket_api.ws_require_user() @websocket_api.ws_require_user()
@callback @callback
def websocket_refresh_tokens( def websocket_refresh_tokens(
hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg): hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg
):
"""Return metadata of users refresh tokens.""" """Return metadata of users refresh tokens."""
current_id = connection.request.get('refresh_token_id') current_id = connection.request.get("refresh_token_id")
connection.to_write.put_nowait(websocket_api.result_message(msg['id'], [{ connection.to_write.put_nowait(
'id': refresh.id, websocket_api.result_message(
'client_id': refresh.client_id, msg["id"],
'client_name': refresh.client_name, [
'client_icon': refresh.client_icon, {
'type': refresh.token_type, "id": refresh.id,
'created_at': refresh.created_at, "client_id": refresh.client_id,
'is_current': refresh.id == current_id, "client_name": refresh.client_name,
'last_used_at': refresh.last_used_at, "client_icon": refresh.client_icon,
'last_used_ip': refresh.last_used_ip, "type": refresh.token_type,
} for refresh in connection.user.refresh_tokens.values()])) "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() @websocket_api.ws_require_user()
@callback @callback
def websocket_delete_refresh_token( 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.""" """Handle a delete refresh token request."""
async def async_delete_refresh_token(user, refresh_token_id): async def async_delete_refresh_token(user, refresh_token_id):
"""Delete a refresh token.""" """Delete a refresh token."""
refresh_token = connection.user.refresh_tokens.get(refresh_token_id) refresh_token = connection.user.refresh_tokens.get(refresh_token_id)
if refresh_token is None: if refresh_token is None:
return websocket_api.error_message( 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) await hass.auth.async_remove_refresh_token(refresh_token)
connection.send_message_outside( connection.send_message_outside(websocket_api.result_message(msg["id"], {}))
websocket_api.result_message(msg['id'], {}))
hass.async_create_task( 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 from aiohttp.client_exceptions import ClientError
# IP addresses of loopback interfaces # IP addresses of loopback interfaces
ALLOWED_IPS = ( ALLOWED_IPS = (ip_address("127.0.0.1"), ip_address("::1"))
ip_address('127.0.0.1'),
ip_address('::1'),
)
# RFC1918 - Address allocation for Private Internets # RFC1918 - Address allocation for Private Internets
ALLOWED_NETWORKS = ( ALLOWED_NETWORKS = (
ip_network('10.0.0.0/8'), ip_network("10.0.0.0/8"),
ip_network('172.16.0.0/12'), ip_network("172.16.0.0/12"),
ip_network('192.168.0.0/16'), 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. # Verify redirect url and client url have same scheme and domain.
is_valid = ( is_valid = (
client_id_parts.scheme == redirect_parts.scheme and client_id_parts.scheme == redirect_parts.scheme
client_id_parts.netloc == redirect_parts.netloc and client_id_parts.netloc == redirect_parts.netloc
) )
if is_valid: if is_valid:
@ -56,13 +53,13 @@ class LinkTagParser(HTMLParser):
def handle_starttag(self, tag, attrs): def handle_starttag(self, tag, attrs):
"""Handle finding a start tag.""" """Handle finding a start tag."""
if tag != 'link': if tag != "link":
return return
attrs = dict(attrs) attrs = dict(attrs)
if attrs.get('rel') == self.rel: if attrs.get("rel") == self.rel:
self.found.append(attrs.get('href')) self.found.append(attrs.get("href"))
async def fetch_redirect_uris(hass, url): 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. We do not implement extracting redirect uris from headers.
""" """
parser = LinkTagParser('redirect_uri') parser = LinkTagParser("redirect_uri")
chunks = 0 chunks = 0
try: try:
async with aiohttp.ClientSession() as session: async with aiohttp.ClientSession() as session:
@ -119,8 +116,8 @@ def _parse_url(url):
# If a URL with no path component is ever encountered, # If a URL with no path component is ever encountered,
# it MUST be treated as if it had the path /. # it MUST be treated as if it had the path /.
if parts.path == '': if parts.path == "":
parts = parts._replace(path='/') parts = parts._replace(path="/")
return parts return parts
@ -134,34 +131,35 @@ def _parse_client_id(client_id):
# Client identifier URLs # Client identifier URLs
# MUST have either an https or http scheme # MUST have either an https or http scheme
if parts.scheme not in ('http', 'https'): if parts.scheme not in ("http", "https"):
raise ValueError() raise ValueError()
# MUST contain a path component # MUST contain a path component
# Handled by url canonicalization. # Handled by url canonicalization.
# MUST NOT contain single-dot or double-dot path segments # 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( 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 # MUST NOT contain a fragment component
if parts.fragment != '': if parts.fragment != "":
raise ValueError('Client ID cannot contain a fragment') raise ValueError("Client ID cannot contain a fragment")
# MUST NOT contain a username or password component # MUST NOT contain a username or password component
if parts.username is not None: 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: if parts.password is not None:
raise ValueError('Client ID cannot contain password') raise ValueError("Client ID cannot contain password")
# MAY contain a port # MAY contain a port
try: try:
# parts raises ValueError when port cannot be parsed as int # parts raises ValueError when port cannot be parsed as int
parts.port parts.port
except ValueError: except ValueError:
raise ValueError('Client ID contains invalid port') raise ValueError("Client ID contains invalid port")
# Additionally, hostnames # Additionally, hostnames
# MUST be domain names or a loopback interface and # MUST be domain names or a loopback interface and
@ -177,7 +175,7 @@ def _parse_client_id(client_id):
netloc = parts.netloc netloc = parts.netloc
# Strip the [, ] from ipv6 addresses before parsing # Strip the [, ] from ipv6 addresses before parsing
if netloc[0] == '[' and netloc[-1] == ']': if netloc[0] == "[" and netloc[-1] == "]":
netloc = netloc[1:-1] netloc = netloc[1:-1]
address = ip_address(netloc) address = ip_address(netloc)
@ -185,9 +183,11 @@ def _parse_client_id(client_id):
# Not an ip address # Not an ip address
pass pass
if (address is None or if (
address in ALLOWED_IPS or address is None
any(address in network for network in ALLOWED_NETWORKS)): or address in ALLOWED_IPS
or any(address in network for network in ALLOWED_NETWORKS)
):
return parts 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 import data_entry_flow
from homeassistant.components.http import KEY_REAL_IP from homeassistant.components.http import KEY_REAL_IP
from homeassistant.components.http.ban import process_wrong_login, \ from homeassistant.components.http.ban import process_wrong_login, log_invalid_auth
log_invalid_auth
from homeassistant.components.http.data_validator import RequestDataValidator from homeassistant.components.http.data_validator import RequestDataValidator
from homeassistant.components.http.view import HomeAssistantView from homeassistant.components.http.view import HomeAssistantView
from . import indieauth from . import indieauth
@ -82,55 +81,55 @@ async def async_setup(hass, store_result):
"""Component to allow users to login.""" """Component to allow users to login."""
hass.http.register_view(AuthProvidersView) hass.http.register_view(AuthProvidersView)
hass.http.register_view(LoginFlowIndexView(hass.auth.login_flow)) hass.http.register_view(LoginFlowIndexView(hass.auth.login_flow))
hass.http.register_view( hass.http.register_view(LoginFlowResourceView(hass.auth.login_flow, store_result))
LoginFlowResourceView(hass.auth.login_flow, store_result))
class AuthProvidersView(HomeAssistantView): class AuthProvidersView(HomeAssistantView):
"""View to get available auth providers.""" """View to get available auth providers."""
url = '/auth/providers' url = "/auth/providers"
name = 'api:auth:providers' name = "api:auth:providers"
requires_auth = False requires_auth = False
async def get(self, request): async def get(self, request):
"""Get available auth providers.""" """Get available auth providers."""
hass = request.app['hass'] hass = request.app["hass"]
if not hass.components.onboarding.async_is_onboarded(): if not hass.components.onboarding.async_is_onboarded():
return self.json_message( return self.json_message(
message='Onboarding not finished', message="Onboarding not finished",
status_code=400, status_code=400,
message_code='onboarding_required' message_code="onboarding_required",
) )
return self.json([{ return self.json(
'name': provider.name, [
'id': provider.id, {"name": provider.name, "id": provider.id, "type": provider.type}
'type': provider.type, for provider in hass.auth.auth_providers
} for provider in hass.auth.auth_providers]) ]
)
def _prepare_result_json(result): def _prepare_result_json(result):
"""Convert result to JSON.""" """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 = result.copy()
data.pop('result') data.pop("result")
data.pop('data') data.pop("data")
return data return data
if result['type'] != data_entry_flow.RESULT_TYPE_FORM: if result["type"] != data_entry_flow.RESULT_TYPE_FORM:
return result return result
import voluptuous_serialize import voluptuous_serialize
data = result.copy() data = result.copy()
schema = data['data_schema'] schema = data["data_schema"]
if schema is None: if schema is None:
data['data_schema'] = [] data["data_schema"] = []
else: else:
data['data_schema'] = voluptuous_serialize.convert(schema) data["data_schema"] = voluptuous_serialize.convert(schema)
return data return data
@ -138,8 +137,8 @@ def _prepare_result_json(result):
class LoginFlowIndexView(HomeAssistantView): class LoginFlowIndexView(HomeAssistantView):
"""View to create a config flow.""" """View to create a config flow."""
url = '/auth/login_flow' url = "/auth/login_flow"
name = 'api:auth:login_flow' name = "api:auth:login_flow"
requires_auth = False requires_auth = False
def __init__(self, flow_mgr): def __init__(self, flow_mgr):
@ -150,34 +149,41 @@ class LoginFlowIndexView(HomeAssistantView):
"""Do not allow index of flows in progress.""" """Do not allow index of flows in progress."""
return web.Response(status=405) return web.Response(status=405)
@RequestDataValidator(vol.Schema({ @RequestDataValidator(
vol.Required('client_id'): str, vol.Schema(
vol.Required('handler'): vol.Any(str, list), {
vol.Required('redirect_uri'): str, vol.Required("client_id"): str,
vol.Optional('type', default='authorize'): str, vol.Required("handler"): vol.Any(str, list),
})) vol.Required("redirect_uri"): str,
vol.Optional("type", default="authorize"): str,
}
)
)
@log_invalid_auth @log_invalid_auth
async def post(self, request, data): async def post(self, request, data):
"""Create a new login flow.""" """Create a new login flow."""
if not await indieauth.verify_redirect_uri( if not await indieauth.verify_redirect_uri(
request.app['hass'], data['client_id'], data['redirect_uri']): request.app["hass"], data["client_id"], data["redirect_uri"]
return self.json_message('invalid client id or redirect uri', 400) ):
return self.json_message("invalid client id or redirect uri", 400)
if isinstance(data['handler'], list): if isinstance(data["handler"], list):
handler = tuple(data['handler']) handler = tuple(data["handler"])
else: else:
handler = data['handler'] handler = data["handler"]
try: try:
result = await self._flow_mgr.async_init( result = await self._flow_mgr.async_init(
handler, context={ handler,
'ip_address': request[KEY_REAL_IP], context={
'credential_only': data.get('type') == 'link_user', "ip_address": request[KEY_REAL_IP],
}) "credential_only": data.get("type") == "link_user",
},
)
except data_entry_flow.UnknownHandler: 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: 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)) return self.json(_prepare_result_json(result))
@ -185,8 +191,8 @@ class LoginFlowIndexView(HomeAssistantView):
class LoginFlowResourceView(HomeAssistantView): class LoginFlowResourceView(HomeAssistantView):
"""View to interact with the flow manager.""" """View to interact with the flow manager."""
url = '/auth/login_flow/{flow_id}' url = "/auth/login_flow/{flow_id}"
name = 'api:auth:login_flow:resource' name = "api:auth:login_flow:resource"
requires_auth = False requires_auth = False
def __init__(self, flow_mgr, store_result): def __init__(self, flow_mgr, store_result):
@ -196,43 +202,43 @@ class LoginFlowResourceView(HomeAssistantView):
async def get(self, request): async def get(self, request):
"""Do not allow getting status of a flow in progress.""" """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({ @RequestDataValidator(vol.Schema({"client_id": str}, extra=vol.ALLOW_EXTRA))
'client_id': str
}, extra=vol.ALLOW_EXTRA))
@log_invalid_auth @log_invalid_auth
async def post(self, request, flow_id, data): async def post(self, request, flow_id, data):
"""Handle progressing a login flow request.""" """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): 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: try:
# do not allow change ip during login flow # do not allow change ip during login flow
for flow in self._flow_mgr.async_progress(): for flow in self._flow_mgr.async_progress():
if (flow['flow_id'] == flow_id and if flow["flow_id"] == flow_id and flow["context"][
flow['context']['ip_address'] != "ip_address"
request.get(KEY_REAL_IP)): ] != request.get(KEY_REAL_IP):
return self.json_message('IP address changed', 400) return self.json_message("IP address changed", 400)
result = await self._flow_mgr.async_configure(flow_id, data) result = await self._flow_mgr.async_configure(flow_id, data)
except data_entry_flow.UnknownFlow: except data_entry_flow.UnknownFlow:
return self.json_message('Invalid flow specified', 404) return self.json_message("Invalid flow specified", 404)
except vol.Invalid: 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 # @log_invalid_auth does not work here since it returns HTTP 200
# need manually log failed login attempts # need manually log failed login attempts
if result['errors'] is not None and \ if (
result['errors'].get('base') == 'invalid_auth': result["errors"] is not None
and result["errors"].get("base") == "invalid_auth"
):
await process_wrong_login(request) await process_wrong_login(request)
return self.json(_prepare_result_json(result)) return self.json(_prepare_result_json(result))
result.pop('data') result.pop("data")
result['result'] = self._store_result(client_id, result['result']) result["result"] = self._store_result(client_id, result["result"])
return self.json(result) return self.json(result)
@ -241,6 +247,6 @@ class LoginFlowResourceView(HomeAssistantView):
try: try:
self._flow_mgr.async_abort(flow_id) self._flow_mgr.async_abort(flow_id)
except data_entry_flow.UnknownFlow: 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.components import websocket_api
from homeassistant.core import callback, HomeAssistant from homeassistant.core import callback, HomeAssistant
WS_TYPE_SETUP_MFA = 'auth/setup_mfa' WS_TYPE_SETUP_MFA = "auth/setup_mfa"
SCHEMA_WS_SETUP_MFA = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({ 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.Required("type"): WS_TYPE_SETUP_MFA,
vol.Exclusive('flow_id', 'module_or_flow_id'): str, vol.Exclusive("mfa_module_id", "module_or_flow_id"): str,
vol.Optional('user_input'): object, vol.Exclusive("flow_id", "module_or_flow_id"): str,
}) vol.Optional("user_input"): object,
}
)
WS_TYPE_DEPOSE_MFA = 'auth/depose_mfa' WS_TYPE_DEPOSE_MFA = "auth/depose_mfa"
SCHEMA_WS_DEPOSE_MFA = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({ SCHEMA_WS_DEPOSE_MFA = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend(
vol.Required('type'): WS_TYPE_DEPOSE_MFA, {vol.Required("type"): WS_TYPE_DEPOSE_MFA, vol.Required("mfa_module_id"): str}
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__) _LOGGER = logging.getLogger(__name__)
async def async_setup(hass): async def async_setup(hass):
"""Init mfa setup flow manager.""" """Init mfa setup flow manager."""
async def _async_create_setup_flow(handler, context, data): async def _async_create_setup_flow(handler, context, data):
"""Create a setup flow. hanlder is a mfa module.""" """Create a setup flow. hanlder is a mfa module."""
mfa_module = hass.auth.get_auth_mfa_module(handler) mfa_module = hass.auth.get_auth_mfa_module(handler)
if mfa_module is None: 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) return await mfa_module.async_setup_flow(user_id)
async def _async_finish_setup_flow(flow, flow_result): 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 return flow_result
hass.data[DATA_SETUP_FLOW_MGR] = data_entry_flow.FlowManager( 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( 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( 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 @callback
@websocket_api.ws_require_user(allow_system_user=False) @websocket_api.ws_require_user(allow_system_user=False)
def websocket_setup_mfa( 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.""" """Return a setup flow for mfa auth module."""
async def async_setup_flow(msg): async def async_setup_flow(msg):
"""Return a setup flow for mfa auth module.""" """Return a setup flow for mfa auth module."""
flow_manager = hass.data[DATA_SETUP_FLOW_MGR] 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: if flow_id is not None:
result = await flow_manager.async_configure( result = await flow_manager.async_configure(flow_id, msg.get("user_input"))
flow_id, msg.get('user_input'))
connection.send_message_outside( connection.send_message_outside(
websocket_api.result_message( websocket_api.result_message(msg["id"], _prepare_result_json(result))
msg['id'], _prepare_result_json(result))) )
return 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) mfa_module = hass.auth.get_auth_mfa_module(mfa_module_id)
if mfa_module is None: if mfa_module is None:
connection.send_message_outside(websocket_api.error_message( connection.send_message_outside(
msg['id'], 'no_module', websocket_api.error_message(
'MFA module {} is not found'.format(mfa_module_id))) msg["id"],
"no_module",
"MFA module {} is not found".format(mfa_module_id),
)
)
return return
result = await flow_manager.async_init( 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( connection.send_message_outside(
websocket_api.result_message( websocket_api.result_message(msg["id"], _prepare_result_json(result))
msg['id'], _prepare_result_json(result))) )
hass.async_create_task(async_setup_flow(msg)) hass.async_create_task(async_setup_flow(msg))
@ -90,45 +101,49 @@ def websocket_setup_mfa(
@callback @callback
@websocket_api.ws_require_user(allow_system_user=False) @websocket_api.ws_require_user(allow_system_user=False)
def websocket_depose_mfa( def websocket_depose_mfa(
hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg): hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg
):
"""Remove user from mfa module.""" """Remove user from mfa module."""
async def async_depose(msg): async def async_depose(msg):
"""Remove user from mfa auth module.""" """Remove user from mfa auth module."""
mfa_module_id = msg['mfa_module_id'] mfa_module_id = msg["mfa_module_id"]
try: try:
await hass.auth.async_disable_user_mfa( await hass.auth.async_disable_user_mfa(
connection.user, msg['mfa_module_id']) connection.user, msg["mfa_module_id"]
)
except ValueError as err: except ValueError as err:
connection.send_message_outside(websocket_api.error_message( connection.send_message_outside(
msg['id'], 'disable_failed', websocket_api.error_message(
'Cannot disable MFA Module {}: {}'.format( msg["id"],
mfa_module_id, err))) "disable_failed",
"Cannot disable MFA Module {}: {}".format(mfa_module_id, err),
)
)
return return
connection.send_message_outside( connection.send_message_outside(websocket_api.result_message(msg["id"], "done"))
websocket_api.result_message(
msg['id'], 'done'))
hass.async_create_task(async_depose(msg)) hass.async_create_task(async_depose(msg))
def _prepare_result_json(result): def _prepare_result_json(result):
"""Convert result to JSON.""" """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 = result.copy()
return data return data
if result['type'] != data_entry_flow.RESULT_TYPE_FORM: if result["type"] != data_entry_flow.RESULT_TYPE_FORM:
return result return result
import voluptuous_serialize import voluptuous_serialize
data = result.copy() data = result.copy()
schema = data['data_schema'] schema = data["data_schema"]
if schema is None: if schema is None:
data['data_schema'] = [] data["data_schema"] = []
else: else:
data['data_schema'] = voluptuous_serialize.convert(schema) data["data_schema"] = voluptuous_serialize.convert(schema)
return data return data

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -12,17 +12,23 @@ import voluptuous as vol
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.const import ( 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 from homeassistant.helpers.event import async_track_sunrise, async_track_sunset
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
TRIGGER_SCHEMA = vol.Schema({ TRIGGER_SCHEMA = vol.Schema(
vol.Required(CONF_PLATFORM): 'sun', {
vol.Required(CONF_EVENT): cv.sun_event, vol.Required(CONF_PLATFORM): "sun",
vol.Required(CONF_OFFSET, default=timedelta(0)): cv.time_period, vol.Required(CONF_EVENT): cv.sun_event,
}) vol.Required(CONF_OFFSET, default=timedelta(0)): cv.time_period,
}
)
@asyncio.coroutine @asyncio.coroutine
@ -34,13 +40,9 @@ def async_trigger(hass, config, action):
@callback @callback
def call_action(): def call_action():
"""Call action with right context.""" """Call action with right context."""
hass.async_run_job(action, { hass.async_run_job(
'trigger': { action, {"trigger": {"platform": "sun", "event": event, "offset": offset}}
'platform': 'sun', )
'event': event,
'offset': offset,
},
})
if event == SUN_EVENT_SUNRISE: if event == SUN_EVENT_SUNRISE:
return async_track_sunrise(hass, call_action, offset) 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__) _LOGGER = logging.getLogger(__name__)
TRIGGER_SCHEMA = IF_ACTION_SCHEMA = vol.Schema({ TRIGGER_SCHEMA = IF_ACTION_SCHEMA = vol.Schema(
vol.Required(CONF_PLATFORM): 'template', {
vol.Required(CONF_VALUE_TEMPLATE): cv.template, vol.Required(CONF_PLATFORM): "template",
}) vol.Required(CONF_VALUE_TEMPLATE): cv.template,
}
)
@asyncio.coroutine @asyncio.coroutine
@ -32,13 +34,18 @@ def async_trigger(hass, config, action):
@callback @callback
def template_listener(entity_id, from_s, to_s): def template_listener(entity_id, from_s, to_s):
"""Listen for state changes and calls action.""" """Listen for state changes and calls action."""
hass.async_run_job(action({ hass.async_run_job(
'trigger': { action(
'platform': 'template', {
'entity_id': entity_id, "trigger": {
'from_state': from_s, "platform": "template",
'to_state': to_s, "entity_id": entity_id,
}, "from_state": from_s,
}, context=to_s.context)) "to_state": to_s,
}
},
context=to_s.context,
)
)
return async_track_template(hass, value_template, template_listener) 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 import config_validation as cv
from homeassistant.helpers.event import async_track_time_change from homeassistant.helpers.event import async_track_time_change
CONF_HOURS = 'hours' CONF_HOURS = "hours"
CONF_MINUTES = 'minutes' CONF_MINUTES = "minutes"
CONF_SECONDS = 'seconds' CONF_SECONDS = "seconds"
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
TRIGGER_SCHEMA = vol.All(vol.Schema({ TRIGGER_SCHEMA = vol.All(
vol.Required(CONF_PLATFORM): 'time', vol.Schema(
CONF_AT: cv.time, {
CONF_HOURS: vol.Any(vol.Coerce(int), vol.Coerce(str)), vol.Required(CONF_PLATFORM): "time",
CONF_MINUTES: vol.Any(vol.Coerce(int), vol.Coerce(str)), CONF_AT: cv.time,
CONF_SECONDS: vol.Any(vol.Coerce(int), vol.Coerce(str)), CONF_HOURS: vol.Any(vol.Coerce(int), vol.Coerce(str)),
}), cv.has_at_least_one_key(CONF_HOURS, CONF_MINUTES, CONF_SECONDS, CONF_AT)) 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 @asyncio.coroutine
@ -43,12 +48,8 @@ def async_trigger(hass, config, action):
@callback @callback
def time_automation_listener(now): def time_automation_listener(now):
"""Listen for time changes and calls action.""" """Listen for time changes and calls action."""
hass.async_run_job(action, { hass.async_run_job(action, {"trigger": {"platform": "time", "now": now}})
'trigger': {
'platform': 'time',
'now': now,
},
})
return async_track_time_change(hass, time_automation_listener, return async_track_time_change(
hour=hours, minute=minutes, second=seconds) 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.core import callback
from homeassistant.const import ( 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.event import async_track_state_change
from homeassistant.helpers import ( from homeassistant.helpers import condition, config_validation as cv, location
condition, config_validation as cv, location)
EVENT_ENTER = 'enter' EVENT_ENTER = "enter"
EVENT_LEAVE = 'leave' EVENT_LEAVE = "leave"
DEFAULT_EVENT = EVENT_ENTER DEFAULT_EVENT = EVENT_ENTER
TRIGGER_SCHEMA = vol.Schema({ TRIGGER_SCHEMA = vol.Schema(
vol.Required(CONF_PLATFORM): 'zone', {
vol.Required(CONF_ENTITY_ID): cv.entity_ids, vol.Required(CONF_PLATFORM): "zone",
vol.Required(CONF_ZONE): cv.entity_id, vol.Required(CONF_ENTITY_ID): cv.entity_ids,
vol.Required(CONF_EVENT, default=DEFAULT_EVENT): vol.Required(CONF_ZONE): cv.entity_id,
vol.Any(EVENT_ENTER, EVENT_LEAVE), vol.Required(CONF_EVENT, default=DEFAULT_EVENT): vol.Any(
}) EVENT_ENTER, EVENT_LEAVE
),
}
)
@asyncio.coroutine @asyncio.coroutine
@ -37,8 +44,11 @@ def async_trigger(hass, config, action):
@callback @callback
def zone_automation_listener(entity, from_s, to_s): def zone_automation_listener(entity, from_s, to_s):
"""Listen for state changes and calls action.""" """Listen for state changes and calls action."""
if from_s and not location.has_location(from_s) or \ if (
not location.has_location(to_s): from_s
and not location.has_location(from_s)
or not location.has_location(to_s)
):
return return
zone_state = hass.states.get(zone_entity_id) 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) to_match = condition.zone(hass, zone_state, to_s)
# pylint: disable=too-many-boolean-expressions # pylint: disable=too-many-boolean-expressions
if event == EVENT_ENTER and not from_match and to_match or \ if (
event == EVENT_LEAVE and from_match and not to_match: event == EVENT_ENTER
hass.async_run_job(action({ and not from_match
'trigger': { and to_match
'platform': 'zone', or event == EVENT_LEAVE
'entity_id': entity, and from_match
'from_state': from_s, and not to_match
'to_state': to_s, ):
'zone': zone_state, hass.async_run_job(
'event': event, action(
}, {
}, context=to_s.context)) "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, return async_track_state_change(
MATCH_ALL, MATCH_ALL) 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.components.discovery import SERVICE_AXIS
from homeassistant.const import ( from homeassistant.const import (
ATTR_LOCATION, ATTR_TRIPPED, CONF_EVENT, CONF_HOST, CONF_INCLUDE, ATTR_LOCATION,
CONF_NAME, CONF_PASSWORD, CONF_PORT, CONF_TRIGGER_TIME, CONF_USERNAME, ATTR_TRIPPED,
EVENT_HOMEASSISTANT_STOP) 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 config_validation as cv
from homeassistant.helpers import discovery from homeassistant.helpers import discovery
from homeassistant.helpers.dispatcher import dispatcher_send from homeassistant.helpers.dispatcher import dispatcher_send
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
from homeassistant.util.json import load_json, save_json from homeassistant.util.json import load_json, save_json
REQUIREMENTS = ['axis==14'] REQUIREMENTS = ["axis==14"]
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
DOMAIN = 'axis' DOMAIN = "axis"
CONFIG_FILE = 'axis.conf' CONFIG_FILE = "axis.conf"
AXIS_DEVICES = {} AXIS_DEVICES = {}
EVENT_TYPES = ['motion', 'vmd3', 'pir', 'sound', EVENT_TYPES = ["motion", "vmd3", "pir", "sound", "daynight", "tampering", "input"]
'daynight', 'tampering', 'input']
PLATFORMS = ['camera'] PLATFORMS = ["camera"]
AXIS_INCLUDE = EVENT_TYPES + PLATFORMS AXIS_INCLUDE = EVENT_TYPES + PLATFORMS
AXIS_DEFAULT_HOST = '192.168.0.90' AXIS_DEFAULT_HOST = "192.168.0.90"
AXIS_DEFAULT_USERNAME = 'root' AXIS_DEFAULT_USERNAME = "root"
AXIS_DEFAULT_PASSWORD = 'pass' AXIS_DEFAULT_PASSWORD = "pass"
DEVICE_SCHEMA = vol.Schema({ DEVICE_SCHEMA = vol.Schema(
vol.Required(CONF_INCLUDE): {
vol.All(cv.ensure_list, [vol.In(AXIS_INCLUDE)]), vol.Required(CONF_INCLUDE): vol.All(cv.ensure_list, [vol.In(AXIS_INCLUDE)]),
vol.Optional(CONF_NAME): cv.string, vol.Optional(CONF_NAME): cv.string,
vol.Optional(CONF_HOST, default=AXIS_DEFAULT_HOST): 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_USERNAME, default=AXIS_DEFAULT_USERNAME): cv.string,
vol.Optional(CONF_PASSWORD, default=AXIS_DEFAULT_PASSWORD): 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_TRIGGER_TIME, default=0): cv.positive_int,
vol.Optional(CONF_PORT, default=80): cv.positive_int, vol.Optional(CONF_PORT, default=80): cv.positive_int,
vol.Optional(ATTR_LOCATION, default=''): cv.string, vol.Optional(ATTR_LOCATION, default=""): cv.string,
}) }
)
CONFIG_SCHEMA = vol.Schema({ CONFIG_SCHEMA = vol.Schema(
DOMAIN: vol.Schema({ {DOMAIN: vol.Schema({cv.slug: DEVICE_SCHEMA})}, extra=vol.ALLOW_EXTRA
cv.slug: DEVICE_SCHEMA, )
}),
}, extra=vol.ALLOW_EXTRA)
SERVICE_VAPIX_CALL = 'vapix_call' SERVICE_VAPIX_CALL = "vapix_call"
SERVICE_VAPIX_CALL_RESPONSE = 'vapix_call_response' SERVICE_VAPIX_CALL_RESPONSE = "vapix_call_response"
SERVICE_CGI = 'cgi' SERVICE_CGI = "cgi"
SERVICE_ACTION = 'action' SERVICE_ACTION = "action"
SERVICE_PARAM = 'param' SERVICE_PARAM = "param"
SERVICE_DEFAULT_CGI = 'param.cgi' SERVICE_DEFAULT_CGI = "param.cgi"
SERVICE_DEFAULT_ACTION = 'update' SERVICE_DEFAULT_ACTION = "update"
SERVICE_SCHEMA = vol.Schema({ SERVICE_SCHEMA = vol.Schema(
vol.Required(CONF_NAME): cv.string, {
vol.Required(SERVICE_PARAM): cv.string, vol.Required(CONF_NAME): cv.string,
vol.Optional(SERVICE_CGI, default=SERVICE_DEFAULT_CGI): cv.string, vol.Required(SERVICE_PARAM): cv.string,
vol.Optional(SERVICE_ACTION, default=SERVICE_DEFAULT_ACTION): 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): 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): def configuration_callback(callback_data):
"""Call when configuration is submitted.""" """Call when configuration is submitted."""
if CONF_INCLUDE not in callback_data: if CONF_INCLUDE not in callback_data:
configurator.notify_errors( configurator.notify_errors(request_id, "Functionality mandatory.")
request_id, "Functionality mandatory.")
return False return False
callback_data[CONF_INCLUDE] = callback_data[CONF_INCLUDE].split() callback_data[CONF_INCLUDE] = callback_data[CONF_INCLUDE].split()
@ -93,58 +101,58 @@ def request_configuration(hass, config, name, host, serialnumber):
try: try:
device_config = DEVICE_SCHEMA(callback_data) device_config = DEVICE_SCHEMA(callback_data)
except vol.Invalid: except vol.Invalid:
configurator.notify_errors( configurator.notify_errors(request_id, "Bad input, please check spelling.")
request_id, "Bad input, please check spelling.")
return False return False
if setup_device(hass, config, device_config): if setup_device(hass, config, device_config):
del device_config['events'] del device_config["events"]
del device_config['signal'] del device_config["signal"]
config_file = load_json(hass.config.path(CONFIG_FILE)) config_file = load_json(hass.config.path(CONFIG_FILE))
config_file[serialnumber] = dict(device_config) config_file[serialnumber] = dict(device_config)
save_json(hass.config.path(CONFIG_FILE), config_file) save_json(hass.config.path(CONFIG_FILE), config_file)
configurator.request_done(request_id) configurator.request_done(request_id)
else: else:
configurator.notify_errors( configurator.notify_errors(
request_id, "Failed to register, please try again.") request_id, "Failed to register, please try again."
)
return False return False
title = '{} ({})'.format(name, host) title = "{} ({})".format(name, host)
request_id = configurator.request_config( request_id = configurator.request_config(
title, configuration_callback, title,
description='Functionality: ' + str(AXIS_INCLUDE), configuration_callback,
description="Functionality: " + str(AXIS_INCLUDE),
entity_picture="/static/images/logo_axis.png", entity_picture="/static/images/logo_axis.png",
link_name='Axis platform documentation', link_name="Axis platform documentation",
link_url='https://home-assistant.io/components/axis/', link_url="https://home-assistant.io/components/axis/",
submit_caption="Confirm", submit_caption="Confirm",
fields=[ fields=[
{'id': CONF_NAME, {"id": CONF_NAME, "name": "Device name", "type": "text"},
'name': "Device name", {"id": CONF_USERNAME, "name": "User name", "type": "text"},
'type': 'text'}, {"id": CONF_PASSWORD, "name": "Password", "type": "password"},
{'id': CONF_USERNAME, {
'name': "User name", "id": CONF_INCLUDE,
'type': 'text'}, "name": "Device functionality (space separated list)",
{'id': CONF_PASSWORD, "type": "text",
'name': 'Password', },
'type': 'password'}, {
{'id': CONF_INCLUDE, "id": ATTR_LOCATION,
'name': "Device functionality (space separated list)", "name": "Physical location of device (optional)",
'type': 'text'}, "type": "text",
{'id': ATTR_LOCATION, },
'name': "Physical location of device (optional)", {"id": CONF_PORT, "name": "HTTP port (default=80)", "type": "number"},
'type': 'text'}, {
{'id': CONF_PORT, "id": CONF_TRIGGER_TIME,
'name': "HTTP port (default=80)", "name": "Sensor update interval (optional)",
'type': 'number'}, "type": "number",
{'id': CONF_TRIGGER_TIME, },
'name': "Sensor update interval (optional)", ],
'type': 'number'},
]
) )
def setup(hass, config): def setup(hass, config):
"""Set up for Axis devices.""" """Set up for Axis devices."""
def _shutdown(call): def _shutdown(call):
"""Stop the event stream on shutdown.""" """Stop the event stream on shutdown."""
for serialnumber, device in AXIS_DEVICES.items(): for serialnumber, device in AXIS_DEVICES.items():
@ -156,8 +164,8 @@ def setup(hass, config):
def axis_device_discovered(service, discovery_info): def axis_device_discovered(service, discovery_info):
"""Call when axis devices has been found.""" """Call when axis devices has been found."""
host = discovery_info[CONF_HOST] host = discovery_info[CONF_HOST]
name = discovery_info['hostname'] name = discovery_info["hostname"]
serialnumber = discovery_info['properties']['macaddress'] serialnumber = discovery_info["properties"]["macaddress"]
if serialnumber not in AXIS_DEVICES: if serialnumber not in AXIS_DEVICES:
config_file = load_json(hass.config.path(CONFIG_FILE)) 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) _LOGGER.error("Bad data from %s. %s", CONFIG_FILE, err)
return False return False
if not setup_device(hass, config, device_config): if not setup_device(hass, config, device_config):
_LOGGER.error( _LOGGER.error("Couldn't set up %s", device_config[CONF_NAME])
"Couldn't set up %s", device_config[CONF_NAME])
else: else:
# New device, create configuration request for UI # New device, create configuration request for UI
request_configuration(hass, config, name, host, serialnumber) request_configuration(hass, config, name, host, serialnumber)
@ -179,7 +186,7 @@ def setup(hass, config):
# Device already registered, but on a different IP # Device already registered, but on a different IP
device = AXIS_DEVICES[serialnumber] device = AXIS_DEVICES[serialnumber]
device.config.host = host 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 # Register discovery service
discovery.listen(hass, SERVICE_AXIS, axis_device_discovered) discovery.listen(hass, SERVICE_AXIS, axis_device_discovered)
@ -199,7 +206,8 @@ def setup(hass, config):
response = device.vapix.do_request( response = device.vapix.do_request(
call.data[SERVICE_CGI], call.data[SERVICE_CGI],
call.data[SERVICE_ACTION], call.data[SERVICE_ACTION],
call.data[SERVICE_PARAM]) call.data[SERVICE_PARAM],
)
hass.bus.fire(SERVICE_VAPIX_CALL_RESPONSE, response) hass.bus.fire(SERVICE_VAPIX_CALL_RESPONSE, response)
return True return True
_LOGGER.info("Couldn't find device %s", call.data[CONF_NAME]) _LOGGER.info("Couldn't find device %s", call.data[CONF_NAME])
@ -207,7 +215,8 @@ def setup(hass, config):
# Register service with Home Assistant. # Register service with Home Assistant.
hass.services.register( hass.services.register(
DOMAIN, SERVICE_VAPIX_CALL, vapix_service, schema=SERVICE_SCHEMA) DOMAIN, SERVICE_VAPIX_CALL, vapix_service, schema=SERVICE_SCHEMA
)
return True return True
@ -217,21 +226,19 @@ def setup_device(hass, config, device_config):
def signal_callback(action, event): def signal_callback(action, event):
"""Call to configure events when initialized on event stream.""" """Call to configure events when initialized on event stream."""
if action == 'add': if action == "add":
event_config = { event_config = {
CONF_EVENT: event, CONF_EVENT: event,
CONF_NAME: device_config[CONF_NAME], CONF_NAME: device_config[CONF_NAME],
ATTR_LOCATION: device_config[ATTR_LOCATION], 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 component = event.event_platform
discovery.load_platform( discovery.load_platform(hass, component, DOMAIN, event_config, config)
hass, component, DOMAIN, event_config, config)
event_types = list(filter(lambda x: x in device_config[CONF_INCLUDE], event_types = list(filter(lambda x: x in device_config[CONF_INCLUDE], EVENT_TYPES))
EVENT_TYPES)) device_config["events"] = event_types
device_config['events'] = event_types device_config["signal"] = signal_callback
device_config['signal'] = signal_callback
device = AxisDevice(hass.loop, **device_config) device = AxisDevice(hass.loop, **device_config)
device.name = device_config[CONF_NAME] device.name = device_config[CONF_NAME]
@ -241,16 +248,15 @@ def setup_device(hass, config, device_config):
return False return False
for component in device_config[CONF_INCLUDE]: for component in device_config[CONF_INCLUDE]:
if component == 'camera': if component == "camera":
camera_config = { camera_config = {
CONF_NAME: device_config[CONF_NAME], CONF_NAME: device_config[CONF_NAME],
CONF_HOST: device_config[CONF_HOST], CONF_HOST: device_config[CONF_HOST],
CONF_PORT: device_config[CONF_PORT], CONF_PORT: device_config[CONF_PORT],
CONF_USERNAME: device_config[CONF_USERNAME], CONF_USERNAME: device_config[CONF_USERNAME],
CONF_PASSWORD: device_config[CONF_PASSWORD] CONF_PASSWORD: device_config[CONF_PASSWORD],
} }
discovery.load_platform( discovery.load_platform(hass, component, DOMAIN, camera_config, config)
hass, component, DOMAIN, camera_config, config)
AXIS_DEVICES[device.serial_number] = device AXIS_DEVICES[device.serial_number] = device
if event_types: if event_types:
@ -264,9 +270,9 @@ class AxisDeviceEvent(Entity):
def __init__(self, event_config): def __init__(self, event_config):
"""Initialize the event.""" """Initialize the event."""
self.axis_event = event_config[CONF_EVENT] self.axis_event = event_config[CONF_EVENT]
self._name = '{}_{}_{}'.format( self._name = "{}_{}_{}".format(
event_config[CONF_NAME], self.axis_event.event_type, event_config[CONF_NAME], self.axis_event.event_type, self.axis_event.id
self.axis_event.id) )
self.location = event_config[ATTR_LOCATION] self.location = event_config[ATTR_LOCATION]
self.axis_event.callback = self._update_callback self.axis_event.callback = self._update_callback
@ -295,7 +301,7 @@ class AxisDeviceEvent(Entity):
attr = {} attr = {}
tripped = self.axis_event.is_tripped 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 attr[ATTR_LOCATION] = self.location

View file

@ -6,14 +6,13 @@ https://home-assistant.io/components/bbb_gpio/
""" """
import logging import logging
from homeassistant.const import ( from homeassistant.const import EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP
EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP)
REQUIREMENTS = ['Adafruit_BBIO==1.0.0'] REQUIREMENTS = ["Adafruit_BBIO==1.0.0"]
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
DOMAIN = 'bbb_gpio' DOMAIN = "bbb_gpio"
def setup(hass, config): def setup(hass, config):
@ -37,6 +36,7 @@ def setup_output(pin):
"""Set up a GPIO as output.""" """Set up a GPIO as output."""
# pylint: disable=import-error # pylint: disable=import-error
from Adafruit_BBIO import GPIO from Adafruit_BBIO import GPIO
GPIO.setup(pin, GPIO.OUT) GPIO.setup(pin, GPIO.OUT)
@ -44,15 +44,15 @@ def setup_input(pin, pull_mode):
"""Set up a GPIO as input.""" """Set up a GPIO as input."""
# pylint: disable=import-error # pylint: disable=import-error
from Adafruit_BBIO import GPIO from Adafruit_BBIO import GPIO
GPIO.setup(pin, GPIO.IN,
GPIO.PUD_DOWN if pull_mode == 'DOWN' GPIO.setup(pin, GPIO.IN, GPIO.PUD_DOWN if pull_mode == "DOWN" else GPIO.PUD_UP)
else GPIO.PUD_UP)
def write_output(pin, value): def write_output(pin, value):
"""Write a value to a GPIO.""" """Write a value to a GPIO."""
# pylint: disable=import-error # pylint: disable=import-error
from Adafruit_BBIO import GPIO from Adafruit_BBIO import GPIO
GPIO.output(pin, value) GPIO.output(pin, value)
@ -60,6 +60,7 @@ def read_input(pin):
"""Read a value from a GPIO.""" """Read a value from a GPIO."""
# pylint: disable=import-error # pylint: disable=import-error
from Adafruit_BBIO import GPIO from Adafruit_BBIO import GPIO
return GPIO.input(pin) is GPIO.HIGH 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.""" """Add detection for RISING and FALLING events."""
# pylint: disable=import-error # pylint: disable=import-error
from Adafruit_BBIO import GPIO 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_component import EntityComponent
from homeassistant.helpers.entity import Entity 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 from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa
DOMAIN = 'binary_sensor' DOMAIN = "binary_sensor"
SCAN_INTERVAL = timedelta(seconds=30) SCAN_INTERVAL = timedelta(seconds=30)
ENTITY_ID_FORMAT = DOMAIN + '.{}' ENTITY_ID_FORMAT = DOMAIN + ".{}"
DEVICE_CLASSES = [ DEVICE_CLASSES = [
'battery', # On means low, Off means normal "battery", # On means low, Off means normal
'cold', # On means cold, Off means normal "cold", # On means cold, Off means normal
'connectivity', # On means connected, Off means disconnected "connectivity", # On means connected, Off means disconnected
'door', # On means open, Off means closed "door", # On means open, Off means closed
'garage_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) "gas", # On means gas detected, Off means no gas (clear)
'heat', # On means hot, Off means normal "heat", # On means hot, Off means normal
'light', # On means light detected, Off means no light "light", # On means light detected, Off means no light
'lock', # On means open (unlocked), Off means closed (locked) "lock", # On means open (unlocked), Off means closed (locked)
'moisture', # On means wet, Off means dry "moisture", # On means wet, Off means dry
'motion', # On means motion detected, Off means no motion (clear) "motion", # On means motion detected, Off means no motion (clear)
'moving', # On means moving, Off means not moving (stopped) "moving", # On means moving, Off means not moving (stopped)
'occupancy', # On means occupied, Off means not occupied (clear) "occupancy", # On means occupied, Off means not occupied (clear)
'opening', # On means open, Off means closed "opening", # On means open, Off means closed
'plug', # On means plugged in, Off means unplugged "plug", # On means plugged in, Off means unplugged
'power', # On means power detected, Off means no power "power", # On means power detected, Off means no power
'presence', # On means home, Off means away "presence", # On means home, Off means away
'problem', # On means problem detected, Off means no problem (OK) "problem", # On means problem detected, Off means no problem (OK)
'safety', # On means unsafe, Off means safe "safety", # On means unsafe, Off means safe
'smoke', # On means smoke detected, Off means no smoke (clear) "smoke", # On means smoke detected, Off means no smoke (clear)
'sound', # On means sound detected, Off means no sound (clear) "sound", # On means sound detected, Off means no sound (clear)
'vibration', # On means vibration detected, Off means no vibration "vibration", # On means vibration detected, Off means no vibration
'window', # On means open, Off means closed "window", # On means open, Off means closed
] ]
DEVICE_CLASSES_SCHEMA = vol.All(vol.Lower, vol.In(DEVICE_CLASSES)) 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): async def async_setup(hass, config):
"""Track states and offer events for binary sensors.""" """Track states and offer events for binary sensors."""
component = hass.data[DOMAIN] = EntityComponent( component = hass.data[DOMAIN] = EntityComponent(
logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL) logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL
)
await component.async_setup(config) await component.async_setup(config)
return True return True

View file

@ -6,12 +6,15 @@ https://home-assistant.io/components/binary_sensor.abode/
""" """
import logging import logging
from homeassistant.components.abode import (AbodeDevice, AbodeAutomation, from homeassistant.components.abode import (
DOMAIN as ABODE_DOMAIN) AbodeDevice,
AbodeAutomation,
DOMAIN as ABODE_DOMAIN,
)
from homeassistant.components.binary_sensor import BinarySensorDevice from homeassistant.components.binary_sensor import BinarySensorDevice
DEPENDENCIES = ['abode'] DEPENDENCIES = ["abode"]
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -23,9 +26,13 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
data = hass.data[ABODE_DOMAIN] data = hass.data[ABODE_DOMAIN]
device_types = [CONST.TYPE_CONNECTIVITY, CONST.TYPE_MOISTURE, device_types = [
CONST.TYPE_MOTION, CONST.TYPE_OCCUPANCY, CONST.TYPE_CONNECTIVITY,
CONST.TYPE_OPENING] CONST.TYPE_MOISTURE,
CONST.TYPE_MOTION,
CONST.TYPE_OCCUPANCY,
CONST.TYPE_OPENING,
]
devices = [] devices = []
for device in data.abode.get_devices(generic_type=device_types): 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)) devices.append(AbodeBinarySensor(data, device))
for automation in data.abode.get_automations( for automation in data.abode.get_automations(generic_type=CONST.TYPE_QUICK_ACTION):
generic_type=CONST.TYPE_QUICK_ACTION):
if data.is_automation_excluded(automation): if data.is_automation_excluded(automation):
continue continue
devices.append(AbodeQuickActionBinarySensor( devices.append(
data, automation, TIMELINE.AUTOMATION_EDIT_GROUP)) AbodeQuickActionBinarySensor(
data, automation, TIMELINE.AUTOMATION_EDIT_GROUP
)
)
data.devices.extend(devices) 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.ads import CONF_ADS_VAR, DATA_ADS
from homeassistant.components.binary_sensor import ( 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 from homeassistant.const import CONF_DEVICE_CLASS, CONF_NAME
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
DEFAULT_NAME = 'ADS binary sensor' DEFAULT_NAME = "ADS binary sensor"
DEPENDENCIES = ['ads'] DEPENDENCIES = ["ads"]
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
vol.Required(CONF_ADS_VAR): cv.string, {
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Required(CONF_ADS_VAR): cv.string,
vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA, 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): def setup_platform(hass, config, add_entities, discovery_info=None):
@ -46,22 +51,26 @@ class AdsBinarySensor(BinarySensorDevice):
"""Initialize ADS binary sensor.""" """Initialize ADS binary sensor."""
self._name = name self._name = name
self._state = False self._state = False
self._device_class = device_class or 'moving' self._device_class = device_class or "moving"
self._ads_hub = ads_hub self._ads_hub = ads_hub
self.ads_var = ads_var self.ads_var = ads_var
@asyncio.coroutine @asyncio.coroutine
def async_added_to_hass(self): def async_added_to_hass(self):
"""Register device notification.""" """Register device notification."""
def update(name, value): def update(name, value):
"""Handle device notifications.""" """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._state = value
self.schedule_update_ha_state() self.schedule_update_ha_state()
self.hass.async_add_job( self.hass.async_add_job(
self._ads_hub.add_device_notification, 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 @property
def name(self): def name(self):

View file

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

View file

@ -8,14 +8,18 @@ import asyncio
from homeassistant.components.binary_sensor import BinarySensorDevice from homeassistant.components.binary_sensor import BinarySensorDevice
from homeassistant.components.android_ip_webcam import ( 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 @asyncio.coroutine
def async_setup_platform(hass, config, async_add_entities, def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
discovery_info=None):
"""Set up the IP Webcam binary sensors.""" """Set up the IP Webcam binary sensors."""
if discovery_info is None: if discovery_info is None:
return return
@ -24,8 +28,7 @@ def async_setup_platform(hass, config, async_add_entities,
name = discovery_info[CONF_NAME] name = discovery_info[CONF_NAME]
ipcam = hass.data[DATA_IP_WEBCAM][host] ipcam = hass.data[DATA_IP_WEBCAM][host]
async_add_entities( async_add_entities([IPWebcamBinarySensor(name, host, ipcam, "motion_active")], True)
[IPWebcamBinarySensor(name, host, ipcam, 'motion_active')], True)
class IPWebcamBinarySensor(AndroidIPCamEntity, BinarySensorDevice): class IPWebcamBinarySensor(AndroidIPCamEntity, BinarySensorDevice):
@ -37,7 +40,7 @@ class IPWebcamBinarySensor(AndroidIPCamEntity, BinarySensorDevice):
self._sensor = sensor self._sensor = sensor
self._mapped_name = KEY_MAP.get(self._sensor, self._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._state = None
self._unit = None self._unit = None
@ -60,4 +63,4 @@ class IPWebcamBinarySensor(AndroidIPCamEntity, BinarySensorDevice):
@property @property
def device_class(self): def device_class(self):
"""Return the class of this device, from component DEVICE_CLASSES.""" """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 import voluptuous as vol
from homeassistant.components.binary_sensor import ( from homeassistant.components.binary_sensor import BinarySensorDevice, PLATFORM_SCHEMA
BinarySensorDevice, PLATFORM_SCHEMA)
from homeassistant.const import CONF_NAME from homeassistant.const import CONF_NAME
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.components import apcupsd from homeassistant.components import apcupsd
DEFAULT_NAME = 'UPS Online Status' DEFAULT_NAME = "UPS Online Status"
DEPENDENCIES = [apcupsd.DOMAIN] DEPENDENCIES = [apcupsd.DOMAIN]
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, {vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string}
}) )
def setup_platform(hass, config, add_entities, discovery_info=None): def setup_platform(hass, config, add_entities, discovery_info=None):

View file

@ -11,9 +11,11 @@ import requests
import voluptuous as vol import voluptuous as vol
from homeassistant.components.binary_sensor import ( from homeassistant.components.binary_sensor import (
BinarySensorDevice, PLATFORM_SCHEMA, DEVICE_CLASSES_SCHEMA) BinarySensorDevice,
from homeassistant.const import ( PLATFORM_SCHEMA,
CONF_RESOURCE, CONF_PIN, CONF_NAME, CONF_DEVICE_CLASS) DEVICE_CLASSES_SCHEMA,
)
from homeassistant.const import CONF_RESOURCE, CONF_PIN, CONF_NAME, CONF_DEVICE_CLASS
from homeassistant.util import Throttle from homeassistant.util import Throttle
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
@ -21,12 +23,14 @@ _LOGGER = logging.getLogger(__name__)
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=30) MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=30)
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
vol.Required(CONF_RESOURCE): cv.url, {
vol.Optional(CONF_NAME): cv.string, vol.Required(CONF_RESOURCE): cv.url,
vol.Required(CONF_PIN): cv.string, vol.Optional(CONF_NAME): cv.string,
vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA, vol.Required(CONF_PIN): cv.string,
}) vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA,
}
)
def setup_platform(hass, config, add_entities, discovery_info=None): 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: try:
response = requests.get(resource, timeout=10).json() response = requests.get(resource, timeout=10).json()
except requests.exceptions.MissingSchema: except requests.exceptions.MissingSchema:
_LOGGER.error("Missing resource or schema in configuration. " _LOGGER.error(
"Add http:// to your URL") "Missing resource or schema in configuration. " "Add http:// to your URL"
)
return False return False
except requests.exceptions.ConnectionError: except requests.exceptions.ConnectionError:
_LOGGER.error("No route to device at %s", resource) _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) arest = ArestData(resource, pin)
add_entities([ArestBinarySensor( add_entities(
arest, resource, config.get(CONF_NAME, response[CONF_NAME]), [
device_class, pin)], True) ArestBinarySensor(
arest,
resource,
config.get(CONF_NAME, response[CONF_NAME]),
device_class,
pin,
)
],
True,
)
class ArestBinarySensor(BinarySensorDevice): class ArestBinarySensor(BinarySensorDevice):
@ -65,7 +79,8 @@ class ArestBinarySensor(BinarySensorDevice):
if self._pin is not None: if self._pin is not None:
request = requests.get( 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: if request.status_code != 200:
_LOGGER.error("Can't set mode of %s", self._resource) _LOGGER.error("Can't set mode of %s", self._resource)
@ -77,7 +92,7 @@ class ArestBinarySensor(BinarySensorDevice):
@property @property
def is_on(self): def is_on(self):
"""Return true if the binary sensor is on.""" """Return true if the binary sensor is on."""
return bool(self.arest.data.get('state')) return bool(self.arest.data.get("state"))
@property @property
def device_class(self): def device_class(self):
@ -102,8 +117,9 @@ class ArestData:
def update(self): def update(self):
"""Get the latest data from aREST device.""" """Get the latest data from aREST device."""
try: try:
response = requests.get('{}/digital/{}'.format( response = requests.get(
self._resource, self._pin), timeout=10) "{}/digital/{}".format(self._resource, self._pin), timeout=10
self.data = {'state': response.json()['return_value']} )
self.data = {"state": response.json()["return_value"]}
except requests.exceptions.ConnectionError: except requests.exceptions.ConnectionError:
_LOGGER.error("No route to device '%s'", self._resource) _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 datetime import timedelta, datetime
from homeassistant.components.august import DATA_AUGUST 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) SCAN_INTERVAL = timedelta(seconds=5)
@ -22,21 +22,21 @@ def _retrieve_online_state(data, doorbell):
def _retrieve_motion_state(data, doorbell): def _retrieve_motion_state(data, doorbell):
from august.activity import ActivityType from august.activity import ActivityType
return _activity_time_based_state(data, doorbell,
[ActivityType.DOORBELL_MOTION, return _activity_time_based_state(
ActivityType.DOORBELL_DING]) data, doorbell, [ActivityType.DOORBELL_MOTION, ActivityType.DOORBELL_DING]
)
def _retrieve_ding_state(data, doorbell): def _retrieve_ding_state(data, doorbell):
from august.activity import ActivityType 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): def _activity_time_based_state(data, doorbell, activity_types):
"""Get the latest state of the sensor.""" """Get the latest state of the sensor."""
latest = data.get_latest_device_activity(doorbell.device_id, latest = data.get_latest_device_activity(doorbell.device_id, *activity_types)
*activity_types)
if latest is not None: if latest is not None:
start = latest.activity_start_time 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: Name, device_class, state_provider
SENSOR_TYPES = { SENSOR_TYPES = {
'doorbell_ding': ['Ding', 'occupancy', _retrieve_ding_state], "doorbell_ding": ["Ding", "occupancy", _retrieve_ding_state],
'doorbell_motion': ['Motion', 'motion', _retrieve_motion_state], "doorbell_motion": ["Motion", "motion", _retrieve_motion_state],
'doorbell_online': ['Online', 'connectivity', _retrieve_online_state], "doorbell_online": ["Online", "connectivity", _retrieve_online_state],
} }
@ -88,8 +88,9 @@ class AugustBinarySensor(BinarySensorDevice):
@property @property
def name(self): def name(self):
"""Return the name of the binary sensor.""" """Return the name of the binary sensor."""
return "{} {}".format(self._doorbell.device_name, return "{} {}".format(
SENSOR_TYPES[self._sensor_type][0]) self._doorbell.device_name, SENSOR_TYPES[self._sensor_type][0]
)
def update(self): def update(self):
"""Get the latest state of the sensor.""" """Get the latest state of the sensor."""

View file

@ -11,20 +11,18 @@ from aiohttp.hdrs import USER_AGENT
import requests import requests
import voluptuous as vol import voluptuous as vol
from homeassistant.components.binary_sensor import ( from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorDevice
PLATFORM_SCHEMA, BinarySensorDevice)
from homeassistant.const import CONF_NAME, ATTR_ATTRIBUTION from homeassistant.const import CONF_NAME, ATTR_ATTRIBUTION
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.util import Throttle from homeassistant.util import Throttle
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
CONF_ATTRIBUTION = "Data provided by the National Oceanic and Atmospheric" \ CONF_ATTRIBUTION = "Data provided by the National Oceanic and Atmospheric" "Administration"
"Administration" CONF_THRESHOLD = "forecast_threshold"
CONF_THRESHOLD = 'forecast_threshold'
DEFAULT_DEVICE_CLASS = 'visible' DEFAULT_DEVICE_CLASS = "visible"
DEFAULT_NAME = 'Aurora Visibility' DEFAULT_NAME = "Aurora Visibility"
DEFAULT_THRESHOLD = 75 DEFAULT_THRESHOLD = 75
HA_USER_AGENT = "Home Assistant Aurora Tracker v.0.1.0" 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" URL = "http://services.swpc.noaa.gov/text/aurora-nowcast-map.txt"
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, {
vol.Optional(CONF_THRESHOLD, default=DEFAULT_THRESHOLD): cv.positive_int, 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): 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) threshold = config.get(CONF_THRESHOLD)
try: try:
aurora_data = AuroraData( aurora_data = AuroraData(hass.config.latitude, hass.config.longitude, threshold)
hass.config.latitude, hass.config.longitude, threshold)
aurora_data.update() aurora_data.update()
except requests.exceptions.HTTPError as error: except requests.exceptions.HTTPError as error:
_LOGGER.error( _LOGGER.error("Connection to aurora forecast service failed: %s", error)
"Connection to aurora forecast service failed: %s", error)
return False return False
add_entities([AuroraSensor(aurora_data, name)], True) add_entities([AuroraSensor(aurora_data, name)], True)
@ -71,7 +69,7 @@ class AuroraSensor(BinarySensorDevice):
@property @property
def name(self): def name(self):
"""Return the name of the sensor.""" """Return the name of the sensor."""
return '{}'.format(self._name) return "{}".format(self._name)
@property @property
def is_on(self): def is_on(self):
@ -89,8 +87,8 @@ class AuroraSensor(BinarySensorDevice):
attrs = {} attrs = {}
if self.aurora_data: if self.aurora_data:
attrs['visibility_level'] = self.aurora_data.visibility_level attrs["visibility_level"] = self.aurora_data.visibility_level
attrs['message'] = self.aurora_data.is_visible_text attrs["message"] = self.aurora_data.is_visible_text
attrs[ATTR_ATTRIBUTION] = CONF_ATTRIBUTION attrs[ATTR_ATTRIBUTION] = CONF_ATTRIBUTION
return attrs return attrs
@ -127,8 +125,7 @@ class AuroraData:
self.is_visible_text = "nothing's out" self.is_visible_text = "nothing's out"
except requests.exceptions.HTTPError as error: except requests.exceptions.HTTPError as error:
_LOGGER.error( _LOGGER.error("Connection to aurora forecast service failed: %s", error)
"Connection to aurora forecast service failed: %s", error)
return False return False
def get_aurora_forecast(self): def get_aurora_forecast(self):
@ -141,9 +138,11 @@ class AuroraData:
] ]
# Convert lat and long for data points in table # Convert lat and long for data points in table
converted_latitude = round((self.latitude / 180) converted_latitude = round(
* self.number_of_latitude_intervals) (self.latitude / 180) * self.number_of_latitude_intervals
converted_longitude = round((self.longitude / 360) )
* self.number_of_longitude_intervals) converted_longitude = round(
(self.longitude / 360) * self.number_of_longitude_intervals
)
return forecast_table[converted_latitude][converted_longitude] 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.helpers.event import track_point_in_utc_time
from homeassistant.util.dt import utcnow from homeassistant.util.dt import utcnow
DEPENDENCIES = ['axis'] DEPENDENCIES = ["axis"]
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -55,13 +55,14 @@ class AxisBinarySensor(AxisDeviceEvent, BinarySensorDevice):
# Set timer to wait until updating the state # Set timer to wait until updating the state
def _delay_update(now): def _delay_update(now):
"""Timer callback for sensor update.""" """Timer callback for sensor update."""
_LOGGER.debug("%s called delayed (%s sec) update", _LOGGER.debug(
self._name, self._delay) "%s called delayed (%s sec) update", self._name, self._delay
)
self.schedule_update_ha_state() self.schedule_update_ha_state()
self._timer = None self._timer = None
self._timer = track_point_in_utc_time( self._timer = track_point_in_utc_time(
self.hass, _delay_update, self.hass, _delay_update, utcnow() + timedelta(seconds=self._delay)
utcnow() + timedelta(seconds=self._delay)) )
else: else:
self.schedule_update_ha_state() self.schedule_update_ha_state()

View file

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

View file

@ -9,36 +9,35 @@ import logging
import voluptuous as vol import voluptuous as vol
from homeassistant.components import bbb_gpio from homeassistant.components import bbb_gpio
from homeassistant.components.binary_sensor import ( from homeassistant.components.binary_sensor import BinarySensorDevice, PLATFORM_SCHEMA
BinarySensorDevice, PLATFORM_SCHEMA) from homeassistant.const import DEVICE_DEFAULT_NAME, CONF_NAME
from homeassistant.const import (DEVICE_DEFAULT_NAME, CONF_NAME)
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
DEPENDENCIES = ['bbb_gpio'] DEPENDENCIES = ["bbb_gpio"]
CONF_PINS = 'pins' CONF_PINS = "pins"
CONF_BOUNCETIME = 'bouncetime' CONF_BOUNCETIME = "bouncetime"
CONF_INVERT_LOGIC = 'invert_logic' CONF_INVERT_LOGIC = "invert_logic"
CONF_PULL_MODE = 'pull_mode' CONF_PULL_MODE = "pull_mode"
DEFAULT_BOUNCETIME = 50 DEFAULT_BOUNCETIME = 50
DEFAULT_INVERT_LOGIC = False DEFAULT_INVERT_LOGIC = False
DEFAULT_PULL_MODE = 'UP' DEFAULT_PULL_MODE = "UP"
PIN_SCHEMA = vol.Schema({ PIN_SCHEMA = vol.Schema(
vol.Required(CONF_NAME): cv.string, {
vol.Optional(CONF_BOUNCETIME, default=DEFAULT_BOUNCETIME): cv.positive_int, vol.Required(CONF_NAME): cv.string,
vol.Optional(CONF_INVERT_LOGIC, default=DEFAULT_INVERT_LOGIC): cv.boolean, vol.Optional(CONF_BOUNCETIME, default=DEFAULT_BOUNCETIME): cv.positive_int,
vol.Optional(CONF_PULL_MODE, default=DEFAULT_PULL_MODE): vol.Optional(CONF_INVERT_LOGIC, default=DEFAULT_INVERT_LOGIC): cv.boolean,
vol.In(['UP', 'DOWN']) vol.Optional(CONF_PULL_MODE, default=DEFAULT_PULL_MODE): vol.In(["UP", "DOWN"]),
}) }
)
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
vol.Required(CONF_PINS, default={}): {vol.Required(CONF_PINS, default={}): vol.Schema({cv.string: PIN_SCHEMA})}
vol.Schema({cv.string: PIN_SCHEMA}), )
})
def setup_platform(hass, config, add_entities, discovery_info=None): 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.blink import DOMAIN
from homeassistant.components.binary_sensor import BinarySensorDevice from homeassistant.components.binary_sensor import BinarySensorDevice
DEPENDENCIES = ['blink'] DEPENDENCIES = ["blink"]
def setup_platform(hass, config, add_entities, discovery_info=None): def setup_platform(hass, config, add_entities, discovery_info=None):
@ -28,7 +28,7 @@ class BlinkCameraMotionSensor(BinarySensorDevice):
def __init__(self, name, data): def __init__(self, name, data):
"""Initialize the sensor.""" """Initialize the sensor."""
self._name = 'blink_' + name + '_motion_enabled' self._name = "blink_" + name + "_motion_enabled"
self._camera_name = name self._camera_name = name
self.data = data self.data = data
self._state = self.data.cameras[self._camera_name].armed self._state = self.data.cameras[self._camera_name].armed
@ -54,7 +54,7 @@ class BlinkSystemSensor(BinarySensorDevice):
def __init__(self, data): def __init__(self, data):
"""Initialize the sensor.""" """Initialize the sensor."""
self._name = 'blink armed status' self._name = "blink armed status"
self.data = data self.data = data
self._state = self.data.arm self._state = self.data.arm

View file

@ -8,24 +8,23 @@ import logging
import voluptuous as vol import voluptuous as vol
from homeassistant.components.binary_sensor import ( from homeassistant.components.binary_sensor import BinarySensorDevice, PLATFORM_SCHEMA
BinarySensorDevice, PLATFORM_SCHEMA)
from homeassistant.const import CONF_MONITORED_CONDITIONS from homeassistant.const import CONF_MONITORED_CONDITIONS
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
DEPENDENCIES = ['bloomsky'] DEPENDENCIES = ["bloomsky"]
SENSOR_TYPES = { SENSOR_TYPES = {"Rain": "moisture", "Night": None}
'Rain': 'moisture',
'Night': None,
}
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
vol.Optional(CONF_MONITORED_CONDITIONS, default=list(SENSOR_TYPES)): {
vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]), 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): 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 device in bloomsky.BLOOMSKY.devices.values():
for variable in sensors: for variable in sensors:
add_entities( add_entities([BloomSkySensor(bloomsky.BLOOMSKY, device, variable)], True)
[BloomSkySensor(bloomsky.BLOOMSKY, device, variable)], True)
class BloomSkySensor(BinarySensorDevice): class BloomSkySensor(BinarySensorDevice):
@ -46,9 +44,9 @@ class BloomSkySensor(BinarySensorDevice):
def __init__(self, bs, device, sensor_name): def __init__(self, bs, device, sensor_name):
"""Initialize a BloomSky binary sensor.""" """Initialize a BloomSky binary sensor."""
self._bloomsky = bs self._bloomsky = bs
self._device_id = device['DeviceID'] self._device_id = device["DeviceID"]
self._sensor_name = sensor_name self._sensor_name = sensor_name
self._name = '{} {}'.format(device['DeviceName'], sensor_name) self._name = "{} {}".format(device["DeviceName"], sensor_name)
self._state = None self._state = None
@property @property
@ -70,5 +68,4 @@ class BloomSkySensor(BinarySensorDevice):
"""Request an update from the BloomSky API.""" """Request an update from the BloomSky API."""
self._bloomsky.refresh_devices() self._bloomsky.refresh_devices()
self._state = \ self._state = self._bloomsky.devices[self._device_id]["Data"][self._sensor_name]
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.binary_sensor import BinarySensorDevice
from homeassistant.components.bmw_connected_drive import DOMAIN as BMW_DOMAIN from homeassistant.components.bmw_connected_drive import DOMAIN as BMW_DOMAIN
DEPENDENCIES = ['bmw_connected_drive'] DEPENDENCIES = ["bmw_connected_drive"]
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
SENSOR_TYPES = { SENSOR_TYPES = {
'lids': ['Doors', 'opening'], "lids": ["Doors", "opening"],
'windows': ['Windows', 'opening'], "windows": ["Windows", "opening"],
'door_lock_state': ['Door lock state', 'safety'], "door_lock_state": ["Door lock state", "safety"],
'lights_parking': ['Parking lights', 'light'], "lights_parking": ["Parking lights", "light"],
'condition_based_services': ['Condition based services', 'problem'], "condition_based_services": ["Condition based services", "problem"],
'check_control_messages': ['Control messages', 'problem'] "check_control_messages": ["Control messages", "problem"],
} }
SENSOR_TYPES_ELEC = { SENSOR_TYPES_ELEC = {
'charging_status': ['Charging status', 'power'], "charging_status": ["Charging status", "power"],
'connection_status': ['Connection status', 'plug'] "connection_status": ["Connection status", "plug"],
} }
SENSOR_TYPES_ELEC.update(SENSOR_TYPES) 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): def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the BMW sensors.""" """Set up the BMW sensors."""
accounts = hass.data[BMW_DOMAIN] accounts = hass.data[BMW_DOMAIN]
_LOGGER.debug('Found BMW accounts: %s', _LOGGER.debug("Found BMW accounts: %s", ", ".join([a.name for a in accounts]))
', '.join([a.name for a in accounts]))
devices = [] devices = []
for account in accounts: for account in accounts:
for vehicle in account.account.vehicles: for vehicle in account.account.vehicles:
if vehicle.has_hv_battery: 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()): for key, value in sorted(SENSOR_TYPES_ELEC.items()):
device = BMWConnectedDriveSensor(account, vehicle, key, device = BMWConnectedDriveSensor(
value[0], value[1]) account, vehicle, key, value[0], value[1]
)
devices.append(device) devices.append(device)
elif vehicle.has_internal_combustion_engine: 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()): for key, value in sorted(SENSOR_TYPES.items()):
device = BMWConnectedDriveSensor(account, vehicle, key, device = BMWConnectedDriveSensor(
value[0], value[1]) account, vehicle, key, value[0], value[1]
)
devices.append(device) devices.append(device)
add_entities(devices, True) add_entities(devices, True)
@ -57,14 +58,13 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
class BMWConnectedDriveSensor(BinarySensorDevice): class BMWConnectedDriveSensor(BinarySensorDevice):
"""Representation of a BMW vehicle binary sensor.""" """Representation of a BMW vehicle binary sensor."""
def __init__(self, account, vehicle, attribute: str, sensor_name, def __init__(self, account, vehicle, attribute: str, sensor_name, device_class):
device_class):
"""Constructor.""" """Constructor."""
self._account = account self._account = account
self._vehicle = vehicle self._vehicle = vehicle
self._attribute = attribute self._attribute = attribute
self._name = '{} {}'.format(self._vehicle.name, self._attribute) self._name = "{} {}".format(self._vehicle.name, self._attribute)
self._unique_id = '{}-{}'.format(self._vehicle.vin, self._attribute) self._unique_id = "{}-{}".format(self._vehicle.vin, self._attribute)
self._sensor_name = sensor_name self._sensor_name = sensor_name
self._device_class = device_class self._device_class = device_class
self._state = None self._state = None
@ -101,39 +101,37 @@ class BMWConnectedDriveSensor(BinarySensorDevice):
def device_state_attributes(self): def device_state_attributes(self):
"""Return the state attributes of the binary sensor.""" """Return the state attributes of the binary sensor."""
vehicle_state = self._vehicle.state vehicle_state = self._vehicle.state
result = { result = {"car": self._vehicle.name}
'car': self._vehicle.name
}
if self._attribute == 'lids': if self._attribute == "lids":
for lid in vehicle_state.lids: for lid in vehicle_state.lids:
result[lid.name] = lid.state.value result[lid.name] = lid.state.value
elif self._attribute == 'windows': elif self._attribute == "windows":
for window in vehicle_state.windows: for window in vehicle_state.windows:
result[window.name] = window.state.value result[window.name] = window.state.value
elif self._attribute == 'door_lock_state': elif self._attribute == "door_lock_state":
result['door_lock_state'] = vehicle_state.door_lock_state.value result["door_lock_state"] = vehicle_state.door_lock_state.value
result['last_update_reason'] = vehicle_state.last_update_reason result["last_update_reason"] = vehicle_state.last_update_reason
elif self._attribute == 'lights_parking': elif self._attribute == "lights_parking":
result['lights_parking'] = vehicle_state.parking_lights.value result["lights_parking"] = vehicle_state.parking_lights.value
elif self._attribute == 'condition_based_services': elif self._attribute == "condition_based_services":
for report in vehicle_state.condition_based_services: for report in vehicle_state.condition_based_services:
result.update(self._format_cbs_report(report)) 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 check_control_messages = vehicle_state.check_control_messages
if not check_control_messages: if not check_control_messages:
result['check_control_messages'] = 'OK' result["check_control_messages"] = "OK"
else: else:
result['check_control_messages'] = check_control_messages result["check_control_messages"] = check_control_messages
elif self._attribute == 'charging_status': elif self._attribute == "charging_status":
result['charging_status'] = vehicle_state.charging_status.value result["charging_status"] = vehicle_state.charging_status.value
# pylint: disable=protected-access # pylint: disable=protected-access
result['last_charging_end_result'] = \ result["last_charging_end_result"] = vehicle_state._attributes[
vehicle_state._attributes['lastChargingEndResult'] "lastChargingEndResult"
if self._attribute == 'connection_status': ]
if self._attribute == "connection_status":
# pylint: disable=protected-access # pylint: disable=protected-access
result['connection_status'] = \ result["connection_status"] = vehicle_state._attributes["connectionStatus"]
vehicle_state._attributes['connectionStatus']
return sorted(result.items()) return sorted(result.items())
@ -141,49 +139,52 @@ class BMWConnectedDriveSensor(BinarySensorDevice):
"""Read new state data from the library.""" """Read new state data from the library."""
from bimmer_connected.state import LockState from bimmer_connected.state import LockState
from bimmer_connected.state import ChargingState from bimmer_connected.state import ChargingState
vehicle_state = self._vehicle.state vehicle_state = self._vehicle.state
# device class opening: On means open, Off means closed # device class opening: On means open, Off means closed
if self._attribute == 'lids': if self._attribute == "lids":
_LOGGER.debug("Status of lid: %s", vehicle_state.all_lids_closed) _LOGGER.debug("Status of lid: %s", vehicle_state.all_lids_closed)
self._state = not 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 self._state = not vehicle_state.all_windows_closed
# device class safety: On means unsafe, Off means safe # 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 # Possible values: LOCKED, SECURED, SELECTIVE_LOCKED, UNLOCKED
self._state = vehicle_state.door_lock_state not in \ self._state = vehicle_state.door_lock_state not in [
[LockState.LOCKED, LockState.SECURED] LockState.LOCKED,
LockState.SECURED,
]
# device class light: On means light detected, Off means no light # 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 self._state = vehicle_state.are_parking_lights_on
# device class problem: On means problem detected, Off means no problem # 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 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 self._state = vehicle_state.has_check_control_messages
# device class power: On means power detected, Off means no power # device class power: On means power detected, Off means no power
if self._attribute == 'charging_status': if self._attribute == "charging_status":
self._state = vehicle_state.charging_status in \ self._state = vehicle_state.charging_status in [ChargingState.CHARGING]
[ChargingState.CHARGING]
# device class plug: On means device is plugged in, # device class plug: On means device is plugged in,
# Off means device is unplugged # Off means device is unplugged
if self._attribute == 'connection_status': if self._attribute == "connection_status":
# pylint: disable=protected-access # pylint: disable=protected-access
self._state = (vehicle_state._attributes['connectionStatus'] == self._state = vehicle_state._attributes["connectionStatus"] == "CONNECTED"
'CONNECTED')
@staticmethod @staticmethod
def _format_cbs_report(report): def _format_cbs_report(report):
result = {} result = {}
service_type = report.service_type.lower().replace('_', ' ') service_type = report.service_type.lower().replace("_", " ")
result['{} status'.format(service_type)] = report.state.value result["{} status".format(service_type)] = report.state.value
if report.due_date is not None: if report.due_date is not None:
result['{} date'.format(service_type)] = \ result["{} date".format(service_type)] = report.due_date.strftime(
report.due_date.strftime('%Y-%m-%d') "%Y-%m-%d"
)
if report.due_distance is not None: if report.due_distance is not None:
result['{} distance'.format(service_type)] = \ result["{} distance".format(service_type)] = "{} km".format(
'{} km'.format(report.due_distance) report.due_distance
)
return result return result
def update_callback(self): def update_callback(self):

View file

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

View file

@ -11,35 +11,39 @@ import requests
import voluptuous as vol import voluptuous as vol
from homeassistant.components.binary_sensor import ( from homeassistant.components.binary_sensor import (
BinarySensorDevice, PLATFORM_SCHEMA, DEVICE_CLASSES) BinarySensorDevice,
from homeassistant.const import (CONF_HOST, CONF_PORT) PLATFORM_SCHEMA,
DEVICE_CLASSES,
)
from homeassistant.const import CONF_HOST, CONF_PORT
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['concord232==0.15'] REQUIREMENTS = ["concord232==0.15"]
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
CONF_EXCLUDE_ZONES = 'exclude_zones' CONF_EXCLUDE_ZONES = "exclude_zones"
CONF_ZONE_TYPES = 'zone_types' CONF_ZONE_TYPES = "zone_types"
DEFAULT_HOST = 'localhost' DEFAULT_HOST = "localhost"
DEFAULT_NAME = 'Alarm' DEFAULT_NAME = "Alarm"
DEFAULT_PORT = '5007' DEFAULT_PORT = "5007"
DEFAULT_SSL = False DEFAULT_SSL = False
SCAN_INTERVAL = datetime.timedelta(seconds=10) SCAN_INTERVAL = datetime.timedelta(seconds=10)
ZONE_TYPES_SCHEMA = vol.Schema({ ZONE_TYPES_SCHEMA = vol.Schema({cv.positive_int: vol.In(DEVICE_CLASSES)})
cv.positive_int: vol.In(DEVICE_CLASSES),
})
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
vol.Optional(CONF_EXCLUDE_ZONES, default=[]): {
vol.All(cv.ensure_list, [cv.positive_int]), vol.Optional(CONF_EXCLUDE_ZONES, default=[]): vol.All(
vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string, cv.ensure_list, [cv.positive_int]
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, ),
vol.Optional(CONF_ZONE_TYPES, default={}): ZONE_TYPES_SCHEMA, 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): 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: try:
_LOGGER.debug("Initializing client") _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.zones = client.list_zones()
client.last_zone_update = datetime.datetime.now() 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 # name mapping to different sensors in an unpredictable way. Sort
# the zones by zone number to prevent this. # 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: for zone in client.zones:
_LOGGER.info("Loading Zone found: %s", zone['name']) _LOGGER.info("Loading Zone found: %s", zone["name"])
if zone['number'] not in exclude: if zone["number"] not in exclude:
sensors.append( sensors.append(
Concord232ZoneSensor( Concord232ZoneSensor(
hass, client, zone, zone_types.get( hass,
zone['number'], get_opening_type(zone)) 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): def get_opening_type(zone):
"""Return the result of the type guessing from name.""" """Return the result of the type guessing from name."""
if 'MOTION' in zone['name']: if "MOTION" in zone["name"]:
return 'motion' return "motion"
if 'KEY' in zone['name']: if "KEY" in zone["name"]:
return 'safety' return "safety"
if 'SMOKE' in zone['name']: if "SMOKE" in zone["name"]:
return 'smoke' return "smoke"
if 'WATER' in zone['name']: if "WATER" in zone["name"]:
return 'water' return "water"
return 'opening' return "opening"
class Concord232ZoneSensor(BinarySensorDevice): class Concord232ZoneSensor(BinarySensorDevice):
@ -103,7 +109,7 @@ class Concord232ZoneSensor(BinarySensorDevice):
self._hass = hass self._hass = hass
self._client = client self._client = client
self._zone = zone self._zone = zone
self._number = zone['number'] self._number = zone["number"]
self._zone_type = zone_type self._zone_type = zone_type
@property @property
@ -119,13 +125,13 @@ class Concord232ZoneSensor(BinarySensorDevice):
@property @property
def name(self): def name(self):
"""Return the name of the binary sensor.""" """Return the name of the binary sensor."""
return self._zone['name'] return self._zone["name"]
@property @property
def is_on(self): def is_on(self):
"""Return true if the binary sensor is on.""" """Return true if the binary sensor is on."""
# True means "faulted" or "open" or "abnormal state" # True means "faulted" or "open" or "abnormal state"
return bool(self._zone['state'] != 'Normal') return bool(self._zone["state"] != "Normal")
def update(self): def update(self):
"""Get updated stats from API.""" """Get updated stats from API."""
@ -134,8 +140,9 @@ class Concord232ZoneSensor(BinarySensorDevice):
if last_update > datetime.timedelta(seconds=1): if last_update > datetime.timedelta(seconds=1):
self._client.zones = self._client.list_zones() self._client.zones = self._client.list_zones()
self._client.last_zone_update = datetime.datetime.now() 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'): if hasattr(self._client, "zones"):
self._zone = next((x for x in self._client.zones self._zone = next(
if x['number'] == self._number), None) (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.binary_sensor import BinarySensorDevice
from homeassistant.components.deconz.const import ( from homeassistant.components.deconz.const import (
ATTR_DARK, ATTR_ON, CONF_ALLOW_CLIP_SENSOR, DOMAIN as DATA_DECONZ, ATTR_DARK,
DATA_DECONZ_ID, DATA_DECONZ_UNSUB, DECONZ_DOMAIN) 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.const import ATTR_BATTERY_LEVEL
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.helpers.device_registry import CONNECTION_ZIGBEE from homeassistant.helpers.device_registry import CONNECTION_ZIGBEE
from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.dispatcher import async_dispatcher_connect
DEPENDENCIES = ['deconz'] DEPENDENCIES = ["deconz"]
async def async_setup_platform(hass, config, async_add_entities, async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
discovery_info=None):
"""Old way of setting up deCONZ binary sensors.""" """Old way of setting up deCONZ binary sensors."""
pass pass
async def async_setup_entry(hass, config_entry, async_add_entities): async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up the deCONZ binary sensor.""" """Set up the deCONZ binary sensor."""
@callback @callback
def async_add_sensor(sensors): def async_add_sensor(sensors):
"""Add binary sensor from deCONZ.""" """Add binary sensor from deCONZ."""
from pydeconz.sensor import DECONZ_BINARY_SENSOR from pydeconz.sensor import DECONZ_BINARY_SENSOR
entities = [] entities = []
allow_clip_sensor = config_entry.data.get(CONF_ALLOW_CLIP_SENSOR, True) allow_clip_sensor = config_entry.data.get(CONF_ALLOW_CLIP_SENSOR, True)
for sensor in sensors: for sensor in sensors:
if sensor.type in DECONZ_BINARY_SENSOR and \ if sensor.type in DECONZ_BINARY_SENSOR and not (
not (not allow_clip_sensor and sensor.type.startswith('CLIP')): not allow_clip_sensor and sensor.type.startswith("CLIP")
):
entities.append(DeconzBinarySensor(sensor)) entities.append(DeconzBinarySensor(sensor))
async_add_entities(entities, True) async_add_entities(entities, True)
hass.data[DATA_DECONZ_UNSUB].append( 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()) async_add_sensor(hass.data[DATA_DECONZ].sensors.values())
@ -66,10 +75,12 @@ class DeconzBinarySensor(BinarySensorDevice):
If reason is that state is updated, If reason is that state is updated,
or reachable has changed or battery has changed. or reachable has changed or battery has changed.
""" """
if reason['state'] or \ if (
'reachable' in reason['attr'] or \ reason["state"]
'battery' in reason['attr'] or \ or "reachable" in reason["attr"]
'on' in reason['attr']: or "battery" in reason["attr"]
or "on" in reason["attr"]
):
self.async_schedule_update_ha_state() self.async_schedule_update_ha_state()
@property @property
@ -111,6 +122,7 @@ class DeconzBinarySensor(BinarySensorDevice):
def device_state_attributes(self): def device_state_attributes(self):
"""Return the state attributes of the sensor.""" """Return the state attributes of the sensor."""
from pydeconz.sensor import PRESENCE from pydeconz.sensor import PRESENCE
attr = {} attr = {}
if self._sensor.battery: if self._sensor.battery:
attr[ATTR_BATTERY_LEVEL] = self._sensor.battery attr[ATTR_BATTERY_LEVEL] = self._sensor.battery
@ -123,15 +135,14 @@ class DeconzBinarySensor(BinarySensorDevice):
@property @property
def device_info(self): def device_info(self):
"""Return a device description for device registry.""" """Return a device description for device registry."""
if (self._sensor.uniqueid is None or if self._sensor.uniqueid is None or self._sensor.uniqueid.count(":") != 7:
self._sensor.uniqueid.count(':') != 7):
return None return None
serial = self._sensor.uniqueid.split('-', 1)[0] serial = self._sensor.uniqueid.split("-", 1)[0]
return { return {
'connections': {(CONNECTION_ZIGBEE, serial)}, "connections": {(CONNECTION_ZIGBEE, serial)},
'identifiers': {(DECONZ_DOMAIN, serial)}, "identifiers": {(DECONZ_DOMAIN, serial)},
'manufacturer': self._sensor.manufacturer, "manufacturer": self._sensor.manufacturer,
'model': self._sensor.modelid, "model": self._sensor.modelid,
'name': self._sensor.name, "name": self._sensor.name,
'sw_version': self._sensor.swversion, "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): def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the Demo binary sensor platform.""" """Set up the Demo binary sensor platform."""
add_entities([ add_entities(
DemoBinarySensor('Basement Floor Wet', False, 'moisture'), [
DemoBinarySensor('Movement Backyard', True, 'motion'), DemoBinarySensor("Basement Floor Wet", False, "moisture"),
]) DemoBinarySensor("Movement Backyard", True, "motion"),
]
)
class DemoBinarySensor(BinarySensorDevice): class DemoBinarySensor(BinarySensorDevice):

View file

@ -9,23 +9,32 @@ import logging
import voluptuous as vol import voluptuous as vol
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.components.binary_sensor import ( from homeassistant.components.binary_sensor import BinarySensorDevice, PLATFORM_SCHEMA
BinarySensorDevice, PLATFORM_SCHEMA)
from homeassistant.components.digital_ocean import ( from homeassistant.components.digital_ocean import (
CONF_DROPLETS, ATTR_CREATED_AT, ATTR_DROPLET_ID, ATTR_DROPLET_NAME, CONF_DROPLETS,
ATTR_FEATURES, ATTR_IPV4_ADDRESS, ATTR_IPV6_ADDRESS, ATTR_MEMORY, ATTR_CREATED_AT,
ATTR_REGION, ATTR_VCPUS, CONF_ATTRIBUTION, DATA_DIGITAL_OCEAN) 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 from homeassistant.const import ATTR_ATTRIBUTION
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
DEFAULT_NAME = 'Droplet' DEFAULT_NAME = "Droplet"
DEFAULT_DEVICE_CLASS = 'moving' DEFAULT_DEVICE_CLASS = "moving"
DEPENDENCIES = ['digital_ocean'] DEPENDENCIES = ["digital_ocean"]
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
vol.Required(CONF_DROPLETS): vol.All(cv.ensure_list, [cv.string]), {vol.Required(CONF_DROPLETS): vol.All(cv.ensure_list, [cv.string])}
}) )
def setup_platform(hass, config, add_entities, discovery_info=None): def setup_platform(hass, config, add_entities, discovery_info=None):
@ -65,7 +74,7 @@ class DigitalOceanBinarySensor(BinarySensorDevice):
@property @property
def is_on(self): def is_on(self):
"""Return true if the binary sensor is on.""" """Return true if the binary sensor is on."""
return self.data.status == 'active' return self.data.status == "active"
@property @property
def device_class(self): def device_class(self):
@ -84,7 +93,7 @@ class DigitalOceanBinarySensor(BinarySensorDevice):
ATTR_IPV4_ADDRESS: self.data.ip_address, ATTR_IPV4_ADDRESS: self.data.ip_address,
ATTR_IPV6_ADDRESS: self.data.ip_v6_address, ATTR_IPV6_ADDRESS: self.data.ip_v6_address,
ATTR_MEMORY: self.data.memory, ATTR_MEMORY: self.data.memory,
ATTR_REGION: self.data.region['name'], ATTR_REGION: self.data.region["name"],
ATTR_VCPUS: self.data.vcpus, 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 import ecobee
from homeassistant.components.binary_sensor import BinarySensorDevice 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): 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() dev = list()
for index in range(len(data.ecobee.thermostats)): for index in range(len(data.ecobee.thermostats)):
for sensor in data.ecobee.get_remote_sensors(index): for sensor in data.ecobee.get_remote_sensors(index):
for item in sensor['capability']: for item in sensor["capability"]:
if item['type'] != 'occupancy': if item["type"] != "occupancy":
continue continue
dev.append(EcobeeBinarySensor(sensor['name'], index)) dev.append(EcobeeBinarySensor(sensor["name"], index))
add_entities(dev, True) add_entities(dev, True)
@ -34,11 +34,11 @@ class EcobeeBinarySensor(BinarySensorDevice):
def __init__(self, sensor_name, sensor_index): def __init__(self, sensor_name, sensor_index):
"""Initialize the sensor.""" """Initialize the sensor."""
self._name = sensor_name + ' Occupancy' self._name = sensor_name + " Occupancy"
self.sensor_name = sensor_name self.sensor_name = sensor_name
self.index = sensor_index self.index = sensor_index
self._state = None self._state = None
self._device_class = 'occupancy' self._device_class = "occupancy"
@property @property
def name(self): def name(self):
@ -48,7 +48,7 @@ class EcobeeBinarySensor(BinarySensorDevice):
@property @property
def is_on(self): def is_on(self):
"""Return the status of the sensor.""" """Return the status of the sensor."""
return self._state == 'true' return self._state == "true"
@property @property
def device_class(self): def device_class(self):
@ -60,7 +60,6 @@ class EcobeeBinarySensor(BinarySensorDevice):
data = ecobee.NETWORK data = ecobee.NETWORK
data.update() data.update()
for sensor in data.ecobee.get_remote_sensors(self.index): for sensor in data.ecobee.get_remote_sensors(self.index):
for item in sensor['capability']: for item in sensor["capability"]:
if (item['type'] == 'occupancy' and if item["type"] == "occupancy" and self.sensor_name == sensor["name"]:
self.sensor_name == sensor['name']): self._state = item["value"]
self._state = item['value']

View file

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

View file

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

View file

@ -9,22 +9,26 @@ import logging
import voluptuous as vol import voluptuous as vol
from homeassistant.components.binary_sensor import ( 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.components import enocean
from homeassistant.const import ( from homeassistant.const import CONF_NAME, CONF_ID, CONF_DEVICE_CLASS
CONF_NAME, CONF_ID, CONF_DEVICE_CLASS)
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
DEPENDENCIES = ['enocean'] DEPENDENCIES = ["enocean"]
DEFAULT_NAME = 'EnOcean binary sensor' DEFAULT_NAME = "EnOcean binary sensor"
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ 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.Required(CONF_ID): vol.All(cv.ensure_list, [vol.Coerce(int)]),
vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA, 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): 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): def __init__(self, dev_id, devname, device_class):
"""Initialize the EnOcean binary sensor.""" """Initialize the EnOcean binary sensor."""
enocean.EnOceanDevice.__init__(self) enocean.EnOceanDevice.__init__(self)
self.stype = 'listener' self.stype = "listener"
self.dev_id = dev_id self.dev_id = dev_id
self.which = -1 self.which = -1
self.onoff = -1 self.onoff = -1
@ -84,7 +88,12 @@ class EnOceanBinarySensor(enocean.EnOceanDevice, BinarySensorDevice):
elif value2 == 0x15: elif value2 == 0x15:
self.which = 10 self.which = 10
self.onoff = 1 self.onoff = 1
self.hass.bus.fire('button_pressed', {'id': self.dev_id, self.hass.bus.fire(
'pushed': value, "button_pressed",
'which': self.which, {
'onoff': self.onoff}) "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.helpers.dispatcher import async_dispatcher_connect
from homeassistant.components.binary_sensor import BinarySensorDevice from homeassistant.components.binary_sensor import BinarySensorDevice
from homeassistant.components.envisalink import ( from homeassistant.components.envisalink import (
DATA_EVL, ZONE_SCHEMA, CONF_ZONENAME, CONF_ZONETYPE, EnvisalinkDevice, DATA_EVL,
SIGNAL_ZONE_UPDATE) ZONE_SCHEMA,
CONF_ZONENAME,
CONF_ZONETYPE,
EnvisalinkDevice,
SIGNAL_ZONE_UPDATE,
)
from homeassistant.const import ATTR_LAST_TRIP_TIME from homeassistant.const import ATTR_LAST_TRIP_TIME
from homeassistant.util import dt as dt_util from homeassistant.util import dt as dt_util
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
DEPENDENCIES = ['envisalink'] DEPENDENCIES = ["envisalink"]
@asyncio.coroutine @asyncio.coroutine
def async_setup_platform(hass, config, async_add_entities, def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
discovery_info=None):
"""Set up the Envisalink binary sensor devices.""" """Set up the Envisalink binary sensor devices."""
configured_zones = discovery_info['zones'] configured_zones = discovery_info["zones"]
devices = [] devices = []
for zone_num in configured_zones: for zone_num in configured_zones:
@ -36,8 +40,8 @@ def async_setup_platform(hass, config, async_add_entities,
zone_num, zone_num,
device_config_data[CONF_ZONENAME], device_config_data[CONF_ZONENAME],
device_config_data[CONF_ZONETYPE], device_config_data[CONF_ZONETYPE],
hass.data[DATA_EVL].alarm_state['zone'][zone_num], hass.data[DATA_EVL].alarm_state["zone"][zone_num],
hass.data[DATA_EVL] hass.data[DATA_EVL],
) )
devices.append(device) devices.append(device)
@ -47,20 +51,18 @@ def async_setup_platform(hass, config, async_add_entities,
class EnvisalinkBinarySensor(EnvisalinkDevice, BinarySensorDevice): class EnvisalinkBinarySensor(EnvisalinkDevice, BinarySensorDevice):
"""Representation of an Envisalink binary sensor.""" """Representation of an Envisalink binary sensor."""
def __init__(self, hass, zone_number, zone_name, zone_type, info, def __init__(self, hass, zone_number, zone_name, zone_type, info, controller):
controller):
"""Initialize the binary_sensor.""" """Initialize the binary_sensor."""
self._zone_type = zone_type self._zone_type = zone_type
self._zone_number = zone_number 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) super().__init__(zone_name, info, controller)
@asyncio.coroutine @asyncio.coroutine
def async_added_to_hass(self): def async_added_to_hass(self):
"""Register callbacks.""" """Register callbacks."""
async_dispatcher_connect( async_dispatcher_connect(self.hass, SIGNAL_ZONE_UPDATE, self._update_callback)
self.hass, SIGNAL_ZONE_UPDATE, self._update_callback)
@property @property
def device_state_attributes(self): def device_state_attributes(self):
@ -76,7 +78,7 @@ class EnvisalinkBinarySensor(EnvisalinkDevice, BinarySensorDevice):
# interval, so we subtract it from the current second-accurate time # 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 # unless it is already at the maximum value, in which case we set it
# to None since we can't determine the actual value. # 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: if seconds_ago < 65536 * 5:
now = dt_util.now().replace(microsecond=0) now = dt_util.now().replace(microsecond=0)
delta = datetime.timedelta(seconds=seconds_ago) delta = datetime.timedelta(seconds=seconds_ago)
@ -90,7 +92,7 @@ class EnvisalinkBinarySensor(EnvisalinkDevice, BinarySensorDevice):
@property @property
def is_on(self): def is_on(self):
"""Return true if sensor is on.""" """Return true if sensor is on."""
return self._info['status']['open'] return self._info["status"]["open"]
@property @property
def device_class(self): def device_class(self):

View file

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